Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 02e1f536d5 | |||
| a7f5341baa | |||
| 3499652622 | |||
| ee8b5dc3ff | |||
| 0e816379a5 | |||
| aa2c065918 | |||
| a778ad6855 | |||
| 24a1f064ba | |||
| 203a53a45d | |||
| 349d4ba320 | |||
| 399ef3d508 |
@@ -35,5 +35,18 @@
|
||||
},
|
||||
"@ship.zone/szci": {
|
||||
"npmGlobalTools": []
|
||||
},
|
||||
"@git.zone/tsbundle": {
|
||||
"bundles": [
|
||||
{
|
||||
"from": "./html/index.ts",
|
||||
"to": "./dist_bundle/bundle.js",
|
||||
"outputMode": "bundle",
|
||||
"bundler": "esbuild"
|
||||
}
|
||||
]
|
||||
},
|
||||
"@git.zone/tswatch": {
|
||||
"preset": "element"
|
||||
}
|
||||
}
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["/npmextra.json"],
|
||||
"fileMatch": ["/.smartconfig.json"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
59
changelog.md
59
changelog.md
@@ -1,5 +1,64 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-04-12 - 3.8.2 - fix(sidebar)
|
||||
restore search input focus after clearing the sidebar search
|
||||
|
||||
- Updates the sidebar clearSearch behavior to focus the .search-input element after resetting the query and dispatching searchChanged.
|
||||
- Improves search usability by letting users continue typing immediately after clearing the current search.
|
||||
|
||||
## 2026-04-12 - 3.8.1 - fix(build)
|
||||
migrate smart config and update build tooling for latest tsbundle and TypeScript defaults
|
||||
|
||||
- rename npmextra.json to .smartconfig.json and add tsbundle bundle configuration
|
||||
- update build script to use the new tsbundle invocation without the removed element subcommand
|
||||
- adjust tsconfig and dashboard property defaults for newer TypeScript compiler behavior
|
||||
- refresh package dependencies and include the new config file in published package contents
|
||||
|
||||
## 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)
|
||||
use sidebar's internal .menu element for scroll management and expose scrollableContainer getter
|
||||
|
||||
- Add public scrollableContainer getter to wcc-sidebar that returns the .menu element for external scroll control
|
||||
- Update wcc-dashboard to query wcc-sidebar as WccSidebar and attach scroll listeners to sidebar.scrollableContainer instead of the host element
|
||||
- Restore sidebar scroll position by setting scrollTop on the scrollableContainer when applying saved positions
|
||||
- TypeScript casting added to avoid nullable/implicit any issues when querying the sidebar element
|
||||
|
||||
## 2026-01-04 - 3.6.1 - fix(wcc-sidebar)
|
||||
sort sidebar items alphabetically and unify grouped and ungrouped items for consistent ordering
|
||||
|
||||
|
||||
2
license
2
license
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2020 Lossless GmbH (hello@lossless.com)
|
||||
Copyright (c) 2020 Task Venture Capital GmbH (hello@task.vc)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
28
package.json
28
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@design.estate/dees-wcctools",
|
||||
"version": "3.6.1",
|
||||
"version": "3.8.2",
|
||||
"private": false,
|
||||
"description": "A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.",
|
||||
"exports": {
|
||||
@@ -10,27 +10,27 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "(npm run build)",
|
||||
"build": "(tsbuild tsfolders --allowimplicitany && tsbundle element)",
|
||||
"watch": "tswatch element",
|
||||
"build": "(tsbuild tsfolders --allowimplicitany && tsbundle)",
|
||||
"watch": "tswatch",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"author": "Lossless GmbH",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@design.estate/dees-domtools": "^2.3.7",
|
||||
"@design.estate/dees-element": "^2.1.5",
|
||||
"@design.estate/dees-domtools": "^2.5.4",
|
||||
"@design.estate/dees-element": "^2.2.4",
|
||||
"@push.rocks/smartdelay": "^3.0.5",
|
||||
"lit": "^3.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@api.global/typedserver": "^8.1.0",
|
||||
"@git.zone/tsbuild": "^4.0.2",
|
||||
"@git.zone/tsbundle": "^2.6.3",
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@git.zone/tstest": "^3.1.4",
|
||||
"@git.zone/tswatch": "^2.3.13",
|
||||
"@push.rocks/projectinfo": "^5.0.2",
|
||||
"@types/node": "^25.0.3"
|
||||
"@api.global/typedserver": "^8.4.6",
|
||||
"@git.zone/tsbuild": "^4.4.0",
|
||||
"@git.zone/tsbundle": "^2.10.0",
|
||||
"@git.zone/tsrun": "^2.0.2",
|
||||
"@git.zone/tstest": "^3.6.3",
|
||||
"@git.zone/tswatch": "^3.3.2",
|
||||
"@push.rocks/projectinfo": "^5.1.0",
|
||||
"@types/node": "^25.6.0"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@@ -41,7 +41,7 @@
|
||||
"dist_ts_web/**/*",
|
||||
"assets/**/*",
|
||||
"cli.js",
|
||||
"npmextra.json",
|
||||
".smartconfig.json",
|
||||
"readme.md"
|
||||
],
|
||||
"browserslist": [
|
||||
|
||||
6277
pnpm-lock.yaml
generated
6277
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,18 @@
|
||||
# Project Hints and Findings
|
||||
|
||||
## TypeScript 6.0 & Build Tooling (2026-04-12)
|
||||
|
||||
### TypeScript 6.0 Strict Defaults
|
||||
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.
|
||||
|
||||
### Config Migration: npmextra.json → .smartconfig.json
|
||||
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.
|
||||
|
||||
### tsbundle Configuration
|
||||
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`.
|
||||
|
||||
---
|
||||
|
||||
## Section-based Configuration API (2025-12-27)
|
||||
|
||||
### Overview
|
||||
@@ -62,6 +75,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)
|
||||
|
||||
### Changes Made
|
||||
|
||||
21
readme.md
21
readme.md
@@ -293,6 +293,25 @@ export class MyButton extends DeesElement {
|
||||
|
||||
Each demo appears as a numbered item in an expandable folder in the sidebar.
|
||||
|
||||
### 🗂️ Demo Groups
|
||||
|
||||
Organize elements into groups within a section for better discoverability:
|
||||
|
||||
```typescript
|
||||
@customElement('my-input')
|
||||
export class MyInput extends DeesElement {
|
||||
// Single group
|
||||
public static demoGroups = 'Form Controls';
|
||||
|
||||
// Or multiple groups — element appears in each
|
||||
public static demoGroups = ['Form Controls', 'Inputs'];
|
||||
|
||||
public static demo = () => html`<my-input></my-input>`;
|
||||
}
|
||||
```
|
||||
|
||||
Groups appear as collapsible headers in the sidebar, sorted alphabetically. Searching matches group names too — searching "Form Controls" shows all elements in that group.
|
||||
|
||||
### ⏳ Async Demos
|
||||
|
||||
Return a `Promise` from `demo` for async setup:
|
||||
@@ -462,7 +481,7 @@ my-component-library/
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
|
||||
|
||||
**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.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ export * from './test-withwrapper.js';
|
||||
export * from './test-edgecases.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-secondary.js';
|
||||
export * from './test-button-danger.js';
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
@customElement('test-button-danger')
|
||||
export class TestButtonDanger extends DeesElement {
|
||||
// Same group as other buttons
|
||||
public static demoGroup = 'Buttons';
|
||||
public static demoGroups = 'Buttons';
|
||||
|
||||
public static demo = () => html`
|
||||
<test-button-danger>Delete</test-button-danger>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
@customElement('test-button-primary')
|
||||
export class TestButtonPrimary extends DeesElement {
|
||||
// This groups the element with other "Buttons" in the sidebar
|
||||
public static demoGroup = 'Buttons';
|
||||
public static demoGroups = 'Buttons';
|
||||
|
||||
public static demo = () => html`
|
||||
<test-button-primary>Click Me</test-button-primary>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
@customElement('test-button-secondary')
|
||||
export class TestButtonSecondary extends DeesElement {
|
||||
// Same group as test-button-primary - they'll appear together
|
||||
public static demoGroup = 'Buttons';
|
||||
public static demoGroups = 'Buttons';
|
||||
|
||||
public static demo = () => html`
|
||||
<test-button-secondary>Secondary Action</test-button-secondary>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
@customElement('test-input-checkbox')
|
||||
export class TestInputCheckbox extends DeesElement {
|
||||
// Same group as test-input-text
|
||||
public static demoGroup = 'Inputs';
|
||||
public static demoGroups = ['Inputs', 'A Second Group'];
|
||||
|
||||
public static demo = () => html`
|
||||
<test-input-checkbox label="Accept terms and conditions"></test-input-checkbox>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
@customElement('test-input-text')
|
||||
export class TestInputText extends DeesElement {
|
||||
// Different group - "Inputs"
|
||||
public static demoGroup = 'Inputs';
|
||||
public static demoGroups = 'Inputs';
|
||||
|
||||
public static demo = () => html`
|
||||
<test-input-text placeholder="Enter text..."></test-input-text>
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@design.estate/dees-wcctools',
|
||||
version: '3.6.1',
|
||||
version: '3.8.2',
|
||||
description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.'
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import './wcc-properties.js';
|
||||
import { type TTheme } from './wcc-properties.js';
|
||||
import { breakpoints } from '@design.estate/dees-domtools';
|
||||
import { WccFrame } from './wcc-frame.js';
|
||||
import { WccSidebar } from './wcc-sidebar.js';
|
||||
|
||||
/**
|
||||
* Get filtered and sorted items from a section
|
||||
@@ -21,7 +22,8 @@ export const getSectionItems = (section: IWccSection): Array<[string, any]> => {
|
||||
|
||||
// Apply filter if provided
|
||||
if (section.filter) {
|
||||
entries = entries.filter(([name, item]) => section.filter(name, item));
|
||||
const filterFn = section.filter;
|
||||
entries = entries.filter(([name, item]) => filterFn(name, item));
|
||||
}
|
||||
|
||||
// Apply sort if provided
|
||||
@@ -42,13 +44,13 @@ export class WccDashboard extends DeesElement {
|
||||
accessor selectedSection: IWccSection | null = null;
|
||||
|
||||
@property()
|
||||
accessor selectedType: TElementType;
|
||||
accessor selectedType: TElementType = 'element';
|
||||
|
||||
@property()
|
||||
accessor selectedItemName: string;
|
||||
accessor selectedItemName: string = '';
|
||||
|
||||
@property()
|
||||
accessor selectedItem: TTemplateFactory | DeesElement;
|
||||
accessor selectedItem: TTemplateFactory | DeesElement | null = null;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor selectedDemoIndex: number = 0;
|
||||
@@ -76,7 +78,7 @@ export class WccDashboard extends DeesElement {
|
||||
}
|
||||
|
||||
@property()
|
||||
accessor warning: string = null;
|
||||
accessor warning: string | null = null;
|
||||
|
||||
private frameScrollY: number = 0;
|
||||
private sidebarScrollY: number = 0;
|
||||
@@ -491,10 +493,10 @@ export class WccDashboard extends DeesElement {
|
||||
if (this.scrollListenersAttached) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const wccFrame = await this.wccFrame;
|
||||
const wccSidebar = this.shadowRoot.querySelector('wcc-sidebar');
|
||||
|
||||
const wccSidebar = this.shadowRoot.querySelector('wcc-sidebar') as WccSidebar | null;
|
||||
|
||||
if (wccFrame) {
|
||||
// The frame element itself is the scrollable container
|
||||
wccFrame.addEventListener('scroll', () => {
|
||||
@@ -505,11 +507,14 @@ export class WccDashboard extends DeesElement {
|
||||
}
|
||||
|
||||
if (wccSidebar) {
|
||||
// The sidebar element itself is the scrollable container
|
||||
wccSidebar.addEventListener('scroll', () => {
|
||||
this.sidebarScrollY = wccSidebar.scrollTop;
|
||||
this.debouncedScrollUpdate();
|
||||
});
|
||||
// Use the sidebar's scrollable container (.menu element)
|
||||
const scrollContainer = wccSidebar.scrollableContainer;
|
||||
if (scrollContainer) {
|
||||
scrollContainer.addEventListener('scroll', () => {
|
||||
this.sidebarScrollY = scrollContainer.scrollTop;
|
||||
this.debouncedScrollUpdate();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,18 +560,21 @@ export class WccDashboard extends DeesElement {
|
||||
if (this.scrollPositionsApplied) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const wccFrame = await this.wccFrame;
|
||||
const wccSidebar = this.shadowRoot.querySelector('wcc-sidebar');
|
||||
|
||||
const wccSidebar = this.shadowRoot.querySelector('wcc-sidebar') as WccSidebar | null;
|
||||
|
||||
if (wccFrame && this.frameScrollY > 0) {
|
||||
// The frame element itself is the scrollable container
|
||||
wccFrame.scrollTop = this.frameScrollY;
|
||||
}
|
||||
|
||||
if (wccSidebar && this.sidebarScrollY > 0) {
|
||||
// The sidebar element itself is the scrollable container
|
||||
wccSidebar.scrollTop = this.sidebarScrollY;
|
||||
// Use the sidebar's scrollable container (.menu element)
|
||||
const scrollContainer = wccSidebar.scrollableContainer;
|
||||
if (scrollContainer) {
|
||||
scrollContainer.scrollTop = this.sidebarScrollY;
|
||||
}
|
||||
}
|
||||
|
||||
this.scrollPositionsApplied = true;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { WccDashboard, getSectionItems } from './wcc-dashboard.js';
|
||||
import type { TTemplateFactory } from './wcctools.helpers.js';
|
||||
import { getDemoCount, hasMultipleDemos } from './wcctools.helpers.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')
|
||||
export class WccSidebar extends DeesElement {
|
||||
@@ -48,8 +48,19 @@ export class WccSidebar extends DeesElement {
|
||||
@state()
|
||||
accessor isHidden: boolean = false;
|
||||
|
||||
// Track if menu is scrolled for header shadow
|
||||
@state()
|
||||
accessor isMenuScrolled: boolean = false;
|
||||
|
||||
private sectionsInitialized = false;
|
||||
|
||||
/**
|
||||
* Returns the scrollable container element (.menu) for external scroll management
|
||||
*/
|
||||
public get scrollableContainer(): HTMLElement | null {
|
||||
return this.shadowRoot?.querySelector('.menu') as HTMLElement | null;
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet" />
|
||||
@@ -89,6 +100,15 @@ export class WccSidebar extends DeesElement {
|
||||
|
||||
.sidebar-header {
|
||||
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 {
|
||||
@@ -402,6 +422,7 @@ export class WccSidebar extends DeesElement {
|
||||
color: #555;
|
||||
padding: 0.125rem 0.625rem 0.25rem;
|
||||
display: block;
|
||||
cursor: context-menu;
|
||||
}
|
||||
|
||||
.item-group .selectOption {
|
||||
@@ -409,6 +430,15 @@ export class WccSidebar extends DeesElement {
|
||||
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 {
|
||||
position: absolute;
|
||||
@@ -430,7 +460,7 @@ export class WccSidebar extends DeesElement {
|
||||
background: var(--primary);
|
||||
}
|
||||
</style>
|
||||
<div class="sidebar-header">
|
||||
<div class="sidebar-header ${this.isMenuScrolled ? 'scrolled' : ''}">
|
||||
<div class="search-container">
|
||||
<input
|
||||
type="text"
|
||||
@@ -447,7 +477,7 @@ export class WccSidebar extends DeesElement {
|
||||
</div>
|
||||
${this.renderPinnedSection()}
|
||||
</div>
|
||||
<div class="menu">
|
||||
<div class="menu" @scroll=${this.handleMenuScroll}>
|
||||
${this.renderSections()}
|
||||
</div>
|
||||
<div
|
||||
@@ -497,12 +527,49 @@ export class WccSidebar extends DeesElement {
|
||||
|
||||
private showContextMenu(e: MouseEvent, sectionName: string, itemName: string) {
|
||||
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',
|
||||
iconName: isPinned ? 'push_pin' : 'push_pin',
|
||||
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 }));
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -549,7 +616,9 @@ export class WccSidebar extends DeesElement {
|
||||
${pinnedEntries.map(({ sectionName, itemName, item, section }) => {
|
||||
const isSelected = this.selectedItem === item;
|
||||
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`
|
||||
<div
|
||||
@@ -583,7 +652,13 @@ export class WccSidebar extends DeesElement {
|
||||
return this.dashboardRef.sections.map((section) => {
|
||||
// Check if section has any matching items
|
||||
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
|
||||
if (filteredEntries.length === 0 && this.searchQuery) {
|
||||
@@ -615,7 +690,13 @@ export class WccSidebar extends DeesElement {
|
||||
private renderSectionItems(section: IWccSection) {
|
||||
const entries = getSectionItems(section);
|
||||
// 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') {
|
||||
return filteredEntries.map(([pageName, item]) => {
|
||||
@@ -635,16 +716,21 @@ export class WccSidebar extends DeesElement {
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
// type === 'elements' - group by demoGroup
|
||||
// type === 'elements' - group by demoGroups (supports string | string[])
|
||||
const groupedItems = new Map<string | null, Array<[string, any]>>();
|
||||
|
||||
for (const entry of filteredEntries) {
|
||||
const [, item] = entry;
|
||||
const group = (item as any).demoGroup || null;
|
||||
if (!groupedItems.has(group)) {
|
||||
groupedItems.set(group, []);
|
||||
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)) {
|
||||
groupedItems.set(group, []);
|
||||
}
|
||||
groupedItems.get(group)!.push(entry);
|
||||
}
|
||||
groupedItems.get(group)!.push(entry);
|
||||
}
|
||||
|
||||
// Build a unified list of render items (ungrouped elements and groups)
|
||||
@@ -661,11 +747,10 @@ export class WccSidebar extends DeesElement {
|
||||
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) {
|
||||
if (groupName === null) continue;
|
||||
const firstElementName = items[0]?.[0] || '';
|
||||
renderItems.push({ type: 'group', groupName, items, sortKey: firstElementName.toLowerCase() });
|
||||
renderItems.push({ type: 'group', groupName, items, sortKey: groupName.toLowerCase() });
|
||||
}
|
||||
|
||||
// Sort all items alphabetically by sortKey
|
||||
@@ -677,8 +762,11 @@ export class WccSidebar extends DeesElement {
|
||||
return this.renderElementItem(item.entry, section);
|
||||
} else {
|
||||
return html`
|
||||
<div class="item-group">
|
||||
<span class="item-group-legend">${item.groupName}</span>
|
||||
<div class="item-group ${this.isGroupFilterMatch(item.groupName) ? 'group-filter-match' : ''}" data-group="${item.groupName}">
|
||||
<span
|
||||
class="item-group-legend"
|
||||
@contextmenu=${(e: MouseEvent) => this.showGroupContextMenu(e, item.groupName)}
|
||||
>${item.groupName}</span>
|
||||
${item.items.map((entry) => this.renderElementItem(entry, section))}
|
||||
</div>
|
||||
`;
|
||||
@@ -733,6 +821,7 @@ export class WccSidebar extends DeesElement {
|
||||
`;
|
||||
} else {
|
||||
// Single demo element
|
||||
const icon = this.getElementGroups(item).length > 1 ? 'library_books' : 'featured_video';
|
||||
return html`
|
||||
<div
|
||||
class="selectOption ${isSelected ? 'selected' : ''} ${isPinned ? 'pinned' : ''}"
|
||||
@@ -742,7 +831,7 @@ export class WccSidebar extends DeesElement {
|
||||
}}
|
||||
@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>
|
||||
`;
|
||||
@@ -778,6 +867,13 @@ export class WccSidebar extends DeesElement {
|
||||
private clearSearch() {
|
||||
this.searchQuery = '';
|
||||
this.dispatchEvent(new CustomEvent('searchChanged', { detail: this.searchQuery }));
|
||||
const input = this.shadowRoot.querySelector('.search-input') as HTMLInputElement;
|
||||
if (input) input.focus();
|
||||
}
|
||||
|
||||
private handleMenuScroll(e: Event) {
|
||||
const target = e.target as HTMLElement;
|
||||
this.isMenuScrolled = target.scrollTop > 0;
|
||||
}
|
||||
|
||||
private matchesSearch(name: string): boolean {
|
||||
@@ -785,6 +881,37 @@ export class WccSidebar extends DeesElement {
|
||||
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 {
|
||||
if (!this.searchQuery) return html`${text}`;
|
||||
const lowerText = text.toLowerCase();
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true
|
||||
"verbatimModuleSyntax": true,
|
||||
"strict": false,
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": [
|
||||
"dist_*/**/*.d.ts"
|
||||
|
||||
Reference in New Issue
Block a user