Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e14ebde03 | |||
| 28d1227d30 | |||
| 8c60d3bea3 | |||
| 9ed614994f | |||
| 61b79aa4dc | |||
| 1134cba575 | |||
| 29c0df489e | |||
| 53df62a9fd | |||
| 4fe17f5afd | |||
| 63dd6a27b3 | |||
| 287cc4d1c3 | |||
| 14e63738b7 |
BIN
.playwright-mcp/sidebar-check-2.png
Normal file
BIN
.playwright-mcp/sidebar-check-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
.playwright-mcp/sidebar-check.png
Normal file
BIN
.playwright-mcp/sidebar-check.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
43
changelog.md
43
changelog.md
@@ -1,5 +1,48 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-01-04 - 3.5.2 - fix(elements)
|
||||
delay hiding sidebar and properties panels during native-mode transition and use transparent rgba border for frame to avoid layout jumps
|
||||
|
||||
- Add isHidden state to wcc-sidebar and wcc-properties and switch display bindings to use isHidden instead of directly using isNative
|
||||
- Introduce a 300ms delayed hide when entering native mode so UI hides after frame animation completes; show immediately when exiting native mode
|
||||
- Replace hardcoded hex border values in wcc-frame with rgba and set native border to a transparent 0px to prevent abrupt visual jumps
|
||||
|
||||
## 2026-01-04 - 3.5.1 - fix(sidebar)
|
||||
disable frame CSS transition while user is resizing the sidebar to prevent janky animations
|
||||
|
||||
- Added isResizing boolean property to wcc-frame to toggle transitions during resize
|
||||
- Set frame.isResizing = true at resize start and false on mouseup to re-enable transitions
|
||||
- Updated CSS to skip transition while isResizing is true
|
||||
- Files changed: ts_web/elements/wcc-frame.ts, ts_web/elements/wcc-sidebar.ts
|
||||
|
||||
## 2026-01-04 - 3.5.0 - feat(wcctools)
|
||||
add context menu and pinning support, persist pinned state in URL, and add grouped demo test elements
|
||||
|
||||
- Add wcc-contextmenu custom element with a static show() API, proper positioning, visibility transitions, outside-click and Escape handling, and menu item actions.
|
||||
- Introduce pinnedItems (Set<string>) on wcc-dashboard and wcc-sidebar; pass pinnedItems to the sidebar, handle pinnedChanged events, and persist pinned item keys in the URL query param 'pinned'. Changes include defensive updates to avoid unnecessary update loops.
|
||||
- Enhance wcc-sidebar to render pinned state: new styles for pinned items and pinned sections, contextmenu integration for element items, adjusted layout (grid-template-columns) and improved element/demo rendering logic.
|
||||
- Add grouped demo test components and exports to demo the demoGroup feature: test-button-primary, test-button-secondary, test-button-danger, test-input-text, and test-input-checkbox.
|
||||
- Misc: adjust dashboard URL state serialization/deserialization to include pinned items and ensure scroll/search state handling remains stable.
|
||||
|
||||
## 2025-12-30 - 3.4.0 - feat(sidebar)
|
||||
add searchable sidebar with URL-backed query state and highlighted matches
|
||||
|
||||
- Add search input to wcc-sidebar and expose a searchQuery property
|
||||
- Filter sidebar sections and items client-side based on the search query and hide sections with no matches
|
||||
- Highlight matching substrings in sidebar item labels
|
||||
- Emit a 'searchChanged' event from the sidebar and handle it in wcc-dashboard to keep dashboard.searchQuery in sync
|
||||
- Persist the search query in the route query parameter 'search' when building URLs and restore/clear it on navigation
|
||||
- Preserve existing scroll-state handling while adding search state to URL updates
|
||||
|
||||
## 2025-12-28 - 3.3.0 - feat(wcctools)
|
||||
Add section-based configuration API for setupWccTools, new Views, and section-aware routing/sidebar
|
||||
|
||||
- Introduce IWccSection and IWccConfig types and migrate setupWccTools to accept a sections config while preserving legacy (elements, pages) format
|
||||
- WccDashboard and WccSidebar updated to support sections, filtering, sorting, collapsed sections, and section-aware URL routing (uses sectionName in routes with legacy fallbacks)
|
||||
- Add Views: view-dashboard, view-settings, view-empty-state plus test/views index exports and demo variations
|
||||
- Add helpers: getSectionItems, convertLegacyToConfig and isWccConfig; update build URL and routing logic to be section-aware
|
||||
- Update docs and README/readme.hints with sections API, examples, migration notes and UI/UX updates
|
||||
|
||||
## 2025-12-22 - 3.2.0 - feat(wcc-sidebar)
|
||||
auto-expand sidebar folder when selecting an element with multiple demos
|
||||
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@design.estate/dees-wcctools",
|
||||
"version": "3.2.0",
|
||||
"version": "3.5.2",
|
||||
"private": false,
|
||||
"description": "A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.",
|
||||
"exports": {
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -4,3 +4,10 @@ export * from './test-complextypes.js';
|
||||
export * from './test-withwrapper.js';
|
||||
export * from './test-edgecases.js';
|
||||
export * from './test-nested.js';
|
||||
|
||||
// Grouped elements to demo the demoGroup feature
|
||||
export * from './test-button-primary.js';
|
||||
export * from './test-button-secondary.js';
|
||||
export * from './test-button-danger.js';
|
||||
export * from './test-input-text.js';
|
||||
export * from './test-input-checkbox.js';
|
||||
|
||||
45
test/elements/test-button-danger.ts
Normal file
45
test/elements/test-button-danger.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
property,
|
||||
css,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('test-button-danger')
|
||||
export class TestButtonDanger extends DeesElement {
|
||||
// Same group as other buttons
|
||||
public static demoGroup = 'Buttons';
|
||||
|
||||
public static demo = () => html`
|
||||
<test-button-danger>Delete</test-button-danger>
|
||||
`;
|
||||
|
||||
@property({ type: String })
|
||||
accessor label: string = 'Delete';
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
button {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`<button><slot>${this.label}</slot></button>`;
|
||||
}
|
||||
}
|
||||
45
test/elements/test-button-primary.ts
Normal file
45
test/elements/test-button-primary.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
property,
|
||||
css,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('test-button-primary')
|
||||
export class TestButtonPrimary extends DeesElement {
|
||||
// This groups the element with other "Buttons" in the sidebar
|
||||
public static demoGroup = 'Buttons';
|
||||
|
||||
public static demo = () => html`
|
||||
<test-button-primary>Click Me</test-button-primary>
|
||||
`;
|
||||
|
||||
@property({ type: String })
|
||||
accessor label: string = 'Button';
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
button {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`<button><slot>${this.label}</slot></button>`;
|
||||
}
|
||||
}
|
||||
45
test/elements/test-button-secondary.ts
Normal file
45
test/elements/test-button-secondary.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
property,
|
||||
css,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('test-button-secondary')
|
||||
export class TestButtonSecondary extends DeesElement {
|
||||
// Same group as test-button-primary - they'll appear together
|
||||
public static demoGroup = 'Buttons';
|
||||
|
||||
public static demo = () => html`
|
||||
<test-button-secondary>Secondary Action</test-button-secondary>
|
||||
`;
|
||||
|
||||
@property({ type: String })
|
||||
accessor label: string = 'Button';
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
button {
|
||||
background: transparent;
|
||||
color: #3b82f6;
|
||||
border: 1px solid #3b82f6;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`<button><slot>${this.label}</slot></button>`;
|
||||
}
|
||||
}
|
||||
68
test/elements/test-input-checkbox.ts
Normal file
68
test/elements/test-input-checkbox.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
property,
|
||||
css,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('test-input-checkbox')
|
||||
export class TestInputCheckbox extends DeesElement {
|
||||
// Same group as test-input-text
|
||||
public static demoGroup = 'Inputs';
|
||||
|
||||
public static demo = () => html`
|
||||
<test-input-checkbox label="Accept terms and conditions"></test-input-checkbox>
|
||||
`;
|
||||
|
||||
@property({ type: String })
|
||||
accessor label: string = 'Checkbox';
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor checked: boolean = false;
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.checkbox {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
background: #1a1a1a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.checkbox.checked {
|
||||
background: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
.checkbox.checked::after {
|
||||
content: '✓';
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
}
|
||||
.label {
|
||||
color: #e5e5e5;
|
||||
font-size: 14px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<div
|
||||
class="checkbox ${this.checked ? 'checked' : ''}"
|
||||
@click=${() => this.checked = !this.checked}
|
||||
></div>
|
||||
<span class="label">${this.label}</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
59
test/elements/test-input-text.ts
Normal file
59
test/elements/test-input-text.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
property,
|
||||
css,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('test-input-text')
|
||||
export class TestInputText extends DeesElement {
|
||||
// Different group - "Inputs"
|
||||
public static demoGroup = 'Inputs';
|
||||
|
||||
public static demo = () => html`
|
||||
<test-input-text placeholder="Enter text..."></test-input-text>
|
||||
`;
|
||||
|
||||
@property({ type: String })
|
||||
accessor placeholder: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
accessor value: string = '';
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
input {
|
||||
background: #1a1a1a;
|
||||
color: #e5e5e5;
|
||||
border: 1px solid #333;
|
||||
padding: 10px 14px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
min-width: 200px;
|
||||
}
|
||||
input:focus {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
input::placeholder {
|
||||
color: #666;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<input
|
||||
type="text"
|
||||
.placeholder=${this.placeholder}
|
||||
.value=${this.value}
|
||||
@input=${(e: Event) => this.value = (e.target as HTMLInputElement).value}
|
||||
/>
|
||||
`;
|
||||
}
|
||||
}
|
||||
3
test/views/index.ts
Normal file
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.5.2',
|
||||
description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.'
|
||||
}
|
||||
|
||||
211
ts_web/elements/wcc-contextmenu.ts
Normal file
211
ts_web/elements/wcc-contextmenu.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { DeesElement, property, html, customElement, type TemplateResult, state, css, cssManager } from '@design.estate/dees-element';
|
||||
|
||||
export interface IContextMenuItem {
|
||||
name: string;
|
||||
iconName?: string;
|
||||
action: () => void | Promise<void>;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@customElement('wcc-contextmenu')
|
||||
export class WccContextmenu extends DeesElement {
|
||||
// Static method to show context menu at position
|
||||
public static async show(
|
||||
event: MouseEvent,
|
||||
menuItems: IContextMenuItem[]
|
||||
): Promise<void> {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Remove any existing context menu
|
||||
const existing = document.querySelector('wcc-contextmenu');
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
}
|
||||
|
||||
const menu = new WccContextmenu();
|
||||
menu.menuItems = menuItems;
|
||||
menu.x = event.clientX;
|
||||
menu.y = event.clientY;
|
||||
|
||||
document.body.appendChild(menu);
|
||||
|
||||
// Wait for render then adjust position if needed
|
||||
await menu.updateComplete;
|
||||
menu.adjustPosition();
|
||||
}
|
||||
|
||||
@property({ type: Array })
|
||||
accessor menuItems: IContextMenuItem[] = [];
|
||||
|
||||
@property({ type: Number })
|
||||
accessor x: number = 0;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor y: number = 0;
|
||||
|
||||
@state()
|
||||
accessor visible: boolean = false;
|
||||
|
||||
private boundHandleOutsideClick = this.handleOutsideClick.bind(this);
|
||||
private boundHandleKeydown = this.handleKeydown.bind(this);
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-5px);
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:host(.visible) {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.menu {
|
||||
min-width: 160px;
|
||||
background: #0f0f0f;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
padding: 4px 0;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
color: #ccc;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.menu-item.disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.menu-item .icon {
|
||||
font-family: 'Material Symbols Outlined';
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 24;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.menu-item:hover .icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.menu-item .label {
|
||||
flex: 1;
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="menu">
|
||||
${this.menuItems.map(item => html`
|
||||
<div
|
||||
class="menu-item ${item.disabled ? 'disabled' : ''}"
|
||||
@click=${() => this.handleItemClick(item)}
|
||||
>
|
||||
${item.iconName ? html`<span class="icon">${item.iconName}</span>` : null}
|
||||
<span class="label">${item.name}</span>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
await super.connectedCallback();
|
||||
// Delay adding listeners to avoid immediate close
|
||||
requestAnimationFrame(() => {
|
||||
document.addEventListener('click', this.boundHandleOutsideClick);
|
||||
document.addEventListener('contextmenu', this.boundHandleOutsideClick);
|
||||
document.addEventListener('keydown', this.boundHandleKeydown);
|
||||
this.classList.add('visible');
|
||||
});
|
||||
}
|
||||
|
||||
async disconnectedCallback() {
|
||||
await super.disconnectedCallback();
|
||||
document.removeEventListener('click', this.boundHandleOutsideClick);
|
||||
document.removeEventListener('contextmenu', this.boundHandleOutsideClick);
|
||||
document.removeEventListener('keydown', this.boundHandleKeydown);
|
||||
}
|
||||
|
||||
private adjustPosition() {
|
||||
const rect = this.getBoundingClientRect();
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
let x = this.x;
|
||||
let y = this.y;
|
||||
|
||||
// Adjust if menu goes off right edge
|
||||
if (x + rect.width > windowWidth - 10) {
|
||||
x = windowWidth - rect.width - 10;
|
||||
}
|
||||
|
||||
// Adjust if menu goes off bottom edge
|
||||
if (y + rect.height > windowHeight - 10) {
|
||||
y = windowHeight - rect.height - 10;
|
||||
}
|
||||
|
||||
// Ensure not off left or top
|
||||
if (x < 10) x = 10;
|
||||
if (y < 10) y = 10;
|
||||
|
||||
this.style.left = `${x}px`;
|
||||
this.style.top = `${y}px`;
|
||||
}
|
||||
|
||||
private handleOutsideClick(e: Event) {
|
||||
const path = e.composedPath();
|
||||
if (!path.includes(this)) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
private handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
private async handleItemClick(item: IContextMenuItem) {
|
||||
if (item.disabled) return;
|
||||
await item.action();
|
||||
this.close();
|
||||
}
|
||||
|
||||
private close() {
|
||||
this.classList.remove('visible');
|
||||
setTimeout(() => this.remove(), 150);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -34,17 +59,22 @@ export class WccDashboard extends DeesElement {
|
||||
@property()
|
||||
accessor selectedTheme: TTheme = 'dark';
|
||||
|
||||
@property()
|
||||
accessor searchQuery: string = '';
|
||||
|
||||
// Pinned items as Set of "sectionName::itemName"
|
||||
@property({ attribute: false })
|
||||
accessor pinnedItems: Set<string> = new Set();
|
||||
|
||||
// Sidebar width (resizable)
|
||||
@property({ type: Number })
|
||||
accessor sidebarWidth: number = 200;
|
||||
|
||||
// Derived from selectedViewport - no need for separate property
|
||||
public get isNative(): boolean {
|
||||
return this.selectedViewport === 'native';
|
||||
}
|
||||
|
||||
@property()
|
||||
accessor pages: Record<string, TTemplateFactory> = {};
|
||||
|
||||
@property()
|
||||
accessor elements: { [key: string]: DeesElement } = {};
|
||||
|
||||
@property()
|
||||
accessor warning: string = null;
|
||||
|
||||
@@ -55,21 +85,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`
|
||||
@@ -86,6 +129,9 @@ export class WccDashboard extends DeesElement {
|
||||
<wcc-sidebar
|
||||
.dashboardRef=${this}
|
||||
.selectedItem=${this.selectedItem}
|
||||
.searchQuery=${this.searchQuery}
|
||||
.pinnedItems=${this.pinnedItems}
|
||||
.sidebarWidth=${this.sidebarWidth}
|
||||
.isNative=${this.isNative}
|
||||
@selectedType=${(eventArg) => {
|
||||
this.selectedType = eventArg.detail;
|
||||
@@ -96,6 +142,23 @@ export class WccDashboard extends DeesElement {
|
||||
@selectedItem=${(eventArg) => {
|
||||
this.selectedItem = eventArg.detail;
|
||||
}}
|
||||
@searchChanged=${(eventArg: CustomEvent) => {
|
||||
this.searchQuery = eventArg.detail;
|
||||
this.updateUrlWithScrollState();
|
||||
}}
|
||||
@pinnedChanged=${(eventArg: CustomEvent) => {
|
||||
this.pinnedItems = eventArg.detail;
|
||||
this.updateUrlWithScrollState();
|
||||
}}
|
||||
@widthChanged=${async (eventArg: CustomEvent) => {
|
||||
this.sidebarWidth = eventArg.detail;
|
||||
this.updateUrlWithScrollState();
|
||||
const frame = await this.wccFrame;
|
||||
if (frame) {
|
||||
frame.sidebarWidth = eventArg.detail;
|
||||
frame.requestUpdate();
|
||||
}
|
||||
}}
|
||||
></wcc-sidebar>
|
||||
<wcc-properties
|
||||
.dashboardRef=${this}
|
||||
@@ -104,6 +167,7 @@ export class WccDashboard extends DeesElement {
|
||||
.selectedViewport=${this.selectedViewport}
|
||||
.selectedTheme=${this.selectedTheme}
|
||||
.isNative=${this.isNative}
|
||||
.sidebarWidth=${this.sidebarWidth}
|
||||
@selectedViewport=${(eventArg) => {
|
||||
this.selectedViewport = eventArg.detail;
|
||||
this.scheduleUpdate();
|
||||
@@ -122,7 +186,7 @@ export class WccDashboard extends DeesElement {
|
||||
this.toggleNative();
|
||||
}}
|
||||
></wcc-properties>
|
||||
<wcc-frame id="wccFrame" viewport=${this.selectedViewport} .isNative=${this.isNative}>
|
||||
<wcc-frame id="wccFrame" viewport=${this.selectedViewport} .isNative=${this.isNative} .sidebarWidth=${this.sidebarWidth}>
|
||||
</wcc-frame>
|
||||
`;
|
||||
}
|
||||
@@ -159,37 +223,88 @@ export class WccDashboard extends DeesElement {
|
||||
this.setupScrollListeners();
|
||||
}, 500);
|
||||
|
||||
// Route with demo index (new format)
|
||||
// New route format with section name
|
||||
this.domtools.router.on(
|
||||
'/wcctools-route/:itemType/:itemName/:demoIndex/:viewport/:theme',
|
||||
'/wcctools-route/:sectionName/:itemName/:demoIndex/:viewport/:theme',
|
||||
async (routeInfo) => {
|
||||
this.selectedType = routeInfo.params.itemType as TElementType;
|
||||
const sectionName = decodeURIComponent(routeInfo.params.sectionName);
|
||||
this.selectedSection = this.findSectionByName(sectionName);
|
||||
this.selectedItemName = routeInfo.params.itemName;
|
||||
this.selectedDemoIndex = parseInt(routeInfo.params.demoIndex) || 0;
|
||||
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
|
||||
this.selectedTheme = routeInfo.params.theme as TTheme;
|
||||
if (routeInfo.params.itemType === 'element') {
|
||||
this.selectedItem = this.elements[routeInfo.params.itemName];
|
||||
} else if (routeInfo.params.itemType === 'page') {
|
||||
this.selectedItem = this.pages[routeInfo.params.itemName];
|
||||
|
||||
if (this.selectedSection) {
|
||||
// Find item within the section
|
||||
const entries = getSectionItems(this.selectedSection);
|
||||
const found = entries.find(([name]) => name === routeInfo.params.itemName);
|
||||
if (found) {
|
||||
this.selectedItem = found[1];
|
||||
this.selectedType = this.selectedSection.type === 'elements' ? 'element' : 'page';
|
||||
}
|
||||
} else {
|
||||
// Fallback: try legacy format (element/page as section name)
|
||||
const legacyType = routeInfo.params.sectionName;
|
||||
if (legacyType === 'element' || legacyType === 'page') {
|
||||
this.selectedType = legacyType as TElementType;
|
||||
// Find item in any matching section
|
||||
const result = this.findItemByName(routeInfo.params.itemName);
|
||||
if (result) {
|
||||
this.selectedItem = result.item;
|
||||
this.selectedSection = result.section;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore scroll positions from query parameters
|
||||
// Restore state from query parameters
|
||||
if (routeInfo.queryParams) {
|
||||
const search = routeInfo.queryParams.search;
|
||||
const frameScrollY = routeInfo.queryParams.frameScrollY;
|
||||
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
|
||||
const pinned = routeInfo.queryParams.pinned;
|
||||
const sidebarWidth = routeInfo.queryParams.sidebarWidth;
|
||||
|
||||
if (search) {
|
||||
this.searchQuery = search;
|
||||
} else {
|
||||
this.searchQuery = '';
|
||||
}
|
||||
if (frameScrollY) {
|
||||
this.frameScrollY = parseInt(frameScrollY);
|
||||
}
|
||||
if (sidebarScrollY) {
|
||||
this.sidebarScrollY = parseInt(sidebarScrollY);
|
||||
}
|
||||
if (pinned) {
|
||||
const newPinned = new Set(pinned.split(',').filter(Boolean));
|
||||
// Only update if actually different to avoid update loops
|
||||
if (this.pinnedItems.size !== newPinned.size ||
|
||||
![...newPinned].every(k => this.pinnedItems.has(k))) {
|
||||
this.pinnedItems = newPinned;
|
||||
}
|
||||
} else if (this.pinnedItems.size > 0) {
|
||||
this.pinnedItems = new Set();
|
||||
}
|
||||
if (sidebarWidth) {
|
||||
this.sidebarWidth = parseInt(sidebarWidth, 10);
|
||||
}
|
||||
|
||||
// Apply scroll positions after a short delay to ensure DOM is ready
|
||||
setTimeout(() => {
|
||||
// Apply scroll positions and update frame after a short delay to ensure DOM is ready
|
||||
setTimeout(async () => {
|
||||
this.applyScrollPositions();
|
||||
// Ensure frame gets the sidebarWidth
|
||||
const frame = await this.wccFrame;
|
||||
if (frame) {
|
||||
frame.sidebarWidth = this.sidebarWidth;
|
||||
frame.requestUpdate();
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
this.searchQuery = '';
|
||||
// Only clear if not already empty to avoid update loops
|
||||
if (this.pinnedItems.size > 0) {
|
||||
this.pinnedItems = new Set();
|
||||
}
|
||||
}
|
||||
|
||||
const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
|
||||
@@ -201,35 +316,78 @@ export class WccDashboard extends DeesElement {
|
||||
|
||||
// Legacy route without demo index (for backwards compatibility)
|
||||
this.domtools.router.on(
|
||||
'/wcctools-route/:itemType/:itemName/:viewport/:theme',
|
||||
'/wcctools-route/:sectionName/:itemName/:viewport/:theme',
|
||||
async (routeInfo) => {
|
||||
this.selectedType = routeInfo.params.itemType as TElementType;
|
||||
const sectionName = decodeURIComponent(routeInfo.params.sectionName);
|
||||
this.selectedSection = this.findSectionByName(sectionName);
|
||||
this.selectedItemName = routeInfo.params.itemName;
|
||||
this.selectedDemoIndex = 0; // Default to first demo
|
||||
this.selectedDemoIndex = 0;
|
||||
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
|
||||
this.selectedTheme = routeInfo.params.theme as TTheme;
|
||||
if (routeInfo.params.itemType === 'element') {
|
||||
this.selectedItem = this.elements[routeInfo.params.itemName];
|
||||
} else if (routeInfo.params.itemType === 'page') {
|
||||
this.selectedItem = this.pages[routeInfo.params.itemName];
|
||||
|
||||
if (this.selectedSection) {
|
||||
const entries = getSectionItems(this.selectedSection);
|
||||
const found = entries.find(([name]) => name === routeInfo.params.itemName);
|
||||
if (found) {
|
||||
this.selectedItem = found[1];
|
||||
this.selectedType = this.selectedSection.type === 'elements' ? 'element' : 'page';
|
||||
}
|
||||
} else {
|
||||
// Fallback: try legacy format
|
||||
const legacyType = routeInfo.params.sectionName;
|
||||
if (legacyType === 'element' || legacyType === 'page') {
|
||||
this.selectedType = legacyType as TElementType;
|
||||
const result = this.findItemByName(routeInfo.params.itemName);
|
||||
if (result) {
|
||||
this.selectedItem = result.item;
|
||||
this.selectedSection = result.section;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore scroll positions from query parameters
|
||||
// Restore state from query parameters
|
||||
if (routeInfo.queryParams) {
|
||||
const search = routeInfo.queryParams.search;
|
||||
const frameScrollY = routeInfo.queryParams.frameScrollY;
|
||||
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
|
||||
const pinned = routeInfo.queryParams.pinned;
|
||||
const sidebarWidth = routeInfo.queryParams.sidebarWidth;
|
||||
|
||||
if (search) {
|
||||
this.searchQuery = search;
|
||||
} else {
|
||||
this.searchQuery = '';
|
||||
}
|
||||
if (frameScrollY) {
|
||||
this.frameScrollY = parseInt(frameScrollY);
|
||||
}
|
||||
if (sidebarScrollY) {
|
||||
this.sidebarScrollY = parseInt(sidebarScrollY);
|
||||
}
|
||||
if (pinned) {
|
||||
const newPinned = new Set(pinned.split(',').filter(Boolean));
|
||||
// Only update if actually different to avoid update loops
|
||||
if (this.pinnedItems.size !== newPinned.size ||
|
||||
![...newPinned].every(k => this.pinnedItems.has(k))) {
|
||||
this.pinnedItems = newPinned;
|
||||
}
|
||||
} else if (this.pinnedItems.size > 0) {
|
||||
this.pinnedItems = new Set();
|
||||
}
|
||||
if (sidebarWidth) {
|
||||
this.sidebarWidth = parseInt(sidebarWidth, 10);
|
||||
}
|
||||
|
||||
// Apply scroll positions after a short delay to ensure DOM is ready
|
||||
setTimeout(() => {
|
||||
this.applyScrollPositions();
|
||||
}, 100);
|
||||
} else {
|
||||
this.searchQuery = '';
|
||||
// Only clear if not already empty to avoid update loops
|
||||
if (this.pinnedItems.size > 0) {
|
||||
this.pinnedItems = new Set();
|
||||
}
|
||||
}
|
||||
|
||||
const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
|
||||
@@ -297,15 +455,27 @@ export class WccDashboard extends DeesElement {
|
||||
}
|
||||
|
||||
public buildUrl() {
|
||||
const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
|
||||
const sectionName = this.selectedSection
|
||||
? encodeURIComponent(this.selectedSection.name)
|
||||
: this.selectedType; // Fallback for legacy
|
||||
const baseUrl = `/wcctools-route/${sectionName}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (this.searchQuery) {
|
||||
queryParams.set('search', this.searchQuery);
|
||||
}
|
||||
if (this.frameScrollY > 0) {
|
||||
queryParams.set('frameScrollY', this.frameScrollY.toString());
|
||||
}
|
||||
if (this.sidebarScrollY > 0) {
|
||||
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
|
||||
}
|
||||
if (this.pinnedItems.size > 0) {
|
||||
queryParams.set('pinned', Array.from(this.pinnedItems).join(','));
|
||||
}
|
||||
if (this.sidebarWidth !== 200) {
|
||||
queryParams.set('sidebarWidth', this.sidebarWidth.toString());
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||||
@@ -351,15 +521,27 @@ export class WccDashboard extends DeesElement {
|
||||
}
|
||||
|
||||
private updateUrlWithScrollState() {
|
||||
const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
|
||||
const sectionName = this.selectedSection
|
||||
? encodeURIComponent(this.selectedSection.name)
|
||||
: this.selectedType; // Fallback for legacy
|
||||
const baseUrl = `/wcctools-route/${sectionName}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (this.searchQuery) {
|
||||
queryParams.set('search', this.searchQuery);
|
||||
}
|
||||
if (this.frameScrollY > 0) {
|
||||
queryParams.set('frameScrollY', this.frameScrollY.toString());
|
||||
}
|
||||
if (this.sidebarScrollY > 0) {
|
||||
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
|
||||
}
|
||||
if (this.pinnedItems.size > 0) {
|
||||
queryParams.set('pinned', Array.from(this.pinnedItems).join(','));
|
||||
}
|
||||
if (this.sidebarWidth !== 200) {
|
||||
queryParams.set('sidebarWidth', this.sidebarWidth.toString());
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||||
|
||||
@@ -19,13 +19,18 @@ export class WccFrame extends DeesElement {
|
||||
@property({ type: Boolean })
|
||||
accessor isNative: boolean = false;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor sidebarWidth: number = 200;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor isResizing: boolean = false;
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
border: 10px solid #ffaeaf;
|
||||
border: 10px solid rgba(255, 174, 175, 1);
|
||||
position: absolute;
|
||||
background: ${cssManager.bdTheme('#333', '#000')};
|
||||
left: 200px;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
overflow-y: auto;
|
||||
@@ -47,17 +52,17 @@ export class WccFrame extends DeesElement {
|
||||
<style>
|
||||
:host {
|
||||
${this.isNative ? `
|
||||
border: none !important;
|
||||
border: 0px solid rgba(255, 174, 175, 0) !important;
|
||||
left: 0px !important;
|
||||
right: 0px !important;
|
||||
top: 0px !important;
|
||||
bottom: 0px !important;
|
||||
` : `
|
||||
bottom: ${this.advancedEditorOpen ? '400px' : '100px'};
|
||||
border: 10px solid #ffaeaf;
|
||||
left: 200px;
|
||||
border: 10px solid rgba(255, 174, 175, 1);
|
||||
left: ${this.sidebarWidth}px;
|
||||
`}
|
||||
transition: all 0.3s ease;
|
||||
transition: ${this.isResizing ? 'none' : 'all 0.3s ease'};
|
||||
${this.isNative ? 'padding: 0px;' : (() => {
|
||||
switch (this.viewport) {
|
||||
case 'desktop':
|
||||
@@ -67,19 +72,19 @@ export class WccFrame extends DeesElement {
|
||||
case 'tablet':
|
||||
return `
|
||||
padding: 0px ${
|
||||
(document.body.clientWidth - 200 - domtools.breakpoints.tablet) / 2
|
||||
(document.body.clientWidth - this.sidebarWidth - domtools.breakpoints.tablet) / 2
|
||||
}px;
|
||||
`;
|
||||
case 'phablet':
|
||||
return `
|
||||
padding: 0px ${
|
||||
(document.body.clientWidth - 200 - domtools.breakpoints.phablet) / 2
|
||||
(document.body.clientWidth - this.sidebarWidth - domtools.breakpoints.phablet) / 2
|
||||
}px;
|
||||
`;
|
||||
case 'phone':
|
||||
return `
|
||||
padding: 0px ${
|
||||
(document.body.clientWidth - 200 - domtools.breakpoints.phone) / 2
|
||||
(document.body.clientWidth - this.sidebarWidth - domtools.breakpoints.phone) / 2
|
||||
}px;
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ export class WccProperties extends DeesElement {
|
||||
@property()
|
||||
accessor isNative: boolean = false;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor sidebarWidth: number = 200;
|
||||
|
||||
@state()
|
||||
accessor propertyContent: TemplateResult[] = [];
|
||||
|
||||
@@ -60,6 +63,10 @@ export class WccProperties extends DeesElement {
|
||||
@state()
|
||||
accessor recordingDuration: number = 0;
|
||||
|
||||
// Delayed hide for native mode transition
|
||||
@state()
|
||||
accessor isHidden: boolean = false;
|
||||
|
||||
public editorHeight: number = 300;
|
||||
|
||||
public render(): TemplateResult {
|
||||
@@ -89,14 +96,14 @@ export class WccProperties extends DeesElement {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
left: 200px;
|
||||
left: ${this.sidebarWidth}px;
|
||||
height: ${this.editingProperties.length > 0 ? 100 + this.editorHeight : 100}px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
overflow: hidden;
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
display: ${this.isNative ? 'none' : 'block'};
|
||||
display: ${this.isHidden ? 'none' : 'block'};
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
@@ -928,6 +935,19 @@ export class WccProperties extends DeesElement {
|
||||
protected updated(changedProperties: Map<string, unknown>) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
// Handle delayed hide for native mode transition
|
||||
if (changedProperties.has('isNative')) {
|
||||
if (this.isNative) {
|
||||
// Delay hiding until frame animation completes
|
||||
setTimeout(() => {
|
||||
this.isHidden = true;
|
||||
}, 300);
|
||||
} else {
|
||||
// Show immediately when exiting native mode
|
||||
this.isHidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Only recreate properties when selectedItem changes
|
||||
if (changedProperties.has('selectedItem')) {
|
||||
this.createProperties().catch(error => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as plugins from '../wcctools.plugins.js';
|
||||
import { DeesElement, property, html, customElement, type TemplateResult, state } from '@design.estate/dees-element';
|
||||
import { WccDashboard } from './wcc-dashboard.js';
|
||||
import { WccDashboard, getSectionItems } from './wcc-dashboard.js';
|
||||
import type { TTemplateFactory } from './wcctools.helpers.js';
|
||||
import { getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
|
||||
|
||||
export type TElementType = 'element' | 'page';
|
||||
import type { IWccSection, TElementType } from '../wcctools.interfaces.js';
|
||||
import { WccContextmenu } from './wcc-contextmenu.js';
|
||||
|
||||
@customElement('wcc-sidebar')
|
||||
export class WccSidebar extends DeesElement {
|
||||
@@ -24,6 +24,32 @@ export class WccSidebar extends DeesElement {
|
||||
@state()
|
||||
accessor expandedElements: Set<string> = new Set();
|
||||
|
||||
// Track which sections are collapsed
|
||||
@state()
|
||||
accessor collapsedSections: Set<string> = new Set();
|
||||
|
||||
// Search query for filtering sidebar items
|
||||
@property()
|
||||
accessor searchQuery: string = '';
|
||||
|
||||
// Pinned items as Set of "sectionName::itemName"
|
||||
@property({ attribute: false })
|
||||
accessor pinnedItems: Set<string> = new Set();
|
||||
|
||||
// Sidebar width (resizable)
|
||||
@property({ type: Number })
|
||||
accessor sidebarWidth: number = 200;
|
||||
|
||||
// Track if currently resizing
|
||||
@state()
|
||||
accessor isResizing: boolean = false;
|
||||
|
||||
// Delayed hide for native mode transition
|
||||
@state()
|
||||
accessor isHidden: boolean = false;
|
||||
|
||||
private sectionsInitialized = false;
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet" />
|
||||
@@ -45,14 +71,14 @@ export class WccSidebar extends DeesElement {
|
||||
--ring: #3b82f6;
|
||||
--radius: 4px;
|
||||
|
||||
display: ${this.isNative ? 'none' : 'block'};
|
||||
display: ${this.isHidden ? 'none' : 'block'};
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
width: 200px;
|
||||
width: ${this.sidebarWidth}px;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
overflow-y: auto;
|
||||
@@ -65,7 +91,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 +103,45 @@ export class WccSidebar extends DeesElement {
|
||||
background: rgba(59, 130, 246, 0.03);
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
h3:first-child {
|
||||
.section-header:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.section-header:hover {
|
||||
background: rgba(59, 130, 246, 0.08);
|
||||
}
|
||||
|
||||
.section-header .expand-icon {
|
||||
font-size: 14px;
|
||||
opacity: 0.5;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.section-header.collapsed .expand-icon {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.section-header .section-icon {
|
||||
font-size: 14px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-content.collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-family: 'Material Symbols Outlined';
|
||||
font-weight: normal;
|
||||
@@ -117,7 +176,11 @@ export class WccSidebar extends DeesElement {
|
||||
}
|
||||
|
||||
.selectOption.folder {
|
||||
grid-template-columns: 16px 20px 1fr;
|
||||
grid-template-columns: 16px 1fr;
|
||||
}
|
||||
|
||||
.selectOption.folder .text {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.selectOption .expand-icon {
|
||||
@@ -214,47 +277,379 @@ export class WccSidebar extends DeesElement {
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.search-container {
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: var(--input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: var(--foreground);
|
||||
font-size: 0.75rem;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background: rgba(59, 130, 246, 0.3);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Pinned item highlight in original section */
|
||||
.selectOption.pinned {
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
}
|
||||
|
||||
.selectOption.pinned:hover {
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
}
|
||||
|
||||
.selectOption.pinned.selected {
|
||||
background: rgba(245, 158, 11, 0.18);
|
||||
}
|
||||
|
||||
/* Pinned section styling */
|
||||
.section-header.pinned-section {
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.section-header.pinned-section:hover {
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
}
|
||||
|
||||
.section-header.pinned-section .section-icon {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Section tag pill for pinned items */
|
||||
.section-tag {
|
||||
font-size: 0.5rem;
|
||||
color: #888;
|
||||
margin-left: auto;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 9999px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Group container */
|
||||
.item-group {
|
||||
margin: 0.375rem 0.375rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 6px;
|
||||
padding: 0.25rem 0;
|
||||
background: rgba(255, 255, 255, 0.01);
|
||||
}
|
||||
|
||||
.item-group-legend {
|
||||
font-size: 0.55rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #555;
|
||||
padding: 0.125rem 0.625rem 0.25rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.item-group .selectOption {
|
||||
margin-left: 0.25rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
/* Resize handle */
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
cursor: col-resize;
|
||||
background: transparent;
|
||||
transition: background 0.15s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.resize-handle:hover {
|
||||
background: rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.resize-handle.active {
|
||||
background: var(--primary);
|
||||
}
|
||||
</style>
|
||||
<div class="search-container">
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="Search..."
|
||||
.value=${this.searchQuery}
|
||||
@input=${this.handleSearchInput}
|
||||
/>
|
||||
</div>
|
||||
<div class="menu">
|
||||
<h3>Pages</h3>
|
||||
${(() => {
|
||||
const pages = Object.keys(this.dashboardRef.pages);
|
||||
return pages.map(pageName => {
|
||||
const item = this.dashboardRef.pages[pageName];
|
||||
${this.renderPinnedSection()}
|
||||
${this.renderSections()}
|
||||
</div>
|
||||
<div
|
||||
class="resize-handle ${this.isResizing ? 'active' : ''}"
|
||||
@mousedown=${this.startResize}
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize collapsed sections from section config
|
||||
*/
|
||||
private initCollapsedSections() {
|
||||
if (this.sectionsInitialized || !this.dashboardRef?.sections) return;
|
||||
|
||||
const collapsed = new Set<string>();
|
||||
for (const section of this.dashboardRef.sections) {
|
||||
if (section.collapsed) {
|
||||
collapsed.add(section.name);
|
||||
}
|
||||
}
|
||||
this.collapsedSections = collapsed;
|
||||
this.sectionsInitialized = true;
|
||||
}
|
||||
|
||||
// ============ Pinning helpers ============
|
||||
|
||||
private getPinKey(sectionName: string, itemName: string): string {
|
||||
return `${sectionName}::${itemName}`;
|
||||
}
|
||||
|
||||
private isPinned(sectionName: string, itemName: string): boolean {
|
||||
return this.pinnedItems.has(this.getPinKey(sectionName, itemName));
|
||||
}
|
||||
|
||||
private togglePin(sectionName: string, itemName: string) {
|
||||
const key = this.getPinKey(sectionName, itemName);
|
||||
const newPinned = new Set(this.pinnedItems);
|
||||
if (newPinned.has(key)) {
|
||||
newPinned.delete(key);
|
||||
} else {
|
||||
newPinned.add(key);
|
||||
}
|
||||
this.pinnedItems = newPinned;
|
||||
this.dispatchEvent(new CustomEvent('pinnedChanged', { detail: newPinned }));
|
||||
}
|
||||
|
||||
private showContextMenu(e: MouseEvent, sectionName: string, itemName: string) {
|
||||
const isPinned = this.isPinned(sectionName, itemName);
|
||||
WccContextmenu.show(e, [
|
||||
{
|
||||
name: isPinned ? 'Unpin' : 'Pin',
|
||||
iconName: isPinned ? 'push_pin' : 'push_pin',
|
||||
action: () => this.togglePin(sectionName, itemName),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the PINNED section (only if there are pinned items)
|
||||
*/
|
||||
private renderPinnedSection() {
|
||||
if (!this.dashboardRef?.sections || this.pinnedItems.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isCollapsed = this.collapsedSections.has('__pinned__');
|
||||
|
||||
// Collect pinned items with their original section info
|
||||
// Pinned items are NOT filtered by search - they always remain visible
|
||||
const pinnedEntries: Array<{ sectionName: string; itemName: string; item: any; section: IWccSection }> = [];
|
||||
|
||||
for (const key of this.pinnedItems) {
|
||||
const [sectionName, itemName] = key.split('::');
|
||||
const section = this.dashboardRef.sections.find(s => s.name === sectionName);
|
||||
if (section) {
|
||||
const entries = getSectionItems(section);
|
||||
const found = entries.find(([name]) => name === itemName);
|
||||
if (found) {
|
||||
pinnedEntries.push({ sectionName, itemName, item: found[1], section });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pinnedEntries.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="selectOption ${this.selectedItem === item ? 'selected' : null}"
|
||||
@click=${async () => {
|
||||
const domtools = await plugins.deesDomtools.DomTools.setupDomTools();
|
||||
this.selectItem('page', pageName, item, 0);
|
||||
}}
|
||||
class="section-header pinned-section ${isCollapsed ? 'collapsed' : ''}"
|
||||
@click=${() => this.toggleSectionCollapsed('__pinned__')}
|
||||
>
|
||||
<i class="material-symbols-outlined">insert_drive_file</i>
|
||||
<div class="text">${pageName}</div>
|
||||
<i class="material-symbols-outlined expand-icon">expand_more</i>
|
||||
<i class="material-symbols-outlined section-icon">push_pin</i>
|
||||
<span>Pinned</span>
|
||||
</div>
|
||||
<div class="section-content ${isCollapsed ? 'collapsed' : ''}">
|
||||
${pinnedEntries.map(({ sectionName, itemName, item, section }) => {
|
||||
const isSelected = this.selectedItem === item;
|
||||
const type = section.type === 'elements' ? 'element' : 'page';
|
||||
const icon = section.type === 'elements' ? 'featured_video' : 'insert_drive_file';
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="selectOption ${isSelected ? 'selected' : ''}"
|
||||
@click=${async () => {
|
||||
await plugins.deesDomtools.DomTools.setupDomTools();
|
||||
this.selectItem(type, itemName, item, 0, section);
|
||||
}}
|
||||
@contextmenu=${(e: MouseEvent) => this.showContextMenu(e, sectionName, itemName)}
|
||||
>
|
||||
<i class="material-symbols-outlined">${icon}</i>
|
||||
<div class="text">${this.highlightMatch(itemName)}</div>
|
||||
<span class="section-tag">${sectionName}</span>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render all sections
|
||||
*/
|
||||
private renderSections() {
|
||||
if (!this.dashboardRef?.sections) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.initCollapsedSections();
|
||||
|
||||
return this.dashboardRef.sections.map((section) => {
|
||||
// Check if section has any matching items
|
||||
const entries = getSectionItems(section);
|
||||
const filteredEntries = entries.filter(([name]) => this.matchesSearch(name));
|
||||
|
||||
// Hide section if no items match the search
|
||||
if (filteredEntries.length === 0 && this.searchQuery) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isCollapsed = this.collapsedSections.has(section.name);
|
||||
const sectionIcon = section.icon || (section.type === 'pages' ? 'insert_drive_file' : 'widgets');
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="section-header ${isCollapsed ? 'collapsed' : ''}"
|
||||
@click=${() => this.toggleSectionCollapsed(section.name)}
|
||||
>
|
||||
<i class="material-symbols-outlined expand-icon">expand_more</i>
|
||||
${section.icon ? html`<i class="material-symbols-outlined section-icon">${section.icon}</i>` : null}
|
||||
<span>${section.name}</span>
|
||||
</div>
|
||||
<div class="section-content ${isCollapsed ? 'collapsed' : ''}">
|
||||
${this.renderSectionItems(section)}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
})()}
|
||||
<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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render items for a section
|
||||
*/
|
||||
private renderSectionItems(section: IWccSection) {
|
||||
const entries = getSectionItems(section);
|
||||
// Filter entries by search query
|
||||
const filteredEntries = entries.filter(([name]) => this.matchesSearch(name));
|
||||
|
||||
if (section.type === 'pages') {
|
||||
return filteredEntries.map(([pageName, item]) => {
|
||||
const isPinned = this.isPinned(section.name, pageName);
|
||||
return html`
|
||||
<div
|
||||
class="selectOption ${this.selectedItem === item ? 'selected' : ''} ${isPinned ? 'pinned' : ''}"
|
||||
@click=${async () => {
|
||||
await plugins.deesDomtools.DomTools.setupDomTools();
|
||||
this.selectItem('page', pageName, item, 0, section);
|
||||
}}
|
||||
@contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, pageName)}
|
||||
>
|
||||
<i class="material-symbols-outlined">insert_drive_file</i>
|
||||
<div class="text">${this.highlightMatch(pageName)}</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
// type === 'elements' - group by demoGroup
|
||||
const groupedItems = new Map<string | null, Array<[string, any]>>();
|
||||
|
||||
for (const entry of filteredEntries) {
|
||||
const [, item] = entry;
|
||||
const group = (item as any).demoGroup || null;
|
||||
if (!groupedItems.has(group)) {
|
||||
groupedItems.set(group, []);
|
||||
}
|
||||
groupedItems.get(group)!.push(entry);
|
||||
}
|
||||
|
||||
const result: TemplateResult[] = [];
|
||||
|
||||
// Render ungrouped items first
|
||||
const ungrouped = groupedItems.get(null) || [];
|
||||
for (const entry of ungrouped) {
|
||||
result.push(this.renderElementItem(entry, section));
|
||||
}
|
||||
|
||||
// Render grouped items
|
||||
for (const [groupName, items] of groupedItems) {
|
||||
if (groupName === null) continue;
|
||||
|
||||
result.push(html`
|
||||
<div class="item-group">
|
||||
<span class="item-group-legend">${groupName}</span>
|
||||
${items.map((entry) => this.renderElementItem(entry, section))}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single element item (used by renderSectionItems)
|
||||
*/
|
||||
private renderElementItem(entry: [string, any], section: IWccSection): TemplateResult {
|
||||
const [elementName, item] = entry;
|
||||
const anonItem = item as any;
|
||||
const demoCount = anonItem.demo ? getDemoCount(anonItem.demo) : 0;
|
||||
const isMultiDemo = anonItem.demo && hasMultipleDemos(anonItem.demo);
|
||||
const isExpanded = this.expandedElements.has(elementName);
|
||||
const isSelected = this.selectedItem === item;
|
||||
const isPinned = this.isPinned(section.name, elementName);
|
||||
|
||||
if (isMultiDemo) {
|
||||
// Multi-demo element - render as expandable folder
|
||||
return html`
|
||||
<div
|
||||
class="selectOption folder ${isExpanded ? 'expanded' : ''} ${isSelected ? 'selected' : ''}"
|
||||
class="selectOption folder ${isExpanded ? 'expanded' : ''} ${isSelected ? 'selected' : ''} ${isPinned ? 'pinned' : ''}"
|
||||
@click=${() => this.toggleExpanded(elementName)}
|
||||
@contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, elementName)}
|
||||
>
|
||||
<i class="material-symbols-outlined expand-icon">chevron_right</i>
|
||||
<i class="material-symbols-outlined">folder</i>
|
||||
<div class="text">${elementName}</div>
|
||||
<div class="text">${this.highlightMatch(elementName)}</div>
|
||||
</div>
|
||||
${isExpanded ? html`
|
||||
<div class="demo-children">
|
||||
@@ -266,7 +661,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,24 +673,31 @@ 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' : ''} ${isPinned ? 'pinned' : ''}"
|
||||
@click=${async () => {
|
||||
await plugins.deesDomtools.DomTools.setupDomTools();
|
||||
this.selectItem('element', elementName, item, 0);
|
||||
this.selectItem('element', elementName, item, 0, section);
|
||||
}}
|
||||
@contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, elementName)}
|
||||
>
|
||||
<i class="material-symbols-outlined">featured_video</i>
|
||||
<div class="text">${elementName}</div>
|
||||
<div class="text">${this.highlightMatch(elementName)}</div>
|
||||
</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) {
|
||||
@@ -308,35 +710,136 @@ export class WccSidebar extends DeesElement {
|
||||
this.expandedElements = newSet;
|
||||
}
|
||||
|
||||
private handleSearchInput(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
this.searchQuery = input.value;
|
||||
this.dispatchEvent(new CustomEvent('searchChanged', { detail: this.searchQuery }));
|
||||
}
|
||||
|
||||
private matchesSearch(name: string): boolean {
|
||||
if (!this.searchQuery) return true;
|
||||
return name.toLowerCase().includes(this.searchQuery.toLowerCase());
|
||||
}
|
||||
|
||||
private highlightMatch(text: string): TemplateResult {
|
||||
if (!this.searchQuery) return html`${text}`;
|
||||
const lowerText = text.toLowerCase();
|
||||
const lowerQuery = this.searchQuery.toLowerCase();
|
||||
const index = lowerText.indexOf(lowerQuery);
|
||||
if (index === -1) return html`${text}`;
|
||||
const before = text.slice(0, index);
|
||||
const match = text.slice(index, index + this.searchQuery.length);
|
||||
const after = text.slice(index + this.searchQuery.length);
|
||||
return html`${before}<span class="highlight">${match}</span>${after}`;
|
||||
}
|
||||
|
||||
protected updated(changedProperties: Map<string, unknown>) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
// Handle delayed hide for native mode transition
|
||||
if (changedProperties.has('isNative')) {
|
||||
if (this.isNative) {
|
||||
// Delay hiding until frame animation completes
|
||||
setTimeout(() => {
|
||||
this.isHidden = true;
|
||||
}, 300);
|
||||
} else {
|
||||
// Show immediately when exiting native mode
|
||||
this.isHidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
if (changedProperties.has('selectedItem') && this.selectedItem && this.dashboardRef?.sections) {
|
||||
// Find the element in any section
|
||||
for (const section of this.dashboardRef.sections) {
|
||||
if (section.type !== 'elements') continue;
|
||||
|
||||
const entries = getSectionItems(section);
|
||||
const found = entries.find(([_, item]) => item === this.selectedItem);
|
||||
if (found) {
|
||||
const [elementName, item] = found;
|
||||
const anonItem = item as any;
|
||||
if (anonItem.demo && hasMultipleDemos(anonItem.demo)) {
|
||||
if (!this.expandedElements.has(elementName)) {
|
||||
const newSet = new Set(this.expandedElements);
|
||||
newSet.add(elementName);
|
||||
this.expandedElements = newSet;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public selectItem(typeArg: TElementType, itemNameArg: string, itemArg: TTemplateFactory | DeesElement, demoIndex: number = 0) {
|
||||
// ============ Resize functionality ============
|
||||
|
||||
private startResize = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
this.isResizing = true;
|
||||
const startX = e.clientX;
|
||||
const startWidth = this.sidebarWidth;
|
||||
|
||||
// Cache references once at start
|
||||
const frame = this.dashboardRef?.shadowRoot?.querySelector('wcc-frame') as any;
|
||||
const properties = this.dashboardRef?.shadowRoot?.querySelector('wcc-properties') as any;
|
||||
|
||||
// Disable frame transition during resize
|
||||
if (frame) {
|
||||
frame.isResizing = true;
|
||||
}
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
const newWidth = Math.min(400, Math.max(150, startWidth + (e.clientX - startX)));
|
||||
this.sidebarWidth = newWidth;
|
||||
// Update frame and properties directly
|
||||
if (frame) {
|
||||
frame.sidebarWidth = newWidth;
|
||||
}
|
||||
if (properties) {
|
||||
properties.sidebarWidth = newWidth;
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
this.isResizing = false;
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
// Re-enable frame transition
|
||||
if (frame) {
|
||||
frame.isResizing = false;
|
||||
}
|
||||
// Dispatch event on release for URL persistence
|
||||
this.dispatchEvent(new CustomEvent('widthChanged', { detail: this.sidebarWidth }));
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
|
||||
public selectItem(
|
||||
typeArg: TElementType,
|
||||
itemNameArg: string,
|
||||
itemArg: TTemplateFactory | DeesElement,
|
||||
demoIndex: number = 0,
|
||||
section?: IWccSection
|
||||
) {
|
||||
console.log('selected item');
|
||||
console.log(itemNameArg);
|
||||
console.log(itemArg);
|
||||
console.log('demo index:', demoIndex);
|
||||
console.log('section:', section?.name);
|
||||
|
||||
this.selectedItem = itemArg;
|
||||
this.selectedType = typeArg;
|
||||
this.dashboardRef.selectedDemoIndex = demoIndex;
|
||||
|
||||
// Set the selected section on dashboard
|
||||
if (section) {
|
||||
this.dashboardRef.selectedSection = section;
|
||||
}
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('selectedType', {
|
||||
detail: typeArg
|
||||
@@ -356,7 +859,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