@design.estate/dees-wcctools 1.3.0 → 2.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@design.estate/dees-wcctools",
3
- "version": "1.3.0",
3
+ "version": "2.0.0",
4
4
  "private": false,
5
5
  "description": "A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.",
6
6
  "exports": {
@@ -22,7 +22,7 @@
22
22
  "@git.zone/tsbundle": "^2.6.3",
23
23
  "@git.zone/tsrun": "^2.0.0",
24
24
  "@git.zone/tstest": "^3.1.3",
25
- "@git.zone/tswatch": "^2.3.10",
25
+ "@git.zone/tswatch": "^2.3.11",
26
26
  "@push.rocks/projectinfo": "^5.0.2",
27
27
  "@types/node": "^25.0.0"
28
28
  },
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@design.estate/dees-wcctools',
6
- version: '1.3.0',
6
+ version: '2.0.0',
7
7
  description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.'
8
8
  }
@@ -1,5 +1,5 @@
1
1
  import { DeesElement, property, html, customElement, type TemplateResult, queryAsync, render, domtools } from '@design.estate/dees-element';
2
- import { resolveTemplateFactory } from './wcctools.helpers.js';
2
+ import { resolveTemplateFactory, getDemoAtIndex, getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
3
3
  import type { TTemplateFactory } from './wcctools.helpers.js';
4
4
 
5
5
  import * as plugins from '../wcctools.plugins.js';
@@ -25,6 +25,9 @@ export class WccDashboard extends DeesElement {
25
25
  @property()
26
26
  accessor selectedItem: TTemplateFactory | DeesElement;
27
27
 
28
+ @property({ type: Number })
29
+ accessor selectedDemoIndex: number = 0;
30
+
28
31
  @property()
29
32
  accessor selectedViewport: plugins.deesDomtools.breakpoints.TViewport = 'desktop';
30
33
 
@@ -151,11 +154,53 @@ export class WccDashboard extends DeesElement {
151
154
  this.setupScrollListeners();
152
155
  }, 500);
153
156
 
157
+ // Route with demo index (new format)
158
+ this.domtools.router.on(
159
+ '/wcctools-route/:itemType/:itemName/:demoIndex/:viewport/:theme',
160
+ async (routeInfo) => {
161
+ this.selectedType = routeInfo.params.itemType as TElementType;
162
+ this.selectedItemName = routeInfo.params.itemName;
163
+ this.selectedDemoIndex = parseInt(routeInfo.params.demoIndex) || 0;
164
+ this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
165
+ this.selectedTheme = routeInfo.params.theme as TTheme;
166
+ if (routeInfo.params.itemType === 'element') {
167
+ this.selectedItem = this.elements[routeInfo.params.itemName];
168
+ } else if (routeInfo.params.itemType === 'page') {
169
+ this.selectedItem = this.pages[routeInfo.params.itemName];
170
+ }
171
+
172
+ // Restore scroll positions from query parameters
173
+ if (routeInfo.queryParams) {
174
+ const frameScrollY = routeInfo.queryParams.frameScrollY;
175
+ const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
176
+
177
+ if (frameScrollY) {
178
+ this.frameScrollY = parseInt(frameScrollY);
179
+ }
180
+ if (sidebarScrollY) {
181
+ this.sidebarScrollY = parseInt(sidebarScrollY);
182
+ }
183
+
184
+ // Apply scroll positions after a short delay to ensure DOM is ready
185
+ setTimeout(() => {
186
+ this.applyScrollPositions();
187
+ }, 100);
188
+ }
189
+
190
+ const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
191
+ this.selectedTheme === 'bright'
192
+ ? domtoolsInstance.themeManager.goBright()
193
+ : domtoolsInstance.themeManager.goDark();
194
+ }
195
+ );
196
+
197
+ // Legacy route without demo index (for backwards compatibility)
154
198
  this.domtools.router.on(
155
199
  '/wcctools-route/:itemType/:itemName/:viewport/:theme',
156
200
  async (routeInfo) => {
157
201
  this.selectedType = routeInfo.params.itemType as TElementType;
158
202
  this.selectedItemName = routeInfo.params.itemName;
203
+ this.selectedDemoIndex = 0; // Default to first demo
159
204
  this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
160
205
  this.selectedTheme = routeInfo.params.theme as TTheme;
161
206
  if (routeInfo.params.itemType === 'element') {
@@ -163,25 +208,25 @@ export class WccDashboard extends DeesElement {
163
208
  } else if (routeInfo.params.itemType === 'page') {
164
209
  this.selectedItem = this.pages[routeInfo.params.itemName];
165
210
  }
166
-
211
+
167
212
  // Restore scroll positions from query parameters
168
213
  if (routeInfo.queryParams) {
169
214
  const frameScrollY = routeInfo.queryParams.frameScrollY;
170
215
  const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
171
-
216
+
172
217
  if (frameScrollY) {
173
218
  this.frameScrollY = parseInt(frameScrollY);
174
219
  }
175
220
  if (sidebarScrollY) {
176
221
  this.sidebarScrollY = parseInt(sidebarScrollY);
177
222
  }
178
-
223
+
179
224
  // Apply scroll positions after a short delay to ensure DOM is ready
180
225
  setTimeout(() => {
181
226
  this.applyScrollPositions();
182
227
  }, 100);
183
228
  }
184
-
229
+
185
230
  const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
186
231
  this.selectedTheme === 'bright'
187
232
  ? domtoolsInstance.themeManager.goBright()
@@ -218,33 +263,48 @@ export class WccDashboard extends DeesElement {
218
263
  this.setWarning(`component ${anonItem.name} does not expose a demo property.`);
219
264
  return;
220
265
  }
221
- if (!(typeof anonItem.demo === 'function')) {
266
+
267
+ // Support both single demo (function) and multiple demos (array)
268
+ const isArray = Array.isArray(anonItem.demo);
269
+ const isFunction = typeof anonItem.demo === 'function';
270
+
271
+ if (!isArray && !isFunction) {
222
272
  this.setWarning(
223
- `component ${anonItem.name} has demo property, but it is not of type function`
273
+ `component ${anonItem.name} has demo property, but it is not a function or array of functions`
224
274
  );
225
275
  return;
226
276
  }
277
+
278
+ // Get the specific demo to render
279
+ const demoFactory = getDemoAtIndex(anonItem.demo, this.selectedDemoIndex);
280
+ if (!demoFactory) {
281
+ this.setWarning(
282
+ `component ${anonItem.name} does not have a demo at index ${this.selectedDemoIndex + 1}`
283
+ );
284
+ return;
285
+ }
286
+
227
287
  this.setWarning(null);
228
288
  const viewport = await wccFrame.getViewportElement();
229
- const demoTemplate = await resolveTemplateFactory(() => anonItem.demo());
289
+ const demoTemplate = await resolveTemplateFactory(demoFactory);
230
290
  render(demoTemplate, viewport);
231
291
  }
232
292
  }
233
293
 
234
294
  public buildUrl() {
235
- const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedViewport}/${this.selectedTheme}`;
295
+ const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
236
296
  const queryParams = new URLSearchParams();
237
-
297
+
238
298
  if (this.frameScrollY > 0) {
239
299
  queryParams.set('frameScrollY', this.frameScrollY.toString());
240
300
  }
241
301
  if (this.sidebarScrollY > 0) {
242
302
  queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
243
303
  }
244
-
304
+
245
305
  const queryString = queryParams.toString();
246
306
  const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
247
-
307
+
248
308
  this.domtools.router.pushUrl(fullUrl);
249
309
  }
250
310
 
@@ -286,19 +346,19 @@ export class WccDashboard extends DeesElement {
286
346
  }
287
347
 
288
348
  private updateUrlWithScrollState() {
289
- const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedViewport}/${this.selectedTheme}`;
349
+ const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
290
350
  const queryParams = new URLSearchParams();
291
-
351
+
292
352
  if (this.frameScrollY > 0) {
293
353
  queryParams.set('frameScrollY', this.frameScrollY.toString());
294
354
  }
295
355
  if (this.sidebarScrollY > 0) {
296
356
  queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
297
357
  }
298
-
358
+
299
359
  const queryString = queryParams.toString();
300
360
  const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
301
-
361
+
302
362
  // Use replaceState to update URL without navigation
303
363
  window.history.replaceState(null, '', fullUrl);
304
364
  }
@@ -552,6 +552,7 @@ export class WccRecordingPanel extends DeesElement {
552
552
  @keyframes spin {
553
553
  to { transform: rotate(360deg); }
554
554
  }
555
+
555
556
  `
556
557
  ];
557
558
 
@@ -706,6 +707,7 @@ export class WccRecordingPanel extends DeesElement {
706
707
  </button>
707
708
  </div>
708
709
  </div>
710
+
709
711
  </div>
710
712
  <div class="preview-modal-actions">
711
713
  <button class="preview-btn secondary" @click=${() => this.discardRecording()}>Discard</button>
@@ -714,7 +716,7 @@ export class WccRecordingPanel extends DeesElement {
714
716
  ?disabled=${this.isExporting}
715
717
  @click=${() => this.downloadRecording()}
716
718
  >
717
- ${this.isExporting ? html`<span class="export-spinner"></span>Exporting...` : 'Download'}
719
+ ${this.isExporting ? html`<span class="export-spinner"></span>Exporting...` : 'Download WebM'}
718
720
  </button>
719
721
  </div>
720
722
  </div>
@@ -815,6 +817,7 @@ export class WccRecordingPanel extends DeesElement {
815
817
  try {
816
818
  let blobToDownload: Blob;
817
819
 
820
+ // Handle trimming if needed
818
821
  const needsTrim = this.trimStart > 0.1 || this.trimEnd < this.videoDuration - 0.1;
819
822
 
820
823
  if (needsTrim) {
@@ -828,6 +831,7 @@ export class WccRecordingPanel extends DeesElement {
828
831
  blobToDownload = recordedBlob;
829
832
  }
830
833
 
834
+ // Trigger download
831
835
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
832
836
  const filename = `wcctools-recording-${timestamp}.webm`;
833
837
 
@@ -1,7 +1,8 @@
1
1
  import * as plugins from '../wcctools.plugins.js';
2
- import { DeesElement, property, html, customElement, type TemplateResult } from '@design.estate/dees-element';
2
+ import { DeesElement, property, html, customElement, type TemplateResult, state } from '@design.estate/dees-element';
3
3
  import { WccDashboard } from './wcc-dashboard.js';
4
4
  import type { TTemplateFactory } from './wcctools.helpers.js';
5
+ import { getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
5
6
 
6
7
  export type TElementType = 'element' | 'page';
7
8
 
@@ -19,6 +20,10 @@ export class WccSidebar extends DeesElement {
19
20
  @property()
20
21
  accessor isFullscreen: boolean = false;
21
22
 
23
+ // Track which elements are expanded (for multi-demo elements)
24
+ @state()
25
+ accessor expandedElements: Set<string> = new Set();
26
+
22
27
  public render(): TemplateResult {
23
28
  return html`
24
29
  <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" />
@@ -110,7 +115,21 @@ export class WccSidebar extends DeesElement {
110
115
  color: #999;
111
116
  background: transparent;
112
117
  }
113
-
118
+
119
+ .selectOption.folder {
120
+ grid-template-columns: 16px 20px 1fr;
121
+ }
122
+
123
+ .selectOption .expand-icon {
124
+ font-size: 14px;
125
+ opacity: 0.5;
126
+ transition: transform 0.2s ease;
127
+ }
128
+
129
+ .selectOption.expanded .expand-icon {
130
+ transform: rotate(90deg);
131
+ }
132
+
114
133
  .selectOption:hover {
115
134
  background: rgba(59, 130, 246, 0.05);
116
135
  color: #bbb;
@@ -143,6 +162,42 @@ export class WccSidebar extends DeesElement {
143
162
  font-weight: 400;
144
163
  }
145
164
 
165
+ .demo-children {
166
+ margin-left: 1rem;
167
+ overflow: hidden;
168
+ }
169
+
170
+ .demo-child {
171
+ user-select: none;
172
+ position: relative;
173
+ margin: 0.125rem 0.5rem;
174
+ padding: 0.35rem 0.75rem;
175
+ transition: all 0.15s ease;
176
+ display: grid;
177
+ grid-template-columns: 16px 1fr;
178
+ align-items: center;
179
+ gap: 0.5rem;
180
+ border-radius: var(--radius);
181
+ cursor: pointer;
182
+ font-size: 0.7rem;
183
+ color: #777;
184
+ background: transparent;
185
+ }
186
+
187
+ .demo-child:hover {
188
+ background: rgba(59, 130, 246, 0.05);
189
+ color: #bbb;
190
+ }
191
+
192
+ .demo-child.selected {
193
+ background: rgba(59, 130, 246, 0.15);
194
+ color: var(--primary);
195
+ }
196
+
197
+ .demo-child .material-symbols-outlined {
198
+ font-size: 14px;
199
+ }
200
+
146
201
  ::-webkit-scrollbar {
147
202
  width: 8px;
148
203
  }
@@ -171,7 +226,7 @@ export class WccSidebar extends DeesElement {
171
226
  class="selectOption ${this.selectedItem === item ? 'selected' : null}"
172
227
  @click=${async () => {
173
228
  const domtools = await plugins.deesDomtools.DomTools.setupDomTools();
174
- this.selectItem('page', pageName, item);
229
+ this.selectItem('page', pageName, item, 0);
175
230
  }}
176
231
  >
177
232
  <i class="material-symbols-outlined">insert_drive_file</i>
@@ -184,31 +239,83 @@ export class WccSidebar extends DeesElement {
184
239
  ${(() => {
185
240
  const elements = Object.keys(this.dashboardRef.elements);
186
241
  return elements.map(elementName => {
187
- const item = this.dashboardRef.elements[elementName];
188
- return html`
189
- <div
190
- class="selectOption ${this.selectedItem === item ? 'selected' : null}"
191
- @click=${async () => {
192
- const domtools = await plugins.deesDomtools.DomTools.setupDomTools();
193
- this.selectItem('element', elementName, item);
194
- }}
195
- >
196
- <i class="material-symbols-outlined">featured_video</i>
197
- <div class="text">${elementName}</div>
198
- </div>
199
- `;
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
+ }
200
295
  });
201
296
  })()}
202
297
  </div>
203
298
  `;
204
299
  }
205
300
 
206
- public selectItem(typeArg: TElementType, itemNameArg: string, itemArg: TTemplateFactory | DeesElement) {
301
+ private toggleExpanded(elementName: string) {
302
+ const newSet = new Set(this.expandedElements);
303
+ if (newSet.has(elementName)) {
304
+ newSet.delete(elementName);
305
+ } else {
306
+ newSet.add(elementName);
307
+ }
308
+ this.expandedElements = newSet;
309
+ }
310
+
311
+ public selectItem(typeArg: TElementType, itemNameArg: string, itemArg: TTemplateFactory | DeesElement, demoIndex: number = 0) {
207
312
  console.log('selected item');
208
313
  console.log(itemNameArg);
209
314
  console.log(itemArg);
315
+ console.log('demo index:', demoIndex);
210
316
  this.selectedItem = itemArg;
211
317
  this.selectedType = typeArg;
318
+ this.dashboardRef.selectedDemoIndex = demoIndex;
212
319
  this.dispatchEvent(
213
320
  new CustomEvent('selectedType', {
214
321
  detail: typeArg
@@ -224,7 +331,11 @@ export class WccSidebar extends DeesElement {
224
331
  detail: itemArg
225
332
  })
226
333
  );
227
-
334
+
228
335
  this.dashboardRef.buildUrl();
336
+
337
+ // Force re-render to update demo child selection indicator
338
+ // (needed when switching between demos of the same element)
339
+ this.requestUpdate();
229
340
  }
230
341
  }
@@ -2,8 +2,39 @@ import type { TemplateResult } from 'lit';
2
2
 
3
3
  export type TTemplateFactory = () => TemplateResult | Promise<TemplateResult>;
4
4
 
5
+ // Demo can be a single function or an array of functions
6
+ export type TDemoDefinition = TTemplateFactory | TTemplateFactory[];
7
+
5
8
  export const resolveTemplateFactory = async (
6
9
  factoryArg: TTemplateFactory
7
10
  ): Promise<TemplateResult> => {
8
11
  return await Promise.resolve(factoryArg());
9
12
  };
13
+
14
+ /**
15
+ * Get the number of demos for an element
16
+ */
17
+ export const getDemoCount = (demo: TDemoDefinition): number => {
18
+ if (Array.isArray(demo)) {
19
+ return demo.length;
20
+ }
21
+ return 1;
22
+ };
23
+
24
+ /**
25
+ * Get a specific demo by index (0-based internally, displayed as 1-based)
26
+ */
27
+ export const getDemoAtIndex = (demo: TDemoDefinition, index: number): TTemplateFactory | null => {
28
+ if (Array.isArray(demo)) {
29
+ return demo[index] ?? null;
30
+ }
31
+ // Single demo - only index 0 is valid
32
+ return index === 0 ? demo : null;
33
+ };
34
+
35
+ /**
36
+ * Check if an element has multiple demos
37
+ */
38
+ export const hasMultipleDemos = (demo: TDemoDefinition): boolean => {
39
+ return Array.isArray(demo) && demo.length > 1;
40
+ };
@@ -235,9 +235,11 @@ export class RecorderService {
235
235
  }
236
236
  }
237
237
 
238
- private handleRecordingComplete(): void {
238
+ private async handleRecordingComplete(): Promise<void> {
239
239
  // Create blob from recorded chunks
240
- this._recordedBlob = new Blob(this.recordedChunks, { type: 'video/webm' });
240
+ const blob = new Blob(this.recordedChunks, { type: 'video/webm' });
241
+
242
+ this._recordedBlob = blob;
241
243
 
242
244
  // Stop all tracks
243
245
  if (this.currentStream) {