@design.estate/dees-wcctools 3.4.0 → 3.5.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.
@@ -4,6 +4,7 @@ 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
6
  import type { IWccSection, TElementType } from '../wcctools.interfaces.js';
7
+ import { WccContextmenu } from './wcc-contextmenu.js';
7
8
 
8
9
  @customElement('wcc-sidebar')
9
10
  export class WccSidebar extends DeesElement {
@@ -31,6 +32,10 @@ export class WccSidebar extends DeesElement {
31
32
  @property()
32
33
  accessor searchQuery: string = '';
33
34
 
35
+ // Pinned items as Set of "sectionName::itemName"
36
+ @property({ attribute: false })
37
+ accessor pinnedItems: Set<string> = new Set();
38
+
34
39
  private sectionsInitialized = false;
35
40
 
36
41
  public render(): TemplateResult {
@@ -159,7 +164,7 @@ export class WccSidebar extends DeesElement {
159
164
  }
160
165
 
161
166
  .selectOption.folder {
162
- grid-template-columns: 16px 20px 1fr;
167
+ grid-template-columns: 16px 1fr;
163
168
  }
164
169
 
165
170
  .selectOption .expand-icon {
@@ -288,6 +293,65 @@ export class WccSidebar extends DeesElement {
288
293
  background: rgba(59, 130, 246, 0.3);
289
294
  border-radius: 2px;
290
295
  }
296
+
297
+ /* Pinned item highlight in original section */
298
+ .selectOption.pinned {
299
+ background: rgba(245, 158, 11, 0.08);
300
+ }
301
+
302
+ .selectOption.pinned:hover {
303
+ background: rgba(245, 158, 11, 0.12);
304
+ }
305
+
306
+ .selectOption.pinned.selected {
307
+ background: rgba(245, 158, 11, 0.18);
308
+ }
309
+
310
+ /* Pinned section styling */
311
+ .section-header.pinned-section {
312
+ background: rgba(245, 158, 11, 0.08);
313
+ color: #f59e0b;
314
+ }
315
+
316
+ .section-header.pinned-section:hover {
317
+ background: rgba(245, 158, 11, 0.12);
318
+ }
319
+
320
+ .section-header.pinned-section .section-icon {
321
+ opacity: 0.8;
322
+ }
323
+
324
+ /* Section tag for pinned items */
325
+ .section-tag {
326
+ font-size: 0.55rem;
327
+ color: #555;
328
+ margin-left: auto;
329
+ text-transform: uppercase;
330
+ letter-spacing: 0.03em;
331
+ }
332
+
333
+ /* Group container */
334
+ .item-group {
335
+ margin: 0.375rem 0.375rem;
336
+ border: 1px solid rgba(255, 255, 255, 0.08);
337
+ border-radius: 6px;
338
+ padding: 0.25rem 0;
339
+ background: rgba(255, 255, 255, 0.01);
340
+ }
341
+
342
+ .item-group-legend {
343
+ font-size: 0.55rem;
344
+ text-transform: uppercase;
345
+ letter-spacing: 0.05em;
346
+ color: #555;
347
+ padding: 0.125rem 0.625rem 0.25rem;
348
+ display: block;
349
+ }
350
+
351
+ .item-group .selectOption {
352
+ margin-left: 0.25rem;
353
+ margin-right: 0.25rem;
354
+ }
291
355
  </style>
292
356
  <div class="search-container">
293
357
  <input
@@ -299,6 +363,7 @@ export class WccSidebar extends DeesElement {
299
363
  />
300
364
  </div>
301
365
  <div class="menu">
366
+ ${this.renderPinnedSection()}
302
367
  ${this.renderSections()}
303
368
  </div>
304
369
  `;
@@ -308,7 +373,7 @@ export class WccSidebar extends DeesElement {
308
373
  * Initialize collapsed sections from section config
309
374
  */
310
375
  private initCollapsedSections() {
311
- if (this.sectionsInitialized) return;
376
+ if (this.sectionsInitialized || !this.dashboardRef?.sections) return;
312
377
 
313
378
  const collapsed = new Set<string>();
314
379
  for (const section of this.dashboardRef.sections) {
@@ -320,13 +385,116 @@ export class WccSidebar extends DeesElement {
320
385
  this.sectionsInitialized = true;
321
386
  }
322
387
 
388
+ // ============ Pinning helpers ============
389
+
390
+ private getPinKey(sectionName: string, itemName: string): string {
391
+ return `${sectionName}::${itemName}`;
392
+ }
393
+
394
+ private isPinned(sectionName: string, itemName: string): boolean {
395
+ return this.pinnedItems.has(this.getPinKey(sectionName, itemName));
396
+ }
397
+
398
+ private togglePin(sectionName: string, itemName: string) {
399
+ const key = this.getPinKey(sectionName, itemName);
400
+ const newPinned = new Set(this.pinnedItems);
401
+ if (newPinned.has(key)) {
402
+ newPinned.delete(key);
403
+ } else {
404
+ newPinned.add(key);
405
+ }
406
+ this.pinnedItems = newPinned;
407
+ this.dispatchEvent(new CustomEvent('pinnedChanged', { detail: newPinned }));
408
+ }
409
+
410
+ private showContextMenu(e: MouseEvent, sectionName: string, itemName: string) {
411
+ const isPinned = this.isPinned(sectionName, itemName);
412
+ WccContextmenu.show(e, [
413
+ {
414
+ name: isPinned ? 'Unpin' : 'Pin',
415
+ iconName: isPinned ? 'push_pin' : 'push_pin',
416
+ action: () => this.togglePin(sectionName, itemName),
417
+ },
418
+ ]);
419
+ }
420
+
421
+ /**
422
+ * Render the PINNED section (only if there are pinned items)
423
+ */
424
+ private renderPinnedSection() {
425
+ if (!this.dashboardRef?.sections || this.pinnedItems.size === 0) {
426
+ return null;
427
+ }
428
+
429
+ const isCollapsed = this.collapsedSections.has('__pinned__');
430
+
431
+ // Collect pinned items with their original section info
432
+ const pinnedEntries: Array<{ sectionName: string; itemName: string; item: any; section: IWccSection }> = [];
433
+
434
+ for (const key of this.pinnedItems) {
435
+ const [sectionName, itemName] = key.split('::');
436
+ const section = this.dashboardRef.sections.find(s => s.name === sectionName);
437
+ if (section) {
438
+ const entries = getSectionItems(section);
439
+ const found = entries.find(([name]) => name === itemName);
440
+ if (found) {
441
+ pinnedEntries.push({ sectionName, itemName, item: found[1], section });
442
+ }
443
+ }
444
+ }
445
+
446
+ // Filter by search
447
+ const filteredEntries = pinnedEntries.filter(e => this.matchesSearch(e.itemName));
448
+
449
+ if (filteredEntries.length === 0 && this.searchQuery) {
450
+ return null;
451
+ }
452
+
453
+ return html`
454
+ <div
455
+ class="section-header pinned-section ${isCollapsed ? 'collapsed' : ''}"
456
+ @click=${() => this.toggleSectionCollapsed('__pinned__')}
457
+ >
458
+ <i class="material-symbols-outlined expand-icon">expand_more</i>
459
+ <i class="material-symbols-outlined section-icon">push_pin</i>
460
+ <span>Pinned</span>
461
+ </div>
462
+ <div class="section-content ${isCollapsed ? 'collapsed' : ''}">
463
+ ${filteredEntries.map(({ sectionName, itemName, item, section }) => {
464
+ const isSelected = this.selectedItem === item;
465
+ const type = section.type === 'elements' ? 'element' : 'page';
466
+ const icon = section.type === 'elements' ? 'featured_video' : 'insert_drive_file';
467
+
468
+ return html`
469
+ <div
470
+ class="selectOption ${isSelected ? 'selected' : ''}"
471
+ @click=${async () => {
472
+ await plugins.deesDomtools.DomTools.setupDomTools();
473
+ this.selectItem(type, itemName, item, 0, section);
474
+ }}
475
+ @contextmenu=${(e: MouseEvent) => this.showContextMenu(e, sectionName, itemName)}
476
+ >
477
+ <i class="material-symbols-outlined">${icon}</i>
478
+ <div class="text">${this.highlightMatch(itemName)}</div>
479
+ <span class="section-tag">${sectionName}</span>
480
+ </div>
481
+ `;
482
+ })}
483
+ </div>
484
+ `;
485
+ }
486
+
323
487
  /**
324
488
  * Render all sections
325
489
  */
326
490
  private renderSections() {
491
+ if (!this.dashboardRef?.sections) {
492
+ return null;
493
+ }
494
+
327
495
  this.initCollapsedSections();
328
496
 
329
- return this.dashboardRef.sections.map((section, index) => {
497
+ return this.dashboardRef.sections.map((section) => {
330
498
  // Check if section has any matching items
331
499
  const entries = getSectionItems(section);
332
500
  const filteredEntries = entries.filter(([name]) => this.matchesSearch(name));
@@ -365,13 +533,15 @@ export class WccSidebar extends DeesElement {
365
533
 
366
534
  if (section.type === 'pages') {
367
535
  return filteredEntries.map(([pageName, item]) => {
536
+ const isPinned = this.isPinned(section.name, pageName);
368
537
  return html`
369
538
  <div
370
- class="selectOption ${this.selectedItem === item ? 'selected' : ''}"
539
+ class="selectOption ${this.selectedItem === item ? 'selected' : ''} ${isPinned ? 'pinned' : ''}"
371
540
  @click=${async () => {
372
541
  await plugins.deesDomtools.DomTools.setupDomTools();
373
542
  this.selectItem('page', pageName, item, 0, section);
374
543
  }}
544
+ @contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, pageName)}
375
545
  >
376
546
  <i class="material-symbols-outlined">insert_drive_file</i>
377
547
  <div class="text">${this.highlightMatch(pageName)}</div>
@@ -379,62 +549,101 @@ export class WccSidebar extends DeesElement {
379
549
  `;
380
550
  });
381
551
  } 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
- `;
552
+ // type === 'elements' - group by demoGroup
553
+ const groupedItems = new Map<string | null, Array<[string, any]>>();
554
+
555
+ for (const entry of filteredEntries) {
556
+ const [, item] = entry;
557
+ const group = (item as any).demoGroup || null;
558
+ if (!groupedItems.has(group)) {
559
+ groupedItems.set(group, []);
436
560
  }
437
- });
561
+ groupedItems.get(group)!.push(entry);
562
+ }
563
+
564
+ const result: TemplateResult[] = [];
565
+
566
+ // Render ungrouped items first
567
+ const ungrouped = groupedItems.get(null) || [];
568
+ for (const entry of ungrouped) {
569
+ result.push(this.renderElementItem(entry, section));
570
+ }
571
+
572
+ // Render grouped items
573
+ for (const [groupName, items] of groupedItems) {
574
+ if (groupName === null) continue;
575
+
576
+ result.push(html`
577
+ <div class="item-group">
578
+ <span class="item-group-legend">${groupName}</span>
579
+ ${items.map((entry) => this.renderElementItem(entry, section))}
580
+ </div>
581
+ `);
582
+ }
583
+
584
+ return result;
585
+ }
586
+ }
587
+
588
+ /**
589
+ * Render a single element item (used by renderSectionItems)
590
+ */
591
+ private renderElementItem(entry: [string, any], section: IWccSection): TemplateResult {
592
+ const [elementName, item] = entry;
593
+ const anonItem = item as any;
594
+ const demoCount = anonItem.demo ? getDemoCount(anonItem.demo) : 0;
595
+ const isMultiDemo = anonItem.demo && hasMultipleDemos(anonItem.demo);
596
+ const isExpanded = this.expandedElements.has(elementName);
597
+ const isSelected = this.selectedItem === item;
598
+ const isPinned = this.isPinned(section.name, elementName);
599
+
600
+ if (isMultiDemo) {
601
+ // Multi-demo element - render as expandable folder
602
+ return html`
603
+ <div
604
+ class="selectOption folder ${isExpanded ? 'expanded' : ''} ${isSelected ? 'selected' : ''} ${isPinned ? 'pinned' : ''}"
605
+ @click=${() => this.toggleExpanded(elementName)}
606
+ @contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, elementName)}
607
+ >
608
+ <i class="material-symbols-outlined expand-icon">chevron_right</i>
609
+ <div class="text">${this.highlightMatch(elementName)}</div>
610
+ </div>
611
+ ${isExpanded ? html`
612
+ <div class="demo-children">
613
+ ${Array.from({ length: demoCount }, (_, i) => {
614
+ const demoIndex = i;
615
+ const isThisDemoSelected = isSelected && this.dashboardRef.selectedDemoIndex === demoIndex;
616
+ return html`
617
+ <div
618
+ class="demo-child ${isThisDemoSelected ? 'selected' : ''}"
619
+ @click=${async () => {
620
+ await plugins.deesDomtools.DomTools.setupDomTools();
621
+ this.selectItem('element', elementName, item, demoIndex, section);
622
+ }}
623
+ >
624
+ <i class="material-symbols-outlined">play_circle</i>
625
+ <div class="text">demo${demoIndex + 1}</div>
626
+ </div>
627
+ `;
628
+ })}
629
+ </div>
630
+ ` : null}
631
+ `;
632
+ } else {
633
+ // Single demo element
634
+ return html`
635
+ <div
636
+ class="selectOption ${isSelected ? 'selected' : ''} ${isPinned ? 'pinned' : ''}"
637
+ @click=${async () => {
638
+ await plugins.deesDomtools.DomTools.setupDomTools();
639
+ this.selectItem('element', elementName, item, 0, section);
640
+ }}
641
+ @contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, elementName)}
642
+ >
643
+ <i class="material-symbols-outlined">featured_video</i>
644
+ <div class="text">${this.highlightMatch(elementName)}</div>
645
+ </div>
646
+ `;
438
647
  }
439
648
  }
440
649
 
@@ -485,7 +694,7 @@ export class WccSidebar extends DeesElement {
485
694
  super.updated(changedProperties);
486
695
 
487
696
  // Auto-expand folder when a multi-demo element is selected
488
- if (changedProperties.has('selectedItem') && this.selectedItem) {
697
+ if (changedProperties.has('selectedItem') && this.selectedItem && this.dashboardRef?.sections) {
489
698
  // Find the element in any section
490
699
  for (const section of this.dashboardRef.sections) {
491
700
  if (section.type !== 'elements') continue;