feat(sidebar): add searchable sidebar with URL-backed query state and highlighted matches
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user