diff --git a/src/content/components/toolbar.ts b/src/content/components/toolbar.ts index 3fdbc23..dd934f7 100644 --- a/src/content/components/toolbar.ts +++ b/src/content/components/toolbar.ts @@ -1,5 +1,5 @@ import { LitElement, html, css } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; +import { customElement, property, state } from 'lit/decorators.js'; import { FileType } from '../../types/file-selector'; import { I18nService } from '../../utils/i18n'; @@ -48,6 +48,46 @@ export class Toolbar extends LitElement { @property({ type: Boolean }) disabled = false; + @state() + private typeMenuOpen = false; + + private documentClickHandler: ((e: MouseEvent) => void) | null = null; + private documentKeydownHandler: ((e: KeyboardEvent) => void) | null = null; + + connectedCallback() { + super.connectedCallback(); + + this.documentClickHandler = (e: MouseEvent) => { + if (!this.typeMenuOpen) return; + const path = e.composedPath(); + if (path.includes(this)) return; + this.closeTypeMenu(); + }; + + this.documentKeydownHandler = (e: KeyboardEvent) => { + if (!this.typeMenuOpen) return; + if (e.key !== 'Escape') return; + e.preventDefault(); + this.closeTypeMenu({ focusButton: true }); + }; + + document.addEventListener('click', this.documentClickHandler); + document.addEventListener('keydown', this.documentKeydownHandler); + } + + disconnectedCallback() { + super.disconnectedCallback(); + + if (this.documentClickHandler) { + document.removeEventListener('click', this.documentClickHandler); + this.documentClickHandler = null; + } + if (this.documentKeydownHandler) { + document.removeEventListener('keydown', this.documentKeydownHandler); + this.documentKeydownHandler = null; + } + } + /** * Computed: checkbox state * Returns 'all', 'none', or 'partial' @@ -84,13 +124,10 @@ export class Toolbar extends LitElement { } /** - * Handle type filter change + * Handle type filter selection * @private */ - private handleTypeFilterChange(e: Event): void { - const select = e.target as HTMLSelectElement; - const type = select.value as FileType | 'all'; - + private selectType(type: FileType | 'all'): void { this.dispatchEvent( new CustomEvent('type-filter', { detail: { type }, @@ -100,14 +137,71 @@ export class Toolbar extends LitElement { ); } + private getTypeLabel(type: FileType | 'all'): string { + switch (type) { + case 'all': + return I18nService.t('all_types'); + case 'video': + return I18nService.t('type_video'); + case 'image': + return I18nService.t('type_image'); + case 'audio': + return I18nService.t('type_audio'); + case 'document': + return I18nService.t('type_document'); + case 'other': + return I18nService.t('type_other'); + } + } + + private toggleTypeMenu(): void { + if (this.disabled) return; + this.typeMenuOpen = !this.typeMenuOpen; + + if (this.typeMenuOpen) { + requestAnimationFrame(() => { + const selected = + this.shadowRoot?.querySelector('.type-menu-item[data-selected="true"]') || + this.shadowRoot?.querySelector('.type-menu-item'); + selected?.focus(); + }); + } + } + + private closeTypeMenu(options?: { focusButton?: boolean }): void { + if (!this.typeMenuOpen) return; + this.typeMenuOpen = false; + + if (options?.focusButton) { + requestAnimationFrame(() => { + const button = this.shadowRoot?.getElementById('type-filter-button') as HTMLButtonElement | null; + button?.focus(); + }); + } + } + + private handleTypeMenuItemClick(type: FileType | 'all'): void { + if (this.disabled) return; + this.closeTypeMenu({ focusButton: true }); + this.selectType(type); + } + render() { const state = this.checkboxState; + const typeOptions: Array<{ value: FileType | 'all'; label: string }> = [ + { value: 'all', label: I18nService.t('all_types') }, + { value: 'video', label: I18nService.t('type_video') }, + { value: 'image', label: I18nService.t('type_image') }, + { value: 'audio', label: I18nService.t('type_audio') }, + { value: 'document', label: I18nService.t('type_document') }, + { value: 'other', label: I18nService.t('type_other') }, + ]; return html`
-
- -
- - +
+
+ + + +
@@ -260,44 +401,151 @@ export class Toolbar extends LitElement { color: var(--cdr-text-disabled, #bfbfbf); } - .filter-group { + .filter-group { + display: flex; + align-items: center; + gap: 8px; + } + + .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + } + + .type-select { + position: relative; + display: inline-flex; + align-items: center; + flex-shrink: 0; + } + + .type-button { + width: auto; + height: 32px; + padding: 0 10px 0 8px; + border: 1px solid var(--cdr-border-strong, #d9d9d9); + border-radius: 6px; + color: var(--cdr-text, #262626); + background: var(--cdr-surface, #fff); + cursor: pointer; + transition: all 0.2s; + display: inline-flex; + align-items: center; + justify-content: flex-start; + gap: 6px; + min-width: 0; + } + + .type-button:hover:not(:disabled) { + background: var(--cdr-surface-hover, #f5f5f5); + border-color: var(--cdr-primary, #1890ff); + } + + .type-button[data-active='true']:hover:not(:disabled) { + background: var(--cdr-selection-hover-bg, rgba(24, 144, 255, 0.12)); + } + + .type-button:focus-visible { + outline: none; + border-color: var(--cdr-primary, #1890ff); + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + } + + .type-button:disabled { + cursor: not-allowed; + opacity: 0.45; + } + + .type-button[data-active='true'] { + border-color: var(--cdr-primary, #1890ff); + background: var(--cdr-selection-bg, rgba(24, 144, 255, 0.08)); + color: var(--cdr-primary, #1890ff); + } + + .type-button-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + opacity: 0.9; + } + + .type-button-text { + font-size: 13px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 96px; + min-width: 0; + } + + .type-button-chevron { + width: 14px; + height: 14px; + flex-shrink: 0; + opacity: 0.8; + } + + .type-menu { + position: absolute; + top: calc(100% + 6px); + left: 0; + width: 180px; + padding: 6px; + background: var(--cdr-surface, #fff); + border: 1px solid var(--cdr-border-strong, #d9d9d9); + border-radius: 8px; + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18); + z-index: 1000; + } + + .type-menu-item { + width: 100%; display: flex; align-items: center; - gap: 8px; - } - - .filter-label { - font-size: 14px; - color: var(--cdr-text-secondary, #595959); - white-space: nowrap; - } - - .type-filter { - padding: 6px 12px; - border: 1px solid var(--cdr-border-strong, #d9d9d9); - border-radius: 4px; - font-size: 14px; + justify-content: space-between; + padding: 8px 10px; + border: none; + border-radius: 6px; + background: transparent; color: var(--cdr-text, #262626); - background: var(--cdr-surface, #fff); + font-size: 13px; cursor: pointer; - transition: all 0.2s; - min-width: 120px; + transition: background 0.2s, color 0.2s; } - .type-filter:hover:not(:disabled) { - border-color: var(--cdr-primary, #1890ff); + .type-menu-item::after { + content: ''; + width: 16px; + text-align: right; + color: var(--cdr-primary, #1890ff); + font-size: 12px; + line-height: 1; + flex-shrink: 0; } - .type-filter:focus { + .type-menu-item:hover { + background: var(--cdr-surface-hover, #f5f5f5); + } + + .type-menu-item:focus-visible { outline: none; - border-color: #1890ff; box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); } - .type-filter:disabled { - color: var(--cdr-text-disabled, #bfbfbf); - cursor: not-allowed; - opacity: 0.6; + .type-menu-item[data-selected='true'] { + background: var(--cdr-selection-bg, rgba(24, 144, 255, 0.08)); + color: var(--cdr-primary, #1890ff); + } + + .type-menu-item[data-selected='true']::after { + content: '✓'; } .selected-count { diff --git a/src/shared/version.ts b/src/shared/version.ts index 124d00d..8214b94 100644 --- a/src/shared/version.ts +++ b/src/shared/version.ts @@ -10,7 +10,7 @@ * 1. Edit version in package.json * 2. Run: npm run sync:version (or automatically via prebuild/predev) * - * Generated at: 2025-12-30T08:23:44.405Z + * Generated at: 2025-12-30T09:19:07.792Z */ /**