@design.estate/dees-wcctools 3.4.0 → 3.5.1

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,18 @@ 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
+
39
+ // Sidebar width (resizable)
40
+ @property({ type: Number })
41
+ accessor sidebarWidth: number = 200;
42
+
43
+ // Track if currently resizing
44
+ @state()
45
+ accessor isResizing: boolean = false;
46
+
34
47
  private sectionsInitialized = false;
35
48
 
36
49
  public render(): TemplateResult {
@@ -61,7 +74,7 @@ export class WccSidebar extends DeesElement {
61
74
  box-sizing: border-box;
62
75
  position: absolute;
63
76
  left: 0px;
64
- width: 200px;
77
+ width: ${this.sidebarWidth}px;
65
78
  top: 0px;
66
79
  bottom: 0px;
67
80
  overflow-y: auto;
@@ -159,7 +172,11 @@ export class WccSidebar extends DeesElement {
159
172
  }
160
173
 
161
174
  .selectOption.folder {
162
- grid-template-columns: 16px 20px 1fr;
175
+ grid-template-columns: 16px 1fr;
176
+ }
177
+
178
+ .selectOption.folder .text {
179
+ margin-left: 4px;
163
180
  }
164
181
 
165
182
  .selectOption .expand-icon {
@@ -288,6 +305,90 @@ export class WccSidebar extends DeesElement {
288
305
  background: rgba(59, 130, 246, 0.3);
289
306
  border-radius: 2px;
290
307
  }
308
+
309
+ /* Pinned item highlight in original section */
310
+ .selectOption.pinned {
311
+ background: rgba(245, 158, 11, 0.08);
312
+ }
313
+
314
+ .selectOption.pinned:hover {
315
+ background: rgba(245, 158, 11, 0.12);
316
+ }
317
+
318
+ .selectOption.pinned.selected {
319
+ background: rgba(245, 158, 11, 0.18);
320
+ }
321
+
322
+ /* Pinned section styling */
323
+ .section-header.pinned-section {
324
+ background: rgba(245, 158, 11, 0.08);
325
+ color: #f59e0b;
326
+ }
327
+
328
+ .section-header.pinned-section:hover {
329
+ background: rgba(245, 158, 11, 0.12);
330
+ }
331
+
332
+ .section-header.pinned-section .section-icon {
333
+ opacity: 0.8;
334
+ }
335
+
336
+ /* Section tag pill for pinned items */
337
+ .section-tag {
338
+ font-size: 0.5rem;
339
+ color: #888;
340
+ margin-left: auto;
341
+ text-transform: uppercase;
342
+ letter-spacing: 0.02em;
343
+ background: rgba(255, 255, 255, 0.06);
344
+ padding: 0.15rem 0.4rem;
345
+ border-radius: 9999px;
346
+ white-space: nowrap;
347
+ }
348
+
349
+ /* Group container */
350
+ .item-group {
351
+ margin: 0.375rem 0.375rem;
352
+ border: 1px solid rgba(255, 255, 255, 0.08);
353
+ border-radius: 6px;
354
+ padding: 0.25rem 0;
355
+ background: rgba(255, 255, 255, 0.01);
356
+ }
357
+
358
+ .item-group-legend {
359
+ font-size: 0.55rem;
360
+ text-transform: uppercase;
361
+ letter-spacing: 0.05em;
362
+ color: #555;
363
+ padding: 0.125rem 0.625rem 0.25rem;
364
+ display: block;
365
+ }
366
+
367
+ .item-group .selectOption {
368
+ margin-left: 0.25rem;
369
+ margin-right: 0.25rem;
370
+ }
371
+
372
+ /* Resize handle */
373
+ .resize-handle {
374
+ position: absolute;
375
+ top: 0;
376
+ right: 0;
377
+ bottom: 0;
378
+ width: 4px;
379
+ cursor: col-resize;
380
+ background: transparent;
381
+ transition: background 0.15s ease;
382
+ z-index: 10;
383
+ }
384
+
385
+ .resize-handle:hover {
386
+ background: rgba(59, 130, 246, 0.3);
387
+ }
388
+
389
+ .resize-handle.active {
390
+ background: var(--primary);
391
+ }
291
392
  </style>
292
393
  <div class="search-container">
293
394
  <input
@@ -299,8 +400,13 @@ export class WccSidebar extends DeesElement {
299
400
  />
300
401
  </div>
301
402
  <div class="menu">
403
+ ${this.renderPinnedSection()}
302
404
  ${this.renderSections()}
303
405
  </div>
406
+ <div
407
+ class="resize-handle ${this.isResizing ? 'active' : ''}"
408
+ @mousedown=${this.startResize}
409
+ ></div>
304
410
  `;
305
411
  }
306
412
 
@@ -308,7 +414,7 @@ export class WccSidebar extends DeesElement {
308
414
  * Initialize collapsed sections from section config
309
415
  */
310
416
  private initCollapsedSections() {
311
- if (this.sectionsInitialized) return;
417
+ if (this.sectionsInitialized || !this.dashboardRef?.sections) return;
312
418
 
313
419
  const collapsed = new Set<string>();
314
420
  for (const section of this.dashboardRef.sections) {
@@ -320,13 +426,114 @@ export class WccSidebar extends DeesElement {
320
426
  this.sectionsInitialized = true;
321
427
  }
322
428
 
429
+ // ============ Pinning helpers ============
430
+
431
+ private getPinKey(sectionName: string, itemName: string): string {
432
+ return `${sectionName}::${itemName}`;
433
+ }
434
+
435
+ private isPinned(sectionName: string, itemName: string): boolean {
436
+ return this.pinnedItems.has(this.getPinKey(sectionName, itemName));
437
+ }
438
+
439
+ private togglePin(sectionName: string, itemName: string) {
440
+ const key = this.getPinKey(sectionName, itemName);
441
+ const newPinned = new Set(this.pinnedItems);
442
+ if (newPinned.has(key)) {
443
+ newPinned.delete(key);
444
+ } else {
445
+ newPinned.add(key);
446
+ }
447
+ this.pinnedItems = newPinned;
448
+ this.dispatchEvent(new CustomEvent('pinnedChanged', { detail: newPinned }));
449
+ }
450
+
451
+ private showContextMenu(e: MouseEvent, sectionName: string, itemName: string) {
452
+ const isPinned = this.isPinned(sectionName, itemName);
453
+ WccContextmenu.show(e, [
454
+ {
455
+ name: isPinned ? 'Unpin' : 'Pin',
456
+ iconName: isPinned ? 'push_pin' : 'push_pin',
457
+ action: () => this.togglePin(sectionName, itemName),
458
+ },
459
+ ]);
460
+ }
461
+
462
+ /**
463
+ * Render the PINNED section (only if there are pinned items)
464
+ */
465
+ private renderPinnedSection() {
466
+ if (!this.dashboardRef?.sections || this.pinnedItems.size === 0) {
467
+ return null;
468
+ }
469
+
470
+ const isCollapsed = this.collapsedSections.has('__pinned__');
471
+
472
+ // Collect pinned items with their original section info
473
+ // Pinned items are NOT filtered by search - they always remain visible
474
+ const pinnedEntries: Array<{ sectionName: string; itemName: string; item: any; section: IWccSection }> = [];
475
+
476
+ for (const key of this.pinnedItems) {
477
+ const [sectionName, itemName] = key.split('::');
478
+ const section = this.dashboardRef.sections.find(s => s.name === sectionName);
479
+ if (section) {
480
+ const entries = getSectionItems(section);
481
+ const found = entries.find(([name]) => name === itemName);
482
+ if (found) {
483
+ pinnedEntries.push({ sectionName, itemName, item: found[1], section });
484
+ }
485
+ }
486
+ }
487
+
488
+ if (pinnedEntries.length === 0) {
489
+ return null;
490
+ }
491
+
492
+ return html`
493
+ <div
494
+ class="section-header pinned-section ${isCollapsed ? 'collapsed' : ''}"
495
+ @click=${() => this.toggleSectionCollapsed('__pinned__')}
496
+ >
497
+ <i class="material-symbols-outlined expand-icon">expand_more</i>
498
+ <i class="material-symbols-outlined section-icon">push_pin</i>
499
+ <span>Pinned</span>
500
+ </div>
501
+ <div class="section-content ${isCollapsed ? 'collapsed' : ''}">
502
+ ${pinnedEntries.map(({ sectionName, itemName, item, section }) => {
503
+ const isSelected = this.selectedItem === item;
504
+ const type = section.type === 'elements' ? 'element' : 'page';
505
+ const icon = section.type === 'elements' ? 'featured_video' : 'insert_drive_file';
506
+
507
+ return html`
508
+ <div
509
+ class="selectOption ${isSelected ? 'selected' : ''}"
510
+ @click=${async () => {
511
+ await plugins.deesDomtools.DomTools.setupDomTools();
512
+ this.selectItem(type, itemName, item, 0, section);
513
+ }}
514
+ @contextmenu=${(e: MouseEvent) => this.showContextMenu(e, sectionName, itemName)}
515
+ >
516
+ <i class="material-symbols-outlined">${icon}</i>
517
+ <div class="text">${this.highlightMatch(itemName)}</div>
518
+ <span class="section-tag">${sectionName}</span>
519
+ </div>
520
+ `;
521
+ })}
522
+ </div>
523
+ `;
524
+ }
525
+
323
526
  /**
324
527
  * Render all sections
325
528
  */
326
529
  private renderSections() {
530
+ if (!this.dashboardRef?.sections) {
531
+ return null;
532
+ }
533
+
327
534
  this.initCollapsedSections();
328
535
 
329
- return this.dashboardRef.sections.map((section, index) => {
536
+ return this.dashboardRef.sections.map((section) => {
330
537
  // Check if section has any matching items
331
538
  const entries = getSectionItems(section);
332
539
  const filteredEntries = entries.filter(([name]) => this.matchesSearch(name));
@@ -365,13 +572,15 @@ export class WccSidebar extends DeesElement {
365
572
 
366
573
  if (section.type === 'pages') {
367
574
  return filteredEntries.map(([pageName, item]) => {
575
+ const isPinned = this.isPinned(section.name, pageName);
368
576
  return html`
369
577
  <div
370
- class="selectOption ${this.selectedItem === item ? 'selected' : ''}"
578
+ class="selectOption ${this.selectedItem === item ? 'selected' : ''} ${isPinned ? 'pinned' : ''}"
371
579
  @click=${async () => {
372
580
  await plugins.deesDomtools.DomTools.setupDomTools();
373
581
  this.selectItem('page', pageName, item, 0, section);
374
582
  }}
583
+ @contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, pageName)}
375
584
  >
376
585
  <i class="material-symbols-outlined">insert_drive_file</i>
377
586
  <div class="text">${this.highlightMatch(pageName)}</div>
@@ -379,62 +588,101 @@ export class WccSidebar extends DeesElement {
379
588
  `;
380
589
  });
381
590
  } 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
- `;
591
+ // type === 'elements' - group by demoGroup
592
+ const groupedItems = new Map<string | null, Array<[string, any]>>();
593
+
594
+ for (const entry of filteredEntries) {
595
+ const [, item] = entry;
596
+ const group = (item as any).demoGroup || null;
597
+ if (!groupedItems.has(group)) {
598
+ groupedItems.set(group, []);
436
599
  }
437
- });
600
+ groupedItems.get(group)!.push(entry);
601
+ }
602
+
603
+ const result: TemplateResult[] = [];
604
+
605
+ // Render ungrouped items first
606
+ const ungrouped = groupedItems.get(null) || [];
607
+ for (const entry of ungrouped) {
608
+ result.push(this.renderElementItem(entry, section));
609
+ }
610
+
611
+ // Render grouped items
612
+ for (const [groupName, items] of groupedItems) {
613
+ if (groupName === null) continue;
614
+
615
+ result.push(html`
616
+ <div class="item-group">
617
+ <span class="item-group-legend">${groupName}</span>
618
+ ${items.map((entry) => this.renderElementItem(entry, section))}
619
+ </div>
620
+ `);
621
+ }
622
+
623
+ return result;
624
+ }
625
+ }
626
+
627
+ /**
628
+ * Render a single element item (used by renderSectionItems)
629
+ */
630
+ private renderElementItem(entry: [string, any], section: IWccSection): TemplateResult {
631
+ const [elementName, item] = entry;
632
+ const anonItem = item as any;
633
+ const demoCount = anonItem.demo ? getDemoCount(anonItem.demo) : 0;
634
+ const isMultiDemo = anonItem.demo && hasMultipleDemos(anonItem.demo);
635
+ const isExpanded = this.expandedElements.has(elementName);
636
+ const isSelected = this.selectedItem === item;
637
+ const isPinned = this.isPinned(section.name, elementName);
638
+
639
+ if (isMultiDemo) {
640
+ // Multi-demo element - render as expandable folder
641
+ return html`
642
+ <div
643
+ class="selectOption folder ${isExpanded ? 'expanded' : ''} ${isSelected ? 'selected' : ''} ${isPinned ? 'pinned' : ''}"
644
+ @click=${() => this.toggleExpanded(elementName)}
645
+ @contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, elementName)}
646
+ >
647
+ <i class="material-symbols-outlined expand-icon">chevron_right</i>
648
+ <div class="text">${this.highlightMatch(elementName)}</div>
649
+ </div>
650
+ ${isExpanded ? html`
651
+ <div class="demo-children">
652
+ ${Array.from({ length: demoCount }, (_, i) => {
653
+ const demoIndex = i;
654
+ const isThisDemoSelected = isSelected && this.dashboardRef.selectedDemoIndex === demoIndex;
655
+ return html`
656
+ <div
657
+ class="demo-child ${isThisDemoSelected ? 'selected' : ''}"
658
+ @click=${async () => {
659
+ await plugins.deesDomtools.DomTools.setupDomTools();
660
+ this.selectItem('element', elementName, item, demoIndex, section);
661
+ }}
662
+ >
663
+ <i class="material-symbols-outlined">play_circle</i>
664
+ <div class="text">demo${demoIndex + 1}</div>
665
+ </div>
666
+ `;
667
+ })}
668
+ </div>
669
+ ` : null}
670
+ `;
671
+ } else {
672
+ // Single demo element
673
+ return html`
674
+ <div
675
+ class="selectOption ${isSelected ? 'selected' : ''} ${isPinned ? 'pinned' : ''}"
676
+ @click=${async () => {
677
+ await plugins.deesDomtools.DomTools.setupDomTools();
678
+ this.selectItem('element', elementName, item, 0, section);
679
+ }}
680
+ @contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, elementName)}
681
+ >
682
+ <i class="material-symbols-outlined">featured_video</i>
683
+ <div class="text">${this.highlightMatch(elementName)}</div>
684
+ </div>
685
+ `;
438
686
  }
439
687
  }
440
688
 
@@ -485,7 +733,7 @@ export class WccSidebar extends DeesElement {
485
733
  super.updated(changedProperties);
486
734
 
487
735
  // Auto-expand folder when a multi-demo element is selected
488
- if (changedProperties.has('selectedItem') && this.selectedItem) {
736
+ if (changedProperties.has('selectedItem') && this.selectedItem && this.dashboardRef?.sections) {
489
737
  // Find the element in any section
490
738
  for (const section of this.dashboardRef.sections) {
491
739
  if (section.type !== 'elements') continue;
@@ -508,6 +756,51 @@ export class WccSidebar extends DeesElement {
508
756
  }
509
757
  }
510
758
 
759
+ // ============ Resize functionality ============
760
+
761
+ private startResize = (e: MouseEvent) => {
762
+ e.preventDefault();
763
+ this.isResizing = true;
764
+ const startX = e.clientX;
765
+ const startWidth = this.sidebarWidth;
766
+
767
+ // Cache references once at start
768
+ const frame = this.dashboardRef?.shadowRoot?.querySelector('wcc-frame') as any;
769
+ const properties = this.dashboardRef?.shadowRoot?.querySelector('wcc-properties') as any;
770
+
771
+ // Disable frame transition during resize
772
+ if (frame) {
773
+ frame.isResizing = true;
774
+ }
775
+
776
+ const onMouseMove = (e: MouseEvent) => {
777
+ const newWidth = Math.min(400, Math.max(150, startWidth + (e.clientX - startX)));
778
+ this.sidebarWidth = newWidth;
779
+ // Update frame and properties directly
780
+ if (frame) {
781
+ frame.sidebarWidth = newWidth;
782
+ }
783
+ if (properties) {
784
+ properties.sidebarWidth = newWidth;
785
+ }
786
+ };
787
+
788
+ const onMouseUp = () => {
789
+ this.isResizing = false;
790
+ document.removeEventListener('mousemove', onMouseMove);
791
+ document.removeEventListener('mouseup', onMouseUp);
792
+ // Re-enable frame transition
793
+ if (frame) {
794
+ frame.isResizing = false;
795
+ }
796
+ // Dispatch event on release for URL persistence
797
+ this.dispatchEvent(new CustomEvent('widthChanged', { detail: this.sidebarWidth }));
798
+ };
799
+
800
+ document.addEventListener('mousemove', onMouseMove);
801
+ document.addEventListener('mouseup', onMouseUp);
802
+ };
803
+
511
804
  public selectItem(
512
805
  typeArg: TElementType,
513
806
  itemNameArg: string,