feat(sidebar): add searchable sidebar with URL-backed query state and highlighted matches

This commit is contained in:
2025-12-30 12:30:45 +00:00
parent 287cc4d1c3
commit 63dd6a27b3
4 changed files with 127 additions and 8 deletions

View File

@@ -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.'
}

View File

@@ -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 {
<wcc-sidebar
.dashboardRef=${this}
.selectedItem=${this.selectedItem}
.searchQuery=${this.searchQuery}
.isNative=${this.isNative}
@selectedType=${(eventArg) => {
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();
}}
></wcc-sidebar>
<wcc-properties
.dashboardRef=${this}
@@ -224,11 +232,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);
}
@@ -240,6 +254,8 @@ export class WccDashboard extends DeesElement {
setTimeout(() => {
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());
}

View File

@@ -27,6 +27,10 @@ export class WccSidebar extends DeesElement {
@state()
accessor collapsedSections: Set<string> = 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;
}
</style>
<div class="search-container">
<input
type="text"
class="search-input"
placeholder="Search..."
.value=${this.searchQuery}
@input=${this.handleSearchInput}
/>
</div>
<div class="menu">
${this.renderSections()}
</div>
@@ -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`
<div
class="selectOption ${this.selectedItem === item ? 'selected' : ''}"
@@ -318,13 +374,13 @@ export class WccSidebar extends DeesElement {
}}
>
<i class="material-symbols-outlined">insert_drive_file</i>
<div class="text">${pageName}</div>
<div class="text">${this.highlightMatch(pageName)}</div>
</div>
`;
});
} 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 {
>
<i class="material-symbols-outlined expand-icon">chevron_right</i>
<i class="material-symbols-outlined">folder</i>
<div class="text">${elementName}</div>
<div class="text">${this.highlightMatch(elementName)}</div>
</div>
${isExpanded ? html`
<div class="demo-children">
@@ -374,7 +430,7 @@ export class WccSidebar extends DeesElement {
}}
>
<i class="material-symbols-outlined">featured_video</i>
<div class="text">${elementName}</div>
<div class="text">${this.highlightMatch(elementName)}</div>
</div>
`;
}
@@ -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}<span class="highlight">${match}</span>${after}`;
}
protected updated(changedProperties: Map<string, unknown>) {
super.updated(changedProperties);