@design.estate/dees-wcctools 3.7.1 → 3.8.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/npmextra.json CHANGED
@@ -35,5 +35,8 @@
35
35
  },
36
36
  "@ship.zone/szci": {
37
37
  "npmGlobalTools": []
38
+ },
39
+ "@git.zone/tswatch": {
40
+ "preset": "element"
38
41
  }
39
42
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@design.estate/dees-wcctools",
3
- "version": "3.7.1",
3
+ "version": "3.8.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": {
@@ -11,7 +11,7 @@
11
11
  "scripts": {
12
12
  "test": "(npm run build)",
13
13
  "build": "(tsbuild tsfolders --allowimplicitany && tsbundle element)",
14
- "watch": "tswatch element",
14
+ "watch": "tswatch",
15
15
  "buildDocs": "tsdoc"
16
16
  },
17
17
  "author": "Lossless GmbH",
@@ -23,14 +23,14 @@
23
23
  "lit": "^3.3.2"
24
24
  },
25
25
  "devDependencies": {
26
- "@api.global/typedserver": "^8.1.0",
27
- "@git.zone/tsbuild": "^4.0.2",
28
- "@git.zone/tsbundle": "^2.6.3",
26
+ "@api.global/typedserver": "^8.3.0",
27
+ "@git.zone/tsbuild": "^4.1.2",
28
+ "@git.zone/tsbundle": "^2.8.3",
29
29
  "@git.zone/tsrun": "^2.0.1",
30
- "@git.zone/tstest": "^3.1.4",
31
- "@git.zone/tswatch": "^2.3.13",
30
+ "@git.zone/tstest": "^3.1.8",
31
+ "@git.zone/tswatch": "^3.0.1",
32
32
  "@push.rocks/projectinfo": "^5.0.2",
33
- "@types/node": "^25.0.3"
33
+ "@types/node": "^25.0.10"
34
34
  },
35
35
  "files": [
36
36
  "ts/**/*",
package/readme.hints.md CHANGED
@@ -62,6 +62,33 @@ Section names are URL-encoded. Legacy routes (`element`/`page` as section name)
62
62
 
63
63
  ---
64
64
 
65
+ ## Element Demo Groups (2026-01-27)
66
+
67
+ ### Overview
68
+ Elements can declare `demoGroups` (renamed from `demoGroup`) as a static property to appear grouped in the sidebar. Supports `string | string[]` — elements with an array appear in multiple groups simultaneously.
69
+
70
+ ### Usage
71
+ ```typescript
72
+ // Single group
73
+ public static demoGroups = 'Buttons';
74
+
75
+ // Multiple groups — element appears in both
76
+ public static demoGroups = ['Buttons', 'Form Controls'];
77
+ ```
78
+
79
+ ### Features
80
+ - Search matches group names (searching "Buttons" shows all elements in that group)
81
+ - Groups sorted alphabetically by group name
82
+ - Multi-group elements show `library_books` icon instead of `featured_video`
83
+ - Context menu shows "Show in Group:" with clickable group entries that scroll to and highlight the group
84
+ - `data-group` attribute on `.item-group` containers for DOM querying
85
+
86
+ ### Files Changed
87
+ - `ts_web/elements/wcc-sidebar.ts` — grouping logic, search filter, sort key, icon, context menu, scrollToGroup
88
+ - `test/elements/test-button-*.ts`, `test/elements/test-input-*.ts` — renamed `demoGroup` → `demoGroups`
89
+
90
+ ---
91
+
65
92
  ## UI Redesign with Shadcn-like Styles (2025-06-27)
66
93
 
67
94
  ### Changes Made
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@design.estate/dees-wcctools',
6
- version: '3.7.1',
6
+ version: '3.8.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
  }
@@ -4,7 +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
+ import { WccContextmenu, type IContextMenuItem } from './wcc-contextmenu.js';
8
8
 
9
9
  @customElement('wcc-sidebar')
10
10
  export class WccSidebar extends DeesElement {
@@ -422,6 +422,7 @@ export class WccSidebar extends DeesElement {
422
422
  color: #555;
423
423
  padding: 0.125rem 0.625rem 0.25rem;
424
424
  display: block;
425
+ cursor: context-menu;
425
426
  }
426
427
 
427
428
  .item-group .selectOption {
@@ -429,6 +430,15 @@ export class WccSidebar extends DeesElement {
429
430
  margin-right: 0.25rem;
430
431
  }
431
432
 
433
+ .item-group.group-highlight {
434
+ background: rgba(59, 130, 246, 0.15);
435
+ transition: background 0.3s ease;
436
+ }
437
+
438
+ .item-group.group-filter-match {
439
+ border-color: rgba(245, 158, 11, 0.5);
440
+ }
441
+
432
442
  /* Resize handle */
433
443
  .resize-handle {
434
444
  position: absolute;
@@ -517,12 +527,49 @@ export class WccSidebar extends DeesElement {
517
527
 
518
528
  private showContextMenu(e: MouseEvent, sectionName: string, itemName: string) {
519
529
  const isPinned = this.isPinned(sectionName, itemName);
520
- WccContextmenu.show(e, [
530
+ const section = this.dashboardRef?.sections?.find(s => s.name === sectionName);
531
+ const sectionEntries = section ? getSectionItems(section) : [];
532
+ const foundEntry = sectionEntries.find(([name]) => name === itemName);
533
+ const item = foundEntry?.[1];
534
+ const groups = item ? this.getElementGroups(item) : [];
535
+
536
+ const menuItems: IContextMenuItem[] = [
521
537
  {
522
538
  name: isPinned ? 'Unpin' : 'Pin',
523
539
  iconName: isPinned ? 'push_pin' : 'push_pin',
524
540
  action: () => this.togglePin(sectionName, itemName),
525
541
  },
542
+ ];
543
+
544
+ if (groups.length > 0) {
545
+ menuItems.push({
546
+ name: 'Show in Group:',
547
+ iconName: 'folder',
548
+ action: () => {},
549
+ disabled: true,
550
+ });
551
+ for (const groupName of groups) {
552
+ menuItems.push({
553
+ name: groupName,
554
+ iconName: 'label',
555
+ action: () => this.scrollToGroup(sectionName, groupName),
556
+ });
557
+ }
558
+ }
559
+
560
+ WccContextmenu.show(e, menuItems);
561
+ }
562
+
563
+ private showGroupContextMenu(e: MouseEvent, groupName: string) {
564
+ WccContextmenu.show(e, [
565
+ {
566
+ name: `Show "${groupName}"`,
567
+ iconName: 'filter_alt',
568
+ action: () => {
569
+ this.searchQuery = groupName;
570
+ this.dispatchEvent(new CustomEvent('searchChanged', { detail: this.searchQuery }));
571
+ },
572
+ },
526
573
  ]);
527
574
  }
528
575
 
@@ -569,7 +616,9 @@ export class WccSidebar extends DeesElement {
569
616
  ${pinnedEntries.map(({ sectionName, itemName, item, section }) => {
570
617
  const isSelected = this.selectedItem === item;
571
618
  const type = section.type === 'elements' ? 'element' : 'page';
572
- const icon = section.type === 'elements' ? 'featured_video' : 'insert_drive_file';
619
+ const icon = section.type === 'elements'
620
+ ? (this.getElementGroups(item).length > 1 ? 'library_books' : 'featured_video')
621
+ : 'insert_drive_file';
573
622
 
574
623
  return html`
575
624
  <div
@@ -603,7 +652,13 @@ export class WccSidebar extends DeesElement {
603
652
  return this.dashboardRef.sections.map((section) => {
604
653
  // Check if section has any matching items
605
654
  const entries = getSectionItems(section);
606
- const filteredEntries = entries.filter(([name]) => this.matchesSearch(name));
655
+ const filteredEntries = entries.filter(([name, item]) => {
656
+ if (this.matchesSearch(name)) return true;
657
+ const rawGroups = (item as any).demoGroups;
658
+ if (!rawGroups) return false;
659
+ const groups: string[] = Array.isArray(rawGroups) ? rawGroups : [rawGroups];
660
+ return groups.some(g => this.matchesSearch(g));
661
+ });
607
662
 
608
663
  // Hide section if no items match the search
609
664
  if (filteredEntries.length === 0 && this.searchQuery) {
@@ -635,7 +690,13 @@ export class WccSidebar extends DeesElement {
635
690
  private renderSectionItems(section: IWccSection) {
636
691
  const entries = getSectionItems(section);
637
692
  // Filter entries by search query
638
- const filteredEntries = entries.filter(([name]) => this.matchesSearch(name));
693
+ const filteredEntries = entries.filter(([name, item]) => {
694
+ if (this.matchesSearch(name)) return true;
695
+ const rawGroups = (item as any).demoGroups;
696
+ if (!rawGroups) return false;
697
+ const groups: string[] = Array.isArray(rawGroups) ? rawGroups : [rawGroups];
698
+ return groups.some(g => this.matchesSearch(g));
699
+ });
639
700
 
640
701
  if (section.type === 'pages') {
641
702
  return filteredEntries.map(([pageName, item]) => {
@@ -655,16 +716,21 @@ export class WccSidebar extends DeesElement {
655
716
  `;
656
717
  });
657
718
  } else {
658
- // type === 'elements' - group by demoGroup
719
+ // type === 'elements' - group by demoGroups (supports string | string[])
659
720
  const groupedItems = new Map<string | null, Array<[string, any]>>();
660
721
 
661
722
  for (const entry of filteredEntries) {
662
723
  const [, item] = entry;
663
- const group = (item as any).demoGroup || null;
664
- if (!groupedItems.has(group)) {
665
- groupedItems.set(group, []);
724
+ const rawGroups = (item as any).demoGroups;
725
+ const groups: Array<string | null> = rawGroups
726
+ ? (Array.isArray(rawGroups) ? rawGroups : [rawGroups])
727
+ : [null];
728
+ for (const group of groups) {
729
+ if (!groupedItems.has(group)) {
730
+ groupedItems.set(group, []);
731
+ }
732
+ groupedItems.get(group)!.push(entry);
666
733
  }
667
- groupedItems.get(group)!.push(entry);
668
734
  }
669
735
 
670
736
  // Build a unified list of render items (ungrouped elements and groups)
@@ -681,11 +747,10 @@ export class WccSidebar extends DeesElement {
681
747
  renderItems.push({ type: 'element', entry, sortKey: entry[0].toLowerCase() });
682
748
  }
683
749
 
684
- // Add groups (sorted by their first element's name)
750
+ // Add groups (sorted by group name)
685
751
  for (const [groupName, items] of groupedItems) {
686
752
  if (groupName === null) continue;
687
- const firstElementName = items[0]?.[0] || '';
688
- renderItems.push({ type: 'group', groupName, items, sortKey: firstElementName.toLowerCase() });
753
+ renderItems.push({ type: 'group', groupName, items, sortKey: groupName.toLowerCase() });
689
754
  }
690
755
 
691
756
  // Sort all items alphabetically by sortKey
@@ -697,8 +762,11 @@ export class WccSidebar extends DeesElement {
697
762
  return this.renderElementItem(item.entry, section);
698
763
  } else {
699
764
  return html`
700
- <div class="item-group">
701
- <span class="item-group-legend">${item.groupName}</span>
765
+ <div class="item-group ${this.isGroupFilterMatch(item.groupName) ? 'group-filter-match' : ''}" data-group="${item.groupName}">
766
+ <span
767
+ class="item-group-legend"
768
+ @contextmenu=${(e: MouseEvent) => this.showGroupContextMenu(e, item.groupName)}
769
+ >${item.groupName}</span>
702
770
  ${item.items.map((entry) => this.renderElementItem(entry, section))}
703
771
  </div>
704
772
  `;
@@ -753,6 +821,7 @@ export class WccSidebar extends DeesElement {
753
821
  `;
754
822
  } else {
755
823
  // Single demo element
824
+ const icon = this.getElementGroups(item).length > 1 ? 'library_books' : 'featured_video';
756
825
  return html`
757
826
  <div
758
827
  class="selectOption ${isSelected ? 'selected' : ''} ${isPinned ? 'pinned' : ''}"
@@ -762,7 +831,7 @@ export class WccSidebar extends DeesElement {
762
831
  }}
763
832
  @contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, elementName)}
764
833
  >
765
- <i class="material-symbols-outlined">featured_video</i>
834
+ <i class="material-symbols-outlined">${icon}</i>
766
835
  <div class="text">${this.highlightMatch(elementName)}</div>
767
836
  </div>
768
837
  `;
@@ -810,6 +879,37 @@ export class WccSidebar extends DeesElement {
810
879
  return name.toLowerCase().includes(this.searchQuery.toLowerCase());
811
880
  }
812
881
 
882
+ private isGroupFilterMatch(groupName: string): boolean {
883
+ return !!this.searchQuery && groupName.toLowerCase() === this.searchQuery.toLowerCase();
884
+ }
885
+
886
+ private getElementGroups(item: any): string[] {
887
+ const raw = item?.demoGroups;
888
+ if (!raw) return [];
889
+ return Array.isArray(raw) ? raw : [raw];
890
+ }
891
+
892
+ private scrollToGroup(sectionName: string, groupName: string) {
893
+ // Ensure the section is not collapsed
894
+ this.collapsedSections.delete(sectionName);
895
+ // Clear any active search so all groups are visible
896
+ this.searchQuery = '';
897
+ this.requestUpdate();
898
+
899
+ // After render, scroll to the group element
900
+ this.updateComplete.then(() => {
901
+ const groupEl = this.shadowRoot?.querySelector(
902
+ `.item-group[data-group="${groupName}"]`
903
+ );
904
+ if (groupEl) {
905
+ groupEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
906
+ // Brief highlight flash
907
+ groupEl.classList.add('group-highlight');
908
+ setTimeout(() => groupEl.classList.remove('group-highlight'), 1500);
909
+ }
910
+ });
911
+ }
912
+
813
913
  private highlightMatch(text: string): TemplateResult {
814
914
  if (!this.searchQuery) return html`${text}`;
815
915
  const lowerText = text.toLowerCase();