@design.estate/dees-wcctools 3.2.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,9 @@
1
1
  import * as plugins from '../wcctools.plugins.js';
2
2
  import { DeesElement, property, html, customElement, type TemplateResult, state } from '@design.estate/dees-element';
3
- import { WccDashboard } from './wcc-dashboard.js';
3
+ import { WccDashboard, getSectionItems } from './wcc-dashboard.js';
4
4
  import type { TTemplateFactory } from './wcctools.helpers.js';
5
5
  import { getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
6
-
7
- export type TElementType = 'element' | 'page';
6
+ import type { IWccSection, TElementType } from '../wcctools.interfaces.js';
8
7
 
9
8
  @customElement('wcc-sidebar')
10
9
  export class WccSidebar extends DeesElement {
@@ -24,6 +23,16 @@ export class WccSidebar extends DeesElement {
24
23
  @state()
25
24
  accessor expandedElements: Set<string> = new Set();
26
25
 
26
+ // Track which sections are collapsed
27
+ @state()
28
+ accessor collapsedSections: Set<string> = new Set();
29
+
30
+ // Search query for filtering sidebar items
31
+ @property()
32
+ accessor searchQuery: string = '';
33
+
34
+ private sectionsInitialized = false;
35
+
27
36
  public render(): TemplateResult {
28
37
  return html`
29
38
  <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 +74,7 @@ export class WccSidebar extends DeesElement {
65
74
  padding: 0.5rem 0;
66
75
  }
67
76
 
68
- h3 {
77
+ .section-header {
69
78
  padding: 0.3rem 0.75rem;
70
79
  font-size: 0.65rem;
71
80
  font-weight: 500;
@@ -77,12 +86,45 @@ export class WccSidebar extends DeesElement {
77
86
  background: rgba(59, 130, 246, 0.03);
78
87
  border-bottom: 1px solid var(--border);
79
88
  border-top: 1px solid var(--border);
89
+ display: flex;
90
+ align-items: center;
91
+ gap: 0.5rem;
92
+ cursor: pointer;
93
+ user-select: none;
94
+ transition: all 0.15s ease;
80
95
  }
81
96
 
82
- h3:first-child {
97
+ .section-header:first-child {
83
98
  margin-top: 0;
84
99
  }
85
100
 
101
+ .section-header:hover {
102
+ background: rgba(59, 130, 246, 0.08);
103
+ }
104
+
105
+ .section-header .expand-icon {
106
+ font-size: 14px;
107
+ opacity: 0.5;
108
+ transition: transform 0.2s ease;
109
+ }
110
+
111
+ .section-header.collapsed .expand-icon {
112
+ transform: rotate(-90deg);
113
+ }
114
+
115
+ .section-header .section-icon {
116
+ font-size: 14px;
117
+ opacity: 0.6;
118
+ }
119
+
120
+ .section-content {
121
+ overflow: hidden;
122
+ }
123
+
124
+ .section-content.collapsed {
125
+ display: none;
126
+ }
127
+
86
128
  .material-symbols-outlined {
87
129
  font-family: 'Material Symbols Outlined';
88
130
  font-weight: normal;
@@ -214,90 +256,198 @@ export class WccSidebar extends DeesElement {
214
256
  ::-webkit-scrollbar-thumb:hover {
215
257
  background: rgba(255, 255, 255, 0.2);
216
258
  }
259
+
260
+ .search-container {
261
+ padding: 0.5rem;
262
+ border-bottom: 1px solid var(--border);
263
+ }
264
+
265
+ .search-input {
266
+ width: 100%;
267
+ box-sizing: border-box;
268
+ background: var(--input);
269
+ border: 1px solid var(--border);
270
+ border-radius: var(--radius);
271
+ padding: 0.5rem 0.75rem;
272
+ color: var(--foreground);
273
+ font-size: 0.75rem;
274
+ font-family: inherit;
275
+ outline: none;
276
+ transition: border-color 0.15s ease;
277
+ }
278
+
279
+ .search-input:focus {
280
+ border-color: var(--primary);
281
+ }
282
+
283
+ .search-input::placeholder {
284
+ color: var(--muted-foreground);
285
+ }
286
+
287
+ .highlight {
288
+ background: rgba(59, 130, 246, 0.3);
289
+ border-radius: 2px;
290
+ }
217
291
  </style>
292
+ <div class="search-container">
293
+ <input
294
+ type="text"
295
+ class="search-input"
296
+ placeholder="Search..."
297
+ .value=${this.searchQuery}
298
+ @input=${this.handleSearchInput}
299
+ />
300
+ </div>
218
301
  <div class="menu">
219
- <h3>Pages</h3>
220
- ${(() => {
221
- const pages = Object.keys(this.dashboardRef.pages);
222
- return pages.map(pageName => {
223
- const item = this.dashboardRef.pages[pageName];
224
- return html`
225
- <div
226
- class="selectOption ${this.selectedItem === item ? 'selected' : null}"
227
- @click=${async () => {
228
- const domtools = await plugins.deesDomtools.DomTools.setupDomTools();
229
- this.selectItem('page', pageName, item, 0);
230
- }}
231
- >
232
- <i class="material-symbols-outlined">insert_drive_file</i>
233
- <div class="text">${pageName}</div>
234
- </div>
235
- `;
236
- });
237
- })()}
238
- <h3>Elements</h3>
239
- ${(() => {
240
- const elements = Object.keys(this.dashboardRef.elements);
241
- return elements.map(elementName => {
242
- const item = this.dashboardRef.elements[elementName] as any;
243
- const demoCount = item.demo ? getDemoCount(item.demo) : 0;
244
- const isMultiDemo = item.demo && hasMultipleDemos(item.demo);
245
- const isExpanded = this.expandedElements.has(elementName);
246
- const isSelected = this.selectedItem === item;
247
-
248
- if (isMultiDemo) {
249
- // Multi-demo element - render as expandable folder
250
- return html`
251
- <div
252
- class="selectOption folder ${isExpanded ? 'expanded' : ''} ${isSelected ? 'selected' : ''}"
253
- @click=${() => this.toggleExpanded(elementName)}
254
- >
255
- <i class="material-symbols-outlined expand-icon">chevron_right</i>
256
- <i class="material-symbols-outlined">folder</i>
257
- <div class="text">${elementName}</div>
258
- </div>
259
- ${isExpanded ? html`
260
- <div class="demo-children">
261
- ${Array.from({ length: demoCount }, (_, i) => {
262
- const demoIndex = i;
263
- const isThisDemoSelected = isSelected && this.dashboardRef.selectedDemoIndex === demoIndex;
264
- return html`
265
- <div
266
- class="demo-child ${isThisDemoSelected ? 'selected' : ''}"
267
- @click=${async () => {
268
- await plugins.deesDomtools.DomTools.setupDomTools();
269
- this.selectItem('element', elementName, item, demoIndex);
270
- }}
271
- >
272
- <i class="material-symbols-outlined">play_circle</i>
273
- <div class="text">demo${demoIndex + 1}</div>
274
- </div>
275
- `;
276
- })}
277
- </div>
278
- ` : null}
279
- `;
280
- } else {
281
- // Single demo element - render as normal
282
- return html`
283
- <div
284
- class="selectOption ${isSelected ? 'selected' : null}"
285
- @click=${async () => {
286
- await plugins.deesDomtools.DomTools.setupDomTools();
287
- this.selectItem('element', elementName, item, 0);
288
- }}
289
- >
290
- <i class="material-symbols-outlined">featured_video</i>
291
- <div class="text">${elementName}</div>
292
- </div>
293
- `;
294
- }
295
- });
296
- })()}
302
+ ${this.renderSections()}
297
303
  </div>
298
304
  `;
299
305
  }
300
306
 
307
+ /**
308
+ * Initialize collapsed sections from section config
309
+ */
310
+ private initCollapsedSections() {
311
+ if (this.sectionsInitialized) return;
312
+
313
+ const collapsed = new Set<string>();
314
+ for (const section of this.dashboardRef.sections) {
315
+ if (section.collapsed) {
316
+ collapsed.add(section.name);
317
+ }
318
+ }
319
+ this.collapsedSections = collapsed;
320
+ this.sectionsInitialized = true;
321
+ }
322
+
323
+ /**
324
+ * Render all sections
325
+ */
326
+ private renderSections() {
327
+ this.initCollapsedSections();
328
+
329
+ return this.dashboardRef.sections.map((section, index) => {
330
+ // Check if section has any matching items
331
+ const entries = getSectionItems(section);
332
+ const filteredEntries = entries.filter(([name]) => this.matchesSearch(name));
333
+
334
+ // Hide section if no items match the search
335
+ if (filteredEntries.length === 0 && this.searchQuery) {
336
+ return null;
337
+ }
338
+
339
+ const isCollapsed = this.collapsedSections.has(section.name);
340
+ const sectionIcon = section.icon || (section.type === 'pages' ? 'insert_drive_file' : 'widgets');
341
+
342
+ return html`
343
+ <div
344
+ class="section-header ${isCollapsed ? 'collapsed' : ''}"
345
+ @click=${() => this.toggleSectionCollapsed(section.name)}
346
+ >
347
+ <i class="material-symbols-outlined expand-icon">expand_more</i>
348
+ ${section.icon ? html`<i class="material-symbols-outlined section-icon">${section.icon}</i>` : null}
349
+ <span>${section.name}</span>
350
+ </div>
351
+ <div class="section-content ${isCollapsed ? 'collapsed' : ''}">
352
+ ${this.renderSectionItems(section)}
353
+ </div>
354
+ `;
355
+ });
356
+ }
357
+
358
+ /**
359
+ * Render items for a section
360
+ */
361
+ private renderSectionItems(section: IWccSection) {
362
+ const entries = getSectionItems(section);
363
+ // Filter entries by search query
364
+ const filteredEntries = entries.filter(([name]) => this.matchesSearch(name));
365
+
366
+ if (section.type === 'pages') {
367
+ return filteredEntries.map(([pageName, item]) => {
368
+ return html`
369
+ <div
370
+ class="selectOption ${this.selectedItem === item ? 'selected' : ''}"
371
+ @click=${async () => {
372
+ await plugins.deesDomtools.DomTools.setupDomTools();
373
+ this.selectItem('page', pageName, item, 0, section);
374
+ }}
375
+ >
376
+ <i class="material-symbols-outlined">insert_drive_file</i>
377
+ <div class="text">${this.highlightMatch(pageName)}</div>
378
+ </div>
379
+ `;
380
+ });
381
+ } else {
382
+ // type === 'elements'
383
+ return filteredEntries.map(([elementName, item]) => {
384
+ const anonItem = item as any;
385
+ const demoCount = anonItem.demo ? getDemoCount(anonItem.demo) : 0;
386
+ const isMultiDemo = anonItem.demo && hasMultipleDemos(anonItem.demo);
387
+ const isExpanded = this.expandedElements.has(elementName);
388
+ const isSelected = this.selectedItem === item;
389
+
390
+ if (isMultiDemo) {
391
+ // Multi-demo element - render as expandable folder
392
+ return html`
393
+ <div
394
+ class="selectOption folder ${isExpanded ? 'expanded' : ''} ${isSelected ? 'selected' : ''}"
395
+ @click=${() => this.toggleExpanded(elementName)}
396
+ >
397
+ <i class="material-symbols-outlined expand-icon">chevron_right</i>
398
+ <i class="material-symbols-outlined">folder</i>
399
+ <div class="text">${this.highlightMatch(elementName)}</div>
400
+ </div>
401
+ ${isExpanded ? html`
402
+ <div class="demo-children">
403
+ ${Array.from({ length: demoCount }, (_, i) => {
404
+ const demoIndex = i;
405
+ const isThisDemoSelected = isSelected && this.dashboardRef.selectedDemoIndex === demoIndex;
406
+ return html`
407
+ <div
408
+ class="demo-child ${isThisDemoSelected ? 'selected' : ''}"
409
+ @click=${async () => {
410
+ await plugins.deesDomtools.DomTools.setupDomTools();
411
+ this.selectItem('element', elementName, item, demoIndex, section);
412
+ }}
413
+ >
414
+ <i class="material-symbols-outlined">play_circle</i>
415
+ <div class="text">demo${demoIndex + 1}</div>
416
+ </div>
417
+ `;
418
+ })}
419
+ </div>
420
+ ` : null}
421
+ `;
422
+ } else {
423
+ // Single demo element
424
+ return html`
425
+ <div
426
+ class="selectOption ${isSelected ? 'selected' : ''}"
427
+ @click=${async () => {
428
+ await plugins.deesDomtools.DomTools.setupDomTools();
429
+ this.selectItem('element', elementName, item, 0, section);
430
+ }}
431
+ >
432
+ <i class="material-symbols-outlined">featured_video</i>
433
+ <div class="text">${this.highlightMatch(elementName)}</div>
434
+ </div>
435
+ `;
436
+ }
437
+ });
438
+ }
439
+ }
440
+
441
+ private toggleSectionCollapsed(sectionName: string) {
442
+ const newSet = new Set(this.collapsedSections);
443
+ if (newSet.has(sectionName)) {
444
+ newSet.delete(sectionName);
445
+ } else {
446
+ newSet.add(sectionName);
447
+ }
448
+ this.collapsedSections = newSet;
449
+ }
450
+
301
451
  private toggleExpanded(elementName: string) {
302
452
  const newSet = new Set(this.expandedElements);
303
453
  if (newSet.has(elementName)) {
@@ -308,35 +458,78 @@ export class WccSidebar extends DeesElement {
308
458
  this.expandedElements = newSet;
309
459
  }
310
460
 
461
+ private handleSearchInput(e: Event) {
462
+ const input = e.target as HTMLInputElement;
463
+ this.searchQuery = input.value;
464
+ this.dispatchEvent(new CustomEvent('searchChanged', { detail: this.searchQuery }));
465
+ }
466
+
467
+ private matchesSearch(name: string): boolean {
468
+ if (!this.searchQuery) return true;
469
+ return name.toLowerCase().includes(this.searchQuery.toLowerCase());
470
+ }
471
+
472
+ private highlightMatch(text: string): TemplateResult {
473
+ if (!this.searchQuery) return html`${text}`;
474
+ const lowerText = text.toLowerCase();
475
+ const lowerQuery = this.searchQuery.toLowerCase();
476
+ const index = lowerText.indexOf(lowerQuery);
477
+ if (index === -1) return html`${text}`;
478
+ const before = text.slice(0, index);
479
+ const match = text.slice(index, index + this.searchQuery.length);
480
+ const after = text.slice(index + this.searchQuery.length);
481
+ return html`${before}<span class="highlight">${match}</span>${after}`;
482
+ }
483
+
311
484
  protected updated(changedProperties: Map<string, unknown>) {
312
485
  super.updated(changedProperties);
313
486
 
314
487
  // Auto-expand folder when a multi-demo element is selected
315
488
  if (changedProperties.has('selectedItem') && this.selectedItem) {
316
- const elementName = Object.keys(this.dashboardRef.elements).find(
317
- name => this.dashboardRef.elements[name] === this.selectedItem
318
- );
319
- if (elementName) {
320
- const item = this.dashboardRef.elements[elementName] as any;
321
- if (item.demo && hasMultipleDemos(item.demo)) {
322
- if (!this.expandedElements.has(elementName)) {
323
- const newSet = new Set(this.expandedElements);
324
- newSet.add(elementName);
325
- this.expandedElements = newSet;
489
+ // Find the element in any section
490
+ for (const section of this.dashboardRef.sections) {
491
+ if (section.type !== 'elements') continue;
492
+
493
+ const entries = getSectionItems(section);
494
+ const found = entries.find(([_, item]) => item === this.selectedItem);
495
+ if (found) {
496
+ const [elementName, item] = found;
497
+ const anonItem = item as any;
498
+ if (anonItem.demo && hasMultipleDemos(anonItem.demo)) {
499
+ if (!this.expandedElements.has(elementName)) {
500
+ const newSet = new Set(this.expandedElements);
501
+ newSet.add(elementName);
502
+ this.expandedElements = newSet;
503
+ }
326
504
  }
505
+ break;
327
506
  }
328
507
  }
329
508
  }
330
509
  }
331
510
 
332
- public selectItem(typeArg: TElementType, itemNameArg: string, itemArg: TTemplateFactory | DeesElement, demoIndex: number = 0) {
511
+ public selectItem(
512
+ typeArg: TElementType,
513
+ itemNameArg: string,
514
+ itemArg: TTemplateFactory | DeesElement,
515
+ demoIndex: number = 0,
516
+ section?: IWccSection
517
+ ) {
333
518
  console.log('selected item');
334
519
  console.log(itemNameArg);
335
520
  console.log(itemArg);
336
521
  console.log('demo index:', demoIndex);
522
+ console.log('section:', section?.name);
523
+
337
524
  this.selectedItem = itemArg;
338
525
  this.selectedType = typeArg;
339
526
  this.dashboardRef.selectedDemoIndex = demoIndex;
527
+
528
+ // Set the selected section on dashboard
529
+ if (section) {
530
+ this.dashboardRef.selectedSection = section;
531
+ }
532
+
340
533
  this.dispatchEvent(
341
534
  new CustomEvent('selectedType', {
342
535
  detail: typeArg
@@ -356,7 +549,6 @@ export class WccSidebar extends DeesElement {
356
549
  this.dashboardRef.buildUrl();
357
550
 
358
551
  // Force re-render to update demo child selection indicator
359
- // (needed when switching between demos of the same element)
360
552
  this.requestUpdate();
361
553
  }
362
554
  }
package/ts_web/index.ts CHANGED
@@ -1,21 +1,86 @@
1
1
  import { WccDashboard } from './elements/wcc-dashboard.js';
2
2
  import { LitElement } from 'lit';
3
3
  import type { TTemplateFactory } from './elements/wcctools.helpers.js';
4
+ import type { IWccConfig, IWccSection } from './wcctools.interfaces.js';
4
5
 
5
6
  // Export recording components and service
6
7
  export { RecorderService, type IRecorderEvents, type IRecordingOptions } from './services/recorder.service.js';
7
8
  export { WccRecordButton } from './elements/wcc-record-button.js';
8
9
  export { WccRecordingPanel } from './elements/wcc-recording-panel.js';
9
10
 
10
- const setupWccTools = (
11
+ // Export types for external use
12
+ export type { IWccConfig, IWccSection } from './wcctools.interfaces.js';
13
+
14
+ /**
15
+ * Converts legacy (elements, pages) format to new sections config
16
+ */
17
+ const convertLegacyToConfig = (
11
18
  elementsArg?: { [key: string]: LitElement },
12
19
  pagesArg?: Record<string, TTemplateFactory>
20
+ ): IWccConfig => {
21
+ const sections: IWccSection[] = [];
22
+
23
+ if (pagesArg && Object.keys(pagesArg).length > 0) {
24
+ sections.push({
25
+ name: 'Pages',
26
+ type: 'pages',
27
+ items: pagesArg,
28
+ });
29
+ }
30
+
31
+ if (elementsArg && Object.keys(elementsArg).length > 0) {
32
+ sections.push({
33
+ name: 'Elements',
34
+ type: 'elements',
35
+ items: elementsArg,
36
+ });
37
+ }
38
+
39
+ return { sections };
40
+ };
41
+
42
+ /**
43
+ * Check if the argument is the new config format
44
+ */
45
+ const isWccConfig = (arg: any): arg is IWccConfig => {
46
+ return arg && typeof arg === 'object' && 'sections' in arg && Array.isArray(arg.sections);
47
+ };
48
+
49
+ /**
50
+ * Setup WCC Tools dashboard
51
+ *
52
+ * New format (recommended):
53
+ * ```typescript
54
+ * setupWccTools({
55
+ * sections: [
56
+ * { name: 'Elements', type: 'elements', items: elements },
57
+ * { name: 'Pages', type: 'pages', items: pages },
58
+ * ]
59
+ * });
60
+ * ```
61
+ *
62
+ * Legacy format (still supported):
63
+ * ```typescript
64
+ * setupWccTools(elements, pages);
65
+ * ```
66
+ */
67
+ const setupWccTools = (
68
+ configOrElements?: IWccConfig | { [key: string]: LitElement },
69
+ pagesArg?: Record<string, TTemplateFactory>
13
70
  ) => {
71
+ let config: IWccConfig;
72
+
73
+ if (isWccConfig(configOrElements)) {
74
+ config = configOrElements;
75
+ } else {
76
+ config = convertLegacyToConfig(configOrElements, pagesArg);
77
+ }
78
+
14
79
  let hasRun = false;
15
80
  const runWccToolsSetup = async () => {
16
81
  if (document.readyState === 'complete' && !hasRun) {
17
82
  hasRun = true;
18
- const wccTools = new WccDashboard(elementsArg as any, pagesArg);
83
+ const wccTools = new WccDashboard(config);
19
84
  document.querySelector('body').append(wccTools);
20
85
  }
21
86
  };
package/ts_web/readme.md CHANGED
@@ -14,6 +14,40 @@ pnpm add -D @design.estate/dees-wcctools
14
14
 
15
15
  ## Usage
16
16
 
17
+ ### Sections-based Configuration (Recommended)
18
+
19
+ ```typescript
20
+ import { setupWccTools } from '@design.estate/dees-wcctools';
21
+ import * as elements from './elements/index.js';
22
+ import * as views from './views/index.js';
23
+ import * as pages from './pages/index.js';
24
+
25
+ setupWccTools({
26
+ sections: [
27
+ {
28
+ name: 'Pages',
29
+ type: 'pages',
30
+ items: pages,
31
+ },
32
+ {
33
+ name: 'Views',
34
+ type: 'elements',
35
+ items: views,
36
+ icon: 'web',
37
+ },
38
+ {
39
+ name: 'Elements',
40
+ type: 'elements',
41
+ items: elements,
42
+ filter: (name) => !name.startsWith('internal-'),
43
+ sort: ([a], [b]) => a.localeCompare(b),
44
+ },
45
+ ],
46
+ });
47
+ ```
48
+
49
+ ### Legacy Format (Still Supported)
50
+
17
51
  ```typescript
18
52
  import { setupWccTools } from '@design.estate/dees-wcctools';
19
53
  import { MyButton } from './components/my-button.js';
@@ -30,6 +64,8 @@ setupWccTools({
30
64
  | Export | Description |
31
65
  |--------|-------------|
32
66
  | `setupWccTools` | Initialize the component catalogue dashboard |
67
+ | `IWccConfig` | TypeScript interface for sections configuration |
68
+ | `IWccSection` | TypeScript interface for individual section |
33
69
 
34
70
  ### Recording Components
35
71
 
@@ -41,6 +77,18 @@ setupWccTools({
41
77
  | `IRecorderEvents` | TypeScript interface for recorder callbacks |
42
78
  | `IRecordingOptions` | TypeScript interface for recording options |
43
79
 
80
+ ## Section Configuration
81
+
82
+ | Property | Type | Description |
83
+ |----------|------|-------------|
84
+ | `name` | `string` | Display name for the section header |
85
+ | `type` | `'elements' \| 'pages'` | Rendering behavior |
86
+ | `items` | `Record<string, any>` | Element classes or page factories |
87
+ | `filter` | `(name, item) => boolean` | Optional filter function |
88
+ | `sort` | `([a, itemA], [b, itemB]) => number` | Optional sort function |
89
+ | `icon` | `string` | Material Symbols icon name |
90
+ | `collapsed` | `boolean` | Start section collapsed (default: false) |
91
+
44
92
  ## Internal Components
45
93
 
46
94
  The module includes these internal web components:
@@ -48,8 +96,8 @@ The module includes these internal web components:
48
96
  | Component | Description |
49
97
  |-----------|-------------|
50
98
  | `wcc-dashboard` | Main dashboard container with routing |
51
- | `wcc-sidebar` | Navigation sidebar with element/page listing |
52
- | `wcc-frame` | Iframe viewport with responsive sizing |
99
+ | `wcc-sidebar` | Navigation sidebar with collapsible sections |
100
+ | `wcc-frame` | Responsive viewport with size controls |
53
101
  | `wcc-properties` | Property panel with live editing |
54
102
  | `wcc-record-button` | Recording state indicator button |
55
103
  | `wcc-recording-panel` | Recording workflow UI |
@@ -72,14 +120,14 @@ const events: IRecorderEvents = {
72
120
  const recorder = new RecorderService(events);
73
121
 
74
122
  // Load available microphones
75
- const mics = await recorder.loadMicrophones(true); // true = request permission
123
+ const mics = await recorder.loadMicrophones(true);
76
124
 
77
125
  // Start audio level monitoring
78
126
  await recorder.startAudioMonitoring(mics[0].deviceId);
79
127
 
80
128
  // Start recording
81
129
  await recorder.startRecording({
82
- mode: 'viewport', // or 'screen'
130
+ mode: 'viewport',
83
131
  audioDeviceId: mics[0].deviceId,
84
132
  viewportElement: document.querySelector('.viewport'),
85
133
  });
@@ -99,10 +147,11 @@ recorder.dispose();
99
147
  ```
100
148
  ts_web/
101
149
  ├── index.ts # Main exports
150
+ ├── wcctools.interfaces.ts # Type definitions
102
151
  ├── elements/
103
152
  │ ├── wcc-dashboard.ts # Root dashboard component
104
153
  │ ├── wcc-sidebar.ts # Navigation sidebar
105
- │ ├── wcc-frame.ts # Responsive iframe viewport
154
+ │ ├── wcc-frame.ts # Responsive viewport
106
155
  │ ├── wcc-properties.ts # Property editing panel
107
156
  │ ├── wcc-record-button.ts # Recording button
108
157
  │ ├── wcc-recording-panel.ts # Recording options/preview
@@ -116,6 +165,7 @@ ts_web/
116
165
  ## Features
117
166
 
118
167
  - 🎨 Interactive component preview
168
+ - 📂 Section-based sidebar with filtering & sorting
119
169
  - 🔧 Real-time property editing with type detection
120
170
  - 🌓 Theme switching (light/dark)
121
171
  - 📱 Responsive viewport testing
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Configuration for a section in the WCC Tools sidebar
3
+ */
4
+ export interface IWccSection {
5
+ /** Display name for the section header */
6
+ name: string;
7
+ /** How items in this section are rendered - 'elements' show demos, 'pages' render directly */
8
+ type: 'elements' | 'pages';
9
+ /** The items in this section - either element classes or page factory functions */
10
+ items: Record<string, any>;
11
+ /** Optional filter function to include/exclude items */
12
+ filter?: (name: string, item: any) => boolean;
13
+ /** Optional sort function for ordering items */
14
+ sort?: (a: [string, any], b: [string, any]) => number;
15
+ /** Optional Material icon name for the section header */
16
+ icon?: string;
17
+ /** Whether this section should start collapsed (default: false) */
18
+ collapsed?: boolean;
19
+ }
20
+
21
+ /**
22
+ * Configuration object for setupWccTools
23
+ */
24
+ export interface IWccConfig {
25
+ sections: IWccSection[];
26
+ }
27
+
28
+ /**
29
+ * Type for element selection types - now section-based
30
+ */
31
+ export type TElementType = 'element' | 'page';