diff --git a/changelog.md b/changelog.md index c3cca4b..e273671 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # Changelog +## 2025-12-30 - 3.4.0 - feat(sidebar) +add searchable sidebar with URL-backed query state and highlighted matches + +- Add search input to wcc-sidebar and expose a searchQuery property +- Filter sidebar sections and items client-side based on the search query and hide sections with no matches +- Highlight matching substrings in sidebar item labels +- Emit a 'searchChanged' event from the sidebar and handle it in wcc-dashboard to keep dashboard.searchQuery in sync +- Persist the search query in the route query parameter 'search' when building URLs and restore/clear it on navigation +- Preserve existing scroll-state handling while adding search state to URL updates + ## 2025-12-28 - 3.3.0 - feat(wcctools) Add section-based configuration API for setupWccTools, new Views, and section-aware routing/sidebar diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index e9936bc..15edcfe 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@design.estate/dees-wcctools', - version: '3.3.0', + version: '3.4.0', description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.' } diff --git a/ts_web/elements/wcc-dashboard.ts b/ts_web/elements/wcc-dashboard.ts index 1380378..3375894 100644 --- a/ts_web/elements/wcc-dashboard.ts +++ b/ts_web/elements/wcc-dashboard.ts @@ -59,6 +59,9 @@ export class WccDashboard extends DeesElement { @property() accessor selectedTheme: TTheme = 'dark'; + @property() + accessor searchQuery: string = ''; + // Derived from selectedViewport - no need for separate property public get isNative(): boolean { return this.selectedViewport === 'native'; @@ -118,6 +121,7 @@ export class WccDashboard extends DeesElement { { this.selectedType = eventArg.detail; @@ -128,6 +132,10 @@ export class WccDashboard extends DeesElement { @selectedItem=${(eventArg) => { this.selectedItem = eventArg.detail; }} + @searchChanged=${(eventArg: CustomEvent) => { + this.searchQuery = eventArg.detail; + this.updateUrlWithScrollState(); + }} > { this.applyScrollPositions(); }, 100); + } else { + this.searchQuery = ''; } const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup(); @@ -280,11 +296,17 @@ export class WccDashboard extends DeesElement { } } - // Restore scroll positions from query parameters + // Restore state from query parameters if (routeInfo.queryParams) { + const search = routeInfo.queryParams.search; const frameScrollY = routeInfo.queryParams.frameScrollY; const sidebarScrollY = routeInfo.queryParams.sidebarScrollY; + if (search) { + this.searchQuery = search; + } else { + this.searchQuery = ''; + } if (frameScrollY) { this.frameScrollY = parseInt(frameScrollY); } @@ -296,6 +318,8 @@ export class WccDashboard extends DeesElement { setTimeout(() => { this.applyScrollPositions(); }, 100); + } else { + this.searchQuery = ''; } const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup(); @@ -369,6 +393,9 @@ export class WccDashboard extends DeesElement { const baseUrl = `/wcctools-route/${sectionName}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`; const queryParams = new URLSearchParams(); + if (this.searchQuery) { + queryParams.set('search', this.searchQuery); + } if (this.frameScrollY > 0) { queryParams.set('frameScrollY', this.frameScrollY.toString()); } @@ -426,6 +453,9 @@ export class WccDashboard extends DeesElement { const baseUrl = `/wcctools-route/${sectionName}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`; const queryParams = new URLSearchParams(); + if (this.searchQuery) { + queryParams.set('search', this.searchQuery); + } if (this.frameScrollY > 0) { queryParams.set('frameScrollY', this.frameScrollY.toString()); } diff --git a/ts_web/elements/wcc-sidebar.ts b/ts_web/elements/wcc-sidebar.ts index f0e2e84..27b97d0 100644 --- a/ts_web/elements/wcc-sidebar.ts +++ b/ts_web/elements/wcc-sidebar.ts @@ -27,6 +27,10 @@ export class WccSidebar extends DeesElement { @state() accessor collapsedSections: Set = new Set(); + // Search query for filtering sidebar items + @property() + accessor searchQuery: string = ''; + private sectionsInitialized = false; public render(): TemplateResult { @@ -252,7 +256,48 @@ export class WccSidebar extends DeesElement { ::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.2); } + + .search-container { + padding: 0.5rem; + border-bottom: 1px solid var(--border); + } + + .search-input { + width: 100%; + box-sizing: border-box; + background: var(--input); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 0.5rem 0.75rem; + color: var(--foreground); + font-size: 0.75rem; + font-family: inherit; + outline: none; + transition: border-color 0.15s ease; + } + + .search-input:focus { + border-color: var(--primary); + } + + .search-input::placeholder { + color: var(--muted-foreground); + } + + .highlight { + background: rgba(59, 130, 246, 0.3); + border-radius: 2px; + } +
+ +
@@ -282,6 +327,15 @@ export class WccSidebar extends DeesElement { this.initCollapsedSections(); return this.dashboardRef.sections.map((section, index) => { + // Check if section has any matching items + const entries = getSectionItems(section); + const filteredEntries = entries.filter(([name]) => this.matchesSearch(name)); + + // Hide section if no items match the search + if (filteredEntries.length === 0 && this.searchQuery) { + return null; + } + const isCollapsed = this.collapsedSections.has(section.name); const sectionIcon = section.icon || (section.type === 'pages' ? 'insert_drive_file' : 'widgets'); @@ -306,9 +360,11 @@ export class WccSidebar extends DeesElement { */ private renderSectionItems(section: IWccSection) { const entries = getSectionItems(section); + // Filter entries by search query + const filteredEntries = entries.filter(([name]) => this.matchesSearch(name)); if (section.type === 'pages') { - return entries.map(([pageName, item]) => { + return filteredEntries.map(([pageName, item]) => { return html`
insert_drive_file -
${pageName}
+
${this.highlightMatch(pageName)}
`; }); } else { // type === 'elements' - return entries.map(([elementName, item]) => { + return filteredEntries.map(([elementName, item]) => { const anonItem = item as any; const demoCount = anonItem.demo ? getDemoCount(anonItem.demo) : 0; const isMultiDemo = anonItem.demo && hasMultipleDemos(anonItem.demo); @@ -340,7 +396,7 @@ export class WccSidebar extends DeesElement { > chevron_right folder -
${elementName}
+
${this.highlightMatch(elementName)}
${isExpanded ? html`
@@ -374,7 +430,7 @@ export class WccSidebar extends DeesElement { }} > featured_video -
${elementName}
+
${this.highlightMatch(elementName)}
`; } @@ -402,6 +458,29 @@ export class WccSidebar extends DeesElement { this.expandedElements = newSet; } + private handleSearchInput(e: Event) { + const input = e.target as HTMLInputElement; + this.searchQuery = input.value; + this.dispatchEvent(new CustomEvent('searchChanged', { detail: this.searchQuery })); + } + + private matchesSearch(name: string): boolean { + if (!this.searchQuery) return true; + return name.toLowerCase().includes(this.searchQuery.toLowerCase()); + } + + private highlightMatch(text: string): TemplateResult { + if (!this.searchQuery) return html`${text}`; + const lowerText = text.toLowerCase(); + const lowerQuery = this.searchQuery.toLowerCase(); + const index = lowerText.indexOf(lowerQuery); + if (index === -1) return html`${text}`; + const before = text.slice(0, index); + const match = text.slice(index, index + this.searchQuery.length); + const after = text.slice(index + this.searchQuery.length); + return html`${before}${match}${after}`; + } + protected updated(changedProperties: Map) { super.updated(changedProperties);