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
|
# 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)
|
## 2025-12-28 - 3.3.0 - feat(wcctools)
|
||||||
Add section-based configuration API for setupWccTools, new Views, and section-aware routing/sidebar
|
Add section-based configuration API for setupWccTools, new Views, and section-aware routing/sidebar
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-wcctools',
|
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.'
|
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()
|
@property()
|
||||||
accessor selectedTheme: TTheme = 'dark';
|
accessor selectedTheme: TTheme = 'dark';
|
||||||
|
|
||||||
|
@property()
|
||||||
|
accessor searchQuery: string = '';
|
||||||
|
|
||||||
// Derived from selectedViewport - no need for separate property
|
// Derived from selectedViewport - no need for separate property
|
||||||
public get isNative(): boolean {
|
public get isNative(): boolean {
|
||||||
return this.selectedViewport === 'native';
|
return this.selectedViewport === 'native';
|
||||||
@@ -118,6 +121,7 @@ export class WccDashboard extends DeesElement {
|
|||||||
<wcc-sidebar
|
<wcc-sidebar
|
||||||
.dashboardRef=${this}
|
.dashboardRef=${this}
|
||||||
.selectedItem=${this.selectedItem}
|
.selectedItem=${this.selectedItem}
|
||||||
|
.searchQuery=${this.searchQuery}
|
||||||
.isNative=${this.isNative}
|
.isNative=${this.isNative}
|
||||||
@selectedType=${(eventArg) => {
|
@selectedType=${(eventArg) => {
|
||||||
this.selectedType = eventArg.detail;
|
this.selectedType = eventArg.detail;
|
||||||
@@ -128,6 +132,10 @@ export class WccDashboard extends DeesElement {
|
|||||||
@selectedItem=${(eventArg) => {
|
@selectedItem=${(eventArg) => {
|
||||||
this.selectedItem = eventArg.detail;
|
this.selectedItem = eventArg.detail;
|
||||||
}}
|
}}
|
||||||
|
@searchChanged=${(eventArg: CustomEvent) => {
|
||||||
|
this.searchQuery = eventArg.detail;
|
||||||
|
this.updateUrlWithScrollState();
|
||||||
|
}}
|
||||||
></wcc-sidebar>
|
></wcc-sidebar>
|
||||||
<wcc-properties
|
<wcc-properties
|
||||||
.dashboardRef=${this}
|
.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) {
|
if (routeInfo.queryParams) {
|
||||||
|
const search = routeInfo.queryParams.search;
|
||||||
const frameScrollY = routeInfo.queryParams.frameScrollY;
|
const frameScrollY = routeInfo.queryParams.frameScrollY;
|
||||||
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
|
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
this.searchQuery = search;
|
||||||
|
} else {
|
||||||
|
this.searchQuery = '';
|
||||||
|
}
|
||||||
if (frameScrollY) {
|
if (frameScrollY) {
|
||||||
this.frameScrollY = parseInt(frameScrollY);
|
this.frameScrollY = parseInt(frameScrollY);
|
||||||
}
|
}
|
||||||
@@ -240,6 +254,8 @@ export class WccDashboard extends DeesElement {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.applyScrollPositions();
|
this.applyScrollPositions();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
} else {
|
||||||
|
this.searchQuery = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
|
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) {
|
if (routeInfo.queryParams) {
|
||||||
|
const search = routeInfo.queryParams.search;
|
||||||
const frameScrollY = routeInfo.queryParams.frameScrollY;
|
const frameScrollY = routeInfo.queryParams.frameScrollY;
|
||||||
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
|
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
this.searchQuery = search;
|
||||||
|
} else {
|
||||||
|
this.searchQuery = '';
|
||||||
|
}
|
||||||
if (frameScrollY) {
|
if (frameScrollY) {
|
||||||
this.frameScrollY = parseInt(frameScrollY);
|
this.frameScrollY = parseInt(frameScrollY);
|
||||||
}
|
}
|
||||||
@@ -296,6 +318,8 @@ export class WccDashboard extends DeesElement {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.applyScrollPositions();
|
this.applyScrollPositions();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
} else {
|
||||||
|
this.searchQuery = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
|
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 baseUrl = `/wcctools-route/${sectionName}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
|
||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
|
|
||||||
|
if (this.searchQuery) {
|
||||||
|
queryParams.set('search', this.searchQuery);
|
||||||
|
}
|
||||||
if (this.frameScrollY > 0) {
|
if (this.frameScrollY > 0) {
|
||||||
queryParams.set('frameScrollY', this.frameScrollY.toString());
|
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 baseUrl = `/wcctools-route/${sectionName}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
|
||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
|
|
||||||
|
if (this.searchQuery) {
|
||||||
|
queryParams.set('search', this.searchQuery);
|
||||||
|
}
|
||||||
if (this.frameScrollY > 0) {
|
if (this.frameScrollY > 0) {
|
||||||
queryParams.set('frameScrollY', this.frameScrollY.toString());
|
queryParams.set('frameScrollY', this.frameScrollY.toString());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ export class WccSidebar extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
accessor collapsedSections: Set<string> = new Set();
|
accessor collapsedSections: Set<string> = new Set();
|
||||||
|
|
||||||
|
// Search query for filtering sidebar items
|
||||||
|
@property()
|
||||||
|
accessor searchQuery: string = '';
|
||||||
|
|
||||||
private sectionsInitialized = false;
|
private sectionsInitialized = false;
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
@@ -252,7 +256,48 @@ export class WccSidebar extends DeesElement {
|
|||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
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>
|
</style>
|
||||||
|
<div class="search-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="search-input"
|
||||||
|
placeholder="Search..."
|
||||||
|
.value=${this.searchQuery}
|
||||||
|
@input=${this.handleSearchInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
${this.renderSections()}
|
${this.renderSections()}
|
||||||
</div>
|
</div>
|
||||||
@@ -282,6 +327,15 @@ export class WccSidebar extends DeesElement {
|
|||||||
this.initCollapsedSections();
|
this.initCollapsedSections();
|
||||||
|
|
||||||
return this.dashboardRef.sections.map((section, index) => {
|
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 isCollapsed = this.collapsedSections.has(section.name);
|
||||||
const sectionIcon = section.icon || (section.type === 'pages' ? 'insert_drive_file' : 'widgets');
|
const sectionIcon = section.icon || (section.type === 'pages' ? 'insert_drive_file' : 'widgets');
|
||||||
|
|
||||||
@@ -306,9 +360,11 @@ export class WccSidebar extends DeesElement {
|
|||||||
*/
|
*/
|
||||||
private renderSectionItems(section: IWccSection) {
|
private renderSectionItems(section: IWccSection) {
|
||||||
const entries = getSectionItems(section);
|
const entries = getSectionItems(section);
|
||||||
|
// Filter entries by search query
|
||||||
|
const filteredEntries = entries.filter(([name]) => this.matchesSearch(name));
|
||||||
|
|
||||||
if (section.type === 'pages') {
|
if (section.type === 'pages') {
|
||||||
return entries.map(([pageName, item]) => {
|
return filteredEntries.map(([pageName, item]) => {
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
class="selectOption ${this.selectedItem === item ? 'selected' : ''}"
|
class="selectOption ${this.selectedItem === item ? 'selected' : ''}"
|
||||||
@@ -318,13 +374,13 @@ export class WccSidebar extends DeesElement {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<i class="material-symbols-outlined">insert_drive_file</i>
|
<i class="material-symbols-outlined">insert_drive_file</i>
|
||||||
<div class="text">${pageName}</div>
|
<div class="text">${this.highlightMatch(pageName)}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// type === 'elements'
|
// type === 'elements'
|
||||||
return entries.map(([elementName, item]) => {
|
return filteredEntries.map(([elementName, item]) => {
|
||||||
const anonItem = item as any;
|
const anonItem = item as any;
|
||||||
const demoCount = anonItem.demo ? getDemoCount(anonItem.demo) : 0;
|
const demoCount = anonItem.demo ? getDemoCount(anonItem.demo) : 0;
|
||||||
const isMultiDemo = anonItem.demo && hasMultipleDemos(anonItem.demo);
|
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 expand-icon">chevron_right</i>
|
||||||
<i class="material-symbols-outlined">folder</i>
|
<i class="material-symbols-outlined">folder</i>
|
||||||
<div class="text">${elementName}</div>
|
<div class="text">${this.highlightMatch(elementName)}</div>
|
||||||
</div>
|
</div>
|
||||||
${isExpanded ? html`
|
${isExpanded ? html`
|
||||||
<div class="demo-children">
|
<div class="demo-children">
|
||||||
@@ -374,7 +430,7 @@ export class WccSidebar extends DeesElement {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<i class="material-symbols-outlined">featured_video</i>
|
<i class="material-symbols-outlined">featured_video</i>
|
||||||
<div class="text">${elementName}</div>
|
<div class="text">${this.highlightMatch(elementName)}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -402,6 +458,29 @@ export class WccSidebar extends DeesElement {
|
|||||||
this.expandedElements = newSet;
|
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>) {
|
protected updated(changedProperties: Map<string, unknown>) {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user