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

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@design.estate/dees-wcctools',
version: '3.2.0',
version: '3.3.0',
description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.'
}

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();
}
}

View File

@@ -1,21 +1,86 @@
import { WccDashboard } from './elements/wcc-dashboard.js';
import { LitElement } from 'lit';
import type { TTemplateFactory } from './elements/wcctools.helpers.js';
import type { IWccConfig, IWccSection } from './wcctools.interfaces.js';
// Export recording components and service
export { RecorderService, type IRecorderEvents, type IRecordingOptions } from './services/recorder.service.js';
export { WccRecordButton } from './elements/wcc-record-button.js';
export { WccRecordingPanel } from './elements/wcc-recording-panel.js';
const setupWccTools = (
// Export types for external use
export type { IWccConfig, IWccSection } from './wcctools.interfaces.js';
/**
* Converts legacy (elements, pages) format to new sections config
*/
const convertLegacyToConfig = (
elementsArg?: { [key: string]: LitElement },
pagesArg?: Record<string, TTemplateFactory>
): IWccConfig => {
const sections: IWccSection[] = [];
if (pagesArg && Object.keys(pagesArg).length > 0) {
sections.push({
name: 'Pages',
type: 'pages',
items: pagesArg,
});
}
if (elementsArg && Object.keys(elementsArg).length > 0) {
sections.push({
name: 'Elements',
type: 'elements',
items: elementsArg,
});
}
return { sections };
};
/**
* Check if the argument is the new config format
*/
const isWccConfig = (arg: any): arg is IWccConfig => {
return arg && typeof arg === 'object' && 'sections' in arg && Array.isArray(arg.sections);
};
/**
* Setup WCC Tools dashboard
*
* New format (recommended):
* ```typescript
* setupWccTools({
* sections: [
* { name: 'Elements', type: 'elements', items: elements },
* { name: 'Pages', type: 'pages', items: pages },
* ]
* });
* ```
*
* Legacy format (still supported):
* ```typescript
* setupWccTools(elements, pages);
* ```
*/
const setupWccTools = (
configOrElements?: IWccConfig | { [key: string]: LitElement },
pagesArg?: Record<string, TTemplateFactory>
) => {
let config: IWccConfig;
if (isWccConfig(configOrElements)) {
config = configOrElements;
} else {
config = convertLegacyToConfig(configOrElements, pagesArg);
}
let hasRun = false;
const runWccToolsSetup = async () => {
if (document.readyState === 'complete' && !hasRun) {
hasRun = true;
const wccTools = new WccDashboard(elementsArg as any, pagesArg);
const wccTools = new WccDashboard(config);
document.querySelector('body').append(wccTools);
}
};

View File

@@ -14,6 +14,40 @@ pnpm add -D @design.estate/dees-wcctools
## Usage
### Sections-based Configuration (Recommended)
```typescript
import { setupWccTools } from '@design.estate/dees-wcctools';
import * as elements from './elements/index.js';
import * as views from './views/index.js';
import * as pages from './pages/index.js';
setupWccTools({
sections: [
{
name: 'Pages',
type: 'pages',
items: pages,
},
{
name: 'Views',
type: 'elements',
items: views,
icon: 'web',
},
{
name: 'Elements',
type: 'elements',
items: elements,
filter: (name) => !name.startsWith('internal-'),
sort: ([a], [b]) => a.localeCompare(b),
},
],
});
```
### Legacy Format (Still Supported)
```typescript
import { setupWccTools } from '@design.estate/dees-wcctools';
import { MyButton } from './components/my-button.js';
@@ -30,6 +64,8 @@ setupWccTools({
| Export | Description |
|--------|-------------|
| `setupWccTools` | Initialize the component catalogue dashboard |
| `IWccConfig` | TypeScript interface for sections configuration |
| `IWccSection` | TypeScript interface for individual section |
### Recording Components
@@ -41,6 +77,18 @@ setupWccTools({
| `IRecorderEvents` | TypeScript interface for recorder callbacks |
| `IRecordingOptions` | TypeScript interface for recording options |
## Section Configuration
| Property | Type | Description |
|----------|------|-------------|
| `name` | `string` | Display name for the section header |
| `type` | `'elements' \| 'pages'` | Rendering behavior |
| `items` | `Record<string, any>` | Element classes or page factories |
| `filter` | `(name, item) => boolean` | Optional filter function |
| `sort` | `([a, itemA], [b, itemB]) => number` | Optional sort function |
| `icon` | `string` | Material Symbols icon name |
| `collapsed` | `boolean` | Start section collapsed (default: false) |
## Internal Components
The module includes these internal web components:
@@ -48,8 +96,8 @@ The module includes these internal web components:
| Component | Description |
|-----------|-------------|
| `wcc-dashboard` | Main dashboard container with routing |
| `wcc-sidebar` | Navigation sidebar with element/page listing |
| `wcc-frame` | Iframe viewport with responsive sizing |
| `wcc-sidebar` | Navigation sidebar with collapsible sections |
| `wcc-frame` | Responsive viewport with size controls |
| `wcc-properties` | Property panel with live editing |
| `wcc-record-button` | Recording state indicator button |
| `wcc-recording-panel` | Recording workflow UI |
@@ -72,14 +120,14 @@ const events: IRecorderEvents = {
const recorder = new RecorderService(events);
// Load available microphones
const mics = await recorder.loadMicrophones(true); // true = request permission
const mics = await recorder.loadMicrophones(true);
// Start audio level monitoring
await recorder.startAudioMonitoring(mics[0].deviceId);
// Start recording
await recorder.startRecording({
mode: 'viewport', // or 'screen'
mode: 'viewport',
audioDeviceId: mics[0].deviceId,
viewportElement: document.querySelector('.viewport'),
});
@@ -99,10 +147,11 @@ recorder.dispose();
```
ts_web/
├── index.ts # Main exports
├── wcctools.interfaces.ts # Type definitions
├── elements/
│ ├── wcc-dashboard.ts # Root dashboard component
│ ├── wcc-sidebar.ts # Navigation sidebar
│ ├── wcc-frame.ts # Responsive iframe viewport
│ ├── wcc-frame.ts # Responsive viewport
│ ├── wcc-properties.ts # Property editing panel
│ ├── wcc-record-button.ts # Recording button
│ ├── wcc-recording-panel.ts # Recording options/preview
@@ -116,6 +165,7 @@ ts_web/
## Features
- 🎨 Interactive component preview
- 📂 Section-based sidebar with filtering & sorting
- 🔧 Real-time property editing with type detection
- 🌓 Theme switching (light/dark)
- 📱 Responsive viewport testing

View File

@@ -0,0 +1,31 @@
/**
* Configuration for a section in the WCC Tools sidebar
*/
export interface IWccSection {
/** Display name for the section header */
name: string;
/** How items in this section are rendered - 'elements' show demos, 'pages' render directly */
type: 'elements' | 'pages';
/** The items in this section - either element classes or page factory functions */
items: Record<string, any>;
/** Optional filter function to include/exclude items */
filter?: (name: string, item: any) => boolean;
/** Optional sort function for ordering items */
sort?: (a: [string, any], b: [string, any]) => number;
/** Optional Material icon name for the section header */
icon?: string;
/** Whether this section should start collapsed (default: false) */
collapsed?: boolean;
}
/**
* Configuration object for setupWccTools
*/
export interface IWccConfig {
sections: IWccSection[];
}
/**
* Type for element selection types - now section-based
*/
export type TElementType = 'element' | 'page';