Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fe17f5afd | |||
| 63dd6a27b3 | |||
| 287cc4d1c3 | |||
| 14e63738b7 | |||
| dd151bdad8 | |||
| bd409745e6 | |||
| eb7f482b75 | |||
| d16d482120 | |||
| 5ba4e55011 | |||
| 0068c0749d | |||
| 36dd6b5064 | |||
| ddecfcdb4c | |||
| 8f0f8606a1 | |||
| 7dca519d9a | |||
| d48cd063c4 | |||
| bb04895be8 | |||
| 54b34b6faa | |||
| 12c85fa4cb | |||
| d90df9717b | |||
| d4b161437b | |||
| ad033c8104 |
BIN
.playwright-mcp/recording-panel.png
Normal file
BIN
.playwright-mcp/recording-panel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
BIN
.playwright-mcp/wcctools-dashboard.png
Normal file
BIN
.playwright-mcp/wcctools-dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
.playwright-mcp/wcctools-with-element.png
Normal file
BIN
.playwright-mcp/wcctools-with-element.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
81
changelog.md
81
changelog.md
@@ -1,5 +1,86 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-12-30 - 3.4.0 - feat(sidebar)
|
||||||
|
add searchable sidebar with URL-backed query state and highlighted matches
|
||||||
|
|
||||||
|
- Add search input to wcc-sidebar and expose a searchQuery property
|
||||||
|
- Filter sidebar sections and items client-side based on the search query and hide sections with no matches
|
||||||
|
- Highlight matching substrings in sidebar item labels
|
||||||
|
- Emit a 'searchChanged' event from the sidebar and handle it in wcc-dashboard to keep dashboard.searchQuery in sync
|
||||||
|
- Persist the search query in the route query parameter 'search' when building URLs and restore/clear it on navigation
|
||||||
|
- Preserve existing scroll-state handling while adding search state to URL updates
|
||||||
|
|
||||||
|
## 2025-12-28 - 3.3.0 - feat(wcctools)
|
||||||
|
Add section-based configuration API for setupWccTools, new Views, and section-aware routing/sidebar
|
||||||
|
|
||||||
|
- Introduce IWccSection and IWccConfig types and migrate setupWccTools to accept a sections config while preserving legacy (elements, pages) format
|
||||||
|
- WccDashboard and WccSidebar updated to support sections, filtering, sorting, collapsed sections, and section-aware URL routing (uses sectionName in routes with legacy fallbacks)
|
||||||
|
- Add Views: view-dashboard, view-settings, view-empty-state plus test/views index exports and demo variations
|
||||||
|
- Add helpers: getSectionItems, convertLegacyToConfig and isWccConfig; update build URL and routing logic to be section-aware
|
||||||
|
- Update docs and README/readme.hints with sections API, examples, migration notes and UI/UX updates
|
||||||
|
|
||||||
|
## 2025-12-22 - 3.2.0 - feat(wcc-sidebar)
|
||||||
|
auto-expand sidebar folder when selecting an element with multiple demos
|
||||||
|
|
||||||
|
- Add updated() lifecycle to auto-expand folder when selectedItem changes
|
||||||
|
- Find element name by matching selectedItem against dashboardRef.elements
|
||||||
|
- Only auto-expand when the element has multiple demos (checks item.demo and hasMultipleDemos)
|
||||||
|
- Immutably update expandedElements set to trigger re-render and avoid duplicate additions
|
||||||
|
|
||||||
|
## 2025-12-21 - 3.1.2 - fix(wcc-properties)
|
||||||
|
Use LitElement.updated to recreate properties only when selectedItem changes and handle errors; remove custom scheduleUpdate implementation
|
||||||
|
|
||||||
|
- Replaced public async scheduleUpdate() with protected updated(changedProperties) lifecycle method
|
||||||
|
- Call super.updated(...) and only recreate properties when selectedItem changed to avoid unnecessary work
|
||||||
|
- Preserve error handling and clear propertyContent on failure
|
||||||
|
- Removed explicit super.scheduleUpdate() call to rely on LitElement's update lifecycle
|
||||||
|
|
||||||
|
## 2025-12-21 - 3.1.1 - fix(wcc-properties)
|
||||||
|
Improve wcc-properties CSS to prevent grid overflow, properly size and center icon glyphs, and adjust right-side offset
|
||||||
|
|
||||||
|
- Use minmax(0, 1fr) for grid-template-columns (selectorButtons1/2/4/5) to avoid flexbox overflow and ensure consistent column sizing
|
||||||
|
- Add min-width/min-height and inline-flex centering to .material-symbols-outlined to stabilize icon sizing and vertical/horizontal alignment
|
||||||
|
- Increase right calc offset from 520px to 600px to accommodate wider content/controls
|
||||||
|
- Changes applied in ts_web/elements/wcc-properties.ts
|
||||||
|
|
||||||
|
## 2025-12-19 - 3.1.0 - feat(wcc-properties)
|
||||||
|
add Share selector with inline Record button and adjust properties panel grid layout
|
||||||
|
|
||||||
|
- Increase rightmost column width from 70px to 100px in properties grid
|
||||||
|
- Introduce .shareSelector and .selectorButtons1 CSS classes and markup
|
||||||
|
- Replace <wcc-record-button> with inline Record button that toggles icon based on isRecording
|
||||||
|
- Add Share panel heading and integrate record control into selector area
|
||||||
|
|
||||||
|
## 2025-12-19 - 3.0.0 - BREAKING CHANGE(ts_web)
|
||||||
|
Replace fullscreen boolean with native viewport mode across components, add native viewport selector and toggle, and update dev deps and npmextra config
|
||||||
|
|
||||||
|
- Introduce isNative (derived from selectedViewport === 'native') replacing isFullscreen on WccDashboard, WccFrame, WccProperties and WccSidebar (property and styling changes).
|
||||||
|
- WccDashboard: remove isFullscreen property, add isNative getter, implement toggleNative to switch selectedViewport between 'native' and 'desktop', update ESC handler to exit native mode and call buildUrl().
|
||||||
|
- WccProperties: add Native button to viewport selector, adjust grid columns and selector layout, replace fullscreen toggle with toggleNative (dispatches 'toggleNative' event).
|
||||||
|
- WccFrame: rename isFullscreen -> isNative and update style/markup branching to use isNative.
|
||||||
|
- WccSidebar: visibility now driven by isNative instead of isFullscreen.
|
||||||
|
- API/event changes: 'toggleFullscreen' event renamed to 'toggleNative' — this is an incompatible change for consumers relying on the old event/property.
|
||||||
|
- package.json: bump devDependencies @git.zone/tsbuild -> ^4.0.2, @git.zone/tsrun -> ^2.0.1, @git.zone/tswatch -> ^2.3.13, @types/node -> ^25.0.3.
|
||||||
|
- npmextra.json: rename keys (e.g. "gitzone" -> "@git.zone/cli", "tsdoc" -> "@git.zone/tsdoc"), add @ship.zone/szci entry and add release registries/accessLevel for @git.zone/cli.
|
||||||
|
|
||||||
|
## 2025-12-11 - 2.0.1 - fix(@git.zone/tswatch)
|
||||||
|
Bump @git.zone/tswatch devDependency to ^2.3.12
|
||||||
|
|
||||||
|
- Updated devDependency @git.zone/tswatch from ^2.3.11 to ^2.3.12 in package.json
|
||||||
|
|
||||||
|
## 2025-12-11 - 2.0.0 - BREAKING CHANGE(recorder)
|
||||||
|
Remove FFmpeg-based MP4 conversion; simplify recorder/export to WebM and improve recorder/editor robustness
|
||||||
|
|
||||||
|
- Removed FFmpegService and all client-side MP4 conversion logic — exports are now WebM-only (MP4 conversion and related UI/controls removed).
|
||||||
|
- ts_web/elements/wcc-recording-panel: dropped outputFormat and conversion states/UI; download flow simplified to always export WebM.
|
||||||
|
- ts_web/index.ts: removed FFmpegService exports and conversion types from public API.
|
||||||
|
- package.json: removed @ffmpeg/* dependencies.
|
||||||
|
- RecorderService: handleRecordingComplete is now async and fixes recorded blob assignment and cleanup timing.
|
||||||
|
- wcc-properties: improved element detection and robustness — recursive search through light/shadow DOM with retry/delay, plus an advanced JSON editor for Object/Array props (supports multiple open editors and frame resize events).
|
||||||
|
- wcc-sidebar: force re-render after selecting demos to ensure child demo selection indicators update correctly.
|
||||||
|
- dees-demowrapper: ensure slotted content is rendered before calling runAfterRender (small timing/stability improvements).
|
||||||
|
- Test update: demo definitions can be arrays (multiple demos) — test-demoelement updated to use multiple demo entries.
|
||||||
|
|
||||||
## 2025-12-11 - 1.3.0 - feat(recording-panel)
|
## 2025-12-11 - 1.3.0 - feat(recording-panel)
|
||||||
Add demo wrapper utilities, improve recording trim behavior, and harden property panel element detection; update documentation
|
Add demo wrapper utilities, improve recording trim behavior, and harden property panel element detection; update documentation
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,32 @@
|
|||||||
import * as deesWccTools from '../ts_web/index.js';
|
import * as deesWccTools from '../ts_web/index.js';
|
||||||
import * as deesDomTools from '@design.estate/dees-domtools';
|
import * as deesDomTools from '@design.estate/dees-domtools';
|
||||||
|
|
||||||
// elements and pages
|
// elements, views and pages
|
||||||
import * as elements from '../test/elements/index.js';
|
import * as elements from '../test/elements/index.js';
|
||||||
|
import * as views from '../test/views/index.js';
|
||||||
import * as pages from '../test/pages/index.js';
|
import * as pages from '../test/pages/index.js';
|
||||||
|
|
||||||
deesWccTools.setupWccTools(elements as any, pages);
|
// Sections-based API with Views
|
||||||
|
deesWccTools.setupWccTools({
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
name: 'Pages',
|
||||||
|
type: 'pages',
|
||||||
|
items: pages,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Views',
|
||||||
|
type: 'elements',
|
||||||
|
items: views,
|
||||||
|
icon: 'web',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Elements',
|
||||||
|
type: 'elements',
|
||||||
|
items: elements,
|
||||||
|
sort: ([a], [b]) => a.localeCompare(b),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
deesDomTools.elementBasic.setup();
|
deesDomTools.elementBasic.setup();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"gitzone": {
|
"@git.zone/cli": {
|
||||||
"projectType": "wcc",
|
"projectType": "wcc",
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "code.foss.global",
|
"githost": "code.foss.global",
|
||||||
@@ -21,13 +21,19 @@
|
|||||||
"element testing",
|
"element testing",
|
||||||
"page development"
|
"page development"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"registries": [
|
||||||
|
"https://verdaccio.lossless.digital",
|
||||||
|
"https://registry.npmjs.org"
|
||||||
|
],
|
||||||
|
"accessLevel": "public"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npmci": {
|
"@git.zone/tsdoc": {
|
||||||
"npmGlobalTools": [],
|
|
||||||
"npmAccessLevel": "public"
|
|
||||||
},
|
|
||||||
"tsdoc": {
|
|
||||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||||
|
},
|
||||||
|
"@ship.zone/szci": {
|
||||||
|
"npmGlobalTools": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@design.estate/dees-wcctools",
|
"name": "@design.estate/dees-wcctools",
|
||||||
"version": "1.3.0",
|
"version": "3.4.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.",
|
"description": "A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -24,13 +24,13 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@api.global/typedserver": "^7.11.1",
|
"@api.global/typedserver": "^7.11.1",
|
||||||
"@git.zone/tsbuild": "^3.1.2",
|
"@git.zone/tsbuild": "^4.0.2",
|
||||||
"@git.zone/tsbundle": "^2.6.3",
|
"@git.zone/tsbundle": "^2.6.3",
|
||||||
"@git.zone/tsrun": "^2.0.0",
|
"@git.zone/tsrun": "^2.0.1",
|
||||||
"@git.zone/tstest": "^3.1.3",
|
"@git.zone/tstest": "^3.1.3",
|
||||||
"@git.zone/tswatch": "^2.3.10",
|
"@git.zone/tswatch": "^2.3.13",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@types/node": "^25.0.0"
|
"@types/node": "^25.0.3"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
|
|||||||
1916
pnpm-lock.yaml
generated
1916
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,67 @@
|
|||||||
# Project Hints and Findings
|
# Project Hints and Findings
|
||||||
|
|
||||||
|
## Section-based Configuration API (2025-12-27)
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
Refactored `setupWccTools` to accept a section-based configuration object instead of separate elements/pages arguments. This allows multiple custom sections with filtering, sorting, and collapsible headers.
|
||||||
|
|
||||||
|
### New API
|
||||||
|
```typescript
|
||||||
|
import * as deesWccTools from '@design.estate/dees-wcctools';
|
||||||
|
|
||||||
|
deesWccTools.setupWccTools({
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
name: 'Pages',
|
||||||
|
type: 'pages',
|
||||||
|
items: pages,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Elements',
|
||||||
|
type: 'elements',
|
||||||
|
items: elements,
|
||||||
|
filter: (name, item) => !name.startsWith('internal-'),
|
||||||
|
sort: ([a], [b]) => a.localeCompare(b),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Views',
|
||||||
|
type: 'elements',
|
||||||
|
items: elements,
|
||||||
|
filter: (name, item) => name.startsWith('view-'),
|
||||||
|
icon: 'web',
|
||||||
|
collapsed: true, // Start collapsed
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Section Properties
|
||||||
|
- `name`: Display name for the section header
|
||||||
|
- `type`: `'elements'` (shows demos) or `'pages'` (renders directly)
|
||||||
|
- `items`: Record of items (element classes or page factories)
|
||||||
|
- `filter`: Optional `(name, item) => boolean` to filter items
|
||||||
|
- `sort`: Optional `([name, item], [name, item]) => number` for ordering
|
||||||
|
- `icon`: Optional Material icon name for section header
|
||||||
|
- `collapsed`: Optional boolean to start section collapsed
|
||||||
|
|
||||||
|
### Backwards Compatibility
|
||||||
|
Legacy format is still supported:
|
||||||
|
```typescript
|
||||||
|
deesWccTools.setupWccTools(elements, pages); // Still works
|
||||||
|
```
|
||||||
|
|
||||||
|
### URL Routing
|
||||||
|
Changed from `/wcctools-route/:itemType/:itemName/...` to `/wcctools-route/:sectionName/:itemName/...`
|
||||||
|
Section names are URL-encoded. Legacy routes (`element`/`page` as section name) still work for backwards compatibility.
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
- `ts_web/wcctools.interfaces.ts` - New types: `IWccSection`, `IWccConfig`
|
||||||
|
- `ts_web/index.ts` - Updated `setupWccTools` with backwards compat detection
|
||||||
|
- `ts_web/elements/wcc-dashboard.ts` - Uses sections, updated routing
|
||||||
|
- `ts_web/elements/wcc-sidebar.ts` - Dynamic section rendering
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## UI Redesign with Shadcn-like Styles (2025-06-27)
|
## UI Redesign with Shadcn-like Styles (2025-06-27)
|
||||||
|
|
||||||
### Changes Made
|
### Changes Made
|
||||||
|
|||||||
229
readme.md
229
readme.md
@@ -6,12 +6,13 @@
|
|||||||
|
|
||||||
`@design.estate/dees-wcctools` provides a comprehensive development environment for web components, featuring:
|
`@design.estate/dees-wcctools` provides a comprehensive development environment for web components, featuring:
|
||||||
|
|
||||||
- 🎨 **Interactive Component Catalogue** — Live preview with sidebar navigation
|
- 🎨 **Interactive Component Catalogue** — Live preview with customizable sidebar sections
|
||||||
- 🔧 **Real-time Property Editing** — Modify component props on the fly with auto-detected editors
|
- 🔧 **Real-time Property Editing** — Modify component props on the fly with auto-detected editors
|
||||||
- 🌓 **Theme Switching** — Test light/dark modes instantly
|
- 🌓 **Theme Switching** — Test light/dark modes instantly
|
||||||
- 📱 **Responsive Viewport Testing** — Phone, phablet, tablet, and desktop views
|
- 📱 **Responsive Viewport Testing** — Phone, phablet, tablet, and desktop views
|
||||||
- 🎬 **Screen Recording** — Record component demos with audio support and video trimming
|
- 🎬 **Screen Recording** — Record component demos with audio support and video trimming
|
||||||
- 🧪 **Advanced Demo Tools** — Post-render hooks for interactive testing
|
- 🧪 **Advanced Demo Tools** — Post-render hooks for interactive testing
|
||||||
|
- 📂 **Section-based Organization** — Group components into custom sections with filtering and sorting
|
||||||
- 🚀 **Zero-config Setup** — TypeScript and Lit support out of the box
|
- 🚀 **Zero-config Setup** — TypeScript and Lit support out of the box
|
||||||
|
|
||||||
## Issue Reporting and Security
|
## Issue Reporting and Security
|
||||||
@@ -57,29 +58,22 @@ export class MyButton extends DeesElement {
|
|||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 14px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
}
|
||||||
button.primary {
|
button.primary {
|
||||||
background: #007bff;
|
background: #3b82f6;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
button.secondary {
|
button.secondary {
|
||||||
background: #6c757d;
|
background: #6b7280;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
button:hover {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
`
|
`
|
||||||
];
|
];
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return html`
|
return html`
|
||||||
<button class="${this.variant}">
|
<button class="${this.variant}">${this.label}</button>
|
||||||
${this.label}
|
|
||||||
</button>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,33 +87,32 @@ import { setupWccTools } from '@design.estate/dees-wcctools';
|
|||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
|
|
||||||
// Import your components
|
// Import your components
|
||||||
import { MyButton } from './components/my-button.js';
|
import * as elements from './components/index.js';
|
||||||
import { MyCard } from './components/my-card.js';
|
import * as views from './views/index.js';
|
||||||
|
import * as pages from './pages/index.js';
|
||||||
|
|
||||||
// Define elements for the catalogue
|
// Initialize with sections-based configuration
|
||||||
const elements = {
|
setupWccTools({
|
||||||
'my-button': MyButton,
|
sections: [
|
||||||
'my-card': MyCard,
|
{
|
||||||
};
|
name: 'Pages',
|
||||||
|
type: 'pages',
|
||||||
// Optionally define pages
|
items: pages,
|
||||||
const pages = {
|
},
|
||||||
'home': () => html`
|
{
|
||||||
<div style="padding: 20px;">
|
name: 'Views',
|
||||||
<h1>Welcome to My Component Library</h1>
|
type: 'elements',
|
||||||
<p>Browse components using the sidebar.</p>
|
items: views,
|
||||||
</div>
|
icon: 'web',
|
||||||
`,
|
},
|
||||||
'getting-started': () => html`
|
{
|
||||||
<div style="padding: 20px;">
|
name: 'Elements',
|
||||||
<h2>Getting Started</h2>
|
type: 'elements',
|
||||||
<p>Installation and usage instructions...</p>
|
items: elements,
|
||||||
</div>
|
sort: ([a], [b]) => a.localeCompare(b),
|
||||||
`,
|
},
|
||||||
};
|
],
|
||||||
|
});
|
||||||
// Initialize the catalogue
|
|
||||||
setupWccTools(elements, pages);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Create an HTML Entry Point
|
### 3. Create an HTML Entry Point
|
||||||
@@ -137,6 +130,69 @@ setupWccTools(elements, pages);
|
|||||||
</html>
|
</html>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 📂 Sections Configuration
|
||||||
|
|
||||||
|
The sections-based API gives you full control over how components are organized in the sidebar.
|
||||||
|
|
||||||
|
### Section Properties
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
|----------|------|-------------|
|
||||||
|
| `name` | `string` | Display name for the section header |
|
||||||
|
| `type` | `'elements' \| 'pages'` | How items render (`elements` show demos, `pages` render directly) |
|
||||||
|
| `items` | `Record<string, any>` | Object containing element classes or page factories |
|
||||||
|
| `filter` | `(name, item) => boolean` | Optional filter function to include/exclude items |
|
||||||
|
| `sort` | `([a, itemA], [b, itemB]) => number` | Optional sort function for ordering items |
|
||||||
|
| `icon` | `string` | Optional Material Symbols icon name |
|
||||||
|
| `collapsed` | `boolean` | Start section collapsed (default: `false`) |
|
||||||
|
|
||||||
|
### Advanced Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { setupWccTools } from '@design.estate/dees-wcctools';
|
||||||
|
import * as allElements from './elements/index.js';
|
||||||
|
import * as pages from './pages/index.js';
|
||||||
|
|
||||||
|
setupWccTools({
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
name: 'Pages',
|
||||||
|
type: 'pages',
|
||||||
|
items: pages,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Form Controls',
|
||||||
|
type: 'elements',
|
||||||
|
items: allElements,
|
||||||
|
icon: 'edit_note',
|
||||||
|
filter: (name) => name.startsWith('form-') || name.includes('input'),
|
||||||
|
sort: ([a], [b]) => a.localeCompare(b),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Layout',
|
||||||
|
type: 'elements',
|
||||||
|
items: allElements,
|
||||||
|
icon: 'dashboard',
|
||||||
|
filter: (name) => name.startsWith('layout-') || name.startsWith('grid-'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Legacy',
|
||||||
|
type: 'elements',
|
||||||
|
items: allElements,
|
||||||
|
filter: (name) => name.startsWith('legacy-'),
|
||||||
|
collapsed: true, // Start collapsed
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Legacy API (Still Supported)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// The old format still works for simple use cases
|
||||||
|
setupWccTools(elements, pages);
|
||||||
|
```
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### 🎯 Live Property Editing
|
### 🎯 Live Property Editing
|
||||||
@@ -162,19 +218,7 @@ Test your components across different screen sizes:
|
|||||||
|
|
||||||
### 🌓 Theme Support
|
### 🌓 Theme Support
|
||||||
|
|
||||||
Components automatically adapt to light/dark themes using the `goBright` property:
|
Components automatically adapt to light/dark themes. Use CSS custom properties with the theme manager:
|
||||||
|
|
||||||
```typescript
|
|
||||||
public render() {
|
|
||||||
return html`
|
|
||||||
<div class="${this.goBright ? 'light-theme' : 'dark-theme'}">
|
|
||||||
<!-- Your component content -->
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Or use CSS custom properties with the theme manager:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { cssManager } from '@design.estate/dees-element';
|
import { cssManager } from '@design.estate/dees-element';
|
||||||
@@ -182,8 +226,8 @@ import { cssManager } from '@design.estate/dees-element';
|
|||||||
public static styles = [
|
public static styles = [
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
color: ${cssManager.bdTheme('#1a1a1a', '#e5e5e5')};
|
||||||
background: ${cssManager.bdTheme('#fff', '#000')};
|
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
];
|
];
|
||||||
@@ -191,7 +235,7 @@ public static styles = [
|
|||||||
|
|
||||||
### 🎬 Screen Recording
|
### 🎬 Screen Recording
|
||||||
|
|
||||||
Record component demos directly from the catalogue! The built-in recorder supports:
|
Record component demos directly from the catalogue:
|
||||||
|
|
||||||
- **Viewport Recording** — Record just the component viewport
|
- **Viewport Recording** — Record just the component viewport
|
||||||
- **Full Screen Recording** — Capture the entire screen
|
- **Full Screen Recording** — Capture the entire screen
|
||||||
@@ -232,9 +276,26 @@ export class MyComponent extends DeesElement {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 🎭 Multiple Demos
|
||||||
|
|
||||||
|
Components can expose multiple demo variations:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@customElement('my-button')
|
||||||
|
export class MyButton extends DeesElement {
|
||||||
|
public static demo = [
|
||||||
|
() => html`<my-button variant="primary">Primary</my-button>`,
|
||||||
|
() => html`<my-button variant="secondary">Secondary</my-button>`,
|
||||||
|
() => html`<my-button variant="danger">Danger</my-button>`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each demo appears as a numbered item in an expandable folder in the sidebar.
|
||||||
|
|
||||||
### ⏳ Async Demos
|
### ⏳ Async Demos
|
||||||
|
|
||||||
Return a `Promise` from `demo` for async setup. The dashboard waits for resolution:
|
Return a `Promise` from `demo` for async setup:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
public static demo = async () => {
|
public static demo = async () => {
|
||||||
@@ -243,7 +304,7 @@ public static demo = async () => {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🎭 Container Queries
|
### 🎯 Container Queries
|
||||||
|
|
||||||
Components can respond to their container size using the `wccToolsViewport` container:
|
Components can respond to their container size using the `wccToolsViewport` container:
|
||||||
|
|
||||||
@@ -269,7 +330,7 @@ public static styles = [
|
|||||||
|
|
||||||
### Required for Catalogue Display
|
### Required for Catalogue Display
|
||||||
|
|
||||||
1. Components must expose a static `demo` property returning a Lit template
|
1. Components must expose a static `demo` property returning a Lit template (or array of templates)
|
||||||
2. Use `@property()` decorators with the `accessor` keyword for editable properties
|
2. Use `@property()` decorators with the `accessor` keyword for editable properties
|
||||||
3. Export component classes for proper detection
|
3. Export component classes for proper detection
|
||||||
|
|
||||||
@@ -278,7 +339,7 @@ public static styles = [
|
|||||||
```typescript
|
```typescript
|
||||||
@customElement('best-practice-component')
|
@customElement('best-practice-component')
|
||||||
export class BestPracticeComponent extends DeesElement {
|
export class BestPracticeComponent extends DeesElement {
|
||||||
// ✅ Static demo property
|
// ✅ Static demo property (single or array)
|
||||||
public static demo = () => html`
|
public static demo = () => html`
|
||||||
<best-practice-component
|
<best-practice-component
|
||||||
.complexProp=${{ key: 'value' }}
|
.complexProp=${{ key: 'value' }}
|
||||||
@@ -305,23 +366,40 @@ export class BestPracticeComponent extends DeesElement {
|
|||||||
The catalogue uses URL routing for deep linking:
|
The catalogue uses URL routing for deep linking:
|
||||||
|
|
||||||
```
|
```
|
||||||
/wcctools-route/:type/:name/:viewport/:theme
|
/wcctools-route/:sectionName/:itemName/:demoIndex/:viewport/:theme
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
/wcctools-route/element/my-button/desktop/dark
|
/wcctools-route/Elements/my-button/0/desktop/dark
|
||||||
/wcctools-route/page/home/tablet/bright
|
/wcctools-route/Views/view-dashboard/0/tablet/bright
|
||||||
|
/wcctools-route/Pages/home/0/desktop/dark
|
||||||
```
|
```
|
||||||
|
|
||||||
## API Reference
|
## API Reference
|
||||||
|
|
||||||
### `setupWccTools(elements, pages?)`
|
### `setupWccTools(config)`
|
||||||
|
|
||||||
Initialize the WCC Tools dashboard.
|
Initialize the WCC Tools dashboard with sections configuration.
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
```typescript
|
||||||
|-----------|------|-------------|
|
interface IWccSection {
|
||||||
| `elements` | `Record<string, typeof LitElement>` | Map of element names to classes |
|
name: string;
|
||||||
| `pages` | `Record<string, TTemplateFactory>` | Optional map of page names to template functions |
|
type: 'elements' | 'pages';
|
||||||
|
items: Record<string, any>;
|
||||||
|
filter?: (name: string, item: any) => boolean;
|
||||||
|
sort?: (a: [string, any], b: [string, any]) => number;
|
||||||
|
icon?: string;
|
||||||
|
collapsed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IWccConfig {
|
||||||
|
sections: IWccSection[];
|
||||||
|
}
|
||||||
|
|
||||||
|
setupWccTools(config: IWccConfig): void;
|
||||||
|
|
||||||
|
// Legacy (still supported)
|
||||||
|
setupWccTools(elements: Record<string, any>, pages?: Record<string, TTemplateFactory>): void;
|
||||||
|
```
|
||||||
|
|
||||||
### `DeesDemoWrapper`
|
### `DeesDemoWrapper`
|
||||||
|
|
||||||
@@ -357,14 +435,21 @@ recorder.stopRecording();
|
|||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
my-components/
|
my-component-library/
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── components/
|
│ ├── elements/ # UI components
|
||||||
│ │ ├── my-button.ts
|
│ │ ├── my-button.ts
|
||||||
│ │ └── my-card.ts
|
│ │ ├── my-card.ts
|
||||||
│ └── catalogue.ts
|
│ │ └── index.ts
|
||||||
├── dist/
|
│ ├── views/ # Full-page layouts
|
||||||
├── index.html
|
│ │ ├── view-dashboard.ts
|
||||||
|
│ │ └── index.ts
|
||||||
|
│ ├── pages/ # Documentation pages
|
||||||
|
│ │ ├── home.ts
|
||||||
|
│ │ └── index.ts
|
||||||
|
│ └── catalogue.ts # WCC Tools setup
|
||||||
|
├── html/
|
||||||
|
│ └── index.html
|
||||||
└── package.json
|
└── package.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ enum ETestEnum {
|
|||||||
|
|
||||||
@customElement('test-demoelement')
|
@customElement('test-demoelement')
|
||||||
export class TestDemoelement extends DeesElement {
|
export class TestDemoelement extends DeesElement {
|
||||||
public static demo = () => html`<test-demoelement>This is a slot text</test-demoelement>`;
|
public static demo = [
|
||||||
|
() => html`<test-demoelement>This is demo 1</test-demoelement>`,
|
||||||
|
() => html`<test-demoelement>This is demo 2</test-demoelement>`,
|
||||||
|
() => html`<test-demoelement>This is demo 2</test-demoelement>`,
|
||||||
|
]
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
accessor notTyped = 'hello';
|
accessor notTyped = 'hello';
|
||||||
|
|||||||
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 = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-wcctools',
|
name: '@design.estate/dees-wcctools',
|
||||||
version: '1.3.0',
|
version: '3.4.0',
|
||||||
description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.'
|
description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { DeesElement, property, html, customElement, type TemplateResult, queryAsync, render, domtools } from '@design.estate/dees-element';
|
import { DeesElement, property, html, customElement, type TemplateResult, queryAsync, render, domtools } from '@design.estate/dees-element';
|
||||||
import { resolveTemplateFactory } from './wcctools.helpers.js';
|
import { resolveTemplateFactory, getDemoAtIndex, getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
|
||||||
import type { TTemplateFactory } from './wcctools.helpers.js';
|
import type { TTemplateFactory } from './wcctools.helpers.js';
|
||||||
|
import type { IWccConfig, IWccSection, TElementType } from '../wcctools.interfaces.js';
|
||||||
|
|
||||||
import * as plugins from '../wcctools.plugins.js';
|
import * as plugins from '../wcctools.plugins.js';
|
||||||
|
|
||||||
@@ -9,13 +10,37 @@ import './wcc-frame.js';
|
|||||||
import './wcc-sidebar.js';
|
import './wcc-sidebar.js';
|
||||||
import './wcc-properties.js';
|
import './wcc-properties.js';
|
||||||
import { type TTheme } from './wcc-properties.js';
|
import { type TTheme } from './wcc-properties.js';
|
||||||
import { type TElementType } from './wcc-sidebar.js';
|
|
||||||
import { breakpoints } from '@design.estate/dees-domtools';
|
import { breakpoints } from '@design.estate/dees-domtools';
|
||||||
import { WccFrame } from './wcc-frame.js';
|
import { WccFrame } from './wcc-frame.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get filtered and sorted items from a section
|
||||||
|
*/
|
||||||
|
export const getSectionItems = (section: IWccSection): Array<[string, any]> => {
|
||||||
|
let entries = Object.entries(section.items);
|
||||||
|
|
||||||
|
// Apply filter if provided
|
||||||
|
if (section.filter) {
|
||||||
|
entries = entries.filter(([name, item]) => section.filter(name, item));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply sort if provided
|
||||||
|
if (section.sort) {
|
||||||
|
entries.sort(section.sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
};
|
||||||
|
|
||||||
@customElement('wcc-dashboard')
|
@customElement('wcc-dashboard')
|
||||||
export class WccDashboard extends DeesElement {
|
export class WccDashboard extends DeesElement {
|
||||||
|
|
||||||
|
@property()
|
||||||
|
accessor sections: IWccSection[] = [];
|
||||||
|
|
||||||
|
@property()
|
||||||
|
accessor selectedSection: IWccSection | null = null;
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
accessor selectedType: TElementType;
|
accessor selectedType: TElementType;
|
||||||
|
|
||||||
@@ -25,6 +50,9 @@ export class WccDashboard extends DeesElement {
|
|||||||
@property()
|
@property()
|
||||||
accessor selectedItem: TTemplateFactory | DeesElement;
|
accessor selectedItem: TTemplateFactory | DeesElement;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor selectedDemoIndex: number = 0;
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
accessor selectedViewport: plugins.deesDomtools.breakpoints.TViewport = 'desktop';
|
accessor selectedViewport: plugins.deesDomtools.breakpoints.TViewport = 'desktop';
|
||||||
|
|
||||||
@@ -32,38 +60,50 @@ export class WccDashboard extends DeesElement {
|
|||||||
accessor selectedTheme: TTheme = 'dark';
|
accessor selectedTheme: TTheme = 'dark';
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
accessor isFullscreen: boolean = false;
|
accessor searchQuery: string = '';
|
||||||
|
|
||||||
@property()
|
// Derived from selectedViewport - no need for separate property
|
||||||
accessor pages: Record<string, TTemplateFactory> = {};
|
public get isNative(): boolean {
|
||||||
|
return this.selectedViewport === 'native';
|
||||||
@property()
|
}
|
||||||
accessor elements: { [key: string]: DeesElement } = {};
|
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
accessor warning: string = null;
|
accessor warning: string = null;
|
||||||
|
|
||||||
private frameScrollY: number = 0;
|
private frameScrollY: number = 0;
|
||||||
private sidebarScrollY: number = 0;
|
private sidebarScrollY: number = 0;
|
||||||
private scrollPositionsApplied: boolean = false;
|
private scrollPositionsApplied: boolean = false;
|
||||||
|
|
||||||
@queryAsync('wcc-frame')
|
@queryAsync('wcc-frame')
|
||||||
accessor wccFrame: Promise<WccFrame>;
|
accessor wccFrame: Promise<WccFrame>;
|
||||||
|
|
||||||
constructor(
|
constructor(config?: IWccConfig) {
|
||||||
elementsArg?: { [key: string]: DeesElement },
|
|
||||||
pagesArg?: Record<string, TTemplateFactory>
|
|
||||||
) {
|
|
||||||
super();
|
super();
|
||||||
if (elementsArg) {
|
if (config && config.sections) {
|
||||||
this.elements = elementsArg;
|
this.sections = config.sections;
|
||||||
console.log('got elements:');
|
console.log('got sections:', this.sections.map(s => s.name));
|
||||||
console.log(this.elements);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (pagesArg) {
|
/**
|
||||||
this.pages = pagesArg;
|
* Find an item by name across all sections, returns the item and its section
|
||||||
|
*/
|
||||||
|
public findItemByName(name: string): { item: any; section: IWccSection } | null {
|
||||||
|
for (const section of this.sections) {
|
||||||
|
const entries = getSectionItems(section);
|
||||||
|
const found = entries.find(([itemName]) => itemName === name);
|
||||||
|
if (found) {
|
||||||
|
return { item: found[1], section };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a section by name (URL-decoded)
|
||||||
|
*/
|
||||||
|
public findSectionByName(name: string): IWccSection | null {
|
||||||
|
return this.sections.find(s => s.name === name) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
@@ -81,7 +121,8 @@ export class WccDashboard extends DeesElement {
|
|||||||
<wcc-sidebar
|
<wcc-sidebar
|
||||||
.dashboardRef=${this}
|
.dashboardRef=${this}
|
||||||
.selectedItem=${this.selectedItem}
|
.selectedItem=${this.selectedItem}
|
||||||
.isFullscreen=${this.isFullscreen}
|
.searchQuery=${this.searchQuery}
|
||||||
|
.isNative=${this.isNative}
|
||||||
@selectedType=${(eventArg) => {
|
@selectedType=${(eventArg) => {
|
||||||
this.selectedType = eventArg.detail;
|
this.selectedType = eventArg.detail;
|
||||||
}}
|
}}
|
||||||
@@ -91,6 +132,10 @@ export class WccDashboard extends DeesElement {
|
|||||||
@selectedItem=${(eventArg) => {
|
@selectedItem=${(eventArg) => {
|
||||||
this.selectedItem = eventArg.detail;
|
this.selectedItem = eventArg.detail;
|
||||||
}}
|
}}
|
||||||
|
@searchChanged=${(eventArg: CustomEvent) => {
|
||||||
|
this.searchQuery = eventArg.detail;
|
||||||
|
this.updateUrlWithScrollState();
|
||||||
|
}}
|
||||||
></wcc-sidebar>
|
></wcc-sidebar>
|
||||||
<wcc-properties
|
<wcc-properties
|
||||||
.dashboardRef=${this}
|
.dashboardRef=${this}
|
||||||
@@ -98,7 +143,7 @@ export class WccDashboard extends DeesElement {
|
|||||||
.selectedItem=${this.selectedItem}
|
.selectedItem=${this.selectedItem}
|
||||||
.selectedViewport=${this.selectedViewport}
|
.selectedViewport=${this.selectedViewport}
|
||||||
.selectedTheme=${this.selectedTheme}
|
.selectedTheme=${this.selectedTheme}
|
||||||
.isFullscreen=${this.isFullscreen}
|
.isNative=${this.isNative}
|
||||||
@selectedViewport=${(eventArg) => {
|
@selectedViewport=${(eventArg) => {
|
||||||
this.selectedViewport = eventArg.detail;
|
this.selectedViewport = eventArg.detail;
|
||||||
this.scheduleUpdate();
|
this.scheduleUpdate();
|
||||||
@@ -113,11 +158,11 @@ export class WccDashboard extends DeesElement {
|
|||||||
frame.requestUpdate();
|
frame.requestUpdate();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@toggleFullscreen=${() => {
|
@toggleNative=${() => {
|
||||||
this.toggleFullscreen();
|
this.toggleNative();
|
||||||
}}
|
}}
|
||||||
></wcc-properties>
|
></wcc-properties>
|
||||||
<wcc-frame id="wccFrame" viewport=${this.selectedViewport} .isFullscreen=${this.isFullscreen}>
|
<wcc-frame id="wccFrame" viewport=${this.selectedViewport} .isNative=${this.isNative}>
|
||||||
</wcc-frame>
|
</wcc-frame>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -132,17 +177,20 @@ export class WccDashboard extends DeesElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public toggleFullscreen() {
|
public toggleNative() {
|
||||||
this.isFullscreen = !this.isFullscreen;
|
// Toggle between 'native' and 'desktop' viewports
|
||||||
|
this.selectedViewport = this.selectedViewport === 'native' ? 'desktop' : 'native';
|
||||||
|
this.buildUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async firstUpdated() {
|
public async firstUpdated() {
|
||||||
this.domtools = await plugins.deesDomtools.DomTools.setupDomTools();
|
this.domtools = await plugins.deesDomtools.DomTools.setupDomTools();
|
||||||
|
|
||||||
// Add ESC key handler for fullscreen mode
|
// Add ESC key handler for native mode
|
||||||
document.addEventListener('keydown', (event) => {
|
document.addEventListener('keydown', (event) => {
|
||||||
if (event.key === 'Escape' && this.isFullscreen) {
|
if (event.key === 'Escape' && this.isNative) {
|
||||||
this.isFullscreen = false;
|
this.selectedViewport = 'desktop';
|
||||||
|
this.buildUrl();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -151,37 +199,129 @@ export class WccDashboard extends DeesElement {
|
|||||||
this.setupScrollListeners();
|
this.setupScrollListeners();
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
|
// New route format with section name
|
||||||
this.domtools.router.on(
|
this.domtools.router.on(
|
||||||
'/wcctools-route/:itemType/:itemName/:viewport/:theme',
|
'/wcctools-route/:sectionName/:itemName/:demoIndex/:viewport/:theme',
|
||||||
async (routeInfo) => {
|
async (routeInfo) => {
|
||||||
this.selectedType = routeInfo.params.itemType as TElementType;
|
const sectionName = decodeURIComponent(routeInfo.params.sectionName);
|
||||||
|
this.selectedSection = this.findSectionByName(sectionName);
|
||||||
this.selectedItemName = routeInfo.params.itemName;
|
this.selectedItemName = routeInfo.params.itemName;
|
||||||
|
this.selectedDemoIndex = parseInt(routeInfo.params.demoIndex) || 0;
|
||||||
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
|
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
|
||||||
this.selectedTheme = routeInfo.params.theme as TTheme;
|
this.selectedTheme = routeInfo.params.theme as TTheme;
|
||||||
if (routeInfo.params.itemType === 'element') {
|
|
||||||
this.selectedItem = this.elements[routeInfo.params.itemName];
|
if (this.selectedSection) {
|
||||||
} else if (routeInfo.params.itemType === 'page') {
|
// Find item within the section
|
||||||
this.selectedItem = this.pages[routeInfo.params.itemName];
|
const entries = getSectionItems(this.selectedSection);
|
||||||
|
const found = entries.find(([name]) => name === routeInfo.params.itemName);
|
||||||
|
if (found) {
|
||||||
|
this.selectedItem = found[1];
|
||||||
|
this.selectedType = this.selectedSection.type === 'elements' ? 'element' : 'page';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: try legacy format (element/page as section name)
|
||||||
|
const legacyType = routeInfo.params.sectionName;
|
||||||
|
if (legacyType === 'element' || legacyType === 'page') {
|
||||||
|
this.selectedType = legacyType as TElementType;
|
||||||
|
// Find item in any matching section
|
||||||
|
const result = this.findItemByName(routeInfo.params.itemName);
|
||||||
|
if (result) {
|
||||||
|
this.selectedItem = result.item;
|
||||||
|
this.selectedSection = result.section;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore scroll positions from query parameters
|
// Restore state from query parameters
|
||||||
if (routeInfo.queryParams) {
|
if (routeInfo.queryParams) {
|
||||||
|
const search = routeInfo.queryParams.search;
|
||||||
const frameScrollY = routeInfo.queryParams.frameScrollY;
|
const frameScrollY = routeInfo.queryParams.frameScrollY;
|
||||||
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
|
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
this.searchQuery = search;
|
||||||
|
} else {
|
||||||
|
this.searchQuery = '';
|
||||||
|
}
|
||||||
if (frameScrollY) {
|
if (frameScrollY) {
|
||||||
this.frameScrollY = parseInt(frameScrollY);
|
this.frameScrollY = parseInt(frameScrollY);
|
||||||
}
|
}
|
||||||
if (sidebarScrollY) {
|
if (sidebarScrollY) {
|
||||||
this.sidebarScrollY = parseInt(sidebarScrollY);
|
this.sidebarScrollY = parseInt(sidebarScrollY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply scroll positions after a short delay to ensure DOM is ready
|
// Apply scroll positions after a short delay to ensure DOM is ready
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.applyScrollPositions();
|
this.applyScrollPositions();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
} else {
|
||||||
|
this.searchQuery = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
|
||||||
|
this.selectedTheme === 'bright'
|
||||||
|
? domtoolsInstance.themeManager.goBright()
|
||||||
|
: domtoolsInstance.themeManager.goDark();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Legacy route without demo index (for backwards compatibility)
|
||||||
|
this.domtools.router.on(
|
||||||
|
'/wcctools-route/:sectionName/:itemName/:viewport/:theme',
|
||||||
|
async (routeInfo) => {
|
||||||
|
const sectionName = decodeURIComponent(routeInfo.params.sectionName);
|
||||||
|
this.selectedSection = this.findSectionByName(sectionName);
|
||||||
|
this.selectedItemName = routeInfo.params.itemName;
|
||||||
|
this.selectedDemoIndex = 0;
|
||||||
|
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
|
||||||
|
this.selectedTheme = routeInfo.params.theme as TTheme;
|
||||||
|
|
||||||
|
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 state from query parameters
|
||||||
|
if (routeInfo.queryParams) {
|
||||||
|
const search = routeInfo.queryParams.search;
|
||||||
|
const frameScrollY = routeInfo.queryParams.frameScrollY;
|
||||||
|
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
this.searchQuery = search;
|
||||||
|
} else {
|
||||||
|
this.searchQuery = '';
|
||||||
|
}
|
||||||
|
if (frameScrollY) {
|
||||||
|
this.frameScrollY = parseInt(frameScrollY);
|
||||||
|
}
|
||||||
|
if (sidebarScrollY) {
|
||||||
|
this.sidebarScrollY = parseInt(sidebarScrollY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply scroll positions after a short delay to ensure DOM is ready
|
||||||
|
setTimeout(() => {
|
||||||
|
this.applyScrollPositions();
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
this.searchQuery = '';
|
||||||
|
}
|
||||||
|
|
||||||
const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
|
const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
|
||||||
this.selectedTheme === 'bright'
|
this.selectedTheme === 'bright'
|
||||||
? domtoolsInstance.themeManager.goBright()
|
? domtoolsInstance.themeManager.goBright()
|
||||||
@@ -218,33 +358,54 @@ export class WccDashboard extends DeesElement {
|
|||||||
this.setWarning(`component ${anonItem.name} does not expose a demo property.`);
|
this.setWarning(`component ${anonItem.name} does not expose a demo property.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!(typeof anonItem.demo === 'function')) {
|
|
||||||
|
// Support both single demo (function) and multiple demos (array)
|
||||||
|
const isArray = Array.isArray(anonItem.demo);
|
||||||
|
const isFunction = typeof anonItem.demo === 'function';
|
||||||
|
|
||||||
|
if (!isArray && !isFunction) {
|
||||||
this.setWarning(
|
this.setWarning(
|
||||||
`component ${anonItem.name} has demo property, but it is not of type function`
|
`component ${anonItem.name} has demo property, but it is not a function or array of functions`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the specific demo to render
|
||||||
|
const demoFactory = getDemoAtIndex(anonItem.demo, this.selectedDemoIndex);
|
||||||
|
if (!demoFactory) {
|
||||||
|
this.setWarning(
|
||||||
|
`component ${anonItem.name} does not have a demo at index ${this.selectedDemoIndex + 1}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.setWarning(null);
|
this.setWarning(null);
|
||||||
const viewport = await wccFrame.getViewportElement();
|
const viewport = await wccFrame.getViewportElement();
|
||||||
const demoTemplate = await resolveTemplateFactory(() => anonItem.demo());
|
const demoTemplate = await resolveTemplateFactory(demoFactory);
|
||||||
render(demoTemplate, viewport);
|
render(demoTemplate, viewport);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public buildUrl() {
|
public buildUrl() {
|
||||||
const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedViewport}/${this.selectedTheme}`;
|
const sectionName = this.selectedSection
|
||||||
|
? encodeURIComponent(this.selectedSection.name)
|
||||||
|
: this.selectedType; // Fallback for legacy
|
||||||
|
const baseUrl = `/wcctools-route/${sectionName}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
|
||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
|
|
||||||
|
if (this.searchQuery) {
|
||||||
|
queryParams.set('search', this.searchQuery);
|
||||||
|
}
|
||||||
if (this.frameScrollY > 0) {
|
if (this.frameScrollY > 0) {
|
||||||
queryParams.set('frameScrollY', this.frameScrollY.toString());
|
queryParams.set('frameScrollY', this.frameScrollY.toString());
|
||||||
}
|
}
|
||||||
if (this.sidebarScrollY > 0) {
|
if (this.sidebarScrollY > 0) {
|
||||||
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
|
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryString = queryParams.toString();
|
const queryString = queryParams.toString();
|
||||||
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||||||
|
|
||||||
this.domtools.router.pushUrl(fullUrl);
|
this.domtools.router.pushUrl(fullUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,19 +447,25 @@ export class WccDashboard extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateUrlWithScrollState() {
|
private updateUrlWithScrollState() {
|
||||||
const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedViewport}/${this.selectedTheme}`;
|
const sectionName = this.selectedSection
|
||||||
|
? encodeURIComponent(this.selectedSection.name)
|
||||||
|
: this.selectedType; // Fallback for legacy
|
||||||
|
const baseUrl = `/wcctools-route/${sectionName}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
|
||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
|
|
||||||
|
if (this.searchQuery) {
|
||||||
|
queryParams.set('search', this.searchQuery);
|
||||||
|
}
|
||||||
if (this.frameScrollY > 0) {
|
if (this.frameScrollY > 0) {
|
||||||
queryParams.set('frameScrollY', this.frameScrollY.toString());
|
queryParams.set('frameScrollY', this.frameScrollY.toString());
|
||||||
}
|
}
|
||||||
if (this.sidebarScrollY > 0) {
|
if (this.sidebarScrollY > 0) {
|
||||||
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
|
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryString = queryParams.toString();
|
const queryString = queryParams.toString();
|
||||||
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||||||
|
|
||||||
// Use replaceState to update URL without navigation
|
// Use replaceState to update URL without navigation
|
||||||
window.history.replaceState(null, '', fullUrl);
|
window.history.replaceState(null, '', fullUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export class WccFrame extends DeesElement {
|
|||||||
accessor advancedEditorOpen: boolean = false;
|
accessor advancedEditorOpen: boolean = false;
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
accessor isFullscreen: boolean = false;
|
accessor isNative: boolean = false;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
css`
|
css`
|
||||||
@@ -46,7 +46,7 @@ export class WccFrame extends DeesElement {
|
|||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
:host {
|
:host {
|
||||||
${this.isFullscreen ? `
|
${this.isNative ? `
|
||||||
border: none !important;
|
border: none !important;
|
||||||
left: 0px !important;
|
left: 0px !important;
|
||||||
right: 0px !important;
|
right: 0px !important;
|
||||||
@@ -58,7 +58,7 @@ export class WccFrame extends DeesElement {
|
|||||||
left: 200px;
|
left: 200px;
|
||||||
`}
|
`}
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
${this.isFullscreen ? 'padding: 0px;' : (() => {
|
${this.isNative ? 'padding: 0px;' : (() => {
|
||||||
switch (this.viewport) {
|
switch (this.viewport) {
|
||||||
case 'desktop':
|
case 'desktop':
|
||||||
return `
|
return `
|
||||||
@@ -87,7 +87,7 @@ export class WccFrame extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.viewport {
|
.viewport {
|
||||||
${!this.isFullscreen && this.viewport !== 'desktop'
|
${!this.isNative && this.viewport !== 'desktop'
|
||||||
? html` border-right: 1px dotted #444; border-left: 1px dotted #444; `
|
? html` border-right: 1px dotted #444; border-left: 1px dotted #444; `
|
||||||
: html``
|
: html``
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export class WccProperties extends DeesElement {
|
|||||||
accessor warning: string = null;
|
accessor warning: string = null;
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
accessor isFullscreen: boolean = false;
|
accessor isNative: boolean = false;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor propertyContent: TemplateResult[] = [];
|
accessor propertyContent: TemplateResult[] = [];
|
||||||
@@ -96,11 +96,11 @@ export class WccProperties extends DeesElement {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
display: ${this.isFullscreen ? 'none' : 'block'};
|
display: ${this.isNative ? 'none' : 'block'};
|
||||||
}
|
}
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 150px 300px 70px 70px;
|
grid-template-columns: 1fr 150px 350px 100px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.properties {
|
.properties {
|
||||||
@@ -197,7 +197,8 @@ export class WccProperties extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.viewportSelector,
|
.viewportSelector,
|
||||||
.themeSelector {
|
.themeSelector,
|
||||||
|
.shareSelector {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -206,12 +207,22 @@ export class WccProperties extends DeesElement {
|
|||||||
}
|
}
|
||||||
.selectorButtons2 {
|
.selectorButtons2 {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.selectorButtons4 {
|
.selectorButtons4 {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.selectorButtons5 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.selectorButtons1 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.button {
|
.button {
|
||||||
@@ -248,6 +259,11 @@ export class WccProperties extends DeesElement {
|
|||||||
.button .material-symbols-outlined {
|
.button .material-symbols-outlined {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-variation-settings: 'FILL' 0, 'wght' 300;
|
font-variation-settings: 'FILL' 0, 'wght' 300;
|
||||||
|
min-width: 18px;
|
||||||
|
min-height: 18px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.selected .material-symbols-outlined {
|
.button.selected .material-symbols-outlined {
|
||||||
@@ -290,7 +306,7 @@ export class WccProperties extends DeesElement {
|
|||||||
top: 0.5rem;
|
top: 0.5rem;
|
||||||
bottom: 0.5rem;
|
bottom: 0.5rem;
|
||||||
left: 0.5rem;
|
left: 0.5rem;
|
||||||
right: calc(520px + 0.5rem);
|
right: calc(600px + 0.5rem);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -623,7 +639,7 @@ export class WccProperties extends DeesElement {
|
|||||||
</div>
|
</div>
|
||||||
<div class="viewportSelector">
|
<div class="viewportSelector">
|
||||||
<div class="panelheading">Viewport</div>
|
<div class="panelheading">Viewport</div>
|
||||||
<div class="selectorButtons4">
|
<div class="selectorButtons5">
|
||||||
<div
|
<div
|
||||||
class="button ${this.selectedViewport === 'phone' ? 'selected' : null}"
|
class="button ${this.selectedViewport === 'phone' ? 'selected' : null}"
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
@@ -649,29 +665,34 @@ export class WccProperties extends DeesElement {
|
|||||||
Tablet<i class="material-symbols-outlined">tablet</i>
|
Tablet<i class="material-symbols-outlined">tablet</i>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="button ${this.selectedViewport === 'desktop' ||
|
class="button ${this.selectedViewport === 'desktop' ? 'selected' : null}"
|
||||||
this.selectedViewport === 'native'
|
|
||||||
? 'selected'
|
|
||||||
: null}"
|
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
this.selectViewport('native');
|
this.selectViewport('desktop');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Desktop<i class="material-symbols-outlined">desktop_windows</i>
|
Desktop<i class="material-symbols-outlined">desktop_windows</i>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="button ${this.selectedViewport === 'native' ? 'selected' : null}"
|
||||||
|
@click=${() => {
|
||||||
|
this.selectViewport('native');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Native<i class="material-symbols-outlined">fullscreen</i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="docs" @click=${() => this.toggleFullscreen()}>
|
<div class="shareSelector">
|
||||||
<i class="material-symbols-outlined" style="font-size: 20px;">
|
<div class="panelheading">Share</div>
|
||||||
${this.isFullscreen ? 'fullscreen_exit' : 'fullscreen'}
|
<div class="selectorButtons1">
|
||||||
</i>
|
<div
|
||||||
|
class="button ${this.isRecording ? 'selected' : ''}"
|
||||||
|
@click=${() => this.handleRecordButtonClick()}
|
||||||
|
>
|
||||||
|
Record<i class="material-symbols-outlined">${this.isRecording ? 'stop_circle' : 'videocam'}</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Recording Button -->
|
|
||||||
<wcc-record-button
|
|
||||||
.state=${this.isRecording ? 'recording' : 'idle'}
|
|
||||||
.duration=${this.recordingDuration}
|
|
||||||
@record-click=${() => this.handleRecordButtonClick()}
|
|
||||||
></wcc-record-button>
|
|
||||||
</div>
|
</div>
|
||||||
${this.warning ? html`<div class="warning">${this.warning}</div>` : null}
|
${this.warning ? html`<div class="warning">${this.warning}</div>` : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -904,16 +925,16 @@ export class WccProperties extends DeesElement {
|
|||||||
this.dashboardRef.buildUrl();
|
this.dashboardRef.buildUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async scheduleUpdate() {
|
protected updated(changedProperties: Map<string, unknown>) {
|
||||||
try {
|
super.updated(changedProperties);
|
||||||
await this.createProperties();
|
|
||||||
} catch (error) {
|
// Only recreate properties when selectedItem changes
|
||||||
console.error('Error creating properties:', error);
|
if (changedProperties.has('selectedItem')) {
|
||||||
// Clear property content on error to show clean state
|
this.createProperties().catch(error => {
|
||||||
this.propertyContent = [];
|
console.error('Error creating properties:', error);
|
||||||
|
this.propertyContent = [];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// Always call super.scheduleUpdate to ensure component updates
|
|
||||||
super.scheduleUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public selectViewport(viewport: TEnvironment) {
|
public selectViewport(viewport: TEnvironment) {
|
||||||
@@ -1016,9 +1037,9 @@ export class WccProperties extends DeesElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private toggleFullscreen() {
|
private toggleNative() {
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('toggleFullscreen', {
|
new CustomEvent('toggleNative', {
|
||||||
bubbles: true
|
bubbles: true
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -552,6 +552,7 @@ export class WccRecordingPanel extends DeesElement {
|
|||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
`
|
`
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -706,6 +707,7 @@ export class WccRecordingPanel extends DeesElement {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="preview-modal-actions">
|
<div class="preview-modal-actions">
|
||||||
<button class="preview-btn secondary" @click=${() => this.discardRecording()}>Discard</button>
|
<button class="preview-btn secondary" @click=${() => this.discardRecording()}>Discard</button>
|
||||||
@@ -714,7 +716,7 @@ export class WccRecordingPanel extends DeesElement {
|
|||||||
?disabled=${this.isExporting}
|
?disabled=${this.isExporting}
|
||||||
@click=${() => this.downloadRecording()}
|
@click=${() => this.downloadRecording()}
|
||||||
>
|
>
|
||||||
${this.isExporting ? html`<span class="export-spinner"></span>Exporting...` : 'Download'}
|
${this.isExporting ? html`<span class="export-spinner"></span>Exporting...` : 'Download WebM'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -815,6 +817,7 @@ export class WccRecordingPanel extends DeesElement {
|
|||||||
try {
|
try {
|
||||||
let blobToDownload: Blob;
|
let blobToDownload: Blob;
|
||||||
|
|
||||||
|
// Handle trimming if needed
|
||||||
const needsTrim = this.trimStart > 0.1 || this.trimEnd < this.videoDuration - 0.1;
|
const needsTrim = this.trimStart > 0.1 || this.trimEnd < this.videoDuration - 0.1;
|
||||||
|
|
||||||
if (needsTrim) {
|
if (needsTrim) {
|
||||||
@@ -828,6 +831,7 @@ export class WccRecordingPanel extends DeesElement {
|
|||||||
blobToDownload = recordedBlob;
|
blobToDownload = recordedBlob;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger download
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||||||
const filename = `wcctools-recording-${timestamp}.webm`;
|
const filename = `wcctools-recording-${timestamp}.webm`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import * as plugins from '../wcctools.plugins.js';
|
import * as plugins from '../wcctools.plugins.js';
|
||||||
import { DeesElement, property, html, customElement, type TemplateResult } from '@design.estate/dees-element';
|
import { DeesElement, property, html, customElement, type TemplateResult, state } from '@design.estate/dees-element';
|
||||||
import { WccDashboard } from './wcc-dashboard.js';
|
import { WccDashboard, getSectionItems } from './wcc-dashboard.js';
|
||||||
import type { TTemplateFactory } from './wcctools.helpers.js';
|
import type { TTemplateFactory } from './wcctools.helpers.js';
|
||||||
|
import { getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
|
||||||
export type TElementType = 'element' | 'page';
|
import type { IWccSection, TElementType } from '../wcctools.interfaces.js';
|
||||||
|
|
||||||
@customElement('wcc-sidebar')
|
@customElement('wcc-sidebar')
|
||||||
export class WccSidebar extends DeesElement {
|
export class WccSidebar extends DeesElement {
|
||||||
@@ -17,7 +17,21 @@ export class WccSidebar extends DeesElement {
|
|||||||
accessor dashboardRef: WccDashboard;
|
accessor dashboardRef: WccDashboard;
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
accessor isFullscreen: boolean = false;
|
accessor isNative: boolean = false;
|
||||||
|
|
||||||
|
// Track which elements are expanded (for multi-demo elements)
|
||||||
|
@state()
|
||||||
|
accessor expandedElements: Set<string> = new Set();
|
||||||
|
|
||||||
|
// Track which sections are collapsed
|
||||||
|
@state()
|
||||||
|
accessor collapsedSections: Set<string> = new Set();
|
||||||
|
|
||||||
|
// Search query for filtering sidebar items
|
||||||
|
@property()
|
||||||
|
accessor searchQuery: string = '';
|
||||||
|
|
||||||
|
private sectionsInitialized = false;
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
@@ -40,7 +54,7 @@ export class WccSidebar extends DeesElement {
|
|||||||
--ring: #3b82f6;
|
--ring: #3b82f6;
|
||||||
--radius: 4px;
|
--radius: 4px;
|
||||||
|
|
||||||
display: ${this.isFullscreen ? 'none' : 'block'};
|
display: ${this.isNative ? 'none' : 'block'};
|
||||||
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -60,7 +74,7 @@ export class WccSidebar extends DeesElement {
|
|||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
.section-header {
|
||||||
padding: 0.3rem 0.75rem;
|
padding: 0.3rem 0.75rem;
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -72,12 +86,45 @@ export class WccSidebar extends DeesElement {
|
|||||||
background: rgba(59, 130, 246, 0.03);
|
background: rgba(59, 130, 246, 0.03);
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: all 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3:first-child {
|
.section-header:first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-header:hover {
|
||||||
|
background: rgba(59, 130, 246, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header .expand-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header.collapsed .expand-icon {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header .section-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content.collapsed {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.material-symbols-outlined {
|
.material-symbols-outlined {
|
||||||
font-family: 'Material Symbols Outlined';
|
font-family: 'Material Symbols Outlined';
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
@@ -110,7 +157,21 @@ export class WccSidebar extends DeesElement {
|
|||||||
color: #999;
|
color: #999;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selectOption.folder {
|
||||||
|
grid-template-columns: 16px 20px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectOption .expand-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectOption.expanded .expand-icon {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
.selectOption:hover {
|
.selectOption:hover {
|
||||||
background: rgba(59, 130, 246, 0.05);
|
background: rgba(59, 130, 246, 0.05);
|
||||||
color: #bbb;
|
color: #bbb;
|
||||||
@@ -143,6 +204,42 @@ export class WccSidebar extends DeesElement {
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.demo-children {
|
||||||
|
margin-left: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-child {
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
margin: 0.125rem 0.5rem;
|
||||||
|
padding: 0.35rem 0.75rem;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 16px 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #777;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-child:hover {
|
||||||
|
background: rgba(59, 130, 246, 0.05);
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-child.selected {
|
||||||
|
background: rgba(59, 130, 246, 0.15);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-child .material-symbols-outlined {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
@@ -159,56 +256,280 @@ export class WccSidebar extends DeesElement {
|
|||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<div class="search-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="search-input"
|
||||||
|
placeholder="Search..."
|
||||||
|
.value=${this.searchQuery}
|
||||||
|
@input=${this.handleSearchInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<h3>Pages</h3>
|
${this.renderSections()}
|
||||||
${(() => {
|
|
||||||
const pages = Object.keys(this.dashboardRef.pages);
|
|
||||||
return pages.map(pageName => {
|
|
||||||
const item = this.dashboardRef.pages[pageName];
|
|
||||||
return html`
|
|
||||||
<div
|
|
||||||
class="selectOption ${this.selectedItem === item ? 'selected' : null}"
|
|
||||||
@click=${async () => {
|
|
||||||
const domtools = await plugins.deesDomtools.DomTools.setupDomTools();
|
|
||||||
this.selectItem('page', pageName, item);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i class="material-symbols-outlined">insert_drive_file</i>
|
|
||||||
<div class="text">${pageName}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
})()}
|
|
||||||
<h3>Elements</h3>
|
|
||||||
${(() => {
|
|
||||||
const elements = Object.keys(this.dashboardRef.elements);
|
|
||||||
return elements.map(elementName => {
|
|
||||||
const item = this.dashboardRef.elements[elementName];
|
|
||||||
return html`
|
|
||||||
<div
|
|
||||||
class="selectOption ${this.selectedItem === item ? 'selected' : null}"
|
|
||||||
@click=${async () => {
|
|
||||||
const domtools = await plugins.deesDomtools.DomTools.setupDomTools();
|
|
||||||
this.selectItem('element', elementName, item);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i class="material-symbols-outlined">featured_video</i>
|
|
||||||
<div class="text">${elementName}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public selectItem(typeArg: TElementType, itemNameArg: string, itemArg: TTemplateFactory | DeesElement) {
|
/**
|
||||||
|
* Initialize collapsed sections from section config
|
||||||
|
*/
|
||||||
|
private initCollapsedSections() {
|
||||||
|
if (this.sectionsInitialized) return;
|
||||||
|
|
||||||
|
const collapsed = new Set<string>();
|
||||||
|
for (const section of this.dashboardRef.sections) {
|
||||||
|
if (section.collapsed) {
|
||||||
|
collapsed.add(section.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.collapsedSections = collapsed;
|
||||||
|
this.sectionsInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render all sections
|
||||||
|
*/
|
||||||
|
private renderSections() {
|
||||||
|
this.initCollapsedSections();
|
||||||
|
|
||||||
|
return this.dashboardRef.sections.map((section, index) => {
|
||||||
|
// Check if section has any matching items
|
||||||
|
const entries = getSectionItems(section);
|
||||||
|
const filteredEntries = entries.filter(([name]) => this.matchesSearch(name));
|
||||||
|
|
||||||
|
// Hide section if no items match the search
|
||||||
|
if (filteredEntries.length === 0 && this.searchQuery) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCollapsed = this.collapsedSections.has(section.name);
|
||||||
|
const sectionIcon = section.icon || (section.type === 'pages' ? 'insert_drive_file' : 'widgets');
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="section-header ${isCollapsed ? 'collapsed' : ''}"
|
||||||
|
@click=${() => this.toggleSectionCollapsed(section.name)}
|
||||||
|
>
|
||||||
|
<i class="material-symbols-outlined expand-icon">expand_more</i>
|
||||||
|
${section.icon ? html`<i class="material-symbols-outlined section-icon">${section.icon}</i>` : null}
|
||||||
|
<span>${section.name}</span>
|
||||||
|
</div>
|
||||||
|
<div class="section-content ${isCollapsed ? 'collapsed' : ''}">
|
||||||
|
${this.renderSectionItems(section)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render items for a section
|
||||||
|
*/
|
||||||
|
private renderSectionItems(section: IWccSection) {
|
||||||
|
const entries = getSectionItems(section);
|
||||||
|
// Filter entries by search query
|
||||||
|
const filteredEntries = entries.filter(([name]) => this.matchesSearch(name));
|
||||||
|
|
||||||
|
if (section.type === 'pages') {
|
||||||
|
return filteredEntries.map(([pageName, item]) => {
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="selectOption ${this.selectedItem === item ? 'selected' : ''}"
|
||||||
|
@click=${async () => {
|
||||||
|
await plugins.deesDomtools.DomTools.setupDomTools();
|
||||||
|
this.selectItem('page', pageName, item, 0, section);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i class="material-symbols-outlined">insert_drive_file</i>
|
||||||
|
<div class="text">${this.highlightMatch(pageName)}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// type === 'elements'
|
||||||
|
return filteredEntries.map(([elementName, item]) => {
|
||||||
|
const anonItem = item as any;
|
||||||
|
const demoCount = anonItem.demo ? getDemoCount(anonItem.demo) : 0;
|
||||||
|
const isMultiDemo = anonItem.demo && hasMultipleDemos(anonItem.demo);
|
||||||
|
const isExpanded = this.expandedElements.has(elementName);
|
||||||
|
const isSelected = this.selectedItem === item;
|
||||||
|
|
||||||
|
if (isMultiDemo) {
|
||||||
|
// Multi-demo element - render as expandable folder
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="selectOption folder ${isExpanded ? 'expanded' : ''} ${isSelected ? 'selected' : ''}"
|
||||||
|
@click=${() => this.toggleExpanded(elementName)}
|
||||||
|
>
|
||||||
|
<i class="material-symbols-outlined expand-icon">chevron_right</i>
|
||||||
|
<i class="material-symbols-outlined">folder</i>
|
||||||
|
<div class="text">${this.highlightMatch(elementName)}</div>
|
||||||
|
</div>
|
||||||
|
${isExpanded ? html`
|
||||||
|
<div class="demo-children">
|
||||||
|
${Array.from({ length: demoCount }, (_, i) => {
|
||||||
|
const demoIndex = i;
|
||||||
|
const isThisDemoSelected = isSelected && this.dashboardRef.selectedDemoIndex === demoIndex;
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="demo-child ${isThisDemoSelected ? 'selected' : ''}"
|
||||||
|
@click=${async () => {
|
||||||
|
await plugins.deesDomtools.DomTools.setupDomTools();
|
||||||
|
this.selectItem('element', elementName, item, demoIndex, section);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i class="material-symbols-outlined">play_circle</i>
|
||||||
|
<div class="text">demo${demoIndex + 1}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
` : null}
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
// Single demo element
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="selectOption ${isSelected ? 'selected' : ''}"
|
||||||
|
@click=${async () => {
|
||||||
|
await plugins.deesDomtools.DomTools.setupDomTools();
|
||||||
|
this.selectItem('element', elementName, item, 0, section);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i class="material-symbols-outlined">featured_video</i>
|
||||||
|
<div class="text">${this.highlightMatch(elementName)}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private toggleSectionCollapsed(sectionName: string) {
|
||||||
|
const newSet = new Set(this.collapsedSections);
|
||||||
|
if (newSet.has(sectionName)) {
|
||||||
|
newSet.delete(sectionName);
|
||||||
|
} else {
|
||||||
|
newSet.add(sectionName);
|
||||||
|
}
|
||||||
|
this.collapsedSections = newSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
private toggleExpanded(elementName: string) {
|
||||||
|
const newSet = new Set(this.expandedElements);
|
||||||
|
if (newSet.has(elementName)) {
|
||||||
|
newSet.delete(elementName);
|
||||||
|
} else {
|
||||||
|
newSet.add(elementName);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Auto-expand folder when a multi-demo element is selected
|
||||||
|
if (changedProperties.has('selectedItem') && this.selectedItem) {
|
||||||
|
// 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,
|
||||||
|
section?: IWccSection
|
||||||
|
) {
|
||||||
console.log('selected item');
|
console.log('selected item');
|
||||||
console.log(itemNameArg);
|
console.log(itemNameArg);
|
||||||
console.log(itemArg);
|
console.log(itemArg);
|
||||||
|
console.log('demo index:', demoIndex);
|
||||||
|
console.log('section:', section?.name);
|
||||||
|
|
||||||
this.selectedItem = itemArg;
|
this.selectedItem = itemArg;
|
||||||
this.selectedType = typeArg;
|
this.selectedType = typeArg;
|
||||||
|
this.dashboardRef.selectedDemoIndex = demoIndex;
|
||||||
|
|
||||||
|
// Set the selected section on dashboard
|
||||||
|
if (section) {
|
||||||
|
this.dashboardRef.selectedSection = section;
|
||||||
|
}
|
||||||
|
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('selectedType', {
|
new CustomEvent('selectedType', {
|
||||||
detail: typeArg
|
detail: typeArg
|
||||||
@@ -224,7 +545,10 @@ export class WccSidebar extends DeesElement {
|
|||||||
detail: itemArg
|
detail: itemArg
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.dashboardRef.buildUrl();
|
this.dashboardRef.buildUrl();
|
||||||
|
|
||||||
|
// Force re-render to update demo child selection indicator
|
||||||
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,39 @@ import type { TemplateResult } from 'lit';
|
|||||||
|
|
||||||
export type TTemplateFactory = () => TemplateResult | Promise<TemplateResult>;
|
export type TTemplateFactory = () => TemplateResult | Promise<TemplateResult>;
|
||||||
|
|
||||||
|
// Demo can be a single function or an array of functions
|
||||||
|
export type TDemoDefinition = TTemplateFactory | TTemplateFactory[];
|
||||||
|
|
||||||
export const resolveTemplateFactory = async (
|
export const resolveTemplateFactory = async (
|
||||||
factoryArg: TTemplateFactory
|
factoryArg: TTemplateFactory
|
||||||
): Promise<TemplateResult> => {
|
): Promise<TemplateResult> => {
|
||||||
return await Promise.resolve(factoryArg());
|
return await Promise.resolve(factoryArg());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of demos for an element
|
||||||
|
*/
|
||||||
|
export const getDemoCount = (demo: TDemoDefinition): number => {
|
||||||
|
if (Array.isArray(demo)) {
|
||||||
|
return demo.length;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific demo by index (0-based internally, displayed as 1-based)
|
||||||
|
*/
|
||||||
|
export const getDemoAtIndex = (demo: TDemoDefinition, index: number): TTemplateFactory | null => {
|
||||||
|
if (Array.isArray(demo)) {
|
||||||
|
return demo[index] ?? null;
|
||||||
|
}
|
||||||
|
// Single demo - only index 0 is valid
|
||||||
|
return index === 0 ? demo : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an element has multiple demos
|
||||||
|
*/
|
||||||
|
export const hasMultipleDemos = (demo: TDemoDefinition): boolean => {
|
||||||
|
return Array.isArray(demo) && demo.length > 1;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,21 +1,86 @@
|
|||||||
import { WccDashboard } from './elements/wcc-dashboard.js';
|
import { WccDashboard } from './elements/wcc-dashboard.js';
|
||||||
import { LitElement } from 'lit';
|
import { LitElement } from 'lit';
|
||||||
import type { TTemplateFactory } from './elements/wcctools.helpers.js';
|
import type { TTemplateFactory } from './elements/wcctools.helpers.js';
|
||||||
|
import type { IWccConfig, IWccSection } from './wcctools.interfaces.js';
|
||||||
|
|
||||||
// Export recording components and service
|
// Export recording components and service
|
||||||
export { RecorderService, type IRecorderEvents, type IRecordingOptions } from './services/recorder.service.js';
|
export { RecorderService, type IRecorderEvents, type IRecordingOptions } from './services/recorder.service.js';
|
||||||
export { WccRecordButton } from './elements/wcc-record-button.js';
|
export { WccRecordButton } from './elements/wcc-record-button.js';
|
||||||
export { WccRecordingPanel } from './elements/wcc-recording-panel.js';
|
export { WccRecordingPanel } from './elements/wcc-recording-panel.js';
|
||||||
|
|
||||||
const setupWccTools = (
|
// Export types for external use
|
||||||
|
export type { IWccConfig, IWccSection } from './wcctools.interfaces.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts legacy (elements, pages) format to new sections config
|
||||||
|
*/
|
||||||
|
const convertLegacyToConfig = (
|
||||||
elementsArg?: { [key: string]: LitElement },
|
elementsArg?: { [key: string]: LitElement },
|
||||||
pagesArg?: Record<string, TTemplateFactory>
|
pagesArg?: Record<string, TTemplateFactory>
|
||||||
|
): IWccConfig => {
|
||||||
|
const sections: IWccSection[] = [];
|
||||||
|
|
||||||
|
if (pagesArg && Object.keys(pagesArg).length > 0) {
|
||||||
|
sections.push({
|
||||||
|
name: 'Pages',
|
||||||
|
type: 'pages',
|
||||||
|
items: pagesArg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elementsArg && Object.keys(elementsArg).length > 0) {
|
||||||
|
sections.push({
|
||||||
|
name: 'Elements',
|
||||||
|
type: 'elements',
|
||||||
|
items: elementsArg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { sections };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the argument is the new config format
|
||||||
|
*/
|
||||||
|
const isWccConfig = (arg: any): arg is IWccConfig => {
|
||||||
|
return arg && typeof arg === 'object' && 'sections' in arg && Array.isArray(arg.sections);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup WCC Tools dashboard
|
||||||
|
*
|
||||||
|
* New format (recommended):
|
||||||
|
* ```typescript
|
||||||
|
* setupWccTools({
|
||||||
|
* sections: [
|
||||||
|
* { name: 'Elements', type: 'elements', items: elements },
|
||||||
|
* { name: 'Pages', type: 'pages', items: pages },
|
||||||
|
* ]
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Legacy format (still supported):
|
||||||
|
* ```typescript
|
||||||
|
* setupWccTools(elements, pages);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
const setupWccTools = (
|
||||||
|
configOrElements?: IWccConfig | { [key: string]: LitElement },
|
||||||
|
pagesArg?: Record<string, TTemplateFactory>
|
||||||
) => {
|
) => {
|
||||||
|
let config: IWccConfig;
|
||||||
|
|
||||||
|
if (isWccConfig(configOrElements)) {
|
||||||
|
config = configOrElements;
|
||||||
|
} else {
|
||||||
|
config = convertLegacyToConfig(configOrElements, pagesArg);
|
||||||
|
}
|
||||||
|
|
||||||
let hasRun = false;
|
let hasRun = false;
|
||||||
const runWccToolsSetup = async () => {
|
const runWccToolsSetup = async () => {
|
||||||
if (document.readyState === 'complete' && !hasRun) {
|
if (document.readyState === 'complete' && !hasRun) {
|
||||||
hasRun = true;
|
hasRun = true;
|
||||||
const wccTools = new WccDashboard(elementsArg as any, pagesArg);
|
const wccTools = new WccDashboard(config);
|
||||||
document.querySelector('body').append(wccTools);
|
document.querySelector('body').append(wccTools);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,40 @@ pnpm add -D @design.estate/dees-wcctools
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
### Sections-based Configuration (Recommended)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { setupWccTools } from '@design.estate/dees-wcctools';
|
||||||
|
import * as elements from './elements/index.js';
|
||||||
|
import * as views from './views/index.js';
|
||||||
|
import * as pages from './pages/index.js';
|
||||||
|
|
||||||
|
setupWccTools({
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
name: 'Pages',
|
||||||
|
type: 'pages',
|
||||||
|
items: pages,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Views',
|
||||||
|
type: 'elements',
|
||||||
|
items: views,
|
||||||
|
icon: 'web',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Elements',
|
||||||
|
type: 'elements',
|
||||||
|
items: elements,
|
||||||
|
filter: (name) => !name.startsWith('internal-'),
|
||||||
|
sort: ([a], [b]) => a.localeCompare(b),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Legacy Format (Still Supported)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { setupWccTools } from '@design.estate/dees-wcctools';
|
import { setupWccTools } from '@design.estate/dees-wcctools';
|
||||||
import { MyButton } from './components/my-button.js';
|
import { MyButton } from './components/my-button.js';
|
||||||
@@ -30,6 +64,8 @@ setupWccTools({
|
|||||||
| Export | Description |
|
| Export | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `setupWccTools` | Initialize the component catalogue dashboard |
|
| `setupWccTools` | Initialize the component catalogue dashboard |
|
||||||
|
| `IWccConfig` | TypeScript interface for sections configuration |
|
||||||
|
| `IWccSection` | TypeScript interface for individual section |
|
||||||
|
|
||||||
### Recording Components
|
### Recording Components
|
||||||
|
|
||||||
@@ -41,6 +77,18 @@ setupWccTools({
|
|||||||
| `IRecorderEvents` | TypeScript interface for recorder callbacks |
|
| `IRecorderEvents` | TypeScript interface for recorder callbacks |
|
||||||
| `IRecordingOptions` | TypeScript interface for recording options |
|
| `IRecordingOptions` | TypeScript interface for recording options |
|
||||||
|
|
||||||
|
## Section Configuration
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
|----------|------|-------------|
|
||||||
|
| `name` | `string` | Display name for the section header |
|
||||||
|
| `type` | `'elements' \| 'pages'` | Rendering behavior |
|
||||||
|
| `items` | `Record<string, any>` | Element classes or page factories |
|
||||||
|
| `filter` | `(name, item) => boolean` | Optional filter function |
|
||||||
|
| `sort` | `([a, itemA], [b, itemB]) => number` | Optional sort function |
|
||||||
|
| `icon` | `string` | Material Symbols icon name |
|
||||||
|
| `collapsed` | `boolean` | Start section collapsed (default: false) |
|
||||||
|
|
||||||
## Internal Components
|
## Internal Components
|
||||||
|
|
||||||
The module includes these internal web components:
|
The module includes these internal web components:
|
||||||
@@ -48,8 +96,8 @@ The module includes these internal web components:
|
|||||||
| Component | Description |
|
| Component | Description |
|
||||||
|-----------|-------------|
|
|-----------|-------------|
|
||||||
| `wcc-dashboard` | Main dashboard container with routing |
|
| `wcc-dashboard` | Main dashboard container with routing |
|
||||||
| `wcc-sidebar` | Navigation sidebar with element/page listing |
|
| `wcc-sidebar` | Navigation sidebar with collapsible sections |
|
||||||
| `wcc-frame` | Iframe viewport with responsive sizing |
|
| `wcc-frame` | Responsive viewport with size controls |
|
||||||
| `wcc-properties` | Property panel with live editing |
|
| `wcc-properties` | Property panel with live editing |
|
||||||
| `wcc-record-button` | Recording state indicator button |
|
| `wcc-record-button` | Recording state indicator button |
|
||||||
| `wcc-recording-panel` | Recording workflow UI |
|
| `wcc-recording-panel` | Recording workflow UI |
|
||||||
@@ -72,14 +120,14 @@ const events: IRecorderEvents = {
|
|||||||
const recorder = new RecorderService(events);
|
const recorder = new RecorderService(events);
|
||||||
|
|
||||||
// Load available microphones
|
// Load available microphones
|
||||||
const mics = await recorder.loadMicrophones(true); // true = request permission
|
const mics = await recorder.loadMicrophones(true);
|
||||||
|
|
||||||
// Start audio level monitoring
|
// Start audio level monitoring
|
||||||
await recorder.startAudioMonitoring(mics[0].deviceId);
|
await recorder.startAudioMonitoring(mics[0].deviceId);
|
||||||
|
|
||||||
// Start recording
|
// Start recording
|
||||||
await recorder.startRecording({
|
await recorder.startRecording({
|
||||||
mode: 'viewport', // or 'screen'
|
mode: 'viewport',
|
||||||
audioDeviceId: mics[0].deviceId,
|
audioDeviceId: mics[0].deviceId,
|
||||||
viewportElement: document.querySelector('.viewport'),
|
viewportElement: document.querySelector('.viewport'),
|
||||||
});
|
});
|
||||||
@@ -99,10 +147,11 @@ recorder.dispose();
|
|||||||
```
|
```
|
||||||
ts_web/
|
ts_web/
|
||||||
├── index.ts # Main exports
|
├── index.ts # Main exports
|
||||||
|
├── wcctools.interfaces.ts # Type definitions
|
||||||
├── elements/
|
├── elements/
|
||||||
│ ├── wcc-dashboard.ts # Root dashboard component
|
│ ├── wcc-dashboard.ts # Root dashboard component
|
||||||
│ ├── wcc-sidebar.ts # Navigation sidebar
|
│ ├── wcc-sidebar.ts # Navigation sidebar
|
||||||
│ ├── wcc-frame.ts # Responsive iframe viewport
|
│ ├── wcc-frame.ts # Responsive viewport
|
||||||
│ ├── wcc-properties.ts # Property editing panel
|
│ ├── wcc-properties.ts # Property editing panel
|
||||||
│ ├── wcc-record-button.ts # Recording button
|
│ ├── wcc-record-button.ts # Recording button
|
||||||
│ ├── wcc-recording-panel.ts # Recording options/preview
|
│ ├── wcc-recording-panel.ts # Recording options/preview
|
||||||
@@ -116,6 +165,7 @@ ts_web/
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 🎨 Interactive component preview
|
- 🎨 Interactive component preview
|
||||||
|
- 📂 Section-based sidebar with filtering & sorting
|
||||||
- 🔧 Real-time property editing with type detection
|
- 🔧 Real-time property editing with type detection
|
||||||
- 🌓 Theme switching (light/dark)
|
- 🌓 Theme switching (light/dark)
|
||||||
- 📱 Responsive viewport testing
|
- 📱 Responsive viewport testing
|
||||||
|
|||||||
@@ -235,9 +235,11 @@ export class RecorderService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleRecordingComplete(): void {
|
private async handleRecordingComplete(): Promise<void> {
|
||||||
// Create blob from recorded chunks
|
// Create blob from recorded chunks
|
||||||
this._recordedBlob = new Blob(this.recordedChunks, { type: 'video/webm' });
|
const blob = new Blob(this.recordedChunks, { type: 'video/webm' });
|
||||||
|
|
||||||
|
this._recordedBlob = blob;
|
||||||
|
|
||||||
// Stop all tracks
|
// Stop all tracks
|
||||||
if (this.currentStream) {
|
if (this.currentStream) {
|
||||||
|
|||||||
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