@design.estate/dees-wcctools 3.3.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 {
@@ -27,6 +28,14 @@ export class WccSidebar extends DeesElement {
27
28
  @state()
28
29
  accessor collapsedSections: Set<string> = new Set();
29
30
 
31
+ // Search query for filtering sidebar items
32
+ @property()
33
+ accessor searchQuery: string = '';
34
+
35
+ // Pinned items as Set of "sectionName::itemName"
36
+ @property({ attribute: false })
37
+ accessor pinnedItems: Set<string> = new Set();
38
+
30
39
  private sectionsInitialized = false;
31
40
 
32
41
  public render(): TemplateResult {
@@ -155,7 +164,7 @@ export class WccSidebar extends DeesElement {
155
164
  }
156
165
 
157
166
  .selectOption.folder {
158
- grid-template-columns: 16px 20px 1fr;
167
+ grid-template-columns: 16px 1fr;
159
168
  }
160
169
 
161
170
  .selectOption .expand-icon {
@@ -252,8 +261,109 @@ export class WccSidebar extends DeesElement {
252
261
  ::-webkit-scrollbar-thumb:hover {
253
262
  background: rgba(255, 255, 255, 0.2);
254
263
  }
264
+
265
+ .search-container {
266
+ padding: 0.5rem;
267
+ border-bottom: 1px solid var(--border);
268
+ }
269
+
270
+ .search-input {
271
+ width: 100%;
272
+ box-sizing: border-box;
273
+ background: var(--input);
274
+ border: 1px solid var(--border);
275
+ border-radius: var(--radius);
276
+ padding: 0.5rem 0.75rem;
277
+ color: var(--foreground);
278
+ font-size: 0.75rem;
279
+ font-family: inherit;
280
+ outline: none;
281
+ transition: border-color 0.15s ease;
282
+ }
283
+
284
+ .search-input:focus {
285
+ border-color: var(--primary);
286
+ }
287
+
288
+ .search-input::placeholder {
289
+ color: var(--muted-foreground);
290
+ }
291
+
292
+ .highlight {
293
+ background: rgba(59, 130, 246, 0.3);
294
+ border-radius: 2px;
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
+ }
255
355
  </style>
356
+ <div class="search-container">
357
+ <input
358
+ type="text"
359
+ class="search-input"
360
+ placeholder="Search..."
361
+ .value=${this.searchQuery}
362
+ @input=${this.handleSearchInput}
363
+ />
364
+ </div>
256
365
  <div class="menu">
366
+ ${this.renderPinnedSection()}
257
367
  ${this.renderSections()}
258
368
  </div>
259
369
  `;
@@ -263,7 +373,7 @@ export class WccSidebar extends DeesElement {
263
373
  * Initialize collapsed sections from section config
264
374
  */
265
375
  private initCollapsedSections() {
266
- if (this.sectionsInitialized) return;
376
+ if (this.sectionsInitialized || !this.dashboardRef?.sections) return;
267
377
 
268
378
  const collapsed = new Set<string>();
269
379
  for (const section of this.dashboardRef.sections) {
@@ -275,13 +385,125 @@ export class WccSidebar extends DeesElement {
275
385
  this.sectionsInitialized = true;
276
386
  }
277
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
+
278
487
  /**
279
488
  * Render all sections
280
489
  */
281
490
  private renderSections() {
491
+ if (!this.dashboardRef?.sections) {
492
+ return null;
493
+ }
494
+
282
495
  this.initCollapsedSections();
283
496
 
284
- return this.dashboardRef.sections.map((section, index) => {
497
+ return this.dashboardRef.sections.map((section) => {
498
+ // Check if section has any matching items
499
+ const entries = getSectionItems(section);
500
+ const filteredEntries = entries.filter(([name]) => this.matchesSearch(name));
501
+
502
+ // Hide section if no items match the search
503
+ if (filteredEntries.length === 0 && this.searchQuery) {
504
+ return null;
505
+ }
506
+
285
507
  const isCollapsed = this.collapsedSections.has(section.name);
286
508
  const sectionIcon = section.icon || (section.type === 'pages' ? 'insert_drive_file' : 'widgets');
287
509
 
@@ -306,79 +528,122 @@ export class WccSidebar extends DeesElement {
306
528
  */
307
529
  private renderSectionItems(section: IWccSection) {
308
530
  const entries = getSectionItems(section);
531
+ // Filter entries by search query
532
+ const filteredEntries = entries.filter(([name]) => this.matchesSearch(name));
309
533
 
310
534
  if (section.type === 'pages') {
311
- return entries.map(([pageName, item]) => {
535
+ return filteredEntries.map(([pageName, item]) => {
536
+ const isPinned = this.isPinned(section.name, pageName);
312
537
  return html`
313
538
  <div
314
- class="selectOption ${this.selectedItem === item ? 'selected' : ''}"
539
+ class="selectOption ${this.selectedItem === item ? 'selected' : ''} ${isPinned ? 'pinned' : ''}"
315
540
  @click=${async () => {
316
541
  await plugins.deesDomtools.DomTools.setupDomTools();
317
542
  this.selectItem('page', pageName, item, 0, section);
318
543
  }}
544
+ @contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, pageName)}
319
545
  >
320
546
  <i class="material-symbols-outlined">insert_drive_file</i>
321
- <div class="text">${pageName}</div>
547
+ <div class="text">${this.highlightMatch(pageName)}</div>
322
548
  </div>
323
549
  `;
324
550
  });
325
551
  } 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
- `;
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, []);
380
560
  }
381
- });
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
+ `;
382
647
  }
383
648
  }
384
649
 
@@ -402,11 +667,34 @@ export class WccSidebar extends DeesElement {
402
667
  this.expandedElements = newSet;
403
668
  }
404
669
 
670
+ private handleSearchInput(e: Event) {
671
+ const input = e.target as HTMLInputElement;
672
+ this.searchQuery = input.value;
673
+ this.dispatchEvent(new CustomEvent('searchChanged', { detail: this.searchQuery }));
674
+ }
675
+
676
+ private matchesSearch(name: string): boolean {
677
+ if (!this.searchQuery) return true;
678
+ return name.toLowerCase().includes(this.searchQuery.toLowerCase());
679
+ }
680
+
681
+ private highlightMatch(text: string): TemplateResult {
682
+ if (!this.searchQuery) return html`${text}`;
683
+ const lowerText = text.toLowerCase();
684
+ const lowerQuery = this.searchQuery.toLowerCase();
685
+ const index = lowerText.indexOf(lowerQuery);
686
+ if (index === -1) return html`${text}`;
687
+ const before = text.slice(0, index);
688
+ const match = text.slice(index, index + this.searchQuery.length);
689
+ const after = text.slice(index + this.searchQuery.length);
690
+ return html`${before}<span class="highlight">${match}</span>${after}`;
691
+ }
692
+
405
693
  protected updated(changedProperties: Map<string, unknown>) {
406
694
  super.updated(changedProperties);
407
695
 
408
696
  // Auto-expand folder when a multi-demo element is selected
409
- if (changedProperties.has('selectedItem') && this.selectedItem) {
697
+ if (changedProperties.has('selectedItem') && this.selectedItem && this.dashboardRef?.sections) {
410
698
  // Find the element in any section
411
699
  for (const section of this.dashboardRef.sections) {
412
700
  if (section.type !== 'elements') continue;