feat(sidebar): add searchable sidebar with URL-backed query state and highlighted matches
This commit is contained in:
10
changelog.md
10
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
|
||||
|
||||
|
||||
@@ -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.'
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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