feat(wcctools): Add section-based configuration API for setupWccTools, new Views, and section-aware routing/sidebar

This commit is contained in:
2025-12-28 12:51:55 +00:00
parent dd151bdad8
commit 14e63738b7
14 changed files with 1709 additions and 212 deletions

View File

@@ -1,5 +1,14 @@
# Changelog # Changelog
## 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) ## 2025-12-22 - 3.2.0 - feat(wcc-sidebar)
auto-expand sidebar folder when selecting an element with multiple demos auto-expand sidebar folder when selecting an element with multiple demos

View File

@@ -2,9 +2,32 @@
import * as deesWccTools from '../ts_web/index.js'; import * as deesWccTools from '../ts_web/index.js';
import * as deesDomTools from '@design.estate/dees-domtools'; 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 elements from '../test/elements/index.js';
import * as views from '../test/views/index.js';
import * as pages from '../test/pages/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(); deesDomTools.elementBasic.setup();

View File

@@ -1,5 +1,67 @@
# Project Hints and Findings # 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) ## UI Redesign with Shadcn-like Styles (2025-06-27)
### Changes Made ### Changes Made

229
readme.md
View File

@@ -6,12 +6,13 @@
`@design.estate/dees-wcctools` provides a comprehensive development environment for web components, featuring: `@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 - 🔧 **Real-time Property Editing** — Modify component props on the fly with auto-detected editors
- 🌓 **Theme Switching** — Test light/dark modes instantly - 🌓 **Theme Switching** — Test light/dark modes instantly
- 📱 **Responsive Viewport Testing** — Phone, phablet, tablet, and desktop views - 📱 **Responsive Viewport Testing** — Phone, phablet, tablet, and desktop views
- 🎬 **Screen Recording** — Record component demos with audio support and video trimming - 🎬 **Screen Recording** — Record component demos with audio support and video trimming
- 🧪 **Advanced Demo Tools** — Post-render hooks for interactive testing - 🧪 **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 - 🚀 **Zero-config Setup** — TypeScript and Lit support out of the box
## Issue Reporting and Security ## Issue Reporting and Security
@@ -57,29 +58,22 @@ export class MyButton extends DeesElement {
padding: 8px 16px; padding: 8px 16px;
border-radius: 4px; border-radius: 4px;
border: none; border: none;
font-size: 14px;
cursor: pointer; cursor: pointer;
transition: all 0.3s;
} }
button.primary { button.primary {
background: #007bff; background: #3b82f6;
color: white; color: white;
} }
button.secondary { button.secondary {
background: #6c757d; background: #6b7280;
color: white; color: white;
} }
button:hover {
opacity: 0.9;
}
` `
]; ];
public render() { public render() {
return html` return html`
<button class="${this.variant}"> <button class="${this.variant}">${this.label}</button>
${this.label}
</button>
`; `;
} }
} }
@@ -93,33 +87,32 @@ import { setupWccTools } from '@design.estate/dees-wcctools';
import { html } from 'lit'; import { html } from 'lit';
// Import your components // Import your components
import { MyButton } from './components/my-button.js'; import * as elements from './components/index.js';
import { MyCard } from './components/my-card.js'; import * as views from './views/index.js';
import * as pages from './pages/index.js';
// Define elements for the catalogue // Initialize with sections-based configuration
const elements = { setupWccTools({
'my-button': MyButton, sections: [
'my-card': MyCard, {
}; name: 'Pages',
type: 'pages',
// Optionally define pages items: pages,
const pages = { },
'home': () => html` {
<div style="padding: 20px;"> name: 'Views',
<h1>Welcome to My Component Library</h1> type: 'elements',
<p>Browse components using the sidebar.</p> items: views,
</div> icon: 'web',
`, },
'getting-started': () => html` {
<div style="padding: 20px;"> name: 'Elements',
<h2>Getting Started</h2> type: 'elements',
<p>Installation and usage instructions...</p> items: elements,
</div> sort: ([a], [b]) => a.localeCompare(b),
`, },
}; ],
});
// Initialize the catalogue
setupWccTools(elements, pages);
``` ```
### 3. Create an HTML Entry Point ### 3. Create an HTML Entry Point
@@ -137,6 +130,69 @@ setupWccTools(elements, pages);
</html> </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 ## Features
### 🎯 Live Property Editing ### 🎯 Live Property Editing
@@ -162,19 +218,7 @@ Test your components across different screen sizes:
### 🌓 Theme Support ### 🌓 Theme Support
Components automatically adapt to light/dark themes using the `goBright` property: Components automatically adapt to light/dark themes. Use CSS custom properties with the theme manager:
```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:
```typescript ```typescript
import { cssManager } from '@design.estate/dees-element'; import { cssManager } from '@design.estate/dees-element';
@@ -182,8 +226,8 @@ import { cssManager } from '@design.estate/dees-element';
public static styles = [ public static styles = [
css` css`
:host { :host {
color: ${cssManager.bdTheme('#000', '#fff')}; color: ${cssManager.bdTheme('#1a1a1a', '#e5e5e5')};
background: ${cssManager.bdTheme('#fff', '#000')}; background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
} }
` `
]; ];
@@ -191,7 +235,7 @@ public static styles = [
### 🎬 Screen Recording ### 🎬 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 - **Viewport Recording** — Record just the component viewport
- **Full Screen Recording** — Capture the entire screen - **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 ### ⏳ Async Demos
Return a `Promise` from `demo` for async setup. The dashboard waits for resolution: Return a `Promise` from `demo` for async setup:
```typescript ```typescript
public static demo = async () => { 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: Components can respond to their container size using the `wccToolsViewport` container:
@@ -269,7 +330,7 @@ public static styles = [
### Required for Catalogue Display ### 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 2. Use `@property()` decorators with the `accessor` keyword for editable properties
3. Export component classes for proper detection 3. Export component classes for proper detection
@@ -278,7 +339,7 @@ public static styles = [
```typescript ```typescript
@customElement('best-practice-component') @customElement('best-practice-component')
export class BestPracticeComponent extends DeesElement { export class BestPracticeComponent extends DeesElement {
// ✅ Static demo property // ✅ Static demo property (single or array)
public static demo = () => html` public static demo = () => html`
<best-practice-component <best-practice-component
.complexProp=${{ key: 'value' }} .complexProp=${{ key: 'value' }}
@@ -305,23 +366,40 @@ export class BestPracticeComponent extends DeesElement {
The catalogue uses URL routing for deep linking: The catalogue uses URL routing for deep linking:
``` ```
/wcctools-route/:type/:name/:viewport/:theme /wcctools-route/:sectionName/:itemName/:demoIndex/:viewport/:theme
Examples: Examples:
/wcctools-route/element/my-button/desktop/dark /wcctools-route/Elements/my-button/0/desktop/dark
/wcctools-route/page/home/tablet/bright /wcctools-route/Views/view-dashboard/0/tablet/bright
/wcctools-route/Pages/home/0/desktop/dark
``` ```
## API Reference ## API Reference
### `setupWccTools(elements, pages?)` ### `setupWccTools(config)`
Initialize the WCC Tools dashboard. Initialize the WCC Tools dashboard with sections configuration.
| Parameter | Type | Description | ```typescript
|-----------|------|-------------| interface IWccSection {
| `elements` | `Record<string, typeof LitElement>` | Map of element names to classes | name: string;
| `pages` | `Record<string, TTemplateFactory>` | Optional map of page names to template functions | 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` ### `DeesDemoWrapper`
@@ -357,14 +435,21 @@ recorder.stopRecording();
## Project Structure ## Project Structure
``` ```
my-components/ my-component-library/
├── src/ ├── src/
│ ├── components/ │ ├── elements/ # UI components
│ │ ├── my-button.ts │ │ ├── my-button.ts
│ │ ── my-card.ts │ │ ── my-card.ts
│ └── catalogue.ts │ └── index.ts
├── dist/ │ ├── views/ # Full-page layouts
├── index.html │ │ ├── view-dashboard.ts
│ │ └── index.ts
│ ├── pages/ # Documentation pages
│ │ ├── home.ts
│ │ └── index.ts
│ └── catalogue.ts # WCC Tools setup
├── html/
│ └── index.html
└── package.json └── package.json
``` ```

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 = { export const commitinfo = {
name: '@design.estate/dees-wcctools', name: '@design.estate/dees-wcctools',
version: '3.2.0', version: '3.3.0',
description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.' description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.'
} }

View File

@@ -1,6 +1,7 @@
import { DeesElement, property, html, customElement, type TemplateResult, queryAsync, render, domtools } from '@design.estate/dees-element'; 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 { resolveTemplateFactory, getDemoAtIndex, getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
import type { TTemplateFactory } 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'; import * as plugins from '../wcctools.plugins.js';
@@ -9,13 +10,37 @@ import './wcc-frame.js';
import './wcc-sidebar.js'; import './wcc-sidebar.js';
import './wcc-properties.js'; import './wcc-properties.js';
import { type TTheme } from './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 { breakpoints } from '@design.estate/dees-domtools';
import { WccFrame } from './wcc-frame.js'; 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') @customElement('wcc-dashboard')
export class WccDashboard extends DeesElement { export class WccDashboard extends DeesElement {
@property()
accessor sections: IWccSection[] = [];
@property()
accessor selectedSection: IWccSection | null = null;
@property() @property()
accessor selectedType: TElementType; accessor selectedType: TElementType;
@@ -39,12 +64,6 @@ export class WccDashboard extends DeesElement {
return this.selectedViewport === 'native'; return this.selectedViewport === 'native';
} }
@property()
accessor pages: Record<string, TTemplateFactory> = {};
@property()
accessor elements: { [key: string]: DeesElement } = {};
@property() @property()
accessor warning: string = null; accessor warning: string = null;
@@ -55,20 +74,33 @@ export class WccDashboard extends DeesElement {
@queryAsync('wcc-frame') @queryAsync('wcc-frame')
accessor wccFrame: Promise<WccFrame>; accessor wccFrame: Promise<WccFrame>;
constructor( constructor(config?: IWccConfig) {
elementsArg?: { [key: string]: DeesElement },
pagesArg?: Record<string, TTemplateFactory>
) {
super(); super();
if (elementsArg) { if (config && config.sections) {
this.elements = elementsArg; this.sections = config.sections;
console.log('got elements:'); console.log('got sections:', this.sections.map(s => s.name));
console.log(this.elements);
} }
}
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 { public render(): TemplateResult {
@@ -159,19 +191,37 @@ export class WccDashboard extends DeesElement {
this.setupScrollListeners(); this.setupScrollListeners();
}, 500); }, 500);
// Route with demo index (new format) // New route format with section name
this.domtools.router.on( this.domtools.router.on(
'/wcctools-route/:itemType/:itemName/:demoIndex/:viewport/:theme', '/wcctools-route/:sectionName/:itemName/:demoIndex/:viewport/:theme',
async (routeInfo) => { 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.selectedItemName = routeInfo.params.itemName;
this.selectedDemoIndex = parseInt(routeInfo.params.demoIndex) || 0; this.selectedDemoIndex = parseInt(routeInfo.params.demoIndex) || 0;
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport; this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
this.selectedTheme = routeInfo.params.theme as TTheme; this.selectedTheme = routeInfo.params.theme as TTheme;
if (routeInfo.params.itemType === 'element') {
this.selectedItem = this.elements[routeInfo.params.itemName]; if (this.selectedSection) {
} else if (routeInfo.params.itemType === 'page') { // Find item within the section
this.selectedItem = this.pages[routeInfo.params.itemName]; 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 scroll positions from query parameters
@@ -201,17 +251,33 @@ export class WccDashboard extends DeesElement {
// Legacy route without demo index (for backwards compatibility) // Legacy route without demo index (for backwards compatibility)
this.domtools.router.on( this.domtools.router.on(
'/wcctools-route/:itemType/:itemName/:viewport/:theme', '/wcctools-route/:sectionName/:itemName/:viewport/:theme',
async (routeInfo) => { 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.selectedItemName = routeInfo.params.itemName;
this.selectedDemoIndex = 0; // Default to first demo this.selectedDemoIndex = 0;
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport; this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
this.selectedTheme = routeInfo.params.theme as TTheme; this.selectedTheme = routeInfo.params.theme as TTheme;
if (routeInfo.params.itemType === 'element') {
this.selectedItem = this.elements[routeInfo.params.itemName]; if (this.selectedSection) {
} else if (routeInfo.params.itemType === 'page') { const entries = getSectionItems(this.selectedSection);
this.selectedItem = this.pages[routeInfo.params.itemName]; 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 scroll positions from query parameters
@@ -297,7 +363,10 @@ export class WccDashboard extends DeesElement {
} }
public buildUrl() { 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(); const queryParams = new URLSearchParams();
if (this.frameScrollY > 0) { if (this.frameScrollY > 0) {
@@ -351,7 +420,10 @@ export class WccDashboard extends DeesElement {
} }
private updateUrlWithScrollState() { 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(); const queryParams = new URLSearchParams();
if (this.frameScrollY > 0) { if (this.frameScrollY > 0) {

View File

@@ -1,10 +1,9 @@
import * as plugins from '../wcctools.plugins.js'; import * as plugins from '../wcctools.plugins.js';
import { DeesElement, property, html, customElement, type TemplateResult, state } from '@design.estate/dees-element'; 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 type { TTemplateFactory } from './wcctools.helpers.js';
import { getDemoCount, hasMultipleDemos } from './wcctools.helpers.js'; import { getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
import type { IWccSection, TElementType } from '../wcctools.interfaces.js';
export type TElementType = 'element' | 'page';
@customElement('wcc-sidebar') @customElement('wcc-sidebar')
export class WccSidebar extends DeesElement { export class WccSidebar extends DeesElement {
@@ -24,6 +23,12 @@ export class WccSidebar extends DeesElement {
@state() @state()
accessor expandedElements: Set<string> = new Set(); accessor expandedElements: Set<string> = new Set();
// Track which sections are collapsed
@state()
accessor collapsedSections: Set<string> = new Set();
private sectionsInitialized = false;
public render(): TemplateResult { public render(): TemplateResult {
return html` 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" /> <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" />
@@ -65,7 +70,7 @@ export class WccSidebar extends DeesElement {
padding: 0.5rem 0; padding: 0.5rem 0;
} }
h3 { .section-header {
padding: 0.3rem 0.75rem; padding: 0.3rem 0.75rem;
font-size: 0.65rem; font-size: 0.65rem;
font-weight: 500; font-weight: 500;
@@ -77,12 +82,45 @@ export class WccSidebar extends DeesElement {
background: rgba(59, 130, 246, 0.03); background: rgba(59, 130, 246, 0.03);
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
border-top: 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; 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 { .material-symbols-outlined {
font-family: 'Material Symbols Outlined'; font-family: 'Material Symbols Outlined';
font-weight: normal; font-weight: normal;
@@ -216,88 +254,144 @@ export class WccSidebar extends DeesElement {
} }
</style> </style>
<div class="menu"> <div class="menu">
<h3>Pages</h3> ${this.renderSections()}
${(() => {
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>
`;
}
});
})()}
</div> </div>
`; `;
} }
/**
* Initialize collapsed sections from section config
*/
private initCollapsedSections() {
if (this.sectionsInitialized) 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;
}
/**
* Render all sections
*/
private renderSections() {
this.initCollapsedSections();
return this.dashboardRef.sections.map((section, index) => {
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);
if (section.type === 'pages') {
return entries.map(([pageName, item]) => {
return html`
<div
class="selectOption ${this.selectedItem === item ? 'selected' : ''}"
@click=${async () => {
await plugins.deesDomtools.DomTools.setupDomTools();
this.selectItem('page', pageName, item, 0, section);
}}
>
<i class="material-symbols-outlined">insert_drive_file</i>
<div class="text">${pageName}</div>
</div>
`;
});
} else {
// type === 'elements'
return entries.map(([elementName, item]) => {
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;
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, 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' : ''}"
@click=${async () => {
await plugins.deesDomtools.DomTools.setupDomTools();
this.selectItem('element', elementName, item, 0, section);
}}
>
<i class="material-symbols-outlined">featured_video</i>
<div class="text">${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) { private toggleExpanded(elementName: string) {
const newSet = new Set(this.expandedElements); const newSet = new Set(this.expandedElements);
if (newSet.has(elementName)) { if (newSet.has(elementName)) {
@@ -313,30 +407,50 @@ export class WccSidebar extends DeesElement {
// Auto-expand folder when a multi-demo element is selected // Auto-expand folder when a multi-demo element is selected
if (changedProperties.has('selectedItem') && this.selectedItem) { if (changedProperties.has('selectedItem') && this.selectedItem) {
const elementName = Object.keys(this.dashboardRef.elements).find( // Find the element in any section
name => this.dashboardRef.elements[name] === this.selectedItem for (const section of this.dashboardRef.sections) {
); if (section.type !== 'elements') continue;
if (elementName) {
const item = this.dashboardRef.elements[elementName] as any; const entries = getSectionItems(section);
if (item.demo && hasMultipleDemos(item.demo)) { const found = entries.find(([_, item]) => item === this.selectedItem);
if (!this.expandedElements.has(elementName)) { if (found) {
const newSet = new Set(this.expandedElements); const [elementName, item] = found;
newSet.add(elementName); const anonItem = item as any;
this.expandedElements = newSet; if (anonItem.demo && hasMultipleDemos(anonItem.demo)) {
if (!this.expandedElements.has(elementName)) {
const newSet = new Set(this.expandedElements);
newSet.add(elementName);
this.expandedElements = newSet;
}
} }
break;
} }
} }
} }
} }
public selectItem(typeArg: TElementType, itemNameArg: string, itemArg: TTemplateFactory | DeesElement, demoIndex: number = 0) { public selectItem(
typeArg: TElementType,
itemNameArg: string,
itemArg: TTemplateFactory | DeesElement,
demoIndex: number = 0,
section?: IWccSection
) {
console.log('selected item'); console.log('selected item');
console.log(itemNameArg); console.log(itemNameArg);
console.log(itemArg); console.log(itemArg);
console.log('demo index:', demoIndex); console.log('demo index:', demoIndex);
console.log('section:', section?.name);
this.selectedItem = itemArg; this.selectedItem = itemArg;
this.selectedType = typeArg; this.selectedType = typeArg;
this.dashboardRef.selectedDemoIndex = demoIndex; this.dashboardRef.selectedDemoIndex = demoIndex;
// Set the selected section on dashboard
if (section) {
this.dashboardRef.selectedSection = section;
}
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('selectedType', { new CustomEvent('selectedType', {
detail: typeArg detail: typeArg
@@ -356,7 +470,6 @@ export class WccSidebar extends DeesElement {
this.dashboardRef.buildUrl(); this.dashboardRef.buildUrl();
// Force re-render to update demo child selection indicator // Force re-render to update demo child selection indicator
// (needed when switching between demos of the same element)
this.requestUpdate(); this.requestUpdate();
} }
} }

View File

@@ -1,21 +1,86 @@
import { WccDashboard } from './elements/wcc-dashboard.js'; import { WccDashboard } from './elements/wcc-dashboard.js';
import { LitElement } from 'lit'; import { LitElement } from 'lit';
import type { TTemplateFactory } from './elements/wcctools.helpers.js'; import type { TTemplateFactory } from './elements/wcctools.helpers.js';
import type { IWccConfig, IWccSection } from './wcctools.interfaces.js';
// Export recording components and service // Export recording components and service
export { RecorderService, type IRecorderEvents, type IRecordingOptions } from './services/recorder.service.js'; export { RecorderService, type IRecorderEvents, type IRecordingOptions } from './services/recorder.service.js';
export { WccRecordButton } from './elements/wcc-record-button.js'; export { WccRecordButton } from './elements/wcc-record-button.js';
export { WccRecordingPanel } from './elements/wcc-recording-panel.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 }, elementsArg?: { [key: string]: LitElement },
pagesArg?: Record<string, TTemplateFactory> 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; let hasRun = false;
const runWccToolsSetup = async () => { const runWccToolsSetup = async () => {
if (document.readyState === 'complete' && !hasRun) { if (document.readyState === 'complete' && !hasRun) {
hasRun = true; hasRun = true;
const wccTools = new WccDashboard(elementsArg as any, pagesArg); const wccTools = new WccDashboard(config);
document.querySelector('body').append(wccTools); document.querySelector('body').append(wccTools);
} }
}; };

View File

@@ -14,6 +14,40 @@ pnpm add -D @design.estate/dees-wcctools
## Usage ## 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 ```typescript
import { setupWccTools } from '@design.estate/dees-wcctools'; import { setupWccTools } from '@design.estate/dees-wcctools';
import { MyButton } from './components/my-button.js'; import { MyButton } from './components/my-button.js';
@@ -30,6 +64,8 @@ setupWccTools({
| Export | Description | | Export | Description |
|--------|-------------| |--------|-------------|
| `setupWccTools` | Initialize the component catalogue dashboard | | `setupWccTools` | Initialize the component catalogue dashboard |
| `IWccConfig` | TypeScript interface for sections configuration |
| `IWccSection` | TypeScript interface for individual section |
### Recording Components ### Recording Components
@@ -41,6 +77,18 @@ setupWccTools({
| `IRecorderEvents` | TypeScript interface for recorder callbacks | | `IRecorderEvents` | TypeScript interface for recorder callbacks |
| `IRecordingOptions` | TypeScript interface for recording options | | `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 ## Internal Components
The module includes these internal web components: The module includes these internal web components:
@@ -48,8 +96,8 @@ The module includes these internal web components:
| Component | Description | | Component | Description |
|-----------|-------------| |-----------|-------------|
| `wcc-dashboard` | Main dashboard container with routing | | `wcc-dashboard` | Main dashboard container with routing |
| `wcc-sidebar` | Navigation sidebar with element/page listing | | `wcc-sidebar` | Navigation sidebar with collapsible sections |
| `wcc-frame` | Iframe viewport with responsive sizing | | `wcc-frame` | Responsive viewport with size controls |
| `wcc-properties` | Property panel with live editing | | `wcc-properties` | Property panel with live editing |
| `wcc-record-button` | Recording state indicator button | | `wcc-record-button` | Recording state indicator button |
| `wcc-recording-panel` | Recording workflow UI | | `wcc-recording-panel` | Recording workflow UI |
@@ -72,14 +120,14 @@ const events: IRecorderEvents = {
const recorder = new RecorderService(events); const recorder = new RecorderService(events);
// Load available microphones // Load available microphones
const mics = await recorder.loadMicrophones(true); // true = request permission const mics = await recorder.loadMicrophones(true);
// Start audio level monitoring // Start audio level monitoring
await recorder.startAudioMonitoring(mics[0].deviceId); await recorder.startAudioMonitoring(mics[0].deviceId);
// Start recording // Start recording
await recorder.startRecording({ await recorder.startRecording({
mode: 'viewport', // or 'screen' mode: 'viewport',
audioDeviceId: mics[0].deviceId, audioDeviceId: mics[0].deviceId,
viewportElement: document.querySelector('.viewport'), viewportElement: document.querySelector('.viewport'),
}); });
@@ -99,10 +147,11 @@ recorder.dispose();
``` ```
ts_web/ ts_web/
├── index.ts # Main exports ├── index.ts # Main exports
├── wcctools.interfaces.ts # Type definitions
├── elements/ ├── elements/
│ ├── wcc-dashboard.ts # Root dashboard component │ ├── wcc-dashboard.ts # Root dashboard component
│ ├── wcc-sidebar.ts # Navigation sidebar │ ├── wcc-sidebar.ts # Navigation sidebar
│ ├── wcc-frame.ts # Responsive iframe viewport │ ├── wcc-frame.ts # Responsive viewport
│ ├── wcc-properties.ts # Property editing panel │ ├── wcc-properties.ts # Property editing panel
│ ├── wcc-record-button.ts # Recording button │ ├── wcc-record-button.ts # Recording button
│ ├── wcc-recording-panel.ts # Recording options/preview │ ├── wcc-recording-panel.ts # Recording options/preview
@@ -116,6 +165,7 @@ ts_web/
## Features ## Features
- 🎨 Interactive component preview - 🎨 Interactive component preview
- 📂 Section-based sidebar with filtering & sorting
- 🔧 Real-time property editing with type detection - 🔧 Real-time property editing with type detection
- 🌓 Theme switching (light/dark) - 🌓 Theme switching (light/dark)
- 📱 Responsive viewport testing - 📱 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';