@design.estate/dees-wcctools 3.7.1 → 3.8.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.
package/license CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2020 Lossless GmbH (hello@lossless.com)
1
+ Copyright (c) 2020 Task Venture Capital GmbH (hello@task.vc)
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to deal
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.1",
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": {
@@ -10,27 +10,27 @@
10
10
  "type": "module",
11
11
  "scripts": {
12
12
  "test": "(npm run build)",
13
- "build": "(tsbuild tsfolders --allowimplicitany && tsbundle element)",
14
- "watch": "tswatch element",
13
+ "build": "(tsbuild tsfolders --allowimplicitany && tsbundle)",
14
+ "watch": "tswatch",
15
15
  "buildDocs": "tsdoc"
16
16
  },
17
17
  "author": "Lossless GmbH",
18
18
  "license": "UNLICENSED",
19
19
  "dependencies": {
20
- "@design.estate/dees-domtools": "^2.3.7",
21
- "@design.estate/dees-element": "^2.1.5",
20
+ "@design.estate/dees-domtools": "^2.5.4",
21
+ "@design.estate/dees-element": "^2.2.4",
22
22
  "@push.rocks/smartdelay": "^3.0.5",
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",
29
- "@git.zone/tsrun": "^2.0.1",
30
- "@git.zone/tstest": "^3.1.4",
31
- "@git.zone/tswatch": "^2.3.13",
32
- "@push.rocks/projectinfo": "^5.0.2",
33
- "@types/node": "^25.0.3"
26
+ "@api.global/typedserver": "^8.4.6",
27
+ "@git.zone/tsbuild": "^4.4.0",
28
+ "@git.zone/tsbundle": "^2.10.0",
29
+ "@git.zone/tsrun": "^2.0.2",
30
+ "@git.zone/tstest": "^3.6.3",
31
+ "@git.zone/tswatch": "^3.3.2",
32
+ "@push.rocks/projectinfo": "^5.1.0",
33
+ "@types/node": "^25.6.0"
34
34
  },
35
35
  "files": [
36
36
  "ts/**/*",
@@ -41,7 +41,7 @@
41
41
  "dist_ts_web/**/*",
42
42
  "assets/**/*",
43
43
  "cli.js",
44
- "npmextra.json",
44
+ ".smartconfig.json",
45
45
  "readme.md"
46
46
  ],
47
47
  "browserslist": [
package/readme.hints.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Project Hints and Findings
2
2
 
3
+ ## TypeScript 6.0 & Build Tooling (2026-04-12)
4
+
5
+ ### TypeScript 6.0 Strict Defaults
6
+ TypeScript 6.0.2 (shipped with tsbuild 4.4.0) changes `strict` to `true` by default. The project explicitly sets `"strict": false` in tsconfig.json to preserve pre-TS6 behavior. Also added `"types": ["node"]` since TS6 changed `@types/*` auto-discovery.
7
+
8
+ ### Config Migration: npmextra.json → .smartconfig.json
9
+ Build tools (tsbundle 2.10.0, tswatch 3.3.2) now use `@push.rocks/smartconfig` which reads `.smartconfig.json` (with leading dot). The old `npmextra.json` was renamed.
10
+
11
+ ### tsbundle Configuration
12
+ The `tsbundle element` subcommand no longer exists. Instead, bundle configuration lives in `.smartconfig.json` under `"@git.zone/tsbundle"` with a `bundles` array. Entry point is `./html/index.ts` → `./dist_bundle/bundle.js`.
13
+
14
+ ---
15
+
3
16
  ## Section-based Configuration API (2025-12-27)
4
17
 
5
18
  ### Overview
@@ -62,6 +75,33 @@ Section names are URL-encoded. Legacy routes (`element`/`page` as section name)
62
75
 
63
76
  ---
64
77
 
78
+ ## Element Demo Groups (2026-01-27)
79
+
80
+ ### Overview
81
+ 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.
82
+
83
+ ### Usage
84
+ ```typescript
85
+ // Single group
86
+ public static demoGroups = 'Buttons';
87
+
88
+ // Multiple groups — element appears in both
89
+ public static demoGroups = ['Buttons', 'Form Controls'];
90
+ ```
91
+
92
+ ### Features
93
+ - Search matches group names (searching "Buttons" shows all elements in that group)
94
+ - Groups sorted alphabetically by group name
95
+ - Multi-group elements show `library_books` icon instead of `featured_video`
96
+ - Context menu shows "Show in Group:" with clickable group entries that scroll to and highlight the group
97
+ - `data-group` attribute on `.item-group` containers for DOM querying
98
+
99
+ ### Files Changed
100
+ - `ts_web/elements/wcc-sidebar.ts` — grouping logic, search filter, sort key, icon, context menu, scrollToGroup
101
+ - `test/elements/test-button-*.ts`, `test/elements/test-input-*.ts` — renamed `demoGroup` → `demoGroups`
102
+
103
+ ---
104
+
65
105
  ## UI Redesign with Shadcn-like Styles (2025-06-27)
66
106
 
67
107
  ### Changes Made
package/readme.md CHANGED
@@ -293,6 +293,25 @@ export class MyButton extends DeesElement {
293
293
 
294
294
  Each demo appears as a numbered item in an expandable folder in the sidebar.
295
295
 
296
+ ### 🗂️ Demo Groups
297
+
298
+ Organize elements into groups within a section for better discoverability:
299
+
300
+ ```typescript
301
+ @customElement('my-input')
302
+ export class MyInput extends DeesElement {
303
+ // Single group
304
+ public static demoGroups = 'Form Controls';
305
+
306
+ // Or multiple groups — element appears in each
307
+ public static demoGroups = ['Form Controls', 'Inputs'];
308
+
309
+ public static demo = () => html`<my-input></my-input>`;
310
+ }
311
+ ```
312
+
313
+ Groups appear as collapsible headers in the sidebar, sorted alphabetically. Searching matches group names too — searching "Form Controls" shows all elements in that group.
314
+
296
315
  ### ⏳ Async Demos
297
316
 
298
317
  Return a `Promise` from `demo` for async setup:
@@ -462,7 +481,7 @@ my-component-library/
462
481
 
463
482
  ## License and Legal Information
464
483
 
465
- This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
484
+ This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
466
485
 
467
486
  **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
468
487
 
@@ -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.1',
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
  }
@@ -22,7 +22,8 @@ export const getSectionItems = (section: IWccSection): Array<[string, any]> => {
22
22
 
23
23
  // Apply filter if provided
24
24
  if (section.filter) {
25
- entries = entries.filter(([name, item]) => section.filter(name, item));
25
+ const filterFn = section.filter;
26
+ entries = entries.filter(([name, item]) => filterFn(name, item));
26
27
  }
27
28
 
28
29
  // Apply sort if provided
@@ -43,13 +44,13 @@ export class WccDashboard extends DeesElement {
43
44
  accessor selectedSection: IWccSection | null = null;
44
45
 
45
46
  @property()
46
- accessor selectedType: TElementType;
47
+ accessor selectedType: TElementType = 'element';
47
48
 
48
49
  @property()
49
- accessor selectedItemName: string;
50
+ accessor selectedItemName: string = '';
50
51
 
51
52
  @property()
52
- accessor selectedItem: TTemplateFactory | DeesElement;
53
+ accessor selectedItem: TTemplateFactory | DeesElement | null = null;
53
54
 
54
55
  @property({ type: Number })
55
56
  accessor selectedDemoIndex: number = 0;
@@ -77,7 +78,7 @@ export class WccDashboard extends DeesElement {
77
78
  }
78
79
 
79
80
  @property()
80
- accessor warning: string = null;
81
+ accessor warning: string | null = null;
81
82
 
82
83
  private frameScrollY: number = 0;
83
84
  private sidebarScrollY: number = 0;
@@ -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();