import * as plugins from '../wcctools.plugins.js'; import { DeesElement, property, html, customElement, type TemplateResult, state } from '@design.estate/dees-element'; import { WccDashboard, getSectionItems } from './wcc-dashboard.js'; import type { TTemplateFactory } from './wcctools.helpers.js'; import { getDemoCount, hasMultipleDemos } from './wcctools.helpers.js'; import type { IWccSection, TElementType } from '../wcctools.interfaces.js'; import { WccContextmenu } from './wcc-contextmenu.js'; @customElement('wcc-sidebar') export class WccSidebar extends DeesElement { @property({ attribute: false }) accessor selectedItem: DeesElement | TTemplateFactory; @property({ attribute: false }) accessor selectedType: TElementType; @property() accessor dashboardRef: WccDashboard; @property() accessor isNative: boolean = false; // Track which elements are expanded (for multi-demo elements) @state() accessor expandedElements: Set = new Set(); // Track which sections are collapsed @state() accessor collapsedSections: Set = new Set(); // Search query for filtering sidebar items @property() accessor searchQuery: string = ''; // Pinned items as Set of "sectionName::itemName" @property({ attribute: false }) accessor pinnedItems: Set = new Set(); // Sidebar width (resizable) @property({ type: Number }) accessor sidebarWidth: number = 200; // Track if currently resizing @state() accessor isResizing: boolean = false; // Delayed hide for native mode transition @state() accessor isHidden: boolean = false; private sectionsInitialized = false; public render(): TemplateResult { return html`
`; } /** * Initialize collapsed sections from section config */ private initCollapsedSections() { if (this.sectionsInitialized || !this.dashboardRef?.sections) return; const collapsed = new Set(); for (const section of this.dashboardRef.sections) { if (section.collapsed) { collapsed.add(section.name); } } this.collapsedSections = collapsed; this.sectionsInitialized = true; } // ============ Pinning helpers ============ private getPinKey(sectionName: string, itemName: string): string { return `${sectionName}::${itemName}`; } private isPinned(sectionName: string, itemName: string): boolean { return this.pinnedItems.has(this.getPinKey(sectionName, itemName)); } private togglePin(sectionName: string, itemName: string) { const key = this.getPinKey(sectionName, itemName); const newPinned = new Set(this.pinnedItems); if (newPinned.has(key)) { newPinned.delete(key); } else { newPinned.add(key); } this.pinnedItems = newPinned; this.dispatchEvent(new CustomEvent('pinnedChanged', { detail: newPinned })); } private showContextMenu(e: MouseEvent, sectionName: string, itemName: string) { const isPinned = this.isPinned(sectionName, itemName); WccContextmenu.show(e, [ { name: isPinned ? 'Unpin' : 'Pin', iconName: isPinned ? 'push_pin' : 'push_pin', action: () => this.togglePin(sectionName, itemName), }, ]); } /** * Render the PINNED section (only if there are pinned items) */ private renderPinnedSection() { if (!this.dashboardRef?.sections || this.pinnedItems.size === 0) { return null; } const isCollapsed = this.collapsedSections.has('__pinned__'); // Collect pinned items with their original section info // Pinned items are NOT filtered by search - they always remain visible const pinnedEntries: Array<{ sectionName: string; itemName: string; item: any; section: IWccSection }> = []; for (const key of this.pinnedItems) { const [sectionName, itemName] = key.split('::'); const section = this.dashboardRef.sections.find(s => s.name === sectionName); if (section) { const entries = getSectionItems(section); const found = entries.find(([name]) => name === itemName); if (found) { pinnedEntries.push({ sectionName, itemName, item: found[1], section }); } } } if (pinnedEntries.length === 0) { return null; } return html`
this.toggleSectionCollapsed('__pinned__')} > expand_more push_pin Pinned
${pinnedEntries.map(({ sectionName, itemName, item, section }) => { const isSelected = this.selectedItem === item; const type = section.type === 'elements' ? 'element' : 'page'; const icon = section.type === 'elements' ? 'featured_video' : 'insert_drive_file'; return html`
{ await plugins.deesDomtools.DomTools.setupDomTools(); this.selectItem(type, itemName, item, 0, section); }} @contextmenu=${(e: MouseEvent) => this.showContextMenu(e, sectionName, itemName)} > ${icon}
${this.highlightMatch(itemName)}
`; })}
`; } /** * Render all sections */ private renderSections() { if (!this.dashboardRef?.sections) { return null; } this.initCollapsedSections(); return this.dashboardRef.sections.map((section) => { // 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'); return html`
this.toggleSectionCollapsed(section.name)} > expand_more ${section.icon ? html`${section.icon}` : null} ${section.name}
${this.renderSectionItems(section)}
`; }); } /** * Render items for a section */ 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 filteredEntries.map(([pageName, item]) => { const isPinned = this.isPinned(section.name, pageName); return html`
{ await plugins.deesDomtools.DomTools.setupDomTools(); this.selectItem('page', pageName, item, 0, section); }} @contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, pageName)} > insert_drive_file
${this.highlightMatch(pageName)}
`; }); } else { // type === 'elements' - group by demoGroup const groupedItems = new Map>(); for (const entry of filteredEntries) { const [, item] = entry; const group = (item as any).demoGroup || null; if (!groupedItems.has(group)) { groupedItems.set(group, []); } groupedItems.get(group)!.push(entry); } const result: TemplateResult[] = []; // Render ungrouped items first const ungrouped = groupedItems.get(null) || []; for (const entry of ungrouped) { result.push(this.renderElementItem(entry, section)); } // Render grouped items for (const [groupName, items] of groupedItems) { if (groupName === null) continue; result.push(html`
${groupName} ${items.map((entry) => this.renderElementItem(entry, section))}
`); } return result; } } /** * Render a single element item (used by renderSectionItems) */ private renderElementItem(entry: [string, any], section: IWccSection): TemplateResult { const [elementName, item] = entry; const anonItem = item as any; const demoCount = anonItem.demo ? getDemoCount(anonItem.demo) : 0; const isMultiDemo = anonItem.demo && hasMultipleDemos(anonItem.demo); const isExpanded = this.expandedElements.has(elementName); const isSelected = this.selectedItem === item; const isPinned = this.isPinned(section.name, elementName); if (isMultiDemo) { // Multi-demo element - render as expandable folder return html`
this.toggleExpanded(elementName)} @contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, elementName)} > chevron_right
${this.highlightMatch(elementName)}
${isExpanded ? html`
${Array.from({ length: demoCount }, (_, i) => { const demoIndex = i; const isThisDemoSelected = isSelected && this.dashboardRef.selectedDemoIndex === demoIndex; return html`
{ await plugins.deesDomtools.DomTools.setupDomTools(); this.selectItem('element', elementName, item, demoIndex, section); }} > play_circle
demo${demoIndex + 1}
`; })}
` : null} `; } else { // Single demo element return html`
{ await plugins.deesDomtools.DomTools.setupDomTools(); this.selectItem('element', elementName, item, 0, section); }} @contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, elementName)} > featured_video
${this.highlightMatch(elementName)}
`; } } private toggleSectionCollapsed(sectionName: string) { const newSet = new Set(this.collapsedSections); if (newSet.has(sectionName)) { newSet.delete(sectionName); } else { newSet.add(sectionName); } this.collapsedSections = newSet; } private toggleExpanded(elementName: string) { const newSet = new Set(this.expandedElements); if (newSet.has(elementName)) { newSet.delete(elementName); } else { newSet.add(elementName); } 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 clearSearch() { this.searchQuery = ''; 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); // Handle delayed hide for native mode transition if (changedProperties.has('isNative')) { if (this.isNative) { // Delay hiding until frame animation completes setTimeout(() => { this.isHidden = true; }, 300); } else { // Show immediately when exiting native mode this.isHidden = false; } } // Auto-expand folder when a multi-demo element is selected if (changedProperties.has('selectedItem') && this.selectedItem && this.dashboardRef?.sections) { // Find the element in any section for (const section of this.dashboardRef.sections) { if (section.type !== 'elements') continue; const entries = getSectionItems(section); const found = entries.find(([_, item]) => item === this.selectedItem); if (found) { const [elementName, item] = found; const anonItem = item as any; if (anonItem.demo && hasMultipleDemos(anonItem.demo)) { if (!this.expandedElements.has(elementName)) { const newSet = new Set(this.expandedElements); newSet.add(elementName); this.expandedElements = newSet; } } break; } } } } // ============ Resize functionality ============ private startResize = (e: MouseEvent) => { e.preventDefault(); this.isResizing = true; const startX = e.clientX; const startWidth = this.sidebarWidth; // Cache references once at start const frame = this.dashboardRef?.shadowRoot?.querySelector('wcc-frame') as any; const properties = this.dashboardRef?.shadowRoot?.querySelector('wcc-properties') as any; // Disable frame transition during resize if (frame) { frame.isResizing = true; } const onMouseMove = (e: MouseEvent) => { const newWidth = Math.min(400, Math.max(150, startWidth + (e.clientX - startX))); this.sidebarWidth = newWidth; // Update frame and properties directly if (frame) { frame.sidebarWidth = newWidth; } if (properties) { properties.sidebarWidth = newWidth; } }; const onMouseUp = () => { this.isResizing = false; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); // Re-enable frame transition if (frame) { frame.isResizing = false; } // Dispatch event on release for URL persistence this.dispatchEvent(new CustomEvent('widthChanged', { detail: this.sidebarWidth })); }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }; public selectItem( typeArg: TElementType, itemNameArg: string, itemArg: TTemplateFactory | DeesElement, demoIndex: number = 0, section?: IWccSection ) { console.log('selected item'); console.log(itemNameArg); console.log(itemArg); console.log('demo index:', demoIndex); console.log('section:', section?.name); this.selectedItem = itemArg; this.selectedType = typeArg; this.dashboardRef.selectedDemoIndex = demoIndex; // Set the selected section on dashboard if (section) { this.dashboardRef.selectedSection = section; } this.dispatchEvent( new CustomEvent('selectedType', { detail: typeArg }) ); this.dispatchEvent( new CustomEvent('selectedItemName', { detail: itemNameArg }) ); this.dispatchEvent( new CustomEvent('selectedItem', { detail: itemArg }) ); this.dashboardRef.buildUrl(); // Force re-render to update demo child selection indicator this.requestUpdate(); } }