Compare commits

...

22 Commits

Author SHA1 Message Date
8c60d3bea3 v3.5.1
Some checks failed
Default (tags) / security (push) Failing after 14s
Default (tags) / test (push) Failing after 15s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-04 11:31:02 +00:00
9ed614994f fix(sidebar): disable frame CSS transition while user is resizing the sidebar to prevent janky animations 2026-01-04 11:31:02 +00:00
61b79aa4dc update 2026-01-04 11:29:19 +00:00
1134cba575 update 2026-01-04 10:57:45 +00:00
29c0df489e v3.5.0
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 10:48:03 +00:00
53df62a9fd feat(wcctools): add context menu and pinning support, persist pinned state in URL, and add grouped demo test elements 2026-01-04 10:48:03 +00:00
4fe17f5afd v3.4.0
Some checks failed
Default (tags) / security (push) Failing after 14s
Default (tags) / test (push) Failing after 14s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-30 12:30:45 +00:00
63dd6a27b3 feat(sidebar): add searchable sidebar with URL-backed query state and highlighted matches 2025-12-30 12:30:45 +00:00
287cc4d1c3 v3.3.0
Some checks failed
Default (tags) / security (push) Failing after 17s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-28 12:51:55 +00:00
14e63738b7 feat(wcctools): Add section-based configuration API for setupWccTools, new Views, and section-aware routing/sidebar 2025-12-28 12:51:55 +00:00
dd151bdad8 v3.2.0
Some checks failed
Default (tags) / security (push) Failing after 14s
Default (tags) / test (push) Failing after 14s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-22 10:49:02 +00:00
bd409745e6 feat(wcc-sidebar): auto-expand sidebar folder when selecting an element with multiple demos 2025-12-22 10:49:02 +00:00
eb7f482b75 v3.1.2
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Failing after 11s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-21 18:30:41 +00:00
d16d482120 fix(wcc-properties): Use LitElement.updated to recreate properties only when selectedItem changes and handle errors; remove custom scheduleUpdate implementation 2025-12-21 18:30:41 +00:00
5ba4e55011 v3.1.1
Some checks failed
Default (tags) / security (push) Failing after 14s
Default (tags) / test (push) Failing after 15s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-21 18:24:46 +00:00
0068c0749d fix(wcc-properties): Improve wcc-properties CSS to prevent grid overflow, properly size and center icon glyphs, and adjust right-side offset 2025-12-21 18:24:46 +00:00
36dd6b5064 v3.1.0
Some checks failed
Default (tags) / security (push) Failing after 15s
Default (tags) / test (push) Failing after 11s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-19 09:21:30 +00:00
ddecfcdb4c feat(wcc-properties): add Share selector with inline Record button and adjust properties panel grid layout 2025-12-19 09:21:30 +00:00
8f0f8606a1 v3.0.0
Some checks failed
Default (tags) / security (push) Failing after 15s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-19 09:08:08 +00:00
7dca519d9a BREAKING CHANGE(ts_web): Replace fullscreen boolean with native viewport mode across components, add native viewport selector and toggle, and update dev deps and npmextra config 2025-12-19 09:08:08 +00:00
d48cd063c4 v2.0.1
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Failing after 18s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-11 19:26:59 +00:00
bb04895be8 fix(@git.zone/tswatch): Bump @git.zone/tswatch devDependency to ^2.3.12 2025-12-11 19:26:59 +00:00
28 changed files with 3799 additions and 1265 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -1,5 +1,90 @@
# Changelog
## 2026-01-04 - 3.5.1 - fix(sidebar)
disable frame CSS transition while user is resizing the sidebar to prevent janky animations
- Added isResizing boolean property to wcc-frame to toggle transitions during resize
- Set frame.isResizing = true at resize start and false on mouseup to re-enable transitions
- Updated CSS to skip transition while isResizing is true
- Files changed: ts_web/elements/wcc-frame.ts, ts_web/elements/wcc-sidebar.ts
## 2026-01-04 - 3.5.0 - feat(wcctools)
add context menu and pinning support, persist pinned state in URL, and add grouped demo test elements
- Add wcc-contextmenu custom element with a static show() API, proper positioning, visibility transitions, outside-click and Escape handling, and menu item actions.
- Introduce pinnedItems (Set<string>) on wcc-dashboard and wcc-sidebar; pass pinnedItems to the sidebar, handle pinnedChanged events, and persist pinned item keys in the URL query param 'pinned'. Changes include defensive updates to avoid unnecessary update loops.
- Enhance wcc-sidebar to render pinned state: new styles for pinned items and pinned sections, contextmenu integration for element items, adjusted layout (grid-template-columns) and improved element/demo rendering logic.
- Add grouped demo test components and exports to demo the demoGroup feature: test-button-primary, test-button-secondary, test-button-danger, test-input-text, and test-input-checkbox.
- Misc: adjust dashboard URL state serialization/deserialization to include pinned items and ensure scroll/search state handling remains stable.
## 2025-12-30 - 3.4.0 - feat(sidebar)
add searchable sidebar with URL-backed query state and highlighted matches
- Add search input to wcc-sidebar and expose a searchQuery property
- Filter sidebar sections and items client-side based on the search query and hide sections with no matches
- Highlight matching substrings in sidebar item labels
- Emit a 'searchChanged' event from the sidebar and handle it in wcc-dashboard to keep dashboard.searchQuery in sync
- Persist the search query in the route query parameter 'search' when building URLs and restore/clear it on navigation
- Preserve existing scroll-state handling while adding search state to URL updates
## 2025-12-28 - 3.3.0 - feat(wcctools)
Add section-based configuration API for setupWccTools, new Views, and section-aware routing/sidebar
- Introduce IWccSection and IWccConfig types and migrate setupWccTools to accept a sections config while preserving legacy (elements, pages) format
- WccDashboard and WccSidebar updated to support sections, filtering, sorting, collapsed sections, and section-aware URL routing (uses sectionName in routes with legacy fallbacks)
- Add Views: view-dashboard, view-settings, view-empty-state plus test/views index exports and demo variations
- Add helpers: getSectionItems, convertLegacyToConfig and isWccConfig; update build URL and routing logic to be section-aware
- Update docs and README/readme.hints with sections API, examples, migration notes and UI/UX updates
## 2025-12-22 - 3.2.0 - feat(wcc-sidebar)
auto-expand sidebar folder when selecting an element with multiple demos
- Add updated() lifecycle to auto-expand folder when selectedItem changes
- Find element name by matching selectedItem against dashboardRef.elements
- Only auto-expand when the element has multiple demos (checks item.demo and hasMultipleDemos)
- Immutably update expandedElements set to trigger re-render and avoid duplicate additions
## 2025-12-21 - 3.1.2 - fix(wcc-properties)
Use LitElement.updated to recreate properties only when selectedItem changes and handle errors; remove custom scheduleUpdate implementation
- Replaced public async scheduleUpdate() with protected updated(changedProperties) lifecycle method
- Call super.updated(...) and only recreate properties when selectedItem changed to avoid unnecessary work
- Preserve error handling and clear propertyContent on failure
- Removed explicit super.scheduleUpdate() call to rely on LitElement's update lifecycle
## 2025-12-21 - 3.1.1 - fix(wcc-properties)
Improve wcc-properties CSS to prevent grid overflow, properly size and center icon glyphs, and adjust right-side offset
- Use minmax(0, 1fr) for grid-template-columns (selectorButtons1/2/4/5) to avoid flexbox overflow and ensure consistent column sizing
- Add min-width/min-height and inline-flex centering to .material-symbols-outlined to stabilize icon sizing and vertical/horizontal alignment
- Increase right calc offset from 520px to 600px to accommodate wider content/controls
- Changes applied in ts_web/elements/wcc-properties.ts
## 2025-12-19 - 3.1.0 - feat(wcc-properties)
add Share selector with inline Record button and adjust properties panel grid layout
- Increase rightmost column width from 70px to 100px in properties grid
- Introduce .shareSelector and .selectorButtons1 CSS classes and markup
- Replace <wcc-record-button> with inline Record button that toggles icon based on isRecording
- Add Share panel heading and integrate record control into selector area
## 2025-12-19 - 3.0.0 - BREAKING CHANGE(ts_web)
Replace fullscreen boolean with native viewport mode across components, add native viewport selector and toggle, and update dev deps and npmextra config
- Introduce isNative (derived from selectedViewport === 'native') replacing isFullscreen on WccDashboard, WccFrame, WccProperties and WccSidebar (property and styling changes).
- WccDashboard: remove isFullscreen property, add isNative getter, implement toggleNative to switch selectedViewport between 'native' and 'desktop', update ESC handler to exit native mode and call buildUrl().
- WccProperties: add Native button to viewport selector, adjust grid columns and selector layout, replace fullscreen toggle with toggleNative (dispatches 'toggleNative' event).
- WccFrame: rename isFullscreen -> isNative and update style/markup branching to use isNative.
- WccSidebar: visibility now driven by isNative instead of isFullscreen.
- API/event changes: 'toggleFullscreen' event renamed to 'toggleNative' — this is an incompatible change for consumers relying on the old event/property.
- package.json: bump devDependencies @git.zone/tsbuild -> ^4.0.2, @git.zone/tsrun -> ^2.0.1, @git.zone/tswatch -> ^2.3.13, @types/node -> ^25.0.3.
- npmextra.json: rename keys (e.g. "gitzone" -> "@git.zone/cli", "tsdoc" -> "@git.zone/tsdoc"), add @ship.zone/szci entry and add release registries/accessLevel for @git.zone/cli.
## 2025-12-11 - 2.0.1 - fix(@git.zone/tswatch)
Bump @git.zone/tswatch devDependency to ^2.3.12
- Updated devDependency @git.zone/tswatch from ^2.3.11 to ^2.3.12 in package.json
## 2025-12-11 - 2.0.0 - BREAKING CHANGE(recorder)
Remove FFmpeg-based MP4 conversion; simplify recorder/export to WebM and improve recorder/editor robustness

View File

@@ -2,9 +2,32 @@
import * as deesWccTools from '../ts_web/index.js';
import * as deesDomTools from '@design.estate/dees-domtools';
// elements and pages
// elements, views and pages
import * as elements from '../test/elements/index.js';
import * as views from '../test/views/index.js';
import * as pages from '../test/pages/index.js';
deesWccTools.setupWccTools(elements as any, pages);
// Sections-based API with Views
deesWccTools.setupWccTools({
sections: [
{
name: 'Pages',
type: 'pages',
items: pages,
},
{
name: 'Views',
type: 'elements',
items: views,
icon: 'web',
},
{
name: 'Elements',
type: 'elements',
items: elements,
sort: ([a], [b]) => a.localeCompare(b),
},
],
});
deesDomTools.elementBasic.setup();

View File

@@ -1,5 +1,5 @@
{
"gitzone": {
"@git.zone/cli": {
"projectType": "wcc",
"module": {
"githost": "code.foss.global",
@@ -21,13 +21,19 @@
"element testing",
"page development"
]
},
"release": {
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public"
}
},
"npmci": {
"npmGlobalTools": [],
"npmAccessLevel": "public"
},
"tsdoc": {
"@git.zone/tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**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.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
},
"@ship.zone/szci": {
"npmGlobalTools": []
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@design.estate/dees-wcctools",
"version": "2.0.0",
"version": "3.5.1",
"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": {
@@ -24,13 +24,13 @@
},
"devDependencies": {
"@api.global/typedserver": "^7.11.1",
"@git.zone/tsbuild": "^3.1.2",
"@git.zone/tsbuild": "^4.0.2",
"@git.zone/tsbundle": "^2.6.3",
"@git.zone/tsrun": "^2.0.0",
"@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.1.3",
"@git.zone/tswatch": "^2.3.11",
"@git.zone/tswatch": "^2.3.13",
"@push.rocks/projectinfo": "^5.0.2",
"@types/node": "^25.0.0"
"@types/node": "^25.0.3"
},
"files": [
"ts/**/*",

1916
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,67 @@
# Project Hints and Findings
## Section-based Configuration API (2025-12-27)
### Overview
Refactored `setupWccTools` to accept a section-based configuration object instead of separate elements/pages arguments. This allows multiple custom sections with filtering, sorting, and collapsible headers.
### New API
```typescript
import * as deesWccTools from '@design.estate/dees-wcctools';
deesWccTools.setupWccTools({
sections: [
{
name: 'Pages',
type: 'pages',
items: pages,
},
{
name: 'Elements',
type: 'elements',
items: elements,
filter: (name, item) => !name.startsWith('internal-'),
sort: ([a], [b]) => a.localeCompare(b),
},
{
name: 'Views',
type: 'elements',
items: elements,
filter: (name, item) => name.startsWith('view-'),
icon: 'web',
collapsed: true, // Start collapsed
},
],
});
```
### Section Properties
- `name`: Display name for the section header
- `type`: `'elements'` (shows demos) or `'pages'` (renders directly)
- `items`: Record of items (element classes or page factories)
- `filter`: Optional `(name, item) => boolean` to filter items
- `sort`: Optional `([name, item], [name, item]) => number` for ordering
- `icon`: Optional Material icon name for section header
- `collapsed`: Optional boolean to start section collapsed
### Backwards Compatibility
Legacy format is still supported:
```typescript
deesWccTools.setupWccTools(elements, pages); // Still works
```
### URL Routing
Changed from `/wcctools-route/:itemType/:itemName/...` to `/wcctools-route/:sectionName/:itemName/...`
Section names are URL-encoded. Legacy routes (`element`/`page` as section name) still work for backwards compatibility.
### Files Changed
- `ts_web/wcctools.interfaces.ts` - New types: `IWccSection`, `IWccConfig`
- `ts_web/index.ts` - Updated `setupWccTools` with backwards compat detection
- `ts_web/elements/wcc-dashboard.ts` - Uses sections, updated routing
- `ts_web/elements/wcc-sidebar.ts` - Dynamic section rendering
---
## UI Redesign with Shadcn-like Styles (2025-06-27)
### Changes Made

229
readme.md
View File

@@ -6,12 +6,13 @@
`@design.estate/dees-wcctools` provides a comprehensive development environment for web components, featuring:
- 🎨 **Interactive Component Catalogue** — Live preview with sidebar navigation
- 🎨 **Interactive Component Catalogue** — Live preview with customizable sidebar sections
- 🔧 **Real-time Property Editing** — Modify component props on the fly with auto-detected editors
- 🌓 **Theme Switching** — Test light/dark modes instantly
- 📱 **Responsive Viewport Testing** — Phone, phablet, tablet, and desktop views
- 🎬 **Screen Recording** — Record component demos with audio support and video trimming
- 🧪 **Advanced Demo Tools** — Post-render hooks for interactive testing
- 📂 **Section-based Organization** — Group components into custom sections with filtering and sorting
- 🚀 **Zero-config Setup** — TypeScript and Lit support out of the box
## Issue Reporting and Security
@@ -57,29 +58,22 @@ export class MyButton extends DeesElement {
padding: 8px 16px;
border-radius: 4px;
border: none;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
button.primary {
background: #007bff;
background: #3b82f6;
color: white;
}
button.secondary {
background: #6c757d;
background: #6b7280;
color: white;
}
button:hover {
opacity: 0.9;
}
`
];
public render() {
return html`
<button class="${this.variant}">
${this.label}
</button>
<button class="${this.variant}">${this.label}</button>
`;
}
}
@@ -93,33 +87,32 @@ import { setupWccTools } from '@design.estate/dees-wcctools';
import { html } from 'lit';
// Import your components
import { MyButton } from './components/my-button.js';
import { MyCard } from './components/my-card.js';
import * as elements from './components/index.js';
import * as views from './views/index.js';
import * as pages from './pages/index.js';
// Define elements for the catalogue
const elements = {
'my-button': MyButton,
'my-card': MyCard,
};
// Optionally define pages
const pages = {
'home': () => html`
<div style="padding: 20px;">
<h1>Welcome to My Component Library</h1>
<p>Browse components using the sidebar.</p>
</div>
`,
'getting-started': () => html`
<div style="padding: 20px;">
<h2>Getting Started</h2>
<p>Installation and usage instructions...</p>
</div>
`,
};
// Initialize the catalogue
setupWccTools(elements, pages);
// Initialize with sections-based configuration
setupWccTools({
sections: [
{
name: 'Pages',
type: 'pages',
items: pages,
},
{
name: 'Views',
type: 'elements',
items: views,
icon: 'web',
},
{
name: 'Elements',
type: 'elements',
items: elements,
sort: ([a], [b]) => a.localeCompare(b),
},
],
});
```
### 3. Create an HTML Entry Point
@@ -137,6 +130,69 @@ setupWccTools(elements, pages);
</html>
```
## 📂 Sections Configuration
The sections-based API gives you full control over how components are organized in the sidebar.
### Section Properties
| Property | Type | Description |
|----------|------|-------------|
| `name` | `string` | Display name for the section header |
| `type` | `'elements' \| 'pages'` | How items render (`elements` show demos, `pages` render directly) |
| `items` | `Record<string, any>` | Object containing element classes or page factories |
| `filter` | `(name, item) => boolean` | Optional filter function to include/exclude items |
| `sort` | `([a, itemA], [b, itemB]) => number` | Optional sort function for ordering items |
| `icon` | `string` | Optional Material Symbols icon name |
| `collapsed` | `boolean` | Start section collapsed (default: `false`) |
### Advanced Example
```typescript
import { setupWccTools } from '@design.estate/dees-wcctools';
import * as allElements from './elements/index.js';
import * as pages from './pages/index.js';
setupWccTools({
sections: [
{
name: 'Pages',
type: 'pages',
items: pages,
},
{
name: 'Form Controls',
type: 'elements',
items: allElements,
icon: 'edit_note',
filter: (name) => name.startsWith('form-') || name.includes('input'),
sort: ([a], [b]) => a.localeCompare(b),
},
{
name: 'Layout',
type: 'elements',
items: allElements,
icon: 'dashboard',
filter: (name) => name.startsWith('layout-') || name.startsWith('grid-'),
},
{
name: 'Legacy',
type: 'elements',
items: allElements,
filter: (name) => name.startsWith('legacy-'),
collapsed: true, // Start collapsed
},
],
});
```
### Legacy API (Still Supported)
```typescript
// The old format still works for simple use cases
setupWccTools(elements, pages);
```
## Features
### 🎯 Live Property Editing
@@ -162,19 +218,7 @@ Test your components across different screen sizes:
### 🌓 Theme Support
Components automatically adapt to light/dark themes using the `goBright` property:
```typescript
public render() {
return html`
<div class="${this.goBright ? 'light-theme' : 'dark-theme'}">
<!-- Your component content -->
</div>
`;
}
```
Or use CSS custom properties with the theme manager:
Components automatically adapt to light/dark themes. Use CSS custom properties with the theme manager:
```typescript
import { cssManager } from '@design.estate/dees-element';
@@ -182,8 +226,8 @@ import { cssManager } from '@design.estate/dees-element';
public static styles = [
css`
:host {
color: ${cssManager.bdTheme('#000', '#fff')};
background: ${cssManager.bdTheme('#fff', '#000')};
color: ${cssManager.bdTheme('#1a1a1a', '#e5e5e5')};
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
}
`
];
@@ -191,7 +235,7 @@ public static styles = [
### 🎬 Screen Recording
Record component demos directly from the catalogue! The built-in recorder supports:
Record component demos directly from the catalogue:
- **Viewport Recording** — Record just the component viewport
- **Full Screen Recording** — Capture the entire screen
@@ -232,9 +276,26 @@ export class MyComponent extends DeesElement {
}
```
### 🎭 Multiple Demos
Components can expose multiple demo variations:
```typescript
@customElement('my-button')
export class MyButton extends DeesElement {
public static demo = [
() => html`<my-button variant="primary">Primary</my-button>`,
() => html`<my-button variant="secondary">Secondary</my-button>`,
() => html`<my-button variant="danger">Danger</my-button>`,
];
}
```
Each demo appears as a numbered item in an expandable folder in the sidebar.
### ⏳ Async Demos
Return a `Promise` from `demo` for async setup. The dashboard waits for resolution:
Return a `Promise` from `demo` for async setup:
```typescript
public static demo = async () => {
@@ -243,7 +304,7 @@ public static demo = async () => {
};
```
### 🎭 Container Queries
### 🎯 Container Queries
Components can respond to their container size using the `wccToolsViewport` container:
@@ -269,7 +330,7 @@ public static styles = [
### Required for Catalogue Display
1. Components must expose a static `demo` property returning a Lit template
1. Components must expose a static `demo` property returning a Lit template (or array of templates)
2. Use `@property()` decorators with the `accessor` keyword for editable properties
3. Export component classes for proper detection
@@ -278,7 +339,7 @@ public static styles = [
```typescript
@customElement('best-practice-component')
export class BestPracticeComponent extends DeesElement {
// ✅ Static demo property
// ✅ Static demo property (single or array)
public static demo = () => html`
<best-practice-component
.complexProp=${{ key: 'value' }}
@@ -305,23 +366,40 @@ export class BestPracticeComponent extends DeesElement {
The catalogue uses URL routing for deep linking:
```
/wcctools-route/:type/:name/:viewport/:theme
/wcctools-route/:sectionName/:itemName/:demoIndex/:viewport/:theme
Examples:
/wcctools-route/element/my-button/desktop/dark
/wcctools-route/page/home/tablet/bright
/wcctools-route/Elements/my-button/0/desktop/dark
/wcctools-route/Views/view-dashboard/0/tablet/bright
/wcctools-route/Pages/home/0/desktop/dark
```
## API Reference
### `setupWccTools(elements, pages?)`
### `setupWccTools(config)`
Initialize the WCC Tools dashboard.
Initialize the WCC Tools dashboard with sections configuration.
| Parameter | Type | Description |
|-----------|------|-------------|
| `elements` | `Record<string, typeof LitElement>` | Map of element names to classes |
| `pages` | `Record<string, TTemplateFactory>` | Optional map of page names to template functions |
```typescript
interface IWccSection {
name: string;
type: 'elements' | 'pages';
items: Record<string, any>;
filter?: (name: string, item: any) => boolean;
sort?: (a: [string, any], b: [string, any]) => number;
icon?: string;
collapsed?: boolean;
}
interface IWccConfig {
sections: IWccSection[];
}
setupWccTools(config: IWccConfig): void;
// Legacy (still supported)
setupWccTools(elements: Record<string, any>, pages?: Record<string, TTemplateFactory>): void;
```
### `DeesDemoWrapper`
@@ -357,14 +435,21 @@ recorder.stopRecording();
## Project Structure
```
my-components/
my-component-library/
├── src/
│ ├── components/
│ ├── elements/ # UI components
│ │ ├── my-button.ts
│ │ ── my-card.ts
│ └── catalogue.ts
├── dist/
├── index.html
│ │ ── my-card.ts
│ └── index.ts
│ ├── views/ # Full-page layouts
│ │ ├── view-dashboard.ts
│ │ └── index.ts
│ ├── pages/ # Documentation pages
│ │ ├── home.ts
│ │ └── index.ts
│ └── catalogue.ts # WCC Tools setup
├── html/
│ └── index.html
└── package.json
```

View File

@@ -4,3 +4,10 @@ export * from './test-complextypes.js';
export * from './test-withwrapper.js';
export * from './test-edgecases.js';
export * from './test-nested.js';
// Grouped elements to demo the demoGroup feature
export * from './test-button-primary.js';
export * from './test-button-secondary.js';
export * from './test-button-danger.js';
export * from './test-input-text.js';
export * from './test-input-checkbox.js';

View File

@@ -0,0 +1,45 @@
import {
DeesElement,
customElement,
html,
property,
css,
} from '@design.estate/dees-element';
@customElement('test-button-danger')
export class TestButtonDanger extends DeesElement {
// Same group as other buttons
public static demoGroup = 'Buttons';
public static demo = () => html`
<test-button-danger>Delete</test-button-danger>
`;
@property({ type: String })
accessor label: string = 'Delete';
public static styles = [
css`
:host {
display: inline-block;
}
button {
background: #ef4444;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
button:hover {
background: #dc2626;
}
`,
];
public render() {
return html`<button><slot>${this.label}</slot></button>`;
}
}

View File

@@ -0,0 +1,45 @@
import {
DeesElement,
customElement,
html,
property,
css,
} from '@design.estate/dees-element';
@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 demo = () => html`
<test-button-primary>Click Me</test-button-primary>
`;
@property({ type: String })
accessor label: string = 'Button';
public static styles = [
css`
:host {
display: inline-block;
}
button {
background: #3b82f6;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
button:hover {
background: #2563eb;
}
`,
];
public render() {
return html`<button><slot>${this.label}</slot></button>`;
}
}

View File

@@ -0,0 +1,45 @@
import {
DeesElement,
customElement,
html,
property,
css,
} from '@design.estate/dees-element';
@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 demo = () => html`
<test-button-secondary>Secondary Action</test-button-secondary>
`;
@property({ type: String })
accessor label: string = 'Button';
public static styles = [
css`
:host {
display: inline-block;
}
button {
background: transparent;
color: #3b82f6;
border: 1px solid #3b82f6;
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
button:hover {
background: rgba(59, 130, 246, 0.1);
}
`,
];
public render() {
return html`<button><slot>${this.label}</slot></button>`;
}
}

View File

@@ -0,0 +1,68 @@
import {
DeesElement,
customElement,
html,
property,
css,
} from '@design.estate/dees-element';
@customElement('test-input-checkbox')
export class TestInputCheckbox extends DeesElement {
// Same group as test-input-text
public static demoGroup = 'Inputs';
public static demo = () => html`
<test-input-checkbox label="Accept terms and conditions"></test-input-checkbox>
`;
@property({ type: String })
accessor label: string = 'Checkbox';
@property({ type: Boolean })
accessor checked: boolean = false;
public static styles = [
css`
:host {
display: inline-flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.checkbox {
width: 18px;
height: 18px;
border: 1px solid #333;
border-radius: 4px;
background: #1a1a1a;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.checkbox.checked {
background: #3b82f6;
border-color: #3b82f6;
}
.checkbox.checked::after {
content: '✓';
color: white;
font-size: 12px;
}
.label {
color: #e5e5e5;
font-size: 14px;
}
`,
];
public render() {
return html`
<div
class="checkbox ${this.checked ? 'checked' : ''}"
@click=${() => this.checked = !this.checked}
></div>
<span class="label">${this.label}</span>
`;
}
}

View File

@@ -0,0 +1,59 @@
import {
DeesElement,
customElement,
html,
property,
css,
} from '@design.estate/dees-element';
@customElement('test-input-text')
export class TestInputText extends DeesElement {
// Different group - "Inputs"
public static demoGroup = 'Inputs';
public static demo = () => html`
<test-input-text placeholder="Enter text..."></test-input-text>
`;
@property({ type: String })
accessor placeholder: string = '';
@property({ type: String })
accessor value: string = '';
public static styles = [
css`
:host {
display: inline-block;
}
input {
background: #1a1a1a;
color: #e5e5e5;
border: 1px solid #333;
padding: 10px 14px;
border-radius: 6px;
font-size: 14px;
outline: none;
transition: border-color 0.2s;
min-width: 200px;
}
input:focus {
border-color: #3b82f6;
}
input::placeholder {
color: #666;
}
`,
];
public render() {
return html`
<input
type="text"
.placeholder=${this.placeholder}
.value=${this.value}
@input=${(e: Event) => this.value = (e.target as HTMLInputElement).value}
/>
`;
}
}

3
test/views/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from './view-dashboard.js';
export * from './view-settings.js';
export * from './view-empty-state.js';

View File

@@ -0,0 +1,286 @@
import { DeesElement, customElement, html, css, property, cssManager } from '@design.estate/dees-element';
@customElement('view-dashboard')
export class ViewDashboard extends DeesElement {
public static demo = () => html`<view-dashboard></view-dashboard>`;
@property()
accessor title: string = 'Dashboard';
@property({ type: Number })
accessor notificationCount: number = 3;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
min-height: 100%;
background: ${cssManager.bdTheme('#f5f5f5', '#0a0a0a')};
color: ${cssManager.bdTheme('#1a1a1a', '#e5e5e5')};
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
.dashboard {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: 60px 1fr;
min-height: 100vh;
}
.header {
grid-column: 1 / -1;
background: ${cssManager.bdTheme('#fff', '#111')};
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#222')};
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
}
.header h1 {
font-size: 18px;
font-weight: 600;
margin: 0;
}
.header-actions {
display: flex;
align-items: center;
gap: 16px;
}
.notification-badge {
position: relative;
padding: 8px;
cursor: pointer;
}
.notification-badge::after {
content: attr(data-count);
position: absolute;
top: 2px;
right: 2px;
background: #ef4444;
color: white;
font-size: 10px;
padding: 2px 6px;
border-radius: 10px;
font-weight: 600;
}
.sidebar {
background: ${cssManager.bdTheme('#fff', '#0f0f0f')};
border-right: 1px solid ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')};
padding: 16px 0;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 24px;
color: ${cssManager.bdTheme('#666', '#999')};
cursor: pointer;
transition: all 0.15s ease;
}
.nav-item:hover {
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
color: ${cssManager.bdTheme('#1a1a1a', '#fff')};
}
.nav-item.active {
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.15)')};
color: #3b82f6;
border-right: 3px solid #3b82f6;
}
.content {
padding: 24px;
overflow-y: auto;
}
.content-header {
margin-bottom: 24px;
}
.content-header h2 {
font-size: 24px;
font-weight: 600;
margin: 0 0 8px 0;
}
.content-header p {
color: ${cssManager.bdTheme('#666', '#888')};
margin: 0;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: ${cssManager.bdTheme('#fff', '#111')};
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#222')};
border-radius: 8px;
padding: 20px;
}
.stat-card .label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: ${cssManager.bdTheme('#666', '#888')};
margin-bottom: 8px;
}
.stat-card .value {
font-size: 28px;
font-weight: 700;
}
.stat-card .change {
font-size: 12px;
color: #22c55e;
margin-top: 4px;
}
.stat-card .change.negative {
color: #ef4444;
}
.recent-activity {
background: ${cssManager.bdTheme('#fff', '#111')};
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#222')};
border-radius: 8px;
padding: 20px;
}
.recent-activity h3 {
font-size: 16px;
font-weight: 600;
margin: 0 0 16px 0;
}
.activity-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 0;
border-bottom: 1px solid ${cssManager.bdTheme('#f0f0f0', '#1a1a1a')};
}
.activity-item:last-child {
border-bottom: none;
}
.activity-icon {
width: 36px;
height: 36px;
border-radius: 8px;
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.activity-content {
flex: 1;
}
.activity-content .title {
font-weight: 500;
margin-bottom: 2px;
}
.activity-content .time {
font-size: 12px;
color: ${cssManager.bdTheme('#888', '#666')};
}
`,
];
public render() {
return html`
<div class="dashboard">
<header class="header">
<h1>${this.title}</h1>
<div class="header-actions">
<div class="notification-badge" data-count="${this.notificationCount}">
<span>Notifications</span>
</div>
<span>User</span>
</div>
</header>
<nav class="sidebar">
<div class="nav-item active">Overview</div>
<div class="nav-item">Analytics</div>
<div class="nav-item">Projects</div>
<div class="nav-item">Team</div>
<div class="nav-item">Settings</div>
</nav>
<main class="content">
<div class="content-header">
<h2>Overview</h2>
<p>Welcome back! Here's what's happening with your projects.</p>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="label">Total Revenue</div>
<div class="value">$45,231</div>
<div class="change">+20.1% from last month</div>
</div>
<div class="stat-card">
<div class="label">Active Users</div>
<div class="value">2,350</div>
<div class="change">+180 new users</div>
</div>
<div class="stat-card">
<div class="label">Pending Tasks</div>
<div class="value">12</div>
<div class="change negative">-3 from yesterday</div>
</div>
<div class="stat-card">
<div class="label">Completion Rate</div>
<div class="value">94.2%</div>
<div class="change">+2.4% this week</div>
</div>
</div>
<div class="recent-activity">
<h3>Recent Activity</h3>
<div class="activity-item">
<div class="activity-icon">+</div>
<div class="activity-content">
<div class="title">New project created</div>
<div class="time">2 minutes ago</div>
</div>
</div>
<div class="activity-item">
<div class="activity-icon">U</div>
<div class="activity-content">
<div class="title">User settings updated</div>
<div class="time">1 hour ago</div>
</div>
</div>
<div class="activity-item">
<div class="activity-icon">D</div>
<div class="activity-content">
<div class="title">Deployment completed</div>
<div class="time">3 hours ago</div>
</div>
</div>
</div>
</main>
</div>
`;
}
}

View File

@@ -0,0 +1,262 @@
import { DeesElement, customElement, html, css, property, cssManager } from '@design.estate/dees-element';
@customElement('view-empty-state')
export class ViewEmptyState extends DeesElement {
public static demo = [
() => html`<view-empty-state></view-empty-state>`,
() => html`<view-empty-state variant="no-results"></view-empty-state>`,
() => html`<view-empty-state variant="error"></view-empty-state>`,
];
@property()
accessor variant: 'empty' | 'no-results' | 'error' = 'empty';
@property()
accessor title: string = '';
@property()
accessor description: string = '';
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: flex;
align-items: center;
justify-content: center;
min-height: 100%;
background: ${cssManager.bdTheme('#f5f5f5', '#0a0a0a')};
color: ${cssManager.bdTheme('#1a1a1a', '#e5e5e5')};
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
padding: 40px;
box-sizing: border-box;
}
.empty-state {
text-align: center;
max-width: 400px;
}
.icon-container {
width: 120px;
height: 120px;
margin: 0 auto 24px;
background: ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')};
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.icon {
font-size: 48px;
opacity: 0.5;
}
.icon.error {
color: #ef4444;
opacity: 1;
}
h2 {
font-size: 24px;
font-weight: 600;
margin: 0 0 12px 0;
}
p {
color: ${cssManager.bdTheme('#666', '#888')};
margin: 0 0 24px 0;
line-height: 1.6;
}
.actions {
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
}
.btn {
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
border: none;
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-primary:hover {
background: #2563eb;
transform: translateY(-1px);
}
.btn-secondary {
background: ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')};
color: ${cssManager.bdTheme('#1a1a1a', '#e5e5e5')};
}
.btn-secondary:hover {
background: ${cssManager.bdTheme('#ddd', '#222')};
}
.illustration {
margin-bottom: 24px;
}
.illustration svg {
width: 200px;
height: 150px;
}
.folder-icon {
width: 80px;
height: 80px;
margin: 0 auto 16px;
position: relative;
}
.folder-back {
position: absolute;
width: 80px;
height: 60px;
background: ${cssManager.bdTheme('#ddd', '#333')};
border-radius: 4px 4px 8px 8px;
bottom: 0;
}
.folder-front {
position: absolute;
width: 80px;
height: 50px;
background: ${cssManager.bdTheme('#e5e5e5', '#444')};
border-radius: 0 4px 8px 8px;
bottom: 0;
}
.folder-tab {
position: absolute;
width: 30px;
height: 12px;
background: ${cssManager.bdTheme('#ddd', '#333')};
border-radius: 4px 4px 0 0;
top: 0;
left: 0;
}
.search-icon {
width: 60px;
height: 60px;
border: 4px solid ${cssManager.bdTheme('#ccc', '#444')};
border-radius: 50%;
position: relative;
margin: 0 auto 16px;
}
.search-icon::after {
content: '';
position: absolute;
width: 4px;
height: 20px;
background: ${cssManager.bdTheme('#ccc', '#444')};
border-radius: 2px;
bottom: -18px;
right: -8px;
transform: rotate(-45deg);
}
.error-icon {
width: 80px;
height: 80px;
border: 4px solid #ef4444;
border-radius: 50%;
position: relative;
margin: 0 auto 16px;
display: flex;
align-items: center;
justify-content: center;
}
.error-icon::before {
content: '!';
font-size: 40px;
font-weight: 700;
color: #ef4444;
}
`,
];
private getContent() {
switch (this.variant) {
case 'no-results':
return {
title: this.title || 'No results found',
description:
this.description ||
"We couldn't find what you're looking for. Try adjusting your search or filters.",
icon: 'search',
primaryAction: 'Clear Filters',
secondaryAction: 'Go Back',
};
case 'error':
return {
title: this.title || 'Something went wrong',
description:
this.description ||
"We're having trouble loading this page. Please try again or contact support if the problem persists.",
icon: 'error',
primaryAction: 'Try Again',
secondaryAction: 'Contact Support',
};
default:
return {
title: this.title || 'No items yet',
description:
this.description ||
"Get started by creating your first item. It only takes a few seconds.",
icon: 'folder',
primaryAction: 'Create New',
secondaryAction: 'Learn More',
};
}
}
public render() {
const content = this.getContent();
return html`
<div class="empty-state">
${this.renderIcon(content.icon)}
<h2>${content.title}</h2>
<p>${content.description}</p>
<div class="actions">
<button class="btn btn-primary">${content.primaryAction}</button>
<button class="btn btn-secondary">${content.secondaryAction}</button>
</div>
</div>
`;
}
private renderIcon(type: string) {
switch (type) {
case 'search':
return html`<div class="search-icon"></div>`;
case 'error':
return html`<div class="error-icon"></div>`;
default:
return html`
<div class="folder-icon">
<div class="folder-tab"></div>
<div class="folder-back"></div>
<div class="folder-front"></div>
</div>
`;
}
}
}

436
test/views/view-settings.ts Normal file
View File

@@ -0,0 +1,436 @@
import { DeesElement, customElement, html, css, property, cssManager } from '@design.estate/dees-element';
@customElement('view-settings')
export class ViewSettings extends DeesElement {
public static demo = [
() => html`<view-settings></view-settings>`,
() => html`<view-settings activeTab="notifications"></view-settings>`,
() => html`<view-settings activeTab="security"></view-settings>`,
];
@property()
accessor activeTab: 'profile' | 'notifications' | 'security' = 'profile';
@property()
accessor userName: string = 'John Doe';
@property()
accessor userEmail: string = 'john@example.com';
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
min-height: 100%;
background: ${cssManager.bdTheme('#f5f5f5', '#0a0a0a')};
color: ${cssManager.bdTheme('#1a1a1a', '#e5e5e5')};
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
.settings-layout {
max-width: 1000px;
margin: 0 auto;
padding: 40px 24px;
}
.settings-header {
margin-bottom: 32px;
}
.settings-header h1 {
font-size: 28px;
font-weight: 700;
margin: 0 0 8px 0;
}
.settings-header p {
color: ${cssManager.bdTheme('#666', '#888')};
margin: 0;
}
.settings-content {
display: grid;
grid-template-columns: 200px 1fr;
gap: 32px;
}
.settings-nav {
display: flex;
flex-direction: column;
gap: 4px;
}
.nav-item {
padding: 12px 16px;
border-radius: 6px;
cursor: pointer;
transition: all 0.15s ease;
color: ${cssManager.bdTheme('#666', '#999')};
}
.nav-item:hover {
background: ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')};
color: ${cssManager.bdTheme('#1a1a1a', '#fff')};
}
.nav-item.active {
background: ${cssManager.bdTheme('#1a1a1a', '#3b82f6')};
color: #fff;
}
.settings-panel {
background: ${cssManager.bdTheme('#fff', '#111')};
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#222')};
border-radius: 12px;
padding: 24px;
}
.panel-header {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#222')};
}
.panel-header h2 {
font-size: 18px;
font-weight: 600;
margin: 0 0 4px 0;
}
.panel-header p {
font-size: 14px;
color: ${cssManager.bdTheme('#666', '#888')};
margin: 0;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
.form-group input {
width: 100%;
padding: 10px 14px;
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
border-radius: 6px;
font-size: 14px;
background: ${cssManager.bdTheme('#fff', '#0a0a0a')};
color: ${cssManager.bdTheme('#1a1a1a', '#e5e5e5')};
box-sizing: border-box;
}
.form-group input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.toggle-group {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0;
border-bottom: 1px solid ${cssManager.bdTheme('#f0f0f0', '#1a1a1a')};
}
.toggle-group:last-child {
border-bottom: none;
}
.toggle-label {
display: flex;
flex-direction: column;
gap: 4px;
}
.toggle-label .title {
font-weight: 500;
}
.toggle-label .description {
font-size: 13px;
color: ${cssManager.bdTheme('#666', '#888')};
}
.toggle {
width: 44px;
height: 24px;
background: ${cssManager.bdTheme('#ddd', '#333')};
border-radius: 12px;
position: relative;
cursor: pointer;
transition: background 0.2s ease;
}
.toggle.active {
background: #3b82f6;
}
.toggle::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
transition: transform 0.2s ease;
}
.toggle.active::after {
transform: translateX(20px);
}
.button-group {
display: flex;
gap: 12px;
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid ${cssManager.bdTheme('#e5e5e5', '#222')};
}
.btn {
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.btn-primary {
background: #3b82f6;
color: white;
border: none;
}
.btn-primary:hover {
background: #2563eb;
}
.btn-secondary {
background: transparent;
color: ${cssManager.bdTheme('#666', '#999')};
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
}
.btn-secondary:hover {
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
}
.btn-danger {
background: #ef4444;
color: white;
border: none;
}
.btn-danger:hover {
background: #dc2626;
}
.security-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
background: ${cssManager.bdTheme('#f9f9f9', '#0a0a0a')};
border-radius: 8px;
margin-bottom: 12px;
}
.security-item .info {
display: flex;
flex-direction: column;
gap: 4px;
}
.security-item .title {
font-weight: 500;
}
.security-item .status {
font-size: 13px;
color: ${cssManager.bdTheme('#666', '#888')};
}
.security-item .status.enabled {
color: #22c55e;
}
`,
];
private handleTabClick(tab: 'profile' | 'notifications' | 'security') {
this.activeTab = tab;
}
public render() {
return html`
<div class="settings-layout">
<div class="settings-header">
<h1>Settings</h1>
<p>Manage your account settings and preferences</p>
</div>
<div class="settings-content">
<nav class="settings-nav">
<div
class="nav-item ${this.activeTab === 'profile' ? 'active' : ''}"
@click=${() => this.handleTabClick('profile')}
>
Profile
</div>
<div
class="nav-item ${this.activeTab === 'notifications' ? 'active' : ''}"
@click=${() => this.handleTabClick('notifications')}
>
Notifications
</div>
<div
class="nav-item ${this.activeTab === 'security' ? 'active' : ''}"
@click=${() => this.handleTabClick('security')}
>
Security
</div>
</nav>
<div class="settings-panel">
${this.activeTab === 'profile' ? this.renderProfile() : null}
${this.activeTab === 'notifications' ? this.renderNotifications() : null}
${this.activeTab === 'security' ? this.renderSecurity() : null}
</div>
</div>
</div>
`;
}
private renderProfile() {
return html`
<div class="panel-header">
<h2>Profile Information</h2>
<p>Update your personal details and email address</p>
</div>
<div class="form-row">
<div class="form-group">
<label>First Name</label>
<input type="text" value="John" />
</div>
<div class="form-group">
<label>Last Name</label>
<input type="text" value="Doe" />
</div>
</div>
<div class="form-group">
<label>Email Address</label>
<input type="email" value="${this.userEmail}" />
</div>
<div class="form-group">
<label>Bio</label>
<input type="text" placeholder="Tell us about yourself..." />
</div>
<div class="button-group">
<button class="btn btn-primary">Save Changes</button>
<button class="btn btn-secondary">Cancel</button>
</div>
`;
}
private renderNotifications() {
return html`
<div class="panel-header">
<h2>Notification Preferences</h2>
<p>Choose what notifications you want to receive</p>
</div>
<div class="toggle-group">
<div class="toggle-label">
<span class="title">Email Notifications</span>
<span class="description">Receive email updates about your account activity</span>
</div>
<div class="toggle active"></div>
</div>
<div class="toggle-group">
<div class="toggle-label">
<span class="title">Push Notifications</span>
<span class="description">Receive push notifications on your device</span>
</div>
<div class="toggle active"></div>
</div>
<div class="toggle-group">
<div class="toggle-label">
<span class="title">Weekly Digest</span>
<span class="description">Get a weekly summary of your activity</span>
</div>
<div class="toggle"></div>
</div>
<div class="toggle-group">
<div class="toggle-label">
<span class="title">Marketing Emails</span>
<span class="description">Receive tips, updates, and promotions</span>
</div>
<div class="toggle"></div>
</div>
<div class="button-group">
<button class="btn btn-primary">Save Preferences</button>
</div>
`;
}
private renderSecurity() {
return html`
<div class="panel-header">
<h2>Security Settings</h2>
<p>Manage your password and security options</p>
</div>
<div class="security-item">
<div class="info">
<span class="title">Password</span>
<span class="status">Last changed 30 days ago</span>
</div>
<button class="btn btn-secondary">Change Password</button>
</div>
<div class="security-item">
<div class="info">
<span class="title">Two-Factor Authentication</span>
<span class="status enabled">Enabled</span>
</div>
<button class="btn btn-secondary">Manage</button>
</div>
<div class="security-item">
<div class="info">
<span class="title">Active Sessions</span>
<span class="status">3 devices</span>
</div>
<button class="btn btn-secondary">View All</button>
</div>
<div class="button-group">
<button class="btn btn-danger">Delete Account</button>
</div>
`;
}
}

View File

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

View File

@@ -0,0 +1,211 @@
import { DeesElement, property, html, customElement, type TemplateResult, state, css, cssManager } from '@design.estate/dees-element';
export interface IContextMenuItem {
name: string;
iconName?: string;
action: () => void | Promise<void>;
disabled?: boolean;
}
@customElement('wcc-contextmenu')
export class WccContextmenu extends DeesElement {
// Static method to show context menu at position
public static async show(
event: MouseEvent,
menuItems: IContextMenuItem[]
): Promise<void> {
event.preventDefault();
event.stopPropagation();
// Remove any existing context menu
const existing = document.querySelector('wcc-contextmenu');
if (existing) {
existing.remove();
}
const menu = new WccContextmenu();
menu.menuItems = menuItems;
menu.x = event.clientX;
menu.y = event.clientY;
document.body.appendChild(menu);
// Wait for render then adjust position if needed
await menu.updateComplete;
menu.adjustPosition();
}
@property({ type: Array })
accessor menuItems: IContextMenuItem[] = [];
@property({ type: Number })
accessor x: number = 0;
@property({ type: Number })
accessor y: number = 0;
@state()
accessor visible: boolean = false;
private boundHandleOutsideClick = this.handleOutsideClick.bind(this);
private boundHandleKeydown = this.handleKeydown.bind(this);
public static styles = [
css`
:host {
position: fixed;
z-index: 10000;
opacity: 0;
transform: scale(0.95) translateY(-5px);
transition: opacity 0.15s ease, transform 0.15s ease;
pointer-events: none;
}
:host(.visible) {
opacity: 1;
transform: scale(1) translateY(0);
pointer-events: auto;
}
.menu {
min-width: 160px;
background: #0f0f0f;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
padding: 4px 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
font-size: 12px;
}
.menu-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
color: #ccc;
cursor: pointer;
transition: background 0.1s ease;
user-select: none;
}
.menu-item:hover {
background: rgba(59, 130, 246, 0.15);
color: #fff;
}
.menu-item.disabled {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}
.menu-item .icon {
font-family: 'Material Symbols Outlined';
font-size: 16px;
font-weight: normal;
font-style: normal;
line-height: 1;
letter-spacing: normal;
text-transform: none;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 24;
opacity: 0.7;
}
.menu-item:hover .icon {
opacity: 1;
}
.menu-item .label {
flex: 1;
}
`
];
public render(): TemplateResult {
return html`
<div class="menu">
${this.menuItems.map(item => html`
<div
class="menu-item ${item.disabled ? 'disabled' : ''}"
@click=${() => this.handleItemClick(item)}
>
${item.iconName ? html`<span class="icon">${item.iconName}</span>` : null}
<span class="label">${item.name}</span>
</div>
`)}
</div>
`;
}
async connectedCallback() {
await super.connectedCallback();
// Delay adding listeners to avoid immediate close
requestAnimationFrame(() => {
document.addEventListener('click', this.boundHandleOutsideClick);
document.addEventListener('contextmenu', this.boundHandleOutsideClick);
document.addEventListener('keydown', this.boundHandleKeydown);
this.classList.add('visible');
});
}
async disconnectedCallback() {
await super.disconnectedCallback();
document.removeEventListener('click', this.boundHandleOutsideClick);
document.removeEventListener('contextmenu', this.boundHandleOutsideClick);
document.removeEventListener('keydown', this.boundHandleKeydown);
}
private adjustPosition() {
const rect = this.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
let x = this.x;
let y = this.y;
// Adjust if menu goes off right edge
if (x + rect.width > windowWidth - 10) {
x = windowWidth - rect.width - 10;
}
// Adjust if menu goes off bottom edge
if (y + rect.height > windowHeight - 10) {
y = windowHeight - rect.height - 10;
}
// Ensure not off left or top
if (x < 10) x = 10;
if (y < 10) y = 10;
this.style.left = `${x}px`;
this.style.top = `${y}px`;
}
private handleOutsideClick(e: Event) {
const path = e.composedPath();
if (!path.includes(this)) {
this.close();
}
}
private handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
this.close();
}
}
private async handleItemClick(item: IContextMenuItem) {
if (item.disabled) return;
await item.action();
this.close();
}
private close() {
this.classList.remove('visible');
setTimeout(() => this.remove(), 150);
}
}

View File

@@ -1,6 +1,7 @@
import { DeesElement, property, html, customElement, type TemplateResult, queryAsync, render, domtools } from '@design.estate/dees-element';
import { resolveTemplateFactory, getDemoAtIndex, getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
import type { TTemplateFactory } from './wcctools.helpers.js';
import type { IWccConfig, IWccSection, TElementType } from '../wcctools.interfaces.js';
import * as plugins from '../wcctools.plugins.js';
@@ -9,13 +10,37 @@ import './wcc-frame.js';
import './wcc-sidebar.js';
import './wcc-properties.js';
import { type TTheme } from './wcc-properties.js';
import { type TElementType } from './wcc-sidebar.js';
import { breakpoints } from '@design.estate/dees-domtools';
import { WccFrame } from './wcc-frame.js';
/**
* Get filtered and sorted items from a section
*/
export const getSectionItems = (section: IWccSection): Array<[string, any]> => {
let entries = Object.entries(section.items);
// Apply filter if provided
if (section.filter) {
entries = entries.filter(([name, item]) => section.filter(name, item));
}
// Apply sort if provided
if (section.sort) {
entries.sort(section.sort);
}
return entries;
};
@customElement('wcc-dashboard')
export class WccDashboard extends DeesElement {
@property()
accessor sections: IWccSection[] = [];
@property()
accessor selectedSection: IWccSection | null = null;
@property()
accessor selectedType: TElementType;
@@ -35,38 +60,58 @@ export class WccDashboard extends DeesElement {
accessor selectedTheme: TTheme = 'dark';
@property()
accessor isFullscreen: boolean = false;
accessor searchQuery: string = '';
@property()
accessor pages: Record<string, TTemplateFactory> = {};
// Pinned items as Set of "sectionName::itemName"
@property({ attribute: false })
accessor pinnedItems: Set<string> = new Set();
@property()
accessor elements: { [key: string]: DeesElement } = {};
// Sidebar width (resizable)
@property({ type: Number })
accessor sidebarWidth: number = 200;
// Derived from selectedViewport - no need for separate property
public get isNative(): boolean {
return this.selectedViewport === 'native';
}
@property()
accessor warning: string = null;
private frameScrollY: number = 0;
private sidebarScrollY: number = 0;
private scrollPositionsApplied: boolean = false;
@queryAsync('wcc-frame')
accessor wccFrame: Promise<WccFrame>;
constructor(
elementsArg?: { [key: string]: DeesElement },
pagesArg?: Record<string, TTemplateFactory>
) {
constructor(config?: IWccConfig) {
super();
if (elementsArg) {
this.elements = elementsArg;
console.log('got elements:');
console.log(this.elements);
if (config && config.sections) {
this.sections = config.sections;
console.log('got sections:', this.sections.map(s => s.name));
}
}
if (pagesArg) {
this.pages = pagesArg;
/**
* Find an item by name across all sections, returns the item and its section
*/
public findItemByName(name: string): { item: any; section: IWccSection } | null {
for (const section of this.sections) {
const entries = getSectionItems(section);
const found = entries.find(([itemName]) => itemName === name);
if (found) {
return { item: found[1], section };
}
}
return null;
}
/**
* Find a section by name (URL-decoded)
*/
public findSectionByName(name: string): IWccSection | null {
return this.sections.find(s => s.name === name) || null;
}
public render(): TemplateResult {
@@ -84,7 +129,10 @@ export class WccDashboard extends DeesElement {
<wcc-sidebar
.dashboardRef=${this}
.selectedItem=${this.selectedItem}
.isFullscreen=${this.isFullscreen}
.searchQuery=${this.searchQuery}
.pinnedItems=${this.pinnedItems}
.sidebarWidth=${this.sidebarWidth}
.isNative=${this.isNative}
@selectedType=${(eventArg) => {
this.selectedType = eventArg.detail;
}}
@@ -94,6 +142,23 @@ export class WccDashboard extends DeesElement {
@selectedItem=${(eventArg) => {
this.selectedItem = eventArg.detail;
}}
@searchChanged=${(eventArg: CustomEvent) => {
this.searchQuery = eventArg.detail;
this.updateUrlWithScrollState();
}}
@pinnedChanged=${(eventArg: CustomEvent) => {
this.pinnedItems = eventArg.detail;
this.updateUrlWithScrollState();
}}
@widthChanged=${async (eventArg: CustomEvent) => {
this.sidebarWidth = eventArg.detail;
this.updateUrlWithScrollState();
const frame = await this.wccFrame;
if (frame) {
frame.sidebarWidth = eventArg.detail;
frame.requestUpdate();
}
}}
></wcc-sidebar>
<wcc-properties
.dashboardRef=${this}
@@ -101,7 +166,8 @@ export class WccDashboard extends DeesElement {
.selectedItem=${this.selectedItem}
.selectedViewport=${this.selectedViewport}
.selectedTheme=${this.selectedTheme}
.isFullscreen=${this.isFullscreen}
.isNative=${this.isNative}
.sidebarWidth=${this.sidebarWidth}
@selectedViewport=${(eventArg) => {
this.selectedViewport = eventArg.detail;
this.scheduleUpdate();
@@ -116,11 +182,11 @@ export class WccDashboard extends DeesElement {
frame.requestUpdate();
}
}}
@toggleFullscreen=${() => {
this.toggleFullscreen();
@toggleNative=${() => {
this.toggleNative();
}}
></wcc-properties>
<wcc-frame id="wccFrame" viewport=${this.selectedViewport} .isFullscreen=${this.isFullscreen}>
<wcc-frame id="wccFrame" viewport=${this.selectedViewport} .isNative=${this.isNative} .sidebarWidth=${this.sidebarWidth}>
</wcc-frame>
`;
}
@@ -135,17 +201,20 @@ export class WccDashboard extends DeesElement {
}
}
public toggleFullscreen() {
this.isFullscreen = !this.isFullscreen;
public toggleNative() {
// Toggle between 'native' and 'desktop' viewports
this.selectedViewport = this.selectedViewport === 'native' ? 'desktop' : 'native';
this.buildUrl();
}
public async firstUpdated() {
this.domtools = await plugins.deesDomtools.DomTools.setupDomTools();
// Add ESC key handler for fullscreen mode
// Add ESC key handler for native mode
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && this.isFullscreen) {
this.isFullscreen = false;
if (event.key === 'Escape' && this.isNative) {
this.selectedViewport = 'desktop';
this.buildUrl();
}
});
@@ -154,37 +223,88 @@ export class WccDashboard extends DeesElement {
this.setupScrollListeners();
}, 500);
// Route with demo index (new format)
// New route format with section name
this.domtools.router.on(
'/wcctools-route/:itemType/:itemName/:demoIndex/:viewport/:theme',
'/wcctools-route/:sectionName/:itemName/:demoIndex/:viewport/:theme',
async (routeInfo) => {
this.selectedType = routeInfo.params.itemType as TElementType;
const sectionName = decodeURIComponent(routeInfo.params.sectionName);
this.selectedSection = this.findSectionByName(sectionName);
this.selectedItemName = routeInfo.params.itemName;
this.selectedDemoIndex = parseInt(routeInfo.params.demoIndex) || 0;
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
this.selectedTheme = routeInfo.params.theme as TTheme;
if (routeInfo.params.itemType === 'element') {
this.selectedItem = this.elements[routeInfo.params.itemName];
} else if (routeInfo.params.itemType === 'page') {
this.selectedItem = this.pages[routeInfo.params.itemName];
if (this.selectedSection) {
// Find item within the section
const entries = getSectionItems(this.selectedSection);
const found = entries.find(([name]) => name === routeInfo.params.itemName);
if (found) {
this.selectedItem = found[1];
this.selectedType = this.selectedSection.type === 'elements' ? 'element' : 'page';
}
} else {
// Fallback: try legacy format (element/page as section name)
const legacyType = routeInfo.params.sectionName;
if (legacyType === 'element' || legacyType === 'page') {
this.selectedType = legacyType as TElementType;
// Find item in any matching section
const result = this.findItemByName(routeInfo.params.itemName);
if (result) {
this.selectedItem = result.item;
this.selectedSection = result.section;
}
}
}
// Restore scroll positions from query parameters
// Restore state from query parameters
if (routeInfo.queryParams) {
const search = routeInfo.queryParams.search;
const frameScrollY = routeInfo.queryParams.frameScrollY;
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
const pinned = routeInfo.queryParams.pinned;
const sidebarWidth = routeInfo.queryParams.sidebarWidth;
if (search) {
this.searchQuery = search;
} else {
this.searchQuery = '';
}
if (frameScrollY) {
this.frameScrollY = parseInt(frameScrollY);
}
if (sidebarScrollY) {
this.sidebarScrollY = parseInt(sidebarScrollY);
}
if (pinned) {
const newPinned = new Set(pinned.split(',').filter(Boolean));
// Only update if actually different to avoid update loops
if (this.pinnedItems.size !== newPinned.size ||
![...newPinned].every(k => this.pinnedItems.has(k))) {
this.pinnedItems = newPinned;
}
} else if (this.pinnedItems.size > 0) {
this.pinnedItems = new Set();
}
if (sidebarWidth) {
this.sidebarWidth = parseInt(sidebarWidth, 10);
}
// Apply scroll positions after a short delay to ensure DOM is ready
setTimeout(() => {
// Apply scroll positions and update frame after a short delay to ensure DOM is ready
setTimeout(async () => {
this.applyScrollPositions();
// Ensure frame gets the sidebarWidth
const frame = await this.wccFrame;
if (frame) {
frame.sidebarWidth = this.sidebarWidth;
frame.requestUpdate();
}
}, 100);
} else {
this.searchQuery = '';
// Only clear if not already empty to avoid update loops
if (this.pinnedItems.size > 0) {
this.pinnedItems = new Set();
}
}
const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
@@ -196,35 +316,78 @@ export class WccDashboard extends DeesElement {
// Legacy route without demo index (for backwards compatibility)
this.domtools.router.on(
'/wcctools-route/:itemType/:itemName/:viewport/:theme',
'/wcctools-route/:sectionName/:itemName/:viewport/:theme',
async (routeInfo) => {
this.selectedType = routeInfo.params.itemType as TElementType;
const sectionName = decodeURIComponent(routeInfo.params.sectionName);
this.selectedSection = this.findSectionByName(sectionName);
this.selectedItemName = routeInfo.params.itemName;
this.selectedDemoIndex = 0; // Default to first demo
this.selectedDemoIndex = 0;
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
this.selectedTheme = routeInfo.params.theme as TTheme;
if (routeInfo.params.itemType === 'element') {
this.selectedItem = this.elements[routeInfo.params.itemName];
} else if (routeInfo.params.itemType === 'page') {
this.selectedItem = this.pages[routeInfo.params.itemName];
if (this.selectedSection) {
const entries = getSectionItems(this.selectedSection);
const found = entries.find(([name]) => name === routeInfo.params.itemName);
if (found) {
this.selectedItem = found[1];
this.selectedType = this.selectedSection.type === 'elements' ? 'element' : 'page';
}
} else {
// Fallback: try legacy format
const legacyType = routeInfo.params.sectionName;
if (legacyType === 'element' || legacyType === 'page') {
this.selectedType = legacyType as TElementType;
const result = this.findItemByName(routeInfo.params.itemName);
if (result) {
this.selectedItem = result.item;
this.selectedSection = result.section;
}
}
}
// Restore scroll positions from query parameters
// Restore state from query parameters
if (routeInfo.queryParams) {
const search = routeInfo.queryParams.search;
const frameScrollY = routeInfo.queryParams.frameScrollY;
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
const pinned = routeInfo.queryParams.pinned;
const sidebarWidth = routeInfo.queryParams.sidebarWidth;
if (search) {
this.searchQuery = search;
} else {
this.searchQuery = '';
}
if (frameScrollY) {
this.frameScrollY = parseInt(frameScrollY);
}
if (sidebarScrollY) {
this.sidebarScrollY = parseInt(sidebarScrollY);
}
if (pinned) {
const newPinned = new Set(pinned.split(',').filter(Boolean));
// Only update if actually different to avoid update loops
if (this.pinnedItems.size !== newPinned.size ||
![...newPinned].every(k => this.pinnedItems.has(k))) {
this.pinnedItems = newPinned;
}
} else if (this.pinnedItems.size > 0) {
this.pinnedItems = new Set();
}
if (sidebarWidth) {
this.sidebarWidth = parseInt(sidebarWidth, 10);
}
// Apply scroll positions after a short delay to ensure DOM is ready
setTimeout(() => {
this.applyScrollPositions();
}, 100);
} else {
this.searchQuery = '';
// Only clear if not already empty to avoid update loops
if (this.pinnedItems.size > 0) {
this.pinnedItems = new Set();
}
}
const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
@@ -292,15 +455,27 @@ export class WccDashboard extends DeesElement {
}
public buildUrl() {
const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
const sectionName = this.selectedSection
? encodeURIComponent(this.selectedSection.name)
: this.selectedType; // Fallback for legacy
const baseUrl = `/wcctools-route/${sectionName}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
const queryParams = new URLSearchParams();
if (this.searchQuery) {
queryParams.set('search', this.searchQuery);
}
if (this.frameScrollY > 0) {
queryParams.set('frameScrollY', this.frameScrollY.toString());
}
if (this.sidebarScrollY > 0) {
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
}
if (this.pinnedItems.size > 0) {
queryParams.set('pinned', Array.from(this.pinnedItems).join(','));
}
if (this.sidebarWidth !== 200) {
queryParams.set('sidebarWidth', this.sidebarWidth.toString());
}
const queryString = queryParams.toString();
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
@@ -346,15 +521,27 @@ export class WccDashboard extends DeesElement {
}
private updateUrlWithScrollState() {
const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
const sectionName = this.selectedSection
? encodeURIComponent(this.selectedSection.name)
: this.selectedType; // Fallback for legacy
const baseUrl = `/wcctools-route/${sectionName}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
const queryParams = new URLSearchParams();
if (this.searchQuery) {
queryParams.set('search', this.searchQuery);
}
if (this.frameScrollY > 0) {
queryParams.set('frameScrollY', this.frameScrollY.toString());
}
if (this.sidebarScrollY > 0) {
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
}
if (this.pinnedItems.size > 0) {
queryParams.set('pinned', Array.from(this.pinnedItems).join(','));
}
if (this.sidebarWidth !== 200) {
queryParams.set('sidebarWidth', this.sidebarWidth.toString());
}
const queryString = queryParams.toString();
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;

View File

@@ -17,7 +17,13 @@ export class WccFrame extends DeesElement {
accessor advancedEditorOpen: boolean = false;
@property({ type: Boolean })
accessor isFullscreen: boolean = false;
accessor isNative: boolean = false;
@property({ type: Number })
accessor sidebarWidth: number = 200;
@property({ type: Boolean })
accessor isResizing: boolean = false;
public static styles = [
css`
@@ -25,7 +31,6 @@ export class WccFrame extends DeesElement {
border: 10px solid #ffaeaf;
position: absolute;
background: ${cssManager.bdTheme('#333', '#000')};
left: 200px;
right: 0px;
top: 0px;
overflow-y: auto;
@@ -46,7 +51,7 @@ export class WccFrame extends DeesElement {
return html`
<style>
:host {
${this.isFullscreen ? `
${this.isNative ? `
border: none !important;
left: 0px !important;
right: 0px !important;
@@ -55,10 +60,10 @@ export class WccFrame extends DeesElement {
` : `
bottom: ${this.advancedEditorOpen ? '400px' : '100px'};
border: 10px solid #ffaeaf;
left: 200px;
left: ${this.sidebarWidth}px;
`}
transition: all 0.3s ease;
${this.isFullscreen ? 'padding: 0px;' : (() => {
transition: ${this.isResizing ? 'none' : 'all 0.3s ease'};
${this.isNative ? 'padding: 0px;' : (() => {
switch (this.viewport) {
case 'desktop':
return `
@@ -67,19 +72,19 @@ export class WccFrame extends DeesElement {
case 'tablet':
return `
padding: 0px ${
(document.body.clientWidth - 200 - domtools.breakpoints.tablet) / 2
(document.body.clientWidth - this.sidebarWidth - domtools.breakpoints.tablet) / 2
}px;
`;
case 'phablet':
return `
padding: 0px ${
(document.body.clientWidth - 200 - domtools.breakpoints.phablet) / 2
(document.body.clientWidth - this.sidebarWidth - domtools.breakpoints.phablet) / 2
}px;
`;
case 'phone':
return `
padding: 0px ${
(document.body.clientWidth - 200 - domtools.breakpoints.phone) / 2
(document.body.clientWidth - this.sidebarWidth - domtools.breakpoints.phone) / 2
}px;
`;
}
@@ -87,7 +92,7 @@ export class WccFrame extends DeesElement {
}
.viewport {
${!this.isFullscreen && this.viewport !== 'desktop'
${!this.isNative && this.viewport !== 'desktop'
? html` border-right: 1px dotted #444; border-left: 1px dotted #444; `
: html``
}

View File

@@ -35,7 +35,10 @@ export class WccProperties extends DeesElement {
accessor warning: string = null;
@property()
accessor isFullscreen: boolean = false;
accessor isNative: boolean = false;
@property({ type: Number })
accessor sidebarWidth: number = 200;
@state()
accessor propertyContent: TemplateResult[] = [];
@@ -89,18 +92,18 @@ export class WccProperties extends DeesElement {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
box-sizing: border-box;
position: absolute;
left: 200px;
left: ${this.sidebarWidth}px;
height: ${this.editingProperties.length > 0 ? 100 + this.editorHeight : 100}px;
bottom: 0px;
right: 0px;
overflow: hidden;
background: var(--background);
color: var(--foreground);
display: ${this.isFullscreen ? 'none' : 'block'};
display: ${this.isNative ? 'none' : 'block'};
}
.grid {
display: grid;
grid-template-columns: 1fr 150px 300px 70px 70px;
grid-template-columns: 1fr 150px 350px 100px;
height: 100%;
}
.properties {
@@ -197,7 +200,8 @@ export class WccProperties extends DeesElement {
}
.viewportSelector,
.themeSelector {
.themeSelector,
.shareSelector {
user-select: none;
background: transparent;
display: flex;
@@ -206,12 +210,22 @@ export class WccProperties extends DeesElement {
}
.selectorButtons2 {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: repeat(2, minmax(0, 1fr));
flex: 1;
}
.selectorButtons4 {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(4, minmax(0, 1fr));
flex: 1;
}
.selectorButtons5 {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
flex: 1;
}
.selectorButtons1 {
display: grid;
grid-template-columns: minmax(0, 1fr);
flex: 1;
}
.button {
@@ -248,6 +262,11 @@ export class WccProperties extends DeesElement {
.button .material-symbols-outlined {
font-size: 18px;
font-variation-settings: 'FILL' 0, 'wght' 300;
min-width: 18px;
min-height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.button.selected .material-symbols-outlined {
@@ -290,7 +309,7 @@ export class WccProperties extends DeesElement {
top: 0.5rem;
bottom: 0.5rem;
left: 0.5rem;
right: calc(520px + 0.5rem);
right: calc(600px + 0.5rem);
display: flex;
align-items: center;
justify-content: center;
@@ -623,7 +642,7 @@ export class WccProperties extends DeesElement {
</div>
<div class="viewportSelector">
<div class="panelheading">Viewport</div>
<div class="selectorButtons4">
<div class="selectorButtons5">
<div
class="button ${this.selectedViewport === 'phone' ? 'selected' : null}"
@click=${() => {
@@ -649,29 +668,34 @@ export class WccProperties extends DeesElement {
Tablet<i class="material-symbols-outlined">tablet</i>
</div>
<div
class="button ${this.selectedViewport === 'desktop' ||
this.selectedViewport === 'native'
? 'selected'
: null}"
class="button ${this.selectedViewport === 'desktop' ? 'selected' : null}"
@click=${() => {
this.selectViewport('native');
this.selectViewport('desktop');
}}
>
Desktop<i class="material-symbols-outlined">desktop_windows</i>
</div>
<div
class="button ${this.selectedViewport === 'native' ? 'selected' : null}"
@click=${() => {
this.selectViewport('native');
}}
>
Native<i class="material-symbols-outlined">fullscreen</i>
</div>
</div>
</div>
<div class="docs" @click=${() => this.toggleFullscreen()}>
<i class="material-symbols-outlined" style="font-size: 20px;">
${this.isFullscreen ? 'fullscreen_exit' : 'fullscreen'}
</i>
<div class="shareSelector">
<div class="panelheading">Share</div>
<div class="selectorButtons1">
<div
class="button ${this.isRecording ? 'selected' : ''}"
@click=${() => this.handleRecordButtonClick()}
>
Record<i class="material-symbols-outlined">${this.isRecording ? 'stop_circle' : 'videocam'}</i>
</div>
</div>
</div>
<!-- Recording Button -->
<wcc-record-button
.state=${this.isRecording ? 'recording' : 'idle'}
.duration=${this.recordingDuration}
@record-click=${() => this.handleRecordButtonClick()}
></wcc-record-button>
</div>
${this.warning ? html`<div class="warning">${this.warning}</div>` : null}
</div>
@@ -904,16 +928,16 @@ export class WccProperties extends DeesElement {
this.dashboardRef.buildUrl();
}
public async scheduleUpdate() {
try {
await this.createProperties();
} catch (error) {
console.error('Error creating properties:', error);
// Clear property content on error to show clean state
this.propertyContent = [];
protected updated(changedProperties: Map<string, unknown>) {
super.updated(changedProperties);
// Only recreate properties when selectedItem changes
if (changedProperties.has('selectedItem')) {
this.createProperties().catch(error => {
console.error('Error creating properties:', error);
this.propertyContent = [];
});
}
// Always call super.scheduleUpdate to ensure component updates
super.scheduleUpdate();
}
public selectViewport(viewport: TEnvironment) {
@@ -1016,9 +1040,9 @@ export class WccProperties extends DeesElement {
);
}
private toggleFullscreen() {
private toggleNative() {
this.dispatchEvent(
new CustomEvent('toggleFullscreen', {
new CustomEvent('toggleNative', {
bubbles: true
})
);

View File

@@ -1,10 +1,10 @@
import * as plugins from '../wcctools.plugins.js';
import { DeesElement, property, html, customElement, type TemplateResult, state } from '@design.estate/dees-element';
import { WccDashboard } from './wcc-dashboard.js';
import { WccDashboard, getSectionItems } from './wcc-dashboard.js';
import type { TTemplateFactory } from './wcctools.helpers.js';
import { getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
export type TElementType = 'element' | 'page';
import type { IWccSection, TElementType } from '../wcctools.interfaces.js';
import { WccContextmenu } from './wcc-contextmenu.js';
@customElement('wcc-sidebar')
export class WccSidebar extends DeesElement {
@@ -18,12 +18,34 @@ export class WccSidebar extends DeesElement {
accessor dashboardRef: WccDashboard;
@property()
accessor isFullscreen: boolean = false;
accessor isNative: boolean = false;
// Track which elements are expanded (for multi-demo elements)
@state()
accessor expandedElements: Set<string> = new Set();
// Track which sections are collapsed
@state()
accessor collapsedSections: Set<string> = new Set();
// Search query for filtering sidebar items
@property()
accessor searchQuery: string = '';
// Pinned items as Set of "sectionName::itemName"
@property({ attribute: false })
accessor pinnedItems: Set<string> = new Set();
// Sidebar width (resizable)
@property({ type: Number })
accessor sidebarWidth: number = 200;
// Track if currently resizing
@state()
accessor isResizing: boolean = false;
private sectionsInitialized = false;
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" />
@@ -45,14 +67,14 @@ export class WccSidebar extends DeesElement {
--ring: #3b82f6;
--radius: 4px;
display: ${this.isFullscreen ? 'none' : 'block'};
display: ${this.isNative ? 'none' : 'block'};
border-right: 1px solid rgba(255, 255, 255, 0.08);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
font-size: 14px;
box-sizing: border-box;
position: absolute;
left: 0px;
width: 200px;
width: ${this.sidebarWidth}px;
top: 0px;
bottom: 0px;
overflow-y: auto;
@@ -65,7 +87,7 @@ export class WccSidebar extends DeesElement {
padding: 0.5rem 0;
}
h3 {
.section-header {
padding: 0.3rem 0.75rem;
font-size: 0.65rem;
font-weight: 500;
@@ -77,12 +99,45 @@ export class WccSidebar extends DeesElement {
background: rgba(59, 130, 246, 0.03);
border-bottom: 1px solid var(--border);
border-top: 1px solid var(--border);
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
user-select: none;
transition: all 0.15s ease;
}
h3:first-child {
.section-header:first-child {
margin-top: 0;
}
.section-header:hover {
background: rgba(59, 130, 246, 0.08);
}
.section-header .expand-icon {
font-size: 14px;
opacity: 0.5;
transition: transform 0.2s ease;
}
.section-header.collapsed .expand-icon {
transform: rotate(-90deg);
}
.section-header .section-icon {
font-size: 14px;
opacity: 0.6;
}
.section-content {
overflow: hidden;
}
.section-content.collapsed {
display: none;
}
.material-symbols-outlined {
font-family: 'Material Symbols Outlined';
font-weight: normal;
@@ -117,7 +172,11 @@ export class WccSidebar extends DeesElement {
}
.selectOption.folder {
grid-template-columns: 16px 20px 1fr;
grid-template-columns: 16px 1fr;
}
.selectOption.folder .text {
margin-left: 4px;
}
.selectOption .expand-icon {
@@ -214,90 +273,429 @@ export class WccSidebar extends DeesElement {
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
</style>
<div class="menu">
<h3>Pages</h3>
${(() => {
const pages = Object.keys(this.dashboardRef.pages);
return pages.map(pageName => {
const item = this.dashboardRef.pages[pageName];
return html`
<div
class="selectOption ${this.selectedItem === item ? 'selected' : null}"
@click=${async () => {
const domtools = await plugins.deesDomtools.DomTools.setupDomTools();
this.selectItem('page', pageName, item, 0);
}}
>
<i class="material-symbols-outlined">insert_drive_file</i>
<div class="text">${pageName}</div>
</div>
`;
});
})()}
<h3>Elements</h3>
${(() => {
const elements = Object.keys(this.dashboardRef.elements);
return elements.map(elementName => {
const item = this.dashboardRef.elements[elementName] as any;
const demoCount = item.demo ? getDemoCount(item.demo) : 0;
const isMultiDemo = item.demo && hasMultipleDemos(item.demo);
const isExpanded = this.expandedElements.has(elementName);
const isSelected = this.selectedItem === item;
if (isMultiDemo) {
// Multi-demo element - render as expandable folder
return html`
<div
class="selectOption folder ${isExpanded ? 'expanded' : ''} ${isSelected ? 'selected' : ''}"
@click=${() => this.toggleExpanded(elementName)}
>
<i class="material-symbols-outlined expand-icon">chevron_right</i>
<i class="material-symbols-outlined">folder</i>
<div class="text">${elementName}</div>
</div>
${isExpanded ? html`
<div class="demo-children">
${Array.from({ length: demoCount }, (_, i) => {
const demoIndex = i;
const isThisDemoSelected = isSelected && this.dashboardRef.selectedDemoIndex === demoIndex;
return html`
<div
class="demo-child ${isThisDemoSelected ? 'selected' : ''}"
@click=${async () => {
await plugins.deesDomtools.DomTools.setupDomTools();
this.selectItem('element', elementName, item, demoIndex);
}}
>
<i class="material-symbols-outlined">play_circle</i>
<div class="text">demo${demoIndex + 1}</div>
</div>
`;
})}
</div>
` : null}
`;
} else {
// Single demo element - render as normal
return html`
<div
class="selectOption ${isSelected ? 'selected' : null}"
@click=${async () => {
await plugins.deesDomtools.DomTools.setupDomTools();
this.selectItem('element', elementName, item, 0);
}}
>
<i class="material-symbols-outlined">featured_video</i>
<div class="text">${elementName}</div>
</div>
`;
}
});
})()}
.search-container {
padding: 0.5rem;
border-bottom: 1px solid var(--border);
}
.search-input {
width: 100%;
box-sizing: border-box;
background: var(--input);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.5rem 0.75rem;
color: var(--foreground);
font-size: 0.75rem;
font-family: inherit;
outline: none;
transition: border-color 0.15s ease;
}
.search-input:focus {
border-color: var(--primary);
}
.search-input::placeholder {
color: var(--muted-foreground);
}
.highlight {
background: rgba(59, 130, 246, 0.3);
border-radius: 2px;
}
/* Pinned item highlight in original section */
.selectOption.pinned {
background: rgba(245, 158, 11, 0.08);
}
.selectOption.pinned:hover {
background: rgba(245, 158, 11, 0.12);
}
.selectOption.pinned.selected {
background: rgba(245, 158, 11, 0.18);
}
/* Pinned section styling */
.section-header.pinned-section {
background: rgba(245, 158, 11, 0.08);
color: #f59e0b;
}
.section-header.pinned-section:hover {
background: rgba(245, 158, 11, 0.12);
}
.section-header.pinned-section .section-icon {
opacity: 0.8;
}
/* Section tag pill for pinned items */
.section-tag {
font-size: 0.5rem;
color: #888;
margin-left: auto;
text-transform: uppercase;
letter-spacing: 0.02em;
background: rgba(255, 255, 255, 0.06);
padding: 0.15rem 0.4rem;
border-radius: 9999px;
white-space: nowrap;
}
/* Group container */
.item-group {
margin: 0.375rem 0.375rem;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 6px;
padding: 0.25rem 0;
background: rgba(255, 255, 255, 0.01);
}
.item-group-legend {
font-size: 0.55rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #555;
padding: 0.125rem 0.625rem 0.25rem;
display: block;
}
.item-group .selectOption {
margin-left: 0.25rem;
margin-right: 0.25rem;
}
/* Resize handle */
.resize-handle {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 4px;
cursor: col-resize;
background: transparent;
transition: background 0.15s ease;
z-index: 10;
}
.resize-handle:hover {
background: rgba(59, 130, 246, 0.3);
}
.resize-handle.active {
background: var(--primary);
}
</style>
<div class="search-container">
<input
type="text"
class="search-input"
placeholder="Search..."
.value=${this.searchQuery}
@input=${this.handleSearchInput}
/>
</div>
<div class="menu">
${this.renderPinnedSection()}
${this.renderSections()}
</div>
<div
class="resize-handle ${this.isResizing ? 'active' : ''}"
@mousedown=${this.startResize}
></div>
`;
}
/**
* Initialize collapsed sections from section config
*/
private initCollapsedSections() {
if (this.sectionsInitialized || !this.dashboardRef?.sections) return;
const collapsed = new Set<string>();
for (const section of this.dashboardRef.sections) {
if (section.collapsed) {
collapsed.add(section.name);
}
}
this.collapsedSections = collapsed;
this.sectionsInitialized = true;
}
// ============ Pinning helpers ============
private getPinKey(sectionName: string, itemName: string): string {
return `${sectionName}::${itemName}`;
}
private isPinned(sectionName: string, itemName: string): boolean {
return this.pinnedItems.has(this.getPinKey(sectionName, itemName));
}
private togglePin(sectionName: string, itemName: string) {
const key = this.getPinKey(sectionName, itemName);
const newPinned = new Set(this.pinnedItems);
if (newPinned.has(key)) {
newPinned.delete(key);
} else {
newPinned.add(key);
}
this.pinnedItems = newPinned;
this.dispatchEvent(new CustomEvent('pinnedChanged', { detail: newPinned }));
}
private showContextMenu(e: MouseEvent, sectionName: string, itemName: string) {
const isPinned = this.isPinned(sectionName, itemName);
WccContextmenu.show(e, [
{
name: isPinned ? 'Unpin' : 'Pin',
iconName: isPinned ? 'push_pin' : 'push_pin',
action: () => this.togglePin(sectionName, itemName),
},
]);
}
/**
* Render the PINNED section (only if there are pinned items)
*/
private renderPinnedSection() {
if (!this.dashboardRef?.sections || this.pinnedItems.size === 0) {
return null;
}
const isCollapsed = this.collapsedSections.has('__pinned__');
// Collect pinned items with their original section info
// Pinned items are NOT filtered by search - they always remain visible
const pinnedEntries: Array<{ sectionName: string; itemName: string; item: any; section: IWccSection }> = [];
for (const key of this.pinnedItems) {
const [sectionName, itemName] = key.split('::');
const section = this.dashboardRef.sections.find(s => s.name === sectionName);
if (section) {
const entries = getSectionItems(section);
const found = entries.find(([name]) => name === itemName);
if (found) {
pinnedEntries.push({ sectionName, itemName, item: found[1], section });
}
}
}
if (pinnedEntries.length === 0) {
return null;
}
return html`
<div
class="section-header pinned-section ${isCollapsed ? 'collapsed' : ''}"
@click=${() => this.toggleSectionCollapsed('__pinned__')}
>
<i class="material-symbols-outlined expand-icon">expand_more</i>
<i class="material-symbols-outlined section-icon">push_pin</i>
<span>Pinned</span>
</div>
<div class="section-content ${isCollapsed ? 'collapsed' : ''}">
${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';
return html`
<div
class="selectOption ${isSelected ? 'selected' : ''}"
@click=${async () => {
await plugins.deesDomtools.DomTools.setupDomTools();
this.selectItem(type, itemName, item, 0, section);
}}
@contextmenu=${(e: MouseEvent) => this.showContextMenu(e, sectionName, itemName)}
>
<i class="material-symbols-outlined">${icon}</i>
<div class="text">${this.highlightMatch(itemName)}</div>
<span class="section-tag">${sectionName}</span>
</div>
`;
})}
</div>
`;
}
/**
* Render all sections
*/
private renderSections() {
if (!this.dashboardRef?.sections) {
return null;
}
this.initCollapsedSections();
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));
// Hide section if no items match the search
if (filteredEntries.length === 0 && this.searchQuery) {
return null;
}
const isCollapsed = this.collapsedSections.has(section.name);
const sectionIcon = section.icon || (section.type === 'pages' ? 'insert_drive_file' : 'widgets');
return html`
<div
class="section-header ${isCollapsed ? 'collapsed' : ''}"
@click=${() => this.toggleSectionCollapsed(section.name)}
>
<i class="material-symbols-outlined expand-icon">expand_more</i>
${section.icon ? html`<i class="material-symbols-outlined section-icon">${section.icon}</i>` : null}
<span>${section.name}</span>
</div>
<div class="section-content ${isCollapsed ? 'collapsed' : ''}">
${this.renderSectionItems(section)}
</div>
`;
});
}
/**
* Render items for a section
*/
private renderSectionItems(section: IWccSection) {
const entries = getSectionItems(section);
// Filter entries by search query
const filteredEntries = entries.filter(([name]) => this.matchesSearch(name));
if (section.type === 'pages') {
return filteredEntries.map(([pageName, item]) => {
const isPinned = this.isPinned(section.name, pageName);
return html`
<div
class="selectOption ${this.selectedItem === item ? 'selected' : ''} ${isPinned ? 'pinned' : ''}"
@click=${async () => {
await plugins.deesDomtools.DomTools.setupDomTools();
this.selectItem('page', pageName, item, 0, section);
}}
@contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, pageName)}
>
<i class="material-symbols-outlined">insert_drive_file</i>
<div class="text">${this.highlightMatch(pageName)}</div>
</div>
`;
});
} else {
// type === 'elements' - group by demoGroup
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, []);
}
groupedItems.get(group)!.push(entry);
}
const result: TemplateResult[] = [];
// Render ungrouped items first
const ungrouped = groupedItems.get(null) || [];
for (const entry of ungrouped) {
result.push(this.renderElementItem(entry, section));
}
// Render grouped items
for (const [groupName, items] of groupedItems) {
if (groupName === null) continue;
result.push(html`
<div class="item-group">
<span class="item-group-legend">${groupName}</span>
${items.map((entry) => this.renderElementItem(entry, section))}
</div>
`);
}
return result;
}
}
/**
* Render a single element item (used by renderSectionItems)
*/
private renderElementItem(entry: [string, any], section: IWccSection): TemplateResult {
const [elementName, item] = entry;
const anonItem = item as any;
const demoCount = anonItem.demo ? getDemoCount(anonItem.demo) : 0;
const isMultiDemo = anonItem.demo && hasMultipleDemos(anonItem.demo);
const isExpanded = this.expandedElements.has(elementName);
const isSelected = this.selectedItem === item;
const isPinned = this.isPinned(section.name, elementName);
if (isMultiDemo) {
// Multi-demo element - render as expandable folder
return html`
<div
class="selectOption folder ${isExpanded ? 'expanded' : ''} ${isSelected ? 'selected' : ''} ${isPinned ? 'pinned' : ''}"
@click=${() => this.toggleExpanded(elementName)}
@contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, elementName)}
>
<i class="material-symbols-outlined expand-icon">chevron_right</i>
<div class="text">${this.highlightMatch(elementName)}</div>
</div>
${isExpanded ? html`
<div class="demo-children">
${Array.from({ length: demoCount }, (_, i) => {
const demoIndex = i;
const isThisDemoSelected = isSelected && this.dashboardRef.selectedDemoIndex === demoIndex;
return html`
<div
class="demo-child ${isThisDemoSelected ? 'selected' : ''}"
@click=${async () => {
await plugins.deesDomtools.DomTools.setupDomTools();
this.selectItem('element', elementName, item, demoIndex, section);
}}
>
<i class="material-symbols-outlined">play_circle</i>
<div class="text">demo${demoIndex + 1}</div>
</div>
`;
})}
</div>
` : null}
`;
} else {
// Single demo element
return html`
<div
class="selectOption ${isSelected ? 'selected' : ''} ${isPinned ? 'pinned' : ''}"
@click=${async () => {
await plugins.deesDomtools.DomTools.setupDomTools();
this.selectItem('element', elementName, item, 0, section);
}}
@contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, elementName)}
>
<i class="material-symbols-outlined">featured_video</i>
<div class="text">${this.highlightMatch(elementName)}</div>
</div>
`;
}
}
private toggleSectionCollapsed(sectionName: string) {
const newSet = new Set(this.collapsedSections);
if (newSet.has(sectionName)) {
newSet.delete(sectionName);
} else {
newSet.add(sectionName);
}
this.collapsedSections = newSet;
}
private toggleExpanded(elementName: string) {
const newSet = new Set(this.expandedElements);
if (newSet.has(elementName)) {
@@ -308,14 +706,123 @@ export class WccSidebar extends DeesElement {
this.expandedElements = newSet;
}
public selectItem(typeArg: TElementType, itemNameArg: string, itemArg: TTemplateFactory | DeesElement, demoIndex: number = 0) {
private handleSearchInput(e: Event) {
const input = e.target as HTMLInputElement;
this.searchQuery = input.value;
this.dispatchEvent(new CustomEvent('searchChanged', { detail: this.searchQuery }));
}
private matchesSearch(name: string): boolean {
if (!this.searchQuery) return true;
return name.toLowerCase().includes(this.searchQuery.toLowerCase());
}
private highlightMatch(text: string): TemplateResult {
if (!this.searchQuery) return html`${text}`;
const lowerText = text.toLowerCase();
const lowerQuery = this.searchQuery.toLowerCase();
const index = lowerText.indexOf(lowerQuery);
if (index === -1) return html`${text}`;
const before = text.slice(0, index);
const match = text.slice(index, index + this.searchQuery.length);
const after = text.slice(index + this.searchQuery.length);
return html`${before}<span class="highlight">${match}</span>${after}`;
}
protected updated(changedProperties: Map<string, unknown>) {
super.updated(changedProperties);
// Auto-expand folder when a multi-demo element is selected
if (changedProperties.has('selectedItem') && this.selectedItem && this.dashboardRef?.sections) {
// Find the element in any section
for (const section of this.dashboardRef.sections) {
if (section.type !== 'elements') continue;
const entries = getSectionItems(section);
const found = entries.find(([_, item]) => item === this.selectedItem);
if (found) {
const [elementName, item] = found;
const anonItem = item as any;
if (anonItem.demo && hasMultipleDemos(anonItem.demo)) {
if (!this.expandedElements.has(elementName)) {
const newSet = new Set(this.expandedElements);
newSet.add(elementName);
this.expandedElements = newSet;
}
}
break;
}
}
}
}
// ============ Resize functionality ============
private startResize = (e: MouseEvent) => {
e.preventDefault();
this.isResizing = true;
const startX = e.clientX;
const startWidth = this.sidebarWidth;
// Cache references once at start
const frame = this.dashboardRef?.shadowRoot?.querySelector('wcc-frame') as any;
const properties = this.dashboardRef?.shadowRoot?.querySelector('wcc-properties') as any;
// Disable frame transition during resize
if (frame) {
frame.isResizing = true;
}
const onMouseMove = (e: MouseEvent) => {
const newWidth = Math.min(400, Math.max(150, startWidth + (e.clientX - startX)));
this.sidebarWidth = newWidth;
// Update frame and properties directly
if (frame) {
frame.sidebarWidth = newWidth;
}
if (properties) {
properties.sidebarWidth = newWidth;
}
};
const onMouseUp = () => {
this.isResizing = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
// Re-enable frame transition
if (frame) {
frame.isResizing = false;
}
// Dispatch event on release for URL persistence
this.dispatchEvent(new CustomEvent('widthChanged', { detail: this.sidebarWidth }));
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
public selectItem(
typeArg: TElementType,
itemNameArg: string,
itemArg: TTemplateFactory | DeesElement,
demoIndex: number = 0,
section?: IWccSection
) {
console.log('selected item');
console.log(itemNameArg);
console.log(itemArg);
console.log('demo index:', demoIndex);
console.log('section:', section?.name);
this.selectedItem = itemArg;
this.selectedType = typeArg;
this.dashboardRef.selectedDemoIndex = demoIndex;
// Set the selected section on dashboard
if (section) {
this.dashboardRef.selectedSection = section;
}
this.dispatchEvent(
new CustomEvent('selectedType', {
detail: typeArg
@@ -335,7 +842,6 @@ export class WccSidebar extends DeesElement {
this.dashboardRef.buildUrl();
// Force re-render to update demo child selection indicator
// (needed when switching between demos of the same element)
this.requestUpdate();
}
}

View File

@@ -1,21 +1,86 @@
import { WccDashboard } from './elements/wcc-dashboard.js';
import { LitElement } from 'lit';
import type { TTemplateFactory } from './elements/wcctools.helpers.js';
import type { IWccConfig, IWccSection } from './wcctools.interfaces.js';
// Export recording components and service
export { RecorderService, type IRecorderEvents, type IRecordingOptions } from './services/recorder.service.js';
export { WccRecordButton } from './elements/wcc-record-button.js';
export { WccRecordingPanel } from './elements/wcc-recording-panel.js';
const setupWccTools = (
// Export types for external use
export type { IWccConfig, IWccSection } from './wcctools.interfaces.js';
/**
* Converts legacy (elements, pages) format to new sections config
*/
const convertLegacyToConfig = (
elementsArg?: { [key: string]: LitElement },
pagesArg?: Record<string, TTemplateFactory>
): IWccConfig => {
const sections: IWccSection[] = [];
if (pagesArg && Object.keys(pagesArg).length > 0) {
sections.push({
name: 'Pages',
type: 'pages',
items: pagesArg,
});
}
if (elementsArg && Object.keys(elementsArg).length > 0) {
sections.push({
name: 'Elements',
type: 'elements',
items: elementsArg,
});
}
return { sections };
};
/**
* Check if the argument is the new config format
*/
const isWccConfig = (arg: any): arg is IWccConfig => {
return arg && typeof arg === 'object' && 'sections' in arg && Array.isArray(arg.sections);
};
/**
* Setup WCC Tools dashboard
*
* New format (recommended):
* ```typescript
* setupWccTools({
* sections: [
* { name: 'Elements', type: 'elements', items: elements },
* { name: 'Pages', type: 'pages', items: pages },
* ]
* });
* ```
*
* Legacy format (still supported):
* ```typescript
* setupWccTools(elements, pages);
* ```
*/
const setupWccTools = (
configOrElements?: IWccConfig | { [key: string]: LitElement },
pagesArg?: Record<string, TTemplateFactory>
) => {
let config: IWccConfig;
if (isWccConfig(configOrElements)) {
config = configOrElements;
} else {
config = convertLegacyToConfig(configOrElements, pagesArg);
}
let hasRun = false;
const runWccToolsSetup = async () => {
if (document.readyState === 'complete' && !hasRun) {
hasRun = true;
const wccTools = new WccDashboard(elementsArg as any, pagesArg);
const wccTools = new WccDashboard(config);
document.querySelector('body').append(wccTools);
}
};

View File

@@ -14,6 +14,40 @@ pnpm add -D @design.estate/dees-wcctools
## Usage
### Sections-based Configuration (Recommended)
```typescript
import { setupWccTools } from '@design.estate/dees-wcctools';
import * as elements from './elements/index.js';
import * as views from './views/index.js';
import * as pages from './pages/index.js';
setupWccTools({
sections: [
{
name: 'Pages',
type: 'pages',
items: pages,
},
{
name: 'Views',
type: 'elements',
items: views,
icon: 'web',
},
{
name: 'Elements',
type: 'elements',
items: elements,
filter: (name) => !name.startsWith('internal-'),
sort: ([a], [b]) => a.localeCompare(b),
},
],
});
```
### Legacy Format (Still Supported)
```typescript
import { setupWccTools } from '@design.estate/dees-wcctools';
import { MyButton } from './components/my-button.js';
@@ -30,6 +64,8 @@ setupWccTools({
| Export | Description |
|--------|-------------|
| `setupWccTools` | Initialize the component catalogue dashboard |
| `IWccConfig` | TypeScript interface for sections configuration |
| `IWccSection` | TypeScript interface for individual section |
### Recording Components
@@ -41,6 +77,18 @@ setupWccTools({
| `IRecorderEvents` | TypeScript interface for recorder callbacks |
| `IRecordingOptions` | TypeScript interface for recording options |
## Section Configuration
| Property | Type | Description |
|----------|------|-------------|
| `name` | `string` | Display name for the section header |
| `type` | `'elements' \| 'pages'` | Rendering behavior |
| `items` | `Record<string, any>` | Element classes or page factories |
| `filter` | `(name, item) => boolean` | Optional filter function |
| `sort` | `([a, itemA], [b, itemB]) => number` | Optional sort function |
| `icon` | `string` | Material Symbols icon name |
| `collapsed` | `boolean` | Start section collapsed (default: false) |
## Internal Components
The module includes these internal web components:
@@ -48,8 +96,8 @@ The module includes these internal web components:
| Component | Description |
|-----------|-------------|
| `wcc-dashboard` | Main dashboard container with routing |
| `wcc-sidebar` | Navigation sidebar with element/page listing |
| `wcc-frame` | Iframe viewport with responsive sizing |
| `wcc-sidebar` | Navigation sidebar with collapsible sections |
| `wcc-frame` | Responsive viewport with size controls |
| `wcc-properties` | Property panel with live editing |
| `wcc-record-button` | Recording state indicator button |
| `wcc-recording-panel` | Recording workflow UI |
@@ -72,14 +120,14 @@ const events: IRecorderEvents = {
const recorder = new RecorderService(events);
// Load available microphones
const mics = await recorder.loadMicrophones(true); // true = request permission
const mics = await recorder.loadMicrophones(true);
// Start audio level monitoring
await recorder.startAudioMonitoring(mics[0].deviceId);
// Start recording
await recorder.startRecording({
mode: 'viewport', // or 'screen'
mode: 'viewport',
audioDeviceId: mics[0].deviceId,
viewportElement: document.querySelector('.viewport'),
});
@@ -99,10 +147,11 @@ recorder.dispose();
```
ts_web/
├── index.ts # Main exports
├── wcctools.interfaces.ts # Type definitions
├── elements/
│ ├── wcc-dashboard.ts # Root dashboard component
│ ├── wcc-sidebar.ts # Navigation sidebar
│ ├── wcc-frame.ts # Responsive iframe viewport
│ ├── wcc-frame.ts # Responsive viewport
│ ├── wcc-properties.ts # Property editing panel
│ ├── wcc-record-button.ts # Recording button
│ ├── wcc-recording-panel.ts # Recording options/preview
@@ -116,6 +165,7 @@ ts_web/
## Features
- 🎨 Interactive component preview
- 📂 Section-based sidebar with filtering & sorting
- 🔧 Real-time property editing with type detection
- 🌓 Theme switching (light/dark)
- 📱 Responsive viewport testing

View File

@@ -0,0 +1,31 @@
/**
* Configuration for a section in the WCC Tools sidebar
*/
export interface IWccSection {
/** Display name for the section header */
name: string;
/** How items in this section are rendered - 'elements' show demos, 'pages' render directly */
type: 'elements' | 'pages';
/** The items in this section - either element classes or page factory functions */
items: Record<string, any>;
/** Optional filter function to include/exclude items */
filter?: (name: string, item: any) => boolean;
/** Optional sort function for ordering items */
sort?: (a: [string, any], b: [string, any]) => number;
/** Optional Material icon name for the section header */
icon?: string;
/** Whether this section should start collapsed (default: false) */
collapsed?: boolean;
}
/**
* Configuration object for setupWccTools
*/
export interface IWccConfig {
sections: IWccSection[];
}
/**
* Type for element selection types - now section-based
*/
export type TElementType = 'element' | 'page';