Compare commits

...

6 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
203a53a45d v3.7.0
Some checks failed
Default (tags) / security (push) Failing after 13s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-04 17:02:15 +00:00
349d4ba320 feat(wcc-sidebar): add header shadow and scrolled state for sidebar menu to show elevation when content is scrolled 2026-01-04 17:02:15 +00:00
13 changed files with 1693 additions and 1799 deletions

View File

@@ -1,5 +1,42 @@
# 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)
add header shadow and scrolled state for sidebar menu to show elevation when content is scrolled
- Introduce isMenuScrolled state to track whether the menu has been scrolled
- Add handleMenuScroll handler and bind it to the menu scroll event
- Apply a 'scrolled' class to .sidebar-header to add box-shadow and border-bottom color with transitions
- Update template to conditionally add scrolled class and attach scroll listener
## 2026-01-04 - 3.6.2 - fix(wcc-sidebar) ## 2026-01-04 - 3.6.2 - fix(wcc-sidebar)
use sidebar's internal .menu element for scroll management and expose scrollableContainer getter use sidebar's internal .menu element for scroll management and expose scrollableContainer getter

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.6.2", "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.6.2', 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 {
@@ -48,6 +48,10 @@ export class WccSidebar extends DeesElement {
@state() @state()
accessor isHidden: boolean = false; accessor isHidden: boolean = false;
// Track if menu is scrolled for header shadow
@state()
accessor isMenuScrolled: boolean = false;
private sectionsInitialized = false; private sectionsInitialized = false;
/** /**
@@ -96,6 +100,15 @@ export class WccSidebar extends DeesElement {
.sidebar-header { .sidebar-header {
flex-shrink: 0; flex-shrink: 0;
transition: box-shadow 0.2s ease, border-color 0.2s ease;
border-bottom: 1px solid transparent;
position: relative;
z-index: 1;
}
.sidebar-header.scrolled {
box-shadow: 0 8px 24px -2px rgba(0, 0, 0, 1);
border-bottom-color: var(--border);
} }
.menu { .menu {
@@ -409,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 {
@@ -416,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;
@@ -437,7 +460,7 @@ export class WccSidebar extends DeesElement {
background: var(--primary); background: var(--primary);
} }
</style> </style>
<div class="sidebar-header"> <div class="sidebar-header ${this.isMenuScrolled ? 'scrolled' : ''}">
<div class="search-container"> <div class="search-container">
<input <input
type="text" type="text"
@@ -454,7 +477,7 @@ export class WccSidebar extends DeesElement {
</div> </div>
${this.renderPinnedSection()} ${this.renderPinnedSection()}
</div> </div>
<div class="menu"> <div class="menu" @scroll=${this.handleMenuScroll}>
${this.renderSections()} ${this.renderSections()}
</div> </div>
<div <div
@@ -504,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 }));
},
},
]); ]);
} }
@@ -556,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
@@ -590,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) {
@@ -622,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]) => {
@@ -642,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)
@@ -668,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
@@ -684,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>
`; `;
@@ -740,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' : ''}"
@@ -749,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>
`; `;
@@ -787,11 +869,47 @@ export class WccSidebar extends DeesElement {
this.dispatchEvent(new CustomEvent('searchChanged', { detail: this.searchQuery })); this.dispatchEvent(new CustomEvent('searchChanged', { detail: this.searchQuery }));
} }
private handleMenuScroll(e: Event) {
const target = e.target as HTMLElement;
this.isMenuScrolled = target.scrollTop > 0;
}
private matchesSearch(name: string): boolean { private matchesSearch(name: string): boolean {
if (!this.searchQuery) return true; if (!this.searchQuery) return true;
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();