feat(wcctools): Add section-based configuration API for setupWccTools, new Views, and section-aware routing/sidebar
This commit is contained in:
@@ -1,5 +1,14 @@
|
||||
# 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)
|
||||
auto-expand sidebar folder when selecting an element with multiple demos
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
229
readme.md
@@ -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
|
||||
```
|
||||
|
||||
|
||||
3
test/views/index.ts
Normal file
3
test/views/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './view-dashboard.js';
|
||||
export * from './view-settings.js';
|
||||
export * from './view-empty-state.js';
|
||||
286
test/views/view-dashboard.ts
Normal file
286
test/views/view-dashboard.ts
Normal 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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
262
test/views/view-empty-state.ts
Normal file
262
test/views/view-empty-state.ts
Normal 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
436
test/views/view-settings.ts
Normal 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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
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.'
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -39,12 +64,6 @@ export class WccDashboard extends DeesElement {
|
||||
return this.selectedViewport === 'native';
|
||||
}
|
||||
|
||||
@property()
|
||||
accessor pages: Record<string, TTemplateFactory> = {};
|
||||
|
||||
@property()
|
||||
accessor elements: { [key: string]: DeesElement } = {};
|
||||
|
||||
@property()
|
||||
accessor warning: string = null;
|
||||
|
||||
@@ -55,21 +74,34 @@ export class WccDashboard extends DeesElement {
|
||||
@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 {
|
||||
return html`
|
||||
@@ -159,19 +191,37 @@ 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
|
||||
@@ -201,17 +251,33 @@ 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
|
||||
@@ -297,7 +363,10 @@ 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.frameScrollY > 0) {
|
||||
@@ -351,7 +420,10 @@ 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.frameScrollY > 0) {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
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';
|
||||
|
||||
@customElement('wcc-sidebar')
|
||||
export class WccSidebar extends DeesElement {
|
||||
@@ -24,6 +23,12 @@ export class WccSidebar extends DeesElement {
|
||||
@state()
|
||||
accessor expandedElements: Set<string> = new Set();
|
||||
|
||||
// Track which sections are collapsed
|
||||
@state()
|
||||
accessor collapsedSections: Set<string> = new Set();
|
||||
|
||||
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" />
|
||||
@@ -65,7 +70,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 +82,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;
|
||||
@@ -216,17 +254,67 @@ export class WccSidebar extends DeesElement {
|
||||
}
|
||||
</style>
|
||||
<div class="menu">
|
||||
<h3>Pages</h3>
|
||||
${(() => {
|
||||
const pages = Object.keys(this.dashboardRef.pages);
|
||||
return pages.map(pageName => {
|
||||
const item = this.dashboardRef.pages[pageName];
|
||||
${this.renderSections()}
|
||||
</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="selectOption ${this.selectedItem === item ? 'selected' : null}"
|
||||
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 () => {
|
||||
const domtools = await plugins.deesDomtools.DomTools.setupDomTools();
|
||||
this.selectItem('page', pageName, item, 0);
|
||||
await plugins.deesDomtools.DomTools.setupDomTools();
|
||||
this.selectItem('page', pageName, item, 0, section);
|
||||
}}
|
||||
>
|
||||
<i class="material-symbols-outlined">insert_drive_file</i>
|
||||
@@ -234,14 +322,12 @@ export class WccSidebar extends DeesElement {
|
||||
</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);
|
||||
} 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;
|
||||
|
||||
@@ -266,7 +352,7 @@ export class WccSidebar extends DeesElement {
|
||||
class="demo-child ${isThisDemoSelected ? 'selected' : ''}"
|
||||
@click=${async () => {
|
||||
await plugins.deesDomtools.DomTools.setupDomTools();
|
||||
this.selectItem('element', elementName, item, demoIndex);
|
||||
this.selectItem('element', elementName, item, demoIndex, section);
|
||||
}}
|
||||
>
|
||||
<i class="material-symbols-outlined">play_circle</i>
|
||||
@@ -278,13 +364,13 @@ export class WccSidebar extends DeesElement {
|
||||
` : null}
|
||||
`;
|
||||
} else {
|
||||
// Single demo element - render as normal
|
||||
// Single demo element
|
||||
return html`
|
||||
<div
|
||||
class="selectOption ${isSelected ? 'selected' : null}"
|
||||
class="selectOption ${isSelected ? 'selected' : ''}"
|
||||
@click=${async () => {
|
||||
await plugins.deesDomtools.DomTools.setupDomTools();
|
||||
this.selectItem('element', elementName, item, 0);
|
||||
this.selectItem('element', elementName, item, 0, section);
|
||||
}}
|
||||
>
|
||||
<i class="material-symbols-outlined">featured_video</i>
|
||||
@@ -293,9 +379,17 @@ export class WccSidebar extends DeesElement {
|
||||
`;
|
||||
}
|
||||
});
|
||||
})()}
|
||||
</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) {
|
||||
@@ -313,30 +407,50 @@ export class WccSidebar extends DeesElement {
|
||||
|
||||
// Auto-expand folder when a multi-demo element is selected
|
||||
if (changedProperties.has('selectedItem') && this.selectedItem) {
|
||||
const elementName = Object.keys(this.dashboardRef.elements).find(
|
||||
name => this.dashboardRef.elements[name] === this.selectedItem
|
||||
);
|
||||
if (elementName) {
|
||||
const item = this.dashboardRef.elements[elementName] as any;
|
||||
if (item.demo && hasMultipleDemos(item.demo)) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(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
|
||||
@@ -356,7 +470,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
31
ts_web/wcctools.interfaces.ts
Normal file
31
ts_web/wcctools.interfaces.ts
Normal 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';
|
||||
Reference in New Issue
Block a user