Compare commits

...

4 Commits

Author SHA1 Message Date
0e816379a5 v3.8.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-27 09:03:10 +00:00
aa2c065918 feat(sidebar): rename demoGroup to demoGroups, add multi-group support, search by group name, and context menu group navigation 2026-01-27 09:03:10 +00:00
a778ad6855 v3.7.1
Some checks failed
Default (tags) / security (push) Failing after 13s
Default (tags) / test (push) Failing after 15s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-04 17:07:41 +00:00
24a1f064ba fix(sidebar): increase scrolled sidebar header box-shadow intensity and size to improve visual separation 2026-01-04 17:07:41 +00:00
13 changed files with 1666 additions and 1798 deletions

View File

@@ -1,5 +1,34 @@
# Changelog # Changelog
## 2026-01-27 - 3.8.0 - feat(sidebar)
rename demoGroup to demoGroups, add multi-group support, search by group name, and context menu group navigation
- Static property demoGroup renamed to demoGroups; accepts string | string[] for multi-group membership
- Elements with multiple demoGroups appear in each group's sidebar section simultaneously and show the library_books icon instead of featured_video
- Sidebar search now matches group names in addition to element names; groups are sorted alphabetically by group name
- Context menu for elements includes a "Show in Group:" section with navigable group entries that scroll to and briefly highlight the target group; group headers also have a context menu entry to filter by that group
- Added data-group attribute on .item-group for DOM querying and visual classes for group highlight and filter match
- Updated test elements to use demoGroups and updated docs/changelog/readme.hints to document the new behavior
- Bumped several devDependencies (@api.global/typedserver, @git.zone/tsbuild, @git.zone/tsbundle, @git.zone/tstest, @git.zone/tswatch, @types/node) and adjusted npm script `watch` to use tswatch
## 2026-01-27 - 3.8.0 - feat(sidebar)
rename demoGroup to demoGroups, add multi-group support, search by group name, and context menu group navigation
- Rename static property `demoGroup` to `demoGroups` on element classes; accepts `string | string[]` for multi-group membership
- Elements with an array of `demoGroups` appear in each group's sidebar section simultaneously
- Search now matches group names in addition to element names (e.g. searching "Buttons" shows all elements in the Buttons group)
- Groups are sorted alphabetically by group name instead of by first element name
- Elements belonging to multiple groups display a `library_books` icon instead of `featured_video`
- Right-click context menu on elements with groups shows "Show in Group:" section with navigable group entries
- Clicking a group name in the context menu scrolls to and briefly highlights that group in the sidebar
- Updated test elements (test-button-primary, test-button-secondary, test-button-danger, test-input-text, test-input-checkbox) to use `demoGroups`
## 2026-01-04 - 3.7.1 - fix(sidebar)
increase scrolled sidebar header box-shadow intensity and size to improve visual separation
- Changed .sidebar-header.scrolled box-shadow from `0 4px 12px -2px rgba(0, 0, 0, 0.4)` to `0 8px 24px -2px rgba(0, 0, 0, 1)`
- File modified: ts_web/elements/wcc-sidebar.ts — stronger, larger, and fully opaque shadow for better contrast when scrolled
## 2026-01-04 - 3.7.0 - feat(wcc-sidebar) ## 2026-01-04 - 3.7.0 - feat(wcc-sidebar)
add header shadow and scrolled state for sidebar menu to show elevation when content is scrolled add header shadow and scrolled state for sidebar menu to show elevation when content is scrolled

View File

@@ -35,5 +35,8 @@
}, },
"@ship.zone/szci": { "@ship.zone/szci": {
"npmGlobalTools": [] "npmGlobalTools": []
},
"@git.zone/tswatch": {
"preset": "element"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@design.estate/dees-wcctools", "name": "@design.estate/dees-wcctools",
"version": "3.7.0", "version": "3.8.0",
"private": false, "private": false,
"description": "A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.", "description": "A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.",
"exports": { "exports": {
@@ -11,7 +11,7 @@
"scripts": { "scripts": {
"test": "(npm run build)", "test": "(npm run build)",
"build": "(tsbuild tsfolders --allowimplicitany && tsbundle element)", "build": "(tsbuild tsfolders --allowimplicitany && tsbundle element)",
"watch": "tswatch element", "watch": "tswatch",
"buildDocs": "tsdoc" "buildDocs": "tsdoc"
}, },
"author": "Lossless GmbH", "author": "Lossless GmbH",
@@ -23,14 +23,14 @@
"lit": "^3.3.2" "lit": "^3.3.2"
}, },
"devDependencies": { "devDependencies": {
"@api.global/typedserver": "^8.1.0", "@api.global/typedserver": "^8.3.0",
"@git.zone/tsbuild": "^4.0.2", "@git.zone/tsbuild": "^4.1.2",
"@git.zone/tsbundle": "^2.6.3", "@git.zone/tsbundle": "^2.8.3",
"@git.zone/tsrun": "^2.0.1", "@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.1.4", "@git.zone/tstest": "^3.1.8",
"@git.zone/tswatch": "^2.3.13", "@git.zone/tswatch": "^3.0.1",
"@push.rocks/projectinfo": "^5.0.2", "@push.rocks/projectinfo": "^5.0.2",
"@types/node": "^25.0.3" "@types/node": "^25.0.10"
}, },
"files": [ "files": [
"ts/**/*", "ts/**/*",

3241
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -62,6 +62,33 @@ Section names are URL-encoded. Legacy routes (`element`/`page` as section name)
--- ---
## Element Demo Groups (2026-01-27)
### Overview
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.
### Usage
```typescript
// Single group
public static demoGroups = 'Buttons';
// Multiple groups — element appears in both
public static demoGroups = ['Buttons', 'Form Controls'];
```
### Features
- Search matches group names (searching "Buttons" shows all elements in that group)
- Groups sorted alphabetically by group name
- Multi-group elements show `library_books` icon instead of `featured_video`
- Context menu shows "Show in Group:" with clickable group entries that scroll to and highlight the group
- `data-group` attribute on `.item-group` containers for DOM querying
### Files Changed
- `ts_web/elements/wcc-sidebar.ts` — grouping logic, search filter, sort key, icon, context menu, scrollToGroup
- `test/elements/test-button-*.ts`, `test/elements/test-input-*.ts` — renamed `demoGroup``demoGroups`
---
## UI Redesign with Shadcn-like Styles (2025-06-27) ## UI Redesign with Shadcn-like Styles (2025-06-27)
### Changes Made ### Changes Made

View File

@@ -5,7 +5,7 @@ export * from './test-withwrapper.js';
export * from './test-edgecases.js'; export * from './test-edgecases.js';
export * from './test-nested.js'; export * from './test-nested.js';
// Grouped elements to demo the demoGroup feature // Grouped elements to demo the demoGroups feature
export * from './test-button-primary.js'; export * from './test-button-primary.js';
export * from './test-button-secondary.js'; export * from './test-button-secondary.js';
export * from './test-button-danger.js'; export * from './test-button-danger.js';

View File

@@ -9,7 +9,7 @@ import {
@customElement('test-button-danger') @customElement('test-button-danger')
export class TestButtonDanger extends DeesElement { export class TestButtonDanger extends DeesElement {
// Same group as other buttons // Same group as other buttons
public static demoGroup = 'Buttons'; public static demoGroups = 'Buttons';
public static demo = () => html` public static demo = () => html`
<test-button-danger>Delete</test-button-danger> <test-button-danger>Delete</test-button-danger>

View File

@@ -9,7 +9,7 @@ import {
@customElement('test-button-primary') @customElement('test-button-primary')
export class TestButtonPrimary extends DeesElement { export class TestButtonPrimary extends DeesElement {
// This groups the element with other "Buttons" in the sidebar // This groups the element with other "Buttons" in the sidebar
public static demoGroup = 'Buttons'; public static demoGroups = 'Buttons';
public static demo = () => html` public static demo = () => html`
<test-button-primary>Click Me</test-button-primary> <test-button-primary>Click Me</test-button-primary>

View File

@@ -9,7 +9,7 @@ import {
@customElement('test-button-secondary') @customElement('test-button-secondary')
export class TestButtonSecondary extends DeesElement { export class TestButtonSecondary extends DeesElement {
// Same group as test-button-primary - they'll appear together // Same group as test-button-primary - they'll appear together
public static demoGroup = 'Buttons'; public static demoGroups = 'Buttons';
public static demo = () => html` public static demo = () => html`
<test-button-secondary>Secondary Action</test-button-secondary> <test-button-secondary>Secondary Action</test-button-secondary>

View File

@@ -9,7 +9,7 @@ import {
@customElement('test-input-checkbox') @customElement('test-input-checkbox')
export class TestInputCheckbox extends DeesElement { export class TestInputCheckbox extends DeesElement {
// Same group as test-input-text // Same group as test-input-text
public static demoGroup = 'Inputs'; public static demoGroups = ['Inputs', 'A Second Group'];
public static demo = () => html` public static demo = () => html`
<test-input-checkbox label="Accept terms and conditions"></test-input-checkbox> <test-input-checkbox label="Accept terms and conditions"></test-input-checkbox>

View File

@@ -9,7 +9,7 @@ import {
@customElement('test-input-text') @customElement('test-input-text')
export class TestInputText extends DeesElement { export class TestInputText extends DeesElement {
// Different group - "Inputs" // Different group - "Inputs"
public static demoGroup = 'Inputs'; public static demoGroups = 'Inputs';
public static demo = () => html` public static demo = () => html`
<test-input-text placeholder="Enter text..."></test-input-text> <test-input-text placeholder="Enter text..."></test-input-text>

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@design.estate/dees-wcctools', name: '@design.estate/dees-wcctools',
version: '3.7.0', version: '3.8.0',
description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.' description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.'
} }

View File

@@ -4,7 +4,7 @@ import { WccDashboard, getSectionItems } from './wcc-dashboard.js';
import type { TTemplateFactory } from './wcctools.helpers.js'; import type { TTemplateFactory } from './wcctools.helpers.js';
import { getDemoCount, hasMultipleDemos } from './wcctools.helpers.js'; import { getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
import type { IWccSection, TElementType } from '../wcctools.interfaces.js'; import type { IWccSection, TElementType } from '../wcctools.interfaces.js';
import { WccContextmenu } from './wcc-contextmenu.js'; import { WccContextmenu, type IContextMenuItem } from './wcc-contextmenu.js';
@customElement('wcc-sidebar') @customElement('wcc-sidebar')
export class WccSidebar extends DeesElement { export class WccSidebar extends DeesElement {
@@ -107,7 +107,7 @@ export class WccSidebar extends DeesElement {
} }
.sidebar-header.scrolled { .sidebar-header.scrolled {
box-shadow: 0 4px 12px -2px rgba(0, 0, 0, 0.4); box-shadow: 0 8px 24px -2px rgba(0, 0, 0, 1);
border-bottom-color: var(--border); border-bottom-color: var(--border);
} }
@@ -422,6 +422,7 @@ export class WccSidebar extends DeesElement {
color: #555; color: #555;
padding: 0.125rem 0.625rem 0.25rem; padding: 0.125rem 0.625rem 0.25rem;
display: block; display: block;
cursor: context-menu;
} }
.item-group .selectOption { .item-group .selectOption {
@@ -429,6 +430,15 @@ export class WccSidebar extends DeesElement {
margin-right: 0.25rem; margin-right: 0.25rem;
} }
.item-group.group-highlight {
background: rgba(59, 130, 246, 0.15);
transition: background 0.3s ease;
}
.item-group.group-filter-match {
border-color: rgba(245, 158, 11, 0.5);
}
/* Resize handle */ /* Resize handle */
.resize-handle { .resize-handle {
position: absolute; position: absolute;
@@ -517,12 +527,49 @@ export class WccSidebar extends DeesElement {
private showContextMenu(e: MouseEvent, sectionName: string, itemName: string) { private showContextMenu(e: MouseEvent, sectionName: string, itemName: string) {
const isPinned = this.isPinned(sectionName, itemName); const isPinned = this.isPinned(sectionName, itemName);
WccContextmenu.show(e, [ const section = this.dashboardRef?.sections?.find(s => s.name === sectionName);
const sectionEntries = section ? getSectionItems(section) : [];
const foundEntry = sectionEntries.find(([name]) => name === itemName);
const item = foundEntry?.[1];
const groups = item ? this.getElementGroups(item) : [];
const menuItems: IContextMenuItem[] = [
{ {
name: isPinned ? 'Unpin' : 'Pin', name: isPinned ? 'Unpin' : 'Pin',
iconName: isPinned ? 'push_pin' : 'push_pin', iconName: isPinned ? 'push_pin' : 'push_pin',
action: () => this.togglePin(sectionName, itemName), action: () => this.togglePin(sectionName, itemName),
}, },
];
if (groups.length > 0) {
menuItems.push({
name: 'Show in Group:',
iconName: 'folder',
action: () => {},
disabled: true,
});
for (const groupName of groups) {
menuItems.push({
name: groupName,
iconName: 'label',
action: () => this.scrollToGroup(sectionName, groupName),
});
}
}
WccContextmenu.show(e, menuItems);
}
private showGroupContextMenu(e: MouseEvent, groupName: string) {
WccContextmenu.show(e, [
{
name: `Show "${groupName}"`,
iconName: 'filter_alt',
action: () => {
this.searchQuery = groupName;
this.dispatchEvent(new CustomEvent('searchChanged', { detail: this.searchQuery }));
},
},
]); ]);
} }
@@ -569,7 +616,9 @@ export class WccSidebar extends DeesElement {
${pinnedEntries.map(({ sectionName, itemName, item, section }) => { ${pinnedEntries.map(({ sectionName, itemName, item, section }) => {
const isSelected = this.selectedItem === item; const isSelected = this.selectedItem === item;
const type = section.type === 'elements' ? 'element' : 'page'; const type = section.type === 'elements' ? 'element' : 'page';
const icon = section.type === 'elements' ? 'featured_video' : 'insert_drive_file'; const icon = section.type === 'elements'
? (this.getElementGroups(item).length > 1 ? 'library_books' : 'featured_video')
: 'insert_drive_file';
return html` return html`
<div <div
@@ -603,7 +652,13 @@ export class WccSidebar extends DeesElement {
return this.dashboardRef.sections.map((section) => { return this.dashboardRef.sections.map((section) => {
// Check if section has any matching items // Check if section has any matching items
const entries = getSectionItems(section); const entries = getSectionItems(section);
const filteredEntries = entries.filter(([name]) => this.matchesSearch(name)); const filteredEntries = entries.filter(([name, item]) => {
if (this.matchesSearch(name)) return true;
const rawGroups = (item as any).demoGroups;
if (!rawGroups) return false;
const groups: string[] = Array.isArray(rawGroups) ? rawGroups : [rawGroups];
return groups.some(g => this.matchesSearch(g));
});
// Hide section if no items match the search // Hide section if no items match the search
if (filteredEntries.length === 0 && this.searchQuery) { if (filteredEntries.length === 0 && this.searchQuery) {
@@ -635,7 +690,13 @@ export class WccSidebar extends DeesElement {
private renderSectionItems(section: IWccSection) { private renderSectionItems(section: IWccSection) {
const entries = getSectionItems(section); const entries = getSectionItems(section);
// Filter entries by search query // Filter entries by search query
const filteredEntries = entries.filter(([name]) => this.matchesSearch(name)); const filteredEntries = entries.filter(([name, item]) => {
if (this.matchesSearch(name)) return true;
const rawGroups = (item as any).demoGroups;
if (!rawGroups) return false;
const groups: string[] = Array.isArray(rawGroups) ? rawGroups : [rawGroups];
return groups.some(g => this.matchesSearch(g));
});
if (section.type === 'pages') { if (section.type === 'pages') {
return filteredEntries.map(([pageName, item]) => { return filteredEntries.map(([pageName, item]) => {
@@ -655,17 +716,22 @@ export class WccSidebar extends DeesElement {
`; `;
}); });
} else { } else {
// type === 'elements' - group by demoGroup // type === 'elements' - group by demoGroups (supports string | string[])
const groupedItems = new Map<string | null, Array<[string, any]>>(); const groupedItems = new Map<string | null, Array<[string, any]>>();
for (const entry of filteredEntries) { for (const entry of filteredEntries) {
const [, item] = entry; const [, item] = entry;
const group = (item as any).demoGroup || null; const rawGroups = (item as any).demoGroups;
const groups: Array<string | null> = rawGroups
? (Array.isArray(rawGroups) ? rawGroups : [rawGroups])
: [null];
for (const group of groups) {
if (!groupedItems.has(group)) { if (!groupedItems.has(group)) {
groupedItems.set(group, []); groupedItems.set(group, []);
} }
groupedItems.get(group)!.push(entry); groupedItems.get(group)!.push(entry);
} }
}
// Build a unified list of render items (ungrouped elements and groups) // Build a unified list of render items (ungrouped elements and groups)
// Each item has a sortKey (element name or first element name of group) // Each item has a sortKey (element name or first element name of group)
@@ -681,11 +747,10 @@ export class WccSidebar extends DeesElement {
renderItems.push({ type: 'element', entry, sortKey: entry[0].toLowerCase() }); renderItems.push({ type: 'element', entry, sortKey: entry[0].toLowerCase() });
} }
// Add groups (sorted by their first element's name) // Add groups (sorted by group name)
for (const [groupName, items] of groupedItems) { for (const [groupName, items] of groupedItems) {
if (groupName === null) continue; if (groupName === null) continue;
const firstElementName = items[0]?.[0] || ''; renderItems.push({ type: 'group', groupName, items, sortKey: groupName.toLowerCase() });
renderItems.push({ type: 'group', groupName, items, sortKey: firstElementName.toLowerCase() });
} }
// Sort all items alphabetically by sortKey // Sort all items alphabetically by sortKey
@@ -697,8 +762,11 @@ export class WccSidebar extends DeesElement {
return this.renderElementItem(item.entry, section); return this.renderElementItem(item.entry, section);
} else { } else {
return html` return html`
<div class="item-group"> <div class="item-group ${this.isGroupFilterMatch(item.groupName) ? 'group-filter-match' : ''}" data-group="${item.groupName}">
<span class="item-group-legend">${item.groupName}</span> <span
class="item-group-legend"
@contextmenu=${(e: MouseEvent) => this.showGroupContextMenu(e, item.groupName)}
>${item.groupName}</span>
${item.items.map((entry) => this.renderElementItem(entry, section))} ${item.items.map((entry) => this.renderElementItem(entry, section))}
</div> </div>
`; `;
@@ -753,6 +821,7 @@ export class WccSidebar extends DeesElement {
`; `;
} else { } else {
// Single demo element // Single demo element
const icon = this.getElementGroups(item).length > 1 ? 'library_books' : 'featured_video';
return html` return html`
<div <div
class="selectOption ${isSelected ? 'selected' : ''} ${isPinned ? 'pinned' : ''}" class="selectOption ${isSelected ? 'selected' : ''} ${isPinned ? 'pinned' : ''}"
@@ -762,7 +831,7 @@ export class WccSidebar extends DeesElement {
}} }}
@contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, elementName)} @contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, elementName)}
> >
<i class="material-symbols-outlined">featured_video</i> <i class="material-symbols-outlined">${icon}</i>
<div class="text">${this.highlightMatch(elementName)}</div> <div class="text">${this.highlightMatch(elementName)}</div>
</div> </div>
`; `;
@@ -810,6 +879,37 @@ export class WccSidebar extends DeesElement {
return name.toLowerCase().includes(this.searchQuery.toLowerCase()); return name.toLowerCase().includes(this.searchQuery.toLowerCase());
} }
private isGroupFilterMatch(groupName: string): boolean {
return !!this.searchQuery && groupName.toLowerCase() === this.searchQuery.toLowerCase();
}
private getElementGroups(item: any): string[] {
const raw = item?.demoGroups;
if (!raw) return [];
return Array.isArray(raw) ? raw : [raw];
}
private scrollToGroup(sectionName: string, groupName: string) {
// Ensure the section is not collapsed
this.collapsedSections.delete(sectionName);
// Clear any active search so all groups are visible
this.searchQuery = '';
this.requestUpdate();
// After render, scroll to the group element
this.updateComplete.then(() => {
const groupEl = this.shadowRoot?.querySelector(
`.item-group[data-group="${groupName}"]`
);
if (groupEl) {
groupEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Brief highlight flash
groupEl.classList.add('group-highlight');
setTimeout(() => groupEl.classList.remove('group-highlight'), 1500);
}
});
}
private highlightMatch(text: string): TemplateResult { private highlightMatch(text: string): TemplateResult {
if (!this.searchQuery) return html`${text}`; if (!this.searchQuery) return html`${text}`;
const lowerText = text.toLowerCase(); const lowerText = text.toLowerCase();