feat(wcctools): Add section-based configuration API for setupWccTools, new Views, and section-aware routing/sidebar

This commit is contained in:
2025-12-28 12:51:55 +00:00
parent dd151bdad8
commit 14e63738b7
14 changed files with 1709 additions and 212 deletions

View File

@@ -1,6 +1,7 @@
import { DeesElement, property, html, customElement, type TemplateResult, queryAsync, render, domtools } from '@design.estate/dees-element';
import { resolveTemplateFactory, getDemoAtIndex, getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
import type { TTemplateFactory } from './wcctools.helpers.js';
import type { IWccConfig, IWccSection, TElementType } from '../wcctools.interfaces.js';
import * as plugins from '../wcctools.plugins.js';
@@ -9,13 +10,37 @@ import './wcc-frame.js';
import './wcc-sidebar.js';
import './wcc-properties.js';
import { type TTheme } from './wcc-properties.js';
import { type TElementType } from './wcc-sidebar.js';
import { breakpoints } from '@design.estate/dees-domtools';
import { WccFrame } from './wcc-frame.js';
/**
* Get filtered and sorted items from a section
*/
export const getSectionItems = (section: IWccSection): Array<[string, any]> => {
let entries = Object.entries(section.items);
// Apply filter if provided
if (section.filter) {
entries = entries.filter(([name, item]) => section.filter(name, item));
}
// Apply sort if provided
if (section.sort) {
entries.sort(section.sort);
}
return entries;
};
@customElement('wcc-dashboard')
export class WccDashboard extends DeesElement {
@property()
accessor sections: IWccSection[] = [];
@property()
accessor selectedSection: IWccSection | null = null;
@property()
accessor selectedType: TElementType;
@@ -39,36 +64,43 @@ export class WccDashboard extends DeesElement {
return this.selectedViewport === 'native';
}
@property()
accessor pages: Record<string, TTemplateFactory> = {};
@property()
accessor elements: { [key: string]: DeesElement } = {};
@property()
accessor warning: string = null;
private frameScrollY: number = 0;
private sidebarScrollY: number = 0;
private scrollPositionsApplied: boolean = false;
@queryAsync('wcc-frame')
accessor wccFrame: Promise<WccFrame>;
constructor(
elementsArg?: { [key: string]: DeesElement },
pagesArg?: Record<string, TTemplateFactory>
) {
constructor(config?: IWccConfig) {
super();
if (elementsArg) {
this.elements = elementsArg;
console.log('got elements:');
console.log(this.elements);
if (config && config.sections) {
this.sections = config.sections;
console.log('got sections:', this.sections.map(s => s.name));
}
}
if (pagesArg) {
this.pages = pagesArg;
/**
* Find an item by name across all sections, returns the item and its section
*/
public findItemByName(name: string): { item: any; section: IWccSection } | null {
for (const section of this.sections) {
const entries = getSectionItems(section);
const found = entries.find(([itemName]) => itemName === name);
if (found) {
return { item: found[1], section };
}
}
return null;
}
/**
* Find a section by name (URL-decoded)
*/
public findSectionByName(name: string): IWccSection | null {
return this.sections.find(s => s.name === name) || null;
}
public render(): TemplateResult {
@@ -159,19 +191,37 @@ export class WccDashboard extends DeesElement {
this.setupScrollListeners();
}, 500);
// Route with demo index (new format)
// New route format with section name
this.domtools.router.on(
'/wcctools-route/:itemType/:itemName/:demoIndex/:viewport/:theme',
'/wcctools-route/:sectionName/:itemName/:demoIndex/:viewport/:theme',
async (routeInfo) => {
this.selectedType = routeInfo.params.itemType as TElementType;
const sectionName = decodeURIComponent(routeInfo.params.sectionName);
this.selectedSection = this.findSectionByName(sectionName);
this.selectedItemName = routeInfo.params.itemName;
this.selectedDemoIndex = parseInt(routeInfo.params.demoIndex) || 0;
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
this.selectedTheme = routeInfo.params.theme as TTheme;
if (routeInfo.params.itemType === 'element') {
this.selectedItem = this.elements[routeInfo.params.itemName];
} else if (routeInfo.params.itemType === 'page') {
this.selectedItem = this.pages[routeInfo.params.itemName];
if (this.selectedSection) {
// Find item within the section
const entries = getSectionItems(this.selectedSection);
const found = entries.find(([name]) => name === routeInfo.params.itemName);
if (found) {
this.selectedItem = found[1];
this.selectedType = this.selectedSection.type === 'elements' ? 'element' : 'page';
}
} else {
// Fallback: try legacy format (element/page as section name)
const legacyType = routeInfo.params.sectionName;
if (legacyType === 'element' || legacyType === 'page') {
this.selectedType = legacyType as TElementType;
// Find item in any matching section
const result = this.findItemByName(routeInfo.params.itemName);
if (result) {
this.selectedItem = result.item;
this.selectedSection = result.section;
}
}
}
// Restore scroll positions from query parameters
@@ -201,17 +251,33 @@ export class WccDashboard extends DeesElement {
// Legacy route without demo index (for backwards compatibility)
this.domtools.router.on(
'/wcctools-route/:itemType/:itemName/:viewport/:theme',
'/wcctools-route/:sectionName/:itemName/:viewport/:theme',
async (routeInfo) => {
this.selectedType = routeInfo.params.itemType as TElementType;
const sectionName = decodeURIComponent(routeInfo.params.sectionName);
this.selectedSection = this.findSectionByName(sectionName);
this.selectedItemName = routeInfo.params.itemName;
this.selectedDemoIndex = 0; // Default to first demo
this.selectedDemoIndex = 0;
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
this.selectedTheme = routeInfo.params.theme as TTheme;
if (routeInfo.params.itemType === 'element') {
this.selectedItem = this.elements[routeInfo.params.itemName];
} else if (routeInfo.params.itemType === 'page') {
this.selectedItem = this.pages[routeInfo.params.itemName];
if (this.selectedSection) {
const entries = getSectionItems(this.selectedSection);
const found = entries.find(([name]) => name === routeInfo.params.itemName);
if (found) {
this.selectedItem = found[1];
this.selectedType = this.selectedSection.type === 'elements' ? 'element' : 'page';
}
} else {
// Fallback: try legacy format
const legacyType = routeInfo.params.sectionName;
if (legacyType === 'element' || legacyType === 'page') {
this.selectedType = legacyType as TElementType;
const result = this.findItemByName(routeInfo.params.itemName);
if (result) {
this.selectedItem = result.item;
this.selectedSection = result.section;
}
}
}
// Restore scroll positions from query parameters
@@ -297,7 +363,10 @@ export class WccDashboard extends DeesElement {
}
public buildUrl() {
const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
const sectionName = this.selectedSection
? encodeURIComponent(this.selectedSection.name)
: this.selectedType; // Fallback for legacy
const baseUrl = `/wcctools-route/${sectionName}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
const queryParams = new URLSearchParams();
if (this.frameScrollY > 0) {
@@ -351,7 +420,10 @@ export class WccDashboard extends DeesElement {
}
private updateUrlWithScrollState() {
const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
const sectionName = this.selectedSection
? encodeURIComponent(this.selectedSection.name)
: this.selectedType; // Fallback for legacy
const baseUrl = `/wcctools-route/${sectionName}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
const queryParams = new URLSearchParams();
if (this.frameScrollY > 0) {

View File

@@ -1,10 +1,9 @@
import * as plugins from '../wcctools.plugins.js';
import { DeesElement, property, html, customElement, type TemplateResult, state } from '@design.estate/dees-element';
import { WccDashboard } from './wcc-dashboard.js';
import { WccDashboard, getSectionItems } from './wcc-dashboard.js';
import type { TTemplateFactory } from './wcctools.helpers.js';
import { getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
export type TElementType = 'element' | 'page';
import type { IWccSection, TElementType } from '../wcctools.interfaces.js';
@customElement('wcc-sidebar')
export class WccSidebar extends DeesElement {
@@ -24,6 +23,12 @@ export class WccSidebar extends DeesElement {
@state()
accessor expandedElements: Set<string> = new Set();
// Track which sections are collapsed
@state()
accessor collapsedSections: Set<string> = new Set();
private sectionsInitialized = false;
public render(): TemplateResult {
return html`
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet" />
@@ -65,7 +70,7 @@ export class WccSidebar extends DeesElement {
padding: 0.5rem 0;
}
h3 {
.section-header {
padding: 0.3rem 0.75rem;
font-size: 0.65rem;
font-weight: 500;
@@ -77,12 +82,45 @@ export class WccSidebar extends DeesElement {
background: rgba(59, 130, 246, 0.03);
border-bottom: 1px solid var(--border);
border-top: 1px solid var(--border);
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
user-select: none;
transition: all 0.15s ease;
}
h3:first-child {
.section-header:first-child {
margin-top: 0;
}
.section-header:hover {
background: rgba(59, 130, 246, 0.08);
}
.section-header .expand-icon {
font-size: 14px;
opacity: 0.5;
transition: transform 0.2s ease;
}
.section-header.collapsed .expand-icon {
transform: rotate(-90deg);
}
.section-header .section-icon {
font-size: 14px;
opacity: 0.6;
}
.section-content {
overflow: hidden;
}
.section-content.collapsed {
display: none;
}
.material-symbols-outlined {
font-family: 'Material Symbols Outlined';
font-weight: normal;
@@ -216,88 +254,144 @@ export class WccSidebar extends DeesElement {
}
</style>
<div class="menu">
<h3>Pages</h3>
${(() => {
const pages = Object.keys(this.dashboardRef.pages);
return pages.map(pageName => {
const item = this.dashboardRef.pages[pageName];
return html`
<div
class="selectOption ${this.selectedItem === item ? 'selected' : null}"
@click=${async () => {
const domtools = await plugins.deesDomtools.DomTools.setupDomTools();
this.selectItem('page', pageName, item, 0);
}}
>
<i class="material-symbols-outlined">insert_drive_file</i>
<div class="text">${pageName}</div>
</div>
`;
});
})()}
<h3>Elements</h3>
${(() => {
const elements = Object.keys(this.dashboardRef.elements);
return elements.map(elementName => {
const item = this.dashboardRef.elements[elementName] as any;
const demoCount = item.demo ? getDemoCount(item.demo) : 0;
const isMultiDemo = item.demo && hasMultipleDemos(item.demo);
const isExpanded = this.expandedElements.has(elementName);
const isSelected = this.selectedItem === item;
if (isMultiDemo) {
// Multi-demo element - render as expandable folder
return html`
<div
class="selectOption folder ${isExpanded ? 'expanded' : ''} ${isSelected ? 'selected' : ''}"
@click=${() => this.toggleExpanded(elementName)}
>
<i class="material-symbols-outlined expand-icon">chevron_right</i>
<i class="material-symbols-outlined">folder</i>
<div class="text">${elementName}</div>
</div>
${isExpanded ? html`
<div class="demo-children">
${Array.from({ length: demoCount }, (_, i) => {
const demoIndex = i;
const isThisDemoSelected = isSelected && this.dashboardRef.selectedDemoIndex === demoIndex;
return html`
<div
class="demo-child ${isThisDemoSelected ? 'selected' : ''}"
@click=${async () => {
await plugins.deesDomtools.DomTools.setupDomTools();
this.selectItem('element', elementName, item, demoIndex);
}}
>
<i class="material-symbols-outlined">play_circle</i>
<div class="text">demo${demoIndex + 1}</div>
</div>
`;
})}
</div>
` : null}
`;
} else {
// Single demo element - render as normal
return html`
<div
class="selectOption ${isSelected ? 'selected' : null}"
@click=${async () => {
await plugins.deesDomtools.DomTools.setupDomTools();
this.selectItem('element', elementName, item, 0);
}}
>
<i class="material-symbols-outlined">featured_video</i>
<div class="text">${elementName}</div>
</div>
`;
}
});
})()}
${this.renderSections()}
</div>
`;
}
/**
* Initialize collapsed sections from section config
*/
private initCollapsedSections() {
if (this.sectionsInitialized) return;
const collapsed = new Set<string>();
for (const section of this.dashboardRef.sections) {
if (section.collapsed) {
collapsed.add(section.name);
}
}
this.collapsedSections = collapsed;
this.sectionsInitialized = true;
}
/**
* Render all sections
*/
private renderSections() {
this.initCollapsedSections();
return this.dashboardRef.sections.map((section, index) => {
const isCollapsed = this.collapsedSections.has(section.name);
const sectionIcon = section.icon || (section.type === 'pages' ? 'insert_drive_file' : 'widgets');
return html`
<div
class="section-header ${isCollapsed ? 'collapsed' : ''}"
@click=${() => this.toggleSectionCollapsed(section.name)}
>
<i class="material-symbols-outlined expand-icon">expand_more</i>
${section.icon ? html`<i class="material-symbols-outlined section-icon">${section.icon}</i>` : null}
<span>${section.name}</span>
</div>
<div class="section-content ${isCollapsed ? 'collapsed' : ''}">
${this.renderSectionItems(section)}
</div>
`;
});
}
/**
* Render items for a section
*/
private renderSectionItems(section: IWccSection) {
const entries = getSectionItems(section);
if (section.type === 'pages') {
return entries.map(([pageName, item]) => {
return html`
<div
class="selectOption ${this.selectedItem === item ? 'selected' : ''}"
@click=${async () => {
await plugins.deesDomtools.DomTools.setupDomTools();
this.selectItem('page', pageName, item, 0, section);
}}
>
<i class="material-symbols-outlined">insert_drive_file</i>
<div class="text">${pageName}</div>
</div>
`;
});
} else {
// type === 'elements'
return entries.map(([elementName, item]) => {
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;
if (isMultiDemo) {
// Multi-demo element - render as expandable folder
return html`
<div
class="selectOption folder ${isExpanded ? 'expanded' : ''} ${isSelected ? 'selected' : ''}"
@click=${() => this.toggleExpanded(elementName)}
>
<i class="material-symbols-outlined expand-icon">chevron_right</i>
<i class="material-symbols-outlined">folder</i>
<div class="text">${elementName}</div>
</div>
${isExpanded ? html`
<div class="demo-children">
${Array.from({ length: demoCount }, (_, i) => {
const demoIndex = i;
const isThisDemoSelected = isSelected && this.dashboardRef.selectedDemoIndex === demoIndex;
return html`
<div
class="demo-child ${isThisDemoSelected ? 'selected' : ''}"
@click=${async () => {
await plugins.deesDomtools.DomTools.setupDomTools();
this.selectItem('element', elementName, item, demoIndex, section);
}}
>
<i class="material-symbols-outlined">play_circle</i>
<div class="text">demo${demoIndex + 1}</div>
</div>
`;
})}
</div>
` : null}
`;
} else {
// Single demo element
return html`
<div
class="selectOption ${isSelected ? 'selected' : ''}"
@click=${async () => {
await plugins.deesDomtools.DomTools.setupDomTools();
this.selectItem('element', elementName, item, 0, section);
}}
>
<i class="material-symbols-outlined">featured_video</i>
<div class="text">${elementName}</div>
</div>
`;
}
});
}
}
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)) {
@@ -313,30 +407,50 @@ export class WccSidebar extends DeesElement {
// Auto-expand folder when a multi-demo element is selected
if (changedProperties.has('selectedItem') && this.selectedItem) {
const elementName = Object.keys(this.dashboardRef.elements).find(
name => this.dashboardRef.elements[name] === this.selectedItem
);
if (elementName) {
const item = this.dashboardRef.elements[elementName] as any;
if (item.demo && hasMultipleDemos(item.demo)) {
if (!this.expandedElements.has(elementName)) {
const newSet = new Set(this.expandedElements);
newSet.add(elementName);
this.expandedElements = newSet;
// 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;
}
}
}
}
public selectItem(typeArg: TElementType, itemNameArg: string, itemArg: TTemplateFactory | DeesElement, demoIndex: number = 0) {
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
@@ -356,7 +470,6 @@ export class WccSidebar extends DeesElement {
this.dashboardRef.buildUrl();
// Force re-render to update demo child selection indicator
// (needed when switching between demos of the same element)
this.requestUpdate();
}
}