@design.estate/dees-wcctools 3.1.2 → 3.3.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,12 @@ 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
+ private sectionsInitialized = false;
31
+
27
32
  public render(): TemplateResult {
28
33
  return html`
29
34
  <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 {
65
70
  padding: 0.5rem 0;
66
71
  }
67
72
 
68
- h3 {
73
+ .section-header {
69
74
  padding: 0.3rem 0.75rem;
70
75
  font-size: 0.65rem;
71
76
  font-weight: 500;
@@ -77,12 +82,45 @@ export class WccSidebar extends DeesElement {
77
82
  background: rgba(59, 130, 246, 0.03);
78
83
  border-bottom: 1px solid var(--border);
79
84
  border-top: 1px solid var(--border);
85
+ display: flex;
86
+ align-items: center;
87
+ gap: 0.5rem;
88
+ cursor: pointer;
89
+ user-select: none;
90
+ transition: all 0.15s ease;
80
91
  }
81
92
 
82
- h3:first-child {
93
+ .section-header:first-child {
83
94
  margin-top: 0;
84
95
  }
85
96
 
97
+ .section-header:hover {
98
+ background: rgba(59, 130, 246, 0.08);
99
+ }
100
+
101
+ .section-header .expand-icon {
102
+ font-size: 14px;
103
+ opacity: 0.5;
104
+ transition: transform 0.2s ease;
105
+ }
106
+
107
+ .section-header.collapsed .expand-icon {
108
+ transform: rotate(-90deg);
109
+ }
110
+
111
+ .section-header .section-icon {
112
+ font-size: 14px;
113
+ opacity: 0.6;
114
+ }
115
+
116
+ .section-content {
117
+ overflow: hidden;
118
+ }
119
+
120
+ .section-content.collapsed {
121
+ display: none;
122
+ }
123
+
86
124
  .material-symbols-outlined {
87
125
  font-family: 'Material Symbols Outlined';
88
126
  font-weight: normal;
@@ -216,88 +254,144 @@ export class WccSidebar extends DeesElement {
216
254
  }
217
255
  </style>
218
256
  <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
- })()}
257
+ ${this.renderSections()}
297
258
  </div>
298
259
  `;
299
260
  }
300
261
 
262
+ /**
263
+ * Initialize collapsed sections from section config
264
+ */
265
+ private initCollapsedSections() {
266
+ if (this.sectionsInitialized) return;
267
+
268
+ const collapsed = new Set<string>();
269
+ for (const section of this.dashboardRef.sections) {
270
+ if (section.collapsed) {
271
+ collapsed.add(section.name);
272
+ }
273
+ }
274
+ this.collapsedSections = collapsed;
275
+ this.sectionsInitialized = true;
276
+ }
277
+
278
+ /**
279
+ * Render all sections
280
+ */
281
+ private renderSections() {
282
+ this.initCollapsedSections();
283
+
284
+ return this.dashboardRef.sections.map((section, index) => {
285
+ const isCollapsed = this.collapsedSections.has(section.name);
286
+ const sectionIcon = section.icon || (section.type === 'pages' ? 'insert_drive_file' : 'widgets');
287
+
288
+ return html`
289
+ <div
290
+ class="section-header ${isCollapsed ? 'collapsed' : ''}"
291
+ @click=${() => this.toggleSectionCollapsed(section.name)}
292
+ >
293
+ <i class="material-symbols-outlined expand-icon">expand_more</i>
294
+ ${section.icon ? html`<i class="material-symbols-outlined section-icon">${section.icon}</i>` : null}
295
+ <span>${section.name}</span>
296
+ </div>
297
+ <div class="section-content ${isCollapsed ? 'collapsed' : ''}">
298
+ ${this.renderSectionItems(section)}
299
+ </div>
300
+ `;
301
+ });
302
+ }
303
+
304
+ /**
305
+ * Render items for a section
306
+ */
307
+ private renderSectionItems(section: IWccSection) {
308
+ const entries = getSectionItems(section);
309
+
310
+ if (section.type === 'pages') {
311
+ return entries.map(([pageName, item]) => {
312
+ return html`
313
+ <div
314
+ class="selectOption ${this.selectedItem === item ? 'selected' : ''}"
315
+ @click=${async () => {
316
+ await plugins.deesDomtools.DomTools.setupDomTools();
317
+ this.selectItem('page', pageName, item, 0, section);
318
+ }}
319
+ >
320
+ <i class="material-symbols-outlined">insert_drive_file</i>
321
+ <div class="text">${pageName}</div>
322
+ </div>
323
+ `;
324
+ });
325
+ } else {
326
+ // type === 'elements'
327
+ return entries.map(([elementName, item]) => {
328
+ const anonItem = item as any;
329
+ const demoCount = anonItem.demo ? getDemoCount(anonItem.demo) : 0;
330
+ const isMultiDemo = anonItem.demo && hasMultipleDemos(anonItem.demo);
331
+ const isExpanded = this.expandedElements.has(elementName);
332
+ const isSelected = this.selectedItem === item;
333
+
334
+ if (isMultiDemo) {
335
+ // Multi-demo element - render as expandable folder
336
+ return html`
337
+ <div
338
+ class="selectOption folder ${isExpanded ? 'expanded' : ''} ${isSelected ? 'selected' : ''}"
339
+ @click=${() => this.toggleExpanded(elementName)}
340
+ >
341
+ <i class="material-symbols-outlined expand-icon">chevron_right</i>
342
+ <i class="material-symbols-outlined">folder</i>
343
+ <div class="text">${elementName}</div>
344
+ </div>
345
+ ${isExpanded ? html`
346
+ <div class="demo-children">
347
+ ${Array.from({ length: demoCount }, (_, i) => {
348
+ const demoIndex = i;
349
+ const isThisDemoSelected = isSelected && this.dashboardRef.selectedDemoIndex === demoIndex;
350
+ return html`
351
+ <div
352
+ class="demo-child ${isThisDemoSelected ? 'selected' : ''}"
353
+ @click=${async () => {
354
+ await plugins.deesDomtools.DomTools.setupDomTools();
355
+ this.selectItem('element', elementName, item, demoIndex, section);
356
+ }}
357
+ >
358
+ <i class="material-symbols-outlined">play_circle</i>
359
+ <div class="text">demo${demoIndex + 1}</div>
360
+ </div>
361
+ `;
362
+ })}
363
+ </div>
364
+ ` : null}
365
+ `;
366
+ } else {
367
+ // Single demo element
368
+ return html`
369
+ <div
370
+ class="selectOption ${isSelected ? 'selected' : ''}"
371
+ @click=${async () => {
372
+ await plugins.deesDomtools.DomTools.setupDomTools();
373
+ this.selectItem('element', elementName, item, 0, section);
374
+ }}
375
+ >
376
+ <i class="material-symbols-outlined">featured_video</i>
377
+ <div class="text">${elementName}</div>
378
+ </div>
379
+ `;
380
+ }
381
+ });
382
+ }
383
+ }
384
+
385
+ private toggleSectionCollapsed(sectionName: string) {
386
+ const newSet = new Set(this.collapsedSections);
387
+ if (newSet.has(sectionName)) {
388
+ newSet.delete(sectionName);
389
+ } else {
390
+ newSet.add(sectionName);
391
+ }
392
+ this.collapsedSections = newSet;
393
+ }
394
+
301
395
  private toggleExpanded(elementName: string) {
302
396
  const newSet = new Set(this.expandedElements);
303
397
  if (newSet.has(elementName)) {
@@ -308,14 +402,55 @@ export class WccSidebar extends DeesElement {
308
402
  this.expandedElements = newSet;
309
403
  }
310
404
 
311
- public selectItem(typeArg: TElementType, itemNameArg: string, itemArg: TTemplateFactory | DeesElement, demoIndex: number = 0) {
405
+ protected updated(changedProperties: Map<string, unknown>) {
406
+ super.updated(changedProperties);
407
+
408
+ // Auto-expand folder when a multi-demo element is selected
409
+ if (changedProperties.has('selectedItem') && this.selectedItem) {
410
+ // Find the element in any section
411
+ for (const section of this.dashboardRef.sections) {
412
+ if (section.type !== 'elements') continue;
413
+
414
+ const entries = getSectionItems(section);
415
+ const found = entries.find(([_, item]) => item === this.selectedItem);
416
+ if (found) {
417
+ const [elementName, item] = found;
418
+ const anonItem = item as any;
419
+ if (anonItem.demo && hasMultipleDemos(anonItem.demo)) {
420
+ if (!this.expandedElements.has(elementName)) {
421
+ const newSet = new Set(this.expandedElements);
422
+ newSet.add(elementName);
423
+ this.expandedElements = newSet;
424
+ }
425
+ }
426
+ break;
427
+ }
428
+ }
429
+ }
430
+ }
431
+
432
+ public selectItem(
433
+ typeArg: TElementType,
434
+ itemNameArg: string,
435
+ itemArg: TTemplateFactory | DeesElement,
436
+ demoIndex: number = 0,
437
+ section?: IWccSection
438
+ ) {
312
439
  console.log('selected item');
313
440
  console.log(itemNameArg);
314
441
  console.log(itemArg);
315
442
  console.log('demo index:', demoIndex);
443
+ console.log('section:', section?.name);
444
+
316
445
  this.selectedItem = itemArg;
317
446
  this.selectedType = typeArg;
318
447
  this.dashboardRef.selectedDemoIndex = demoIndex;
448
+
449
+ // Set the selected section on dashboard
450
+ if (section) {
451
+ this.dashboardRef.selectedSection = section;
452
+ }
453
+
319
454
  this.dispatchEvent(
320
455
  new CustomEvent('selectedType', {
321
456
  detail: typeArg
@@ -335,7 +470,6 @@ export class WccSidebar extends DeesElement {
335
470
  this.dashboardRef.buildUrl();
336
471
 
337
472
  // Force re-render to update demo child selection indicator
338
- // (needed when switching between demos of the same element)
339
473
  this.requestUpdate();
340
474
  }
341
475
  }
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';