Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e14ebde03 | |||
| 28d1227d30 | |||
| 8c60d3bea3 | |||
| 9ed614994f | |||
| 61b79aa4dc | |||
| 1134cba575 | |||
| 29c0df489e | |||
| 53df62a9fd | |||
| 4fe17f5afd | |||
| 63dd6a27b3 | |||
| 287cc4d1c3 | |||
| 14e63738b7 | |||
| dd151bdad8 | |||
| bd409745e6 | |||
| eb7f482b75 | |||
| d16d482120 | |||
| 5ba4e55011 | |||
| 0068c0749d | |||
| 36dd6b5064 | |||
| ddecfcdb4c | |||
| 8f0f8606a1 | |||
| 7dca519d9a |
BIN
.playwright-mcp/sidebar-check-2.png
Normal file
BIN
.playwright-mcp/sidebar-check-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
.playwright-mcp/sidebar-check.png
Normal file
BIN
.playwright-mcp/sidebar-check.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
87
changelog.md
87
changelog.md
@@ -1,5 +1,92 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-01-04 - 3.5.2 - fix(elements)
|
||||
delay hiding sidebar and properties panels during native-mode transition and use transparent rgba border for frame to avoid layout jumps
|
||||
|
||||
- Add isHidden state to wcc-sidebar and wcc-properties and switch display bindings to use isHidden instead of directly using isNative
|
||||
- Introduce a 300ms delayed hide when entering native mode so UI hides after frame animation completes; show immediately when exiting native mode
|
||||
- Replace hardcoded hex border values in wcc-frame with rgba and set native border to a transparent 0px to prevent abrupt visual jumps
|
||||
|
||||
## 2026-01-04 - 3.5.1 - fix(sidebar)
|
||||
disable frame CSS transition while user is resizing the sidebar to prevent janky animations
|
||||
|
||||
- Added isResizing boolean property to wcc-frame to toggle transitions during resize
|
||||
- Set frame.isResizing = true at resize start and false on mouseup to re-enable transitions
|
||||
- Updated CSS to skip transition while isResizing is true
|
||||
- Files changed: ts_web/elements/wcc-frame.ts, ts_web/elements/wcc-sidebar.ts
|
||||
|
||||
## 2026-01-04 - 3.5.0 - feat(wcctools)
|
||||
add context menu and pinning support, persist pinned state in URL, and add grouped demo test elements
|
||||
|
||||
- Add wcc-contextmenu custom element with a static show() API, proper positioning, visibility transitions, outside-click and Escape handling, and menu item actions.
|
||||
- Introduce pinnedItems (Set<string>) on wcc-dashboard and wcc-sidebar; pass pinnedItems to the sidebar, handle pinnedChanged events, and persist pinned item keys in the URL query param 'pinned'. Changes include defensive updates to avoid unnecessary update loops.
|
||||
- Enhance wcc-sidebar to render pinned state: new styles for pinned items and pinned sections, contextmenu integration for element items, adjusted layout (grid-template-columns) and improved element/demo rendering logic.
|
||||
- Add grouped demo test components and exports to demo the demoGroup feature: test-button-primary, test-button-secondary, test-button-danger, test-input-text, and test-input-checkbox.
|
||||
- Misc: adjust dashboard URL state serialization/deserialization to include pinned items and ensure scroll/search state handling remains stable.
|
||||
|
||||
## 2025-12-30 - 3.4.0 - feat(sidebar)
|
||||
add searchable sidebar with URL-backed query state and highlighted matches
|
||||
|
||||
- Add search input to wcc-sidebar and expose a searchQuery property
|
||||
- Filter sidebar sections and items client-side based on the search query and hide sections with no matches
|
||||
- Highlight matching substrings in sidebar item labels
|
||||
- Emit a 'searchChanged' event from the sidebar and handle it in wcc-dashboard to keep dashboard.searchQuery in sync
|
||||
- Persist the search query in the route query parameter 'search' when building URLs and restore/clear it on navigation
|
||||
- Preserve existing scroll-state handling while adding search state to URL updates
|
||||
|
||||
## 2025-12-28 - 3.3.0 - feat(wcctools)
|
||||
Add section-based configuration API for setupWccTools, new Views, and section-aware routing/sidebar
|
||||
|
||||
- Introduce IWccSection and IWccConfig types and migrate setupWccTools to accept a sections config while preserving legacy (elements, pages) format
|
||||
- WccDashboard and WccSidebar updated to support sections, filtering, sorting, collapsed sections, and section-aware URL routing (uses sectionName in routes with legacy fallbacks)
|
||||
- Add Views: view-dashboard, view-settings, view-empty-state plus test/views index exports and demo variations
|
||||
- Add helpers: getSectionItems, convertLegacyToConfig and isWccConfig; update build URL and routing logic to be section-aware
|
||||
- Update docs and README/readme.hints with sections API, examples, migration notes and UI/UX updates
|
||||
|
||||
## 2025-12-22 - 3.2.0 - feat(wcc-sidebar)
|
||||
auto-expand sidebar folder when selecting an element with multiple demos
|
||||
|
||||
- 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
|
||||
|
||||
|
||||
@@ -2,9 +2,32 @@
|
||||
import * as deesWccTools from '../ts_web/index.js';
|
||||
import * as deesDomTools from '@design.estate/dees-domtools';
|
||||
|
||||
// elements and pages
|
||||
// elements, views and pages
|
||||
import * as elements from '../test/elements/index.js';
|
||||
import * as views from '../test/views/index.js';
|
||||
import * as pages from '../test/pages/index.js';
|
||||
|
||||
deesWccTools.setupWccTools(elements as any, pages);
|
||||
// Sections-based API with Views
|
||||
deesWccTools.setupWccTools({
|
||||
sections: [
|
||||
{
|
||||
name: 'Pages',
|
||||
type: 'pages',
|
||||
items: pages,
|
||||
},
|
||||
{
|
||||
name: 'Views',
|
||||
type: 'elements',
|
||||
items: views,
|
||||
icon: 'web',
|
||||
},
|
||||
{
|
||||
name: 'Elements',
|
||||
type: 'elements',
|
||||
items: elements,
|
||||
sort: ([a], [b]) => a.localeCompare(b),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
deesDomTools.elementBasic.setup();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"gitzone": {
|
||||
"@git.zone/cli": {
|
||||
"projectType": "wcc",
|
||||
"module": {
|
||||
"githost": "code.foss.global",
|
||||
@@ -21,13 +21,19 @@
|
||||
"element testing",
|
||||
"page development"
|
||||
]
|
||||
},
|
||||
"release": {
|
||||
"registries": [
|
||||
"https://verdaccio.lossless.digital",
|
||||
"https://registry.npmjs.org"
|
||||
],
|
||||
"accessLevel": "public"
|
||||
}
|
||||
},
|
||||
"npmci": {
|
||||
"npmGlobalTools": [],
|
||||
"npmAccessLevel": "public"
|
||||
},
|
||||
"tsdoc": {
|
||||
"@git.zone/tsdoc": {
|
||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||
},
|
||||
"@ship.zone/szci": {
|
||||
"npmGlobalTools": []
|
||||
}
|
||||
}
|
||||
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@design.estate/dees-wcctools",
|
||||
"version": "2.0.1",
|
||||
"version": "3.5.2",
|
||||
"private": false,
|
||||
"description": "A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.",
|
||||
"exports": {
|
||||
@@ -24,13 +24,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@api.global/typedserver": "^7.11.1",
|
||||
"@git.zone/tsbuild": "^3.1.2",
|
||||
"@git.zone/tsbuild": "^4.0.2",
|
||||
"@git.zone/tsbundle": "^2.6.3",
|
||||
"@git.zone/tsrun": "^2.0.0",
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@git.zone/tstest": "^3.1.3",
|
||||
"@git.zone/tswatch": "^2.3.12",
|
||||
"@git.zone/tswatch": "^2.3.13",
|
||||
"@push.rocks/projectinfo": "^5.0.2",
|
||||
"@types/node": "^25.0.0"
|
||||
"@types/node": "^25.0.3"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
|
||||
1916
pnpm-lock.yaml
generated
1916
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,67 @@
|
||||
# Project Hints and Findings
|
||||
|
||||
## Section-based Configuration API (2025-12-27)
|
||||
|
||||
### Overview
|
||||
Refactored `setupWccTools` to accept a section-based configuration object instead of separate elements/pages arguments. This allows multiple custom sections with filtering, sorting, and collapsible headers.
|
||||
|
||||
### New API
|
||||
```typescript
|
||||
import * as deesWccTools from '@design.estate/dees-wcctools';
|
||||
|
||||
deesWccTools.setupWccTools({
|
||||
sections: [
|
||||
{
|
||||
name: 'Pages',
|
||||
type: 'pages',
|
||||
items: pages,
|
||||
},
|
||||
{
|
||||
name: 'Elements',
|
||||
type: 'elements',
|
||||
items: elements,
|
||||
filter: (name, item) => !name.startsWith('internal-'),
|
||||
sort: ([a], [b]) => a.localeCompare(b),
|
||||
},
|
||||
{
|
||||
name: 'Views',
|
||||
type: 'elements',
|
||||
items: elements,
|
||||
filter: (name, item) => name.startsWith('view-'),
|
||||
icon: 'web',
|
||||
collapsed: true, // Start collapsed
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Section Properties
|
||||
- `name`: Display name for the section header
|
||||
- `type`: `'elements'` (shows demos) or `'pages'` (renders directly)
|
||||
- `items`: Record of items (element classes or page factories)
|
||||
- `filter`: Optional `(name, item) => boolean` to filter items
|
||||
- `sort`: Optional `([name, item], [name, item]) => number` for ordering
|
||||
- `icon`: Optional Material icon name for section header
|
||||
- `collapsed`: Optional boolean to start section collapsed
|
||||
|
||||
### Backwards Compatibility
|
||||
Legacy format is still supported:
|
||||
```typescript
|
||||
deesWccTools.setupWccTools(elements, pages); // Still works
|
||||
```
|
||||
|
||||
### URL Routing
|
||||
Changed from `/wcctools-route/:itemType/:itemName/...` to `/wcctools-route/:sectionName/:itemName/...`
|
||||
Section names are URL-encoded. Legacy routes (`element`/`page` as section name) still work for backwards compatibility.
|
||||
|
||||
### Files Changed
|
||||
- `ts_web/wcctools.interfaces.ts` - New types: `IWccSection`, `IWccConfig`
|
||||
- `ts_web/index.ts` - Updated `setupWccTools` with backwards compat detection
|
||||
- `ts_web/elements/wcc-dashboard.ts` - Uses sections, updated routing
|
||||
- `ts_web/elements/wcc-sidebar.ts` - Dynamic section rendering
|
||||
|
||||
---
|
||||
|
||||
## UI Redesign with Shadcn-like Styles (2025-06-27)
|
||||
|
||||
### Changes Made
|
||||
|
||||
229
readme.md
229
readme.md
@@ -6,12 +6,13 @@
|
||||
|
||||
`@design.estate/dees-wcctools` provides a comprehensive development environment for web components, featuring:
|
||||
|
||||
- 🎨 **Interactive Component Catalogue** — Live preview with sidebar navigation
|
||||
- 🎨 **Interactive Component Catalogue** — Live preview with customizable sidebar sections
|
||||
- 🔧 **Real-time Property Editing** — Modify component props on the fly with auto-detected editors
|
||||
- 🌓 **Theme Switching** — Test light/dark modes instantly
|
||||
- 📱 **Responsive Viewport Testing** — Phone, phablet, tablet, and desktop views
|
||||
- 🎬 **Screen Recording** — Record component demos with audio support and video trimming
|
||||
- 🧪 **Advanced Demo Tools** — Post-render hooks for interactive testing
|
||||
- 📂 **Section-based Organization** — Group components into custom sections with filtering and sorting
|
||||
- 🚀 **Zero-config Setup** — TypeScript and Lit support out of the box
|
||||
|
||||
## Issue Reporting and Security
|
||||
@@ -57,29 +58,22 @@ export class MyButton extends DeesElement {
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
button.primary {
|
||||
background: #007bff;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
button.secondary {
|
||||
background: #6c757d;
|
||||
background: #6b7280;
|
||||
color: white;
|
||||
}
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<button class="${this.variant}">
|
||||
${this.label}
|
||||
</button>
|
||||
<button class="${this.variant}">${this.label}</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -93,33 +87,32 @@ import { setupWccTools } from '@design.estate/dees-wcctools';
|
||||
import { html } from 'lit';
|
||||
|
||||
// Import your components
|
||||
import { MyButton } from './components/my-button.js';
|
||||
import { MyCard } from './components/my-card.js';
|
||||
import * as elements from './components/index.js';
|
||||
import * as views from './views/index.js';
|
||||
import * as pages from './pages/index.js';
|
||||
|
||||
// Define elements for the catalogue
|
||||
const elements = {
|
||||
'my-button': MyButton,
|
||||
'my-card': MyCard,
|
||||
};
|
||||
|
||||
// Optionally define pages
|
||||
const pages = {
|
||||
'home': () => html`
|
||||
<div style="padding: 20px;">
|
||||
<h1>Welcome to My Component Library</h1>
|
||||
<p>Browse components using the sidebar.</p>
|
||||
</div>
|
||||
`,
|
||||
'getting-started': () => html`
|
||||
<div style="padding: 20px;">
|
||||
<h2>Getting Started</h2>
|
||||
<p>Installation and usage instructions...</p>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
// Initialize the catalogue
|
||||
setupWccTools(elements, pages);
|
||||
// Initialize with sections-based configuration
|
||||
setupWccTools({
|
||||
sections: [
|
||||
{
|
||||
name: 'Pages',
|
||||
type: 'pages',
|
||||
items: pages,
|
||||
},
|
||||
{
|
||||
name: 'Views',
|
||||
type: 'elements',
|
||||
items: views,
|
||||
icon: 'web',
|
||||
},
|
||||
{
|
||||
name: 'Elements',
|
||||
type: 'elements',
|
||||
items: elements,
|
||||
sort: ([a], [b]) => a.localeCompare(b),
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Create an HTML Entry Point
|
||||
@@ -137,6 +130,69 @@ setupWccTools(elements, pages);
|
||||
</html>
|
||||
```
|
||||
|
||||
## 📂 Sections Configuration
|
||||
|
||||
The sections-based API gives you full control over how components are organized in the sidebar.
|
||||
|
||||
### Section Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `name` | `string` | Display name for the section header |
|
||||
| `type` | `'elements' \| 'pages'` | How items render (`elements` show demos, `pages` render directly) |
|
||||
| `items` | `Record<string, any>` | Object containing element classes or page factories |
|
||||
| `filter` | `(name, item) => boolean` | Optional filter function to include/exclude items |
|
||||
| `sort` | `([a, itemA], [b, itemB]) => number` | Optional sort function for ordering items |
|
||||
| `icon` | `string` | Optional Material Symbols icon name |
|
||||
| `collapsed` | `boolean` | Start section collapsed (default: `false`) |
|
||||
|
||||
### Advanced Example
|
||||
|
||||
```typescript
|
||||
import { setupWccTools } from '@design.estate/dees-wcctools';
|
||||
import * as allElements from './elements/index.js';
|
||||
import * as pages from './pages/index.js';
|
||||
|
||||
setupWccTools({
|
||||
sections: [
|
||||
{
|
||||
name: 'Pages',
|
||||
type: 'pages',
|
||||
items: pages,
|
||||
},
|
||||
{
|
||||
name: 'Form Controls',
|
||||
type: 'elements',
|
||||
items: allElements,
|
||||
icon: 'edit_note',
|
||||
filter: (name) => name.startsWith('form-') || name.includes('input'),
|
||||
sort: ([a], [b]) => a.localeCompare(b),
|
||||
},
|
||||
{
|
||||
name: 'Layout',
|
||||
type: 'elements',
|
||||
items: allElements,
|
||||
icon: 'dashboard',
|
||||
filter: (name) => name.startsWith('layout-') || name.startsWith('grid-'),
|
||||
},
|
||||
{
|
||||
name: 'Legacy',
|
||||
type: 'elements',
|
||||
items: allElements,
|
||||
filter: (name) => name.startsWith('legacy-'),
|
||||
collapsed: true, // Start collapsed
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Legacy API (Still Supported)
|
||||
|
||||
```typescript
|
||||
// The old format still works for simple use cases
|
||||
setupWccTools(elements, pages);
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### 🎯 Live Property Editing
|
||||
@@ -162,19 +218,7 @@ Test your components across different screen sizes:
|
||||
|
||||
### 🌓 Theme Support
|
||||
|
||||
Components automatically adapt to light/dark themes using the `goBright` property:
|
||||
|
||||
```typescript
|
||||
public render() {
|
||||
return html`
|
||||
<div class="${this.goBright ? 'light-theme' : 'dark-theme'}">
|
||||
<!-- Your component content -->
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
Or use CSS custom properties with the theme manager:
|
||||
Components automatically adapt to light/dark themes. Use CSS custom properties with the theme manager:
|
||||
|
||||
```typescript
|
||||
import { cssManager } from '@design.estate/dees-element';
|
||||
@@ -182,8 +226,8 @@ import { cssManager } from '@design.estate/dees-element';
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
background: ${cssManager.bdTheme('#fff', '#000')};
|
||||
color: ${cssManager.bdTheme('#1a1a1a', '#e5e5e5')};
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
}
|
||||
`
|
||||
];
|
||||
@@ -191,7 +235,7 @@ public static styles = [
|
||||
|
||||
### 🎬 Screen Recording
|
||||
|
||||
Record component demos directly from the catalogue! The built-in recorder supports:
|
||||
Record component demos directly from the catalogue:
|
||||
|
||||
- **Viewport Recording** — Record just the component viewport
|
||||
- **Full Screen Recording** — Capture the entire screen
|
||||
@@ -232,9 +276,26 @@ export class MyComponent extends DeesElement {
|
||||
}
|
||||
```
|
||||
|
||||
### 🎭 Multiple Demos
|
||||
|
||||
Components can expose multiple demo variations:
|
||||
|
||||
```typescript
|
||||
@customElement('my-button')
|
||||
export class MyButton extends DeesElement {
|
||||
public static demo = [
|
||||
() => html`<my-button variant="primary">Primary</my-button>`,
|
||||
() => html`<my-button variant="secondary">Secondary</my-button>`,
|
||||
() => html`<my-button variant="danger">Danger</my-button>`,
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
Each demo appears as a numbered item in an expandable folder in the sidebar.
|
||||
|
||||
### ⏳ Async Demos
|
||||
|
||||
Return a `Promise` from `demo` for async setup. The dashboard waits for resolution:
|
||||
Return a `Promise` from `demo` for async setup:
|
||||
|
||||
```typescript
|
||||
public static demo = async () => {
|
||||
@@ -243,7 +304,7 @@ public static demo = async () => {
|
||||
};
|
||||
```
|
||||
|
||||
### 🎭 Container Queries
|
||||
### 🎯 Container Queries
|
||||
|
||||
Components can respond to their container size using the `wccToolsViewport` container:
|
||||
|
||||
@@ -269,7 +330,7 @@ public static styles = [
|
||||
|
||||
### Required for Catalogue Display
|
||||
|
||||
1. Components must expose a static `demo` property returning a Lit template
|
||||
1. Components must expose a static `demo` property returning a Lit template (or array of templates)
|
||||
2. Use `@property()` decorators with the `accessor` keyword for editable properties
|
||||
3. Export component classes for proper detection
|
||||
|
||||
@@ -278,7 +339,7 @@ public static styles = [
|
||||
```typescript
|
||||
@customElement('best-practice-component')
|
||||
export class BestPracticeComponent extends DeesElement {
|
||||
// ✅ Static demo property
|
||||
// ✅ Static demo property (single or array)
|
||||
public static demo = () => html`
|
||||
<best-practice-component
|
||||
.complexProp=${{ key: 'value' }}
|
||||
@@ -305,23 +366,40 @@ export class BestPracticeComponent extends DeesElement {
|
||||
The catalogue uses URL routing for deep linking:
|
||||
|
||||
```
|
||||
/wcctools-route/:type/:name/:viewport/:theme
|
||||
/wcctools-route/:sectionName/:itemName/:demoIndex/:viewport/:theme
|
||||
|
||||
Examples:
|
||||
/wcctools-route/element/my-button/desktop/dark
|
||||
/wcctools-route/page/home/tablet/bright
|
||||
/wcctools-route/Elements/my-button/0/desktop/dark
|
||||
/wcctools-route/Views/view-dashboard/0/tablet/bright
|
||||
/wcctools-route/Pages/home/0/desktop/dark
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `setupWccTools(elements, pages?)`
|
||||
### `setupWccTools(config)`
|
||||
|
||||
Initialize the WCC Tools dashboard.
|
||||
Initialize the WCC Tools dashboard with sections configuration.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `elements` | `Record<string, typeof LitElement>` | Map of element names to classes |
|
||||
| `pages` | `Record<string, TTemplateFactory>` | Optional map of page names to template functions |
|
||||
```typescript
|
||||
interface IWccSection {
|
||||
name: string;
|
||||
type: 'elements' | 'pages';
|
||||
items: Record<string, any>;
|
||||
filter?: (name: string, item: any) => boolean;
|
||||
sort?: (a: [string, any], b: [string, any]) => number;
|
||||
icon?: string;
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
interface IWccConfig {
|
||||
sections: IWccSection[];
|
||||
}
|
||||
|
||||
setupWccTools(config: IWccConfig): void;
|
||||
|
||||
// Legacy (still supported)
|
||||
setupWccTools(elements: Record<string, any>, pages?: Record<string, TTemplateFactory>): void;
|
||||
```
|
||||
|
||||
### `DeesDemoWrapper`
|
||||
|
||||
@@ -357,14 +435,21 @@ recorder.stopRecording();
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
my-components/
|
||||
my-component-library/
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ ├── elements/ # UI components
|
||||
│ │ ├── my-button.ts
|
||||
│ │ └── my-card.ts
|
||||
│ └── catalogue.ts
|
||||
├── dist/
|
||||
├── index.html
|
||||
│ │ ├── my-card.ts
|
||||
│ │ └── index.ts
|
||||
│ ├── views/ # Full-page layouts
|
||||
│ │ ├── view-dashboard.ts
|
||||
│ │ └── index.ts
|
||||
│ ├── pages/ # Documentation pages
|
||||
│ │ ├── home.ts
|
||||
│ │ └── index.ts
|
||||
│ └── catalogue.ts # WCC Tools setup
|
||||
├── html/
|
||||
│ └── index.html
|
||||
└── package.json
|
||||
```
|
||||
|
||||
|
||||
@@ -4,3 +4,10 @@ export * from './test-complextypes.js';
|
||||
export * from './test-withwrapper.js';
|
||||
export * from './test-edgecases.js';
|
||||
export * from './test-nested.js';
|
||||
|
||||
// Grouped elements to demo the demoGroup feature
|
||||
export * from './test-button-primary.js';
|
||||
export * from './test-button-secondary.js';
|
||||
export * from './test-button-danger.js';
|
||||
export * from './test-input-text.js';
|
||||
export * from './test-input-checkbox.js';
|
||||
|
||||
45
test/elements/test-button-danger.ts
Normal file
45
test/elements/test-button-danger.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
property,
|
||||
css,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('test-button-danger')
|
||||
export class TestButtonDanger extends DeesElement {
|
||||
// Same group as other buttons
|
||||
public static demoGroup = 'Buttons';
|
||||
|
||||
public static demo = () => html`
|
||||
<test-button-danger>Delete</test-button-danger>
|
||||
`;
|
||||
|
||||
@property({ type: String })
|
||||
accessor label: string = 'Delete';
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
button {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`<button><slot>${this.label}</slot></button>`;
|
||||
}
|
||||
}
|
||||
45
test/elements/test-button-primary.ts
Normal file
45
test/elements/test-button-primary.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
property,
|
||||
css,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('test-button-primary')
|
||||
export class TestButtonPrimary extends DeesElement {
|
||||
// This groups the element with other "Buttons" in the sidebar
|
||||
public static demoGroup = 'Buttons';
|
||||
|
||||
public static demo = () => html`
|
||||
<test-button-primary>Click Me</test-button-primary>
|
||||
`;
|
||||
|
||||
@property({ type: String })
|
||||
accessor label: string = 'Button';
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
button {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`<button><slot>${this.label}</slot></button>`;
|
||||
}
|
||||
}
|
||||
45
test/elements/test-button-secondary.ts
Normal file
45
test/elements/test-button-secondary.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
property,
|
||||
css,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('test-button-secondary')
|
||||
export class TestButtonSecondary extends DeesElement {
|
||||
// Same group as test-button-primary - they'll appear together
|
||||
public static demoGroup = 'Buttons';
|
||||
|
||||
public static demo = () => html`
|
||||
<test-button-secondary>Secondary Action</test-button-secondary>
|
||||
`;
|
||||
|
||||
@property({ type: String })
|
||||
accessor label: string = 'Button';
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
button {
|
||||
background: transparent;
|
||||
color: #3b82f6;
|
||||
border: 1px solid #3b82f6;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`<button><slot>${this.label}</slot></button>`;
|
||||
}
|
||||
}
|
||||
68
test/elements/test-input-checkbox.ts
Normal file
68
test/elements/test-input-checkbox.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
property,
|
||||
css,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('test-input-checkbox')
|
||||
export class TestInputCheckbox extends DeesElement {
|
||||
// Same group as test-input-text
|
||||
public static demoGroup = 'Inputs';
|
||||
|
||||
public static demo = () => html`
|
||||
<test-input-checkbox label="Accept terms and conditions"></test-input-checkbox>
|
||||
`;
|
||||
|
||||
@property({ type: String })
|
||||
accessor label: string = 'Checkbox';
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor checked: boolean = false;
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.checkbox {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
background: #1a1a1a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.checkbox.checked {
|
||||
background: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
.checkbox.checked::after {
|
||||
content: '✓';
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
}
|
||||
.label {
|
||||
color: #e5e5e5;
|
||||
font-size: 14px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<div
|
||||
class="checkbox ${this.checked ? 'checked' : ''}"
|
||||
@click=${() => this.checked = !this.checked}
|
||||
></div>
|
||||
<span class="label">${this.label}</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
59
test/elements/test-input-text.ts
Normal file
59
test/elements/test-input-text.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
property,
|
||||
css,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('test-input-text')
|
||||
export class TestInputText extends DeesElement {
|
||||
// Different group - "Inputs"
|
||||
public static demoGroup = 'Inputs';
|
||||
|
||||
public static demo = () => html`
|
||||
<test-input-text placeholder="Enter text..."></test-input-text>
|
||||
`;
|
||||
|
||||
@property({ type: String })
|
||||
accessor placeholder: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
accessor value: string = '';
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
input {
|
||||
background: #1a1a1a;
|
||||
color: #e5e5e5;
|
||||
border: 1px solid #333;
|
||||
padding: 10px 14px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
min-width: 200px;
|
||||
}
|
||||
input:focus {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
input::placeholder {
|
||||
color: #666;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<input
|
||||
type="text"
|
||||
.placeholder=${this.placeholder}
|
||||
.value=${this.value}
|
||||
@input=${(e: Event) => this.value = (e.target as HTMLInputElement).value}
|
||||
/>
|
||||
`;
|
||||
}
|
||||
}
|
||||
3
test/views/index.ts
Normal file
3
test/views/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './view-dashboard.js';
|
||||
export * from './view-settings.js';
|
||||
export * from './view-empty-state.js';
|
||||
286
test/views/view-dashboard.ts
Normal file
286
test/views/view-dashboard.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
import { DeesElement, customElement, html, css, property, cssManager } from '@design.estate/dees-element';
|
||||
|
||||
@customElement('view-dashboard')
|
||||
export class ViewDashboard extends DeesElement {
|
||||
public static demo = () => html`<view-dashboard></view-dashboard>`;
|
||||
|
||||
@property()
|
||||
accessor title: string = 'Dashboard';
|
||||
|
||||
@property({ type: Number })
|
||||
accessor notificationCount: number = 3;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
min-height: 100%;
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#0a0a0a')};
|
||||
color: ${cssManager.bdTheme('#1a1a1a', '#e5e5e5')};
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
display: grid;
|
||||
grid-template-columns: 240px 1fr;
|
||||
grid-template-rows: 60px 1fr;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
grid-column: 1 / -1;
|
||||
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#222')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notification-badge::after {
|
||||
content: attr(data-count);
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background: ${cssManager.bdTheme('#fff', '#0f0f0f')};
|
||||
border-right: 1px solid ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')};
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 24px;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
|
||||
color: ${cssManager.bdTheme('#1a1a1a', '#fff')};
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.15)')};
|
||||
color: #3b82f6;
|
||||
border-right: 3px solid #3b82f6;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.content-header h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.content-header p {
|
||||
color: ${cssManager.bdTheme('#666', '#888')};
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#222')};
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.stat-card .label {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: ${cssManager.bdTheme('#666', '#888')};
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-card .value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stat-card .change {
|
||||
font-size: 12px;
|
||||
color: #22c55e;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.stat-card .change.negative {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.recent-activity {
|
||||
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#222')};
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.recent-activity h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#f0f0f0', '#1a1a1a')};
|
||||
}
|
||||
|
||||
.activity-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.activity-content .title {
|
||||
font-weight: 500;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.activity-content .time {
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#888', '#666')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<div class="dashboard">
|
||||
<header class="header">
|
||||
<h1>${this.title}</h1>
|
||||
<div class="header-actions">
|
||||
<div class="notification-badge" data-count="${this.notificationCount}">
|
||||
<span>Notifications</span>
|
||||
</div>
|
||||
<span>User</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="sidebar">
|
||||
<div class="nav-item active">Overview</div>
|
||||
<div class="nav-item">Analytics</div>
|
||||
<div class="nav-item">Projects</div>
|
||||
<div class="nav-item">Team</div>
|
||||
<div class="nav-item">Settings</div>
|
||||
</nav>
|
||||
|
||||
<main class="content">
|
||||
<div class="content-header">
|
||||
<h2>Overview</h2>
|
||||
<p>Welcome back! Here's what's happening with your projects.</p>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="label">Total Revenue</div>
|
||||
<div class="value">$45,231</div>
|
||||
<div class="change">+20.1% from last month</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">Active Users</div>
|
||||
<div class="value">2,350</div>
|
||||
<div class="change">+180 new users</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">Pending Tasks</div>
|
||||
<div class="value">12</div>
|
||||
<div class="change negative">-3 from yesterday</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">Completion Rate</div>
|
||||
<div class="value">94.2%</div>
|
||||
<div class="change">+2.4% this week</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="recent-activity">
|
||||
<h3>Recent Activity</h3>
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon">+</div>
|
||||
<div class="activity-content">
|
||||
<div class="title">New project created</div>
|
||||
<div class="time">2 minutes ago</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon">U</div>
|
||||
<div class="activity-content">
|
||||
<div class="title">User settings updated</div>
|
||||
<div class="time">1 hour ago</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon">D</div>
|
||||
<div class="activity-content">
|
||||
<div class="title">Deployment completed</div>
|
||||
<div class="time">3 hours ago</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
262
test/views/view-empty-state.ts
Normal file
262
test/views/view-empty-state.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
import { DeesElement, customElement, html, css, property, cssManager } from '@design.estate/dees-element';
|
||||
|
||||
@customElement('view-empty-state')
|
||||
export class ViewEmptyState extends DeesElement {
|
||||
public static demo = [
|
||||
() => html`<view-empty-state></view-empty-state>`,
|
||||
() => html`<view-empty-state variant="no-results"></view-empty-state>`,
|
||||
() => html`<view-empty-state variant="error"></view-empty-state>`,
|
||||
];
|
||||
|
||||
@property()
|
||||
accessor variant: 'empty' | 'no-results' | 'error' = 'empty';
|
||||
|
||||
@property()
|
||||
accessor title: string = '';
|
||||
|
||||
@property()
|
||||
accessor description: string = '';
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100%;
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#0a0a0a')};
|
||||
color: ${cssManager.bdTheme('#1a1a1a', '#e5e5e5')};
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
padding: 40px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: 0 auto 24px;
|
||||
background: ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')};
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 48px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.icon.error {
|
||||
color: #ef4444;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
p {
|
||||
color: ${cssManager.bdTheme('#666', '#888')};
|
||||
margin: 0 0 24px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #2563eb;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')};
|
||||
color: ${cssManager.bdTheme('#1a1a1a', '#e5e5e5')};
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: ${cssManager.bdTheme('#ddd', '#222')};
|
||||
}
|
||||
|
||||
.illustration {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.illustration svg {
|
||||
width: 200px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.folder-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.folder-back {
|
||||
position: absolute;
|
||||
width: 80px;
|
||||
height: 60px;
|
||||
background: ${cssManager.bdTheme('#ddd', '#333')};
|
||||
border-radius: 4px 4px 8px 8px;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.folder-front {
|
||||
position: absolute;
|
||||
width: 80px;
|
||||
height: 50px;
|
||||
background: ${cssManager.bdTheme('#e5e5e5', '#444')};
|
||||
border-radius: 0 4px 8px 8px;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.folder-tab {
|
||||
position: absolute;
|
||||
width: 30px;
|
||||
height: 12px;
|
||||
background: ${cssManager.bdTheme('#ddd', '#333')};
|
||||
border-radius: 4px 4px 0 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border: 4px solid ${cssManager.bdTheme('#ccc', '#444')};
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
.search-icon::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
height: 20px;
|
||||
background: ${cssManager.bdTheme('#ccc', '#444')};
|
||||
border-radius: 2px;
|
||||
bottom: -18px;
|
||||
right: -8px;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border: 4px solid #ef4444;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
margin: 0 auto 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.error-icon::before {
|
||||
content: '!';
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
color: #ef4444;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
private getContent() {
|
||||
switch (this.variant) {
|
||||
case 'no-results':
|
||||
return {
|
||||
title: this.title || 'No results found',
|
||||
description:
|
||||
this.description ||
|
||||
"We couldn't find what you're looking for. Try adjusting your search or filters.",
|
||||
icon: 'search',
|
||||
primaryAction: 'Clear Filters',
|
||||
secondaryAction: 'Go Back',
|
||||
};
|
||||
case 'error':
|
||||
return {
|
||||
title: this.title || 'Something went wrong',
|
||||
description:
|
||||
this.description ||
|
||||
"We're having trouble loading this page. Please try again or contact support if the problem persists.",
|
||||
icon: 'error',
|
||||
primaryAction: 'Try Again',
|
||||
secondaryAction: 'Contact Support',
|
||||
};
|
||||
default:
|
||||
return {
|
||||
title: this.title || 'No items yet',
|
||||
description:
|
||||
this.description ||
|
||||
"Get started by creating your first item. It only takes a few seconds.",
|
||||
icon: 'folder',
|
||||
primaryAction: 'Create New',
|
||||
secondaryAction: 'Learn More',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const content = this.getContent();
|
||||
|
||||
return html`
|
||||
<div class="empty-state">
|
||||
${this.renderIcon(content.icon)}
|
||||
<h2>${content.title}</h2>
|
||||
<p>${content.description}</p>
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary">${content.primaryAction}</button>
|
||||
<button class="btn btn-secondary">${content.secondaryAction}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderIcon(type: string) {
|
||||
switch (type) {
|
||||
case 'search':
|
||||
return html`<div class="search-icon"></div>`;
|
||||
case 'error':
|
||||
return html`<div class="error-icon"></div>`;
|
||||
default:
|
||||
return html`
|
||||
<div class="folder-icon">
|
||||
<div class="folder-tab"></div>
|
||||
<div class="folder-back"></div>
|
||||
<div class="folder-front"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
436
test/views/view-settings.ts
Normal file
436
test/views/view-settings.ts
Normal file
@@ -0,0 +1,436 @@
|
||||
import { DeesElement, customElement, html, css, property, cssManager } from '@design.estate/dees-element';
|
||||
|
||||
@customElement('view-settings')
|
||||
export class ViewSettings extends DeesElement {
|
||||
public static demo = [
|
||||
() => html`<view-settings></view-settings>`,
|
||||
() => html`<view-settings activeTab="notifications"></view-settings>`,
|
||||
() => html`<view-settings activeTab="security"></view-settings>`,
|
||||
];
|
||||
|
||||
@property()
|
||||
accessor activeTab: 'profile' | 'notifications' | 'security' = 'profile';
|
||||
|
||||
@property()
|
||||
accessor userName: string = 'John Doe';
|
||||
|
||||
@property()
|
||||
accessor userEmail: string = 'john@example.com';
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
min-height: 100%;
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#0a0a0a')};
|
||||
color: ${cssManager.bdTheme('#1a1a1a', '#e5e5e5')};
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
.settings-layout {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 24px;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.settings-header h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.settings-header p {
|
||||
color: ${cssManager.bdTheme('#666', '#888')};
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.settings-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')};
|
||||
color: ${cssManager.bdTheme('#1a1a1a', '#fff')};
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: ${cssManager.bdTheme('#1a1a1a', '#3b82f6')};
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.settings-panel {
|
||||
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#222')};
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#222')};
|
||||
}
|
||||
|
||||
.panel-header h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.panel-header p {
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#666', '#888')};
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
background: ${cssManager.bdTheme('#fff', '#0a0a0a')};
|
||||
color: ${cssManager.bdTheme('#1a1a1a', '#e5e5e5')};
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.toggle-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#f0f0f0', '#1a1a1a')};
|
||||
}
|
||||
|
||||
.toggle-group:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.toggle-label .title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.toggle-label .description {
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('#666', '#888')};
|
||||
}
|
||||
|
||||
.toggle {
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
background: ${cssManager.bdTheme('#ddd', '#333')};
|
||||
border-radius: 12px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle.active {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.toggle::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle.active::after {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e5e5e5', '#222')};
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.security-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
background: ${cssManager.bdTheme('#f9f9f9', '#0a0a0a')};
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.security-item .info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.security-item .title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.security-item .status {
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('#666', '#888')};
|
||||
}
|
||||
|
||||
.security-item .status.enabled {
|
||||
color: #22c55e;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
private handleTabClick(tab: 'profile' | 'notifications' | 'security') {
|
||||
this.activeTab = tab;
|
||||
}
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<div class="settings-layout">
|
||||
<div class="settings-header">
|
||||
<h1>Settings</h1>
|
||||
<p>Manage your account settings and preferences</p>
|
||||
</div>
|
||||
|
||||
<div class="settings-content">
|
||||
<nav class="settings-nav">
|
||||
<div
|
||||
class="nav-item ${this.activeTab === 'profile' ? 'active' : ''}"
|
||||
@click=${() => this.handleTabClick('profile')}
|
||||
>
|
||||
Profile
|
||||
</div>
|
||||
<div
|
||||
class="nav-item ${this.activeTab === 'notifications' ? 'active' : ''}"
|
||||
@click=${() => this.handleTabClick('notifications')}
|
||||
>
|
||||
Notifications
|
||||
</div>
|
||||
<div
|
||||
class="nav-item ${this.activeTab === 'security' ? 'active' : ''}"
|
||||
@click=${() => this.handleTabClick('security')}
|
||||
>
|
||||
Security
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="settings-panel">
|
||||
${this.activeTab === 'profile' ? this.renderProfile() : null}
|
||||
${this.activeTab === 'notifications' ? this.renderNotifications() : null}
|
||||
${this.activeTab === 'security' ? this.renderSecurity() : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderProfile() {
|
||||
return html`
|
||||
<div class="panel-header">
|
||||
<h2>Profile Information</h2>
|
||||
<p>Update your personal details and email address</p>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>First Name</label>
|
||||
<input type="text" value="John" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Last Name</label>
|
||||
<input type="text" value="Doe" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Email Address</label>
|
||||
<input type="email" value="${this.userEmail}" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Bio</label>
|
||||
<input type="text" placeholder="Tell us about yourself..." />
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn btn-primary">Save Changes</button>
|
||||
<button class="btn btn-secondary">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderNotifications() {
|
||||
return html`
|
||||
<div class="panel-header">
|
||||
<h2>Notification Preferences</h2>
|
||||
<p>Choose what notifications you want to receive</p>
|
||||
</div>
|
||||
|
||||
<div class="toggle-group">
|
||||
<div class="toggle-label">
|
||||
<span class="title">Email Notifications</span>
|
||||
<span class="description">Receive email updates about your account activity</span>
|
||||
</div>
|
||||
<div class="toggle active"></div>
|
||||
</div>
|
||||
|
||||
<div class="toggle-group">
|
||||
<div class="toggle-label">
|
||||
<span class="title">Push Notifications</span>
|
||||
<span class="description">Receive push notifications on your device</span>
|
||||
</div>
|
||||
<div class="toggle active"></div>
|
||||
</div>
|
||||
|
||||
<div class="toggle-group">
|
||||
<div class="toggle-label">
|
||||
<span class="title">Weekly Digest</span>
|
||||
<span class="description">Get a weekly summary of your activity</span>
|
||||
</div>
|
||||
<div class="toggle"></div>
|
||||
</div>
|
||||
|
||||
<div class="toggle-group">
|
||||
<div class="toggle-label">
|
||||
<span class="title">Marketing Emails</span>
|
||||
<span class="description">Receive tips, updates, and promotions</span>
|
||||
</div>
|
||||
<div class="toggle"></div>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn btn-primary">Save Preferences</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderSecurity() {
|
||||
return html`
|
||||
<div class="panel-header">
|
||||
<h2>Security Settings</h2>
|
||||
<p>Manage your password and security options</p>
|
||||
</div>
|
||||
|
||||
<div class="security-item">
|
||||
<div class="info">
|
||||
<span class="title">Password</span>
|
||||
<span class="status">Last changed 30 days ago</span>
|
||||
</div>
|
||||
<button class="btn btn-secondary">Change Password</button>
|
||||
</div>
|
||||
|
||||
<div class="security-item">
|
||||
<div class="info">
|
||||
<span class="title">Two-Factor Authentication</span>
|
||||
<span class="status enabled">Enabled</span>
|
||||
</div>
|
||||
<button class="btn btn-secondary">Manage</button>
|
||||
</div>
|
||||
|
||||
<div class="security-item">
|
||||
<div class="info">
|
||||
<span class="title">Active Sessions</span>
|
||||
<span class="status">3 devices</span>
|
||||
</div>
|
||||
<button class="btn btn-secondary">View All</button>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn btn-danger">Delete Account</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@design.estate/dees-wcctools',
|
||||
version: '2.0.1',
|
||||
version: '3.5.2',
|
||||
description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.'
|
||||
}
|
||||
|
||||
211
ts_web/elements/wcc-contextmenu.ts
Normal file
211
ts_web/elements/wcc-contextmenu.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { DeesElement, property, html, customElement, type TemplateResult, state, css, cssManager } from '@design.estate/dees-element';
|
||||
|
||||
export interface IContextMenuItem {
|
||||
name: string;
|
||||
iconName?: string;
|
||||
action: () => void | Promise<void>;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@customElement('wcc-contextmenu')
|
||||
export class WccContextmenu extends DeesElement {
|
||||
// Static method to show context menu at position
|
||||
public static async show(
|
||||
event: MouseEvent,
|
||||
menuItems: IContextMenuItem[]
|
||||
): Promise<void> {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Remove any existing context menu
|
||||
const existing = document.querySelector('wcc-contextmenu');
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
}
|
||||
|
||||
const menu = new WccContextmenu();
|
||||
menu.menuItems = menuItems;
|
||||
menu.x = event.clientX;
|
||||
menu.y = event.clientY;
|
||||
|
||||
document.body.appendChild(menu);
|
||||
|
||||
// Wait for render then adjust position if needed
|
||||
await menu.updateComplete;
|
||||
menu.adjustPosition();
|
||||
}
|
||||
|
||||
@property({ type: Array })
|
||||
accessor menuItems: IContextMenuItem[] = [];
|
||||
|
||||
@property({ type: Number })
|
||||
accessor x: number = 0;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor y: number = 0;
|
||||
|
||||
@state()
|
||||
accessor visible: boolean = false;
|
||||
|
||||
private boundHandleOutsideClick = this.handleOutsideClick.bind(this);
|
||||
private boundHandleKeydown = this.handleKeydown.bind(this);
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-5px);
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:host(.visible) {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.menu {
|
||||
min-width: 160px;
|
||||
background: #0f0f0f;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
padding: 4px 0;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
color: #ccc;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.menu-item.disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.menu-item .icon {
|
||||
font-family: 'Material Symbols Outlined';
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 24;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.menu-item:hover .icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.menu-item .label {
|
||||
flex: 1;
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="menu">
|
||||
${this.menuItems.map(item => html`
|
||||
<div
|
||||
class="menu-item ${item.disabled ? 'disabled' : ''}"
|
||||
@click=${() => this.handleItemClick(item)}
|
||||
>
|
||||
${item.iconName ? html`<span class="icon">${item.iconName}</span>` : null}
|
||||
<span class="label">${item.name}</span>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
await super.connectedCallback();
|
||||
// Delay adding listeners to avoid immediate close
|
||||
requestAnimationFrame(() => {
|
||||
document.addEventListener('click', this.boundHandleOutsideClick);
|
||||
document.addEventListener('contextmenu', this.boundHandleOutsideClick);
|
||||
document.addEventListener('keydown', this.boundHandleKeydown);
|
||||
this.classList.add('visible');
|
||||
});
|
||||
}
|
||||
|
||||
async disconnectedCallback() {
|
||||
await super.disconnectedCallback();
|
||||
document.removeEventListener('click', this.boundHandleOutsideClick);
|
||||
document.removeEventListener('contextmenu', this.boundHandleOutsideClick);
|
||||
document.removeEventListener('keydown', this.boundHandleKeydown);
|
||||
}
|
||||
|
||||
private adjustPosition() {
|
||||
const rect = this.getBoundingClientRect();
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
let x = this.x;
|
||||
let y = this.y;
|
||||
|
||||
// Adjust if menu goes off right edge
|
||||
if (x + rect.width > windowWidth - 10) {
|
||||
x = windowWidth - rect.width - 10;
|
||||
}
|
||||
|
||||
// Adjust if menu goes off bottom edge
|
||||
if (y + rect.height > windowHeight - 10) {
|
||||
y = windowHeight - rect.height - 10;
|
||||
}
|
||||
|
||||
// Ensure not off left or top
|
||||
if (x < 10) x = 10;
|
||||
if (y < 10) y = 10;
|
||||
|
||||
this.style.left = `${x}px`;
|
||||
this.style.top = `${y}px`;
|
||||
}
|
||||
|
||||
private handleOutsideClick(e: Event) {
|
||||
const path = e.composedPath();
|
||||
if (!path.includes(this)) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
private handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
private async handleItemClick(item: IContextMenuItem) {
|
||||
if (item.disabled) return;
|
||||
await item.action();
|
||||
this.close();
|
||||
}
|
||||
|
||||
private close() {
|
||||
this.classList.remove('visible');
|
||||
setTimeout(() => this.remove(), 150);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DeesElement, property, html, customElement, type TemplateResult, queryAsync, render, domtools } from '@design.estate/dees-element';
|
||||
import { resolveTemplateFactory, getDemoAtIndex, getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
|
||||
import type { TTemplateFactory } from './wcctools.helpers.js';
|
||||
import type { IWccConfig, IWccSection, TElementType } from '../wcctools.interfaces.js';
|
||||
|
||||
import * as plugins from '../wcctools.plugins.js';
|
||||
|
||||
@@ -9,13 +10,37 @@ import './wcc-frame.js';
|
||||
import './wcc-sidebar.js';
|
||||
import './wcc-properties.js';
|
||||
import { type TTheme } from './wcc-properties.js';
|
||||
import { type TElementType } from './wcc-sidebar.js';
|
||||
import { breakpoints } from '@design.estate/dees-domtools';
|
||||
import { WccFrame } from './wcc-frame.js';
|
||||
|
||||
/**
|
||||
* Get filtered and sorted items from a section
|
||||
*/
|
||||
export const getSectionItems = (section: IWccSection): Array<[string, any]> => {
|
||||
let entries = Object.entries(section.items);
|
||||
|
||||
// Apply filter if provided
|
||||
if (section.filter) {
|
||||
entries = entries.filter(([name, item]) => section.filter(name, item));
|
||||
}
|
||||
|
||||
// Apply sort if provided
|
||||
if (section.sort) {
|
||||
entries.sort(section.sort);
|
||||
}
|
||||
|
||||
return entries;
|
||||
};
|
||||
|
||||
@customElement('wcc-dashboard')
|
||||
export class WccDashboard extends DeesElement {
|
||||
|
||||
@property()
|
||||
accessor sections: IWccSection[] = [];
|
||||
|
||||
@property()
|
||||
accessor selectedSection: IWccSection | null = null;
|
||||
|
||||
@property()
|
||||
accessor selectedType: TElementType;
|
||||
|
||||
@@ -35,13 +60,20 @@ export class WccDashboard extends DeesElement {
|
||||
accessor selectedTheme: TTheme = 'dark';
|
||||
|
||||
@property()
|
||||
accessor isFullscreen: boolean = false;
|
||||
accessor searchQuery: string = '';
|
||||
|
||||
@property()
|
||||
accessor pages: Record<string, TTemplateFactory> = {};
|
||||
// Pinned items as Set of "sectionName::itemName"
|
||||
@property({ attribute: false })
|
||||
accessor pinnedItems: Set<string> = new Set();
|
||||
|
||||
@property()
|
||||
accessor elements: { [key: string]: DeesElement } = {};
|
||||
// Sidebar width (resizable)
|
||||
@property({ type: Number })
|
||||
accessor sidebarWidth: number = 200;
|
||||
|
||||
// Derived from selectedViewport - no need for separate property
|
||||
public get isNative(): boolean {
|
||||
return this.selectedViewport === 'native';
|
||||
}
|
||||
|
||||
@property()
|
||||
accessor warning: string = null;
|
||||
@@ -53,20 +85,33 @@ export class WccDashboard extends DeesElement {
|
||||
@queryAsync('wcc-frame')
|
||||
accessor wccFrame: Promise<WccFrame>;
|
||||
|
||||
constructor(
|
||||
elementsArg?: { [key: string]: DeesElement },
|
||||
pagesArg?: Record<string, TTemplateFactory>
|
||||
) {
|
||||
constructor(config?: IWccConfig) {
|
||||
super();
|
||||
if (elementsArg) {
|
||||
this.elements = elementsArg;
|
||||
console.log('got elements:');
|
||||
console.log(this.elements);
|
||||
if (config && config.sections) {
|
||||
this.sections = config.sections;
|
||||
console.log('got sections:', this.sections.map(s => s.name));
|
||||
}
|
||||
}
|
||||
|
||||
if (pagesArg) {
|
||||
this.pages = pagesArg;
|
||||
/**
|
||||
* Find an item by name across all sections, returns the item and its section
|
||||
*/
|
||||
public findItemByName(name: string): { item: any; section: IWccSection } | null {
|
||||
for (const section of this.sections) {
|
||||
const entries = getSectionItems(section);
|
||||
const found = entries.find(([itemName]) => itemName === name);
|
||||
if (found) {
|
||||
return { item: found[1], section };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a section by name (URL-decoded)
|
||||
*/
|
||||
public findSectionByName(name: string): IWccSection | null {
|
||||
return this.sections.find(s => s.name === name) || null;
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
@@ -84,7 +129,10 @@ export class WccDashboard extends DeesElement {
|
||||
<wcc-sidebar
|
||||
.dashboardRef=${this}
|
||||
.selectedItem=${this.selectedItem}
|
||||
.isFullscreen=${this.isFullscreen}
|
||||
.searchQuery=${this.searchQuery}
|
||||
.pinnedItems=${this.pinnedItems}
|
||||
.sidebarWidth=${this.sidebarWidth}
|
||||
.isNative=${this.isNative}
|
||||
@selectedType=${(eventArg) => {
|
||||
this.selectedType = eventArg.detail;
|
||||
}}
|
||||
@@ -94,6 +142,23 @@ export class WccDashboard extends DeesElement {
|
||||
@selectedItem=${(eventArg) => {
|
||||
this.selectedItem = eventArg.detail;
|
||||
}}
|
||||
@searchChanged=${(eventArg: CustomEvent) => {
|
||||
this.searchQuery = eventArg.detail;
|
||||
this.updateUrlWithScrollState();
|
||||
}}
|
||||
@pinnedChanged=${(eventArg: CustomEvent) => {
|
||||
this.pinnedItems = eventArg.detail;
|
||||
this.updateUrlWithScrollState();
|
||||
}}
|
||||
@widthChanged=${async (eventArg: CustomEvent) => {
|
||||
this.sidebarWidth = eventArg.detail;
|
||||
this.updateUrlWithScrollState();
|
||||
const frame = await this.wccFrame;
|
||||
if (frame) {
|
||||
frame.sidebarWidth = eventArg.detail;
|
||||
frame.requestUpdate();
|
||||
}
|
||||
}}
|
||||
></wcc-sidebar>
|
||||
<wcc-properties
|
||||
.dashboardRef=${this}
|
||||
@@ -101,7 +166,8 @@ export class WccDashboard extends DeesElement {
|
||||
.selectedItem=${this.selectedItem}
|
||||
.selectedViewport=${this.selectedViewport}
|
||||
.selectedTheme=${this.selectedTheme}
|
||||
.isFullscreen=${this.isFullscreen}
|
||||
.isNative=${this.isNative}
|
||||
.sidebarWidth=${this.sidebarWidth}
|
||||
@selectedViewport=${(eventArg) => {
|
||||
this.selectedViewport = eventArg.detail;
|
||||
this.scheduleUpdate();
|
||||
@@ -116,11 +182,11 @@ export class WccDashboard extends DeesElement {
|
||||
frame.requestUpdate();
|
||||
}
|
||||
}}
|
||||
@toggleFullscreen=${() => {
|
||||
this.toggleFullscreen();
|
||||
@toggleNative=${() => {
|
||||
this.toggleNative();
|
||||
}}
|
||||
></wcc-properties>
|
||||
<wcc-frame id="wccFrame" viewport=${this.selectedViewport} .isFullscreen=${this.isFullscreen}>
|
||||
<wcc-frame id="wccFrame" viewport=${this.selectedViewport} .isNative=${this.isNative} .sidebarWidth=${this.sidebarWidth}>
|
||||
</wcc-frame>
|
||||
`;
|
||||
}
|
||||
@@ -135,17 +201,20 @@ export class WccDashboard extends DeesElement {
|
||||
}
|
||||
}
|
||||
|
||||
public toggleFullscreen() {
|
||||
this.isFullscreen = !this.isFullscreen;
|
||||
public toggleNative() {
|
||||
// Toggle between 'native' and 'desktop' viewports
|
||||
this.selectedViewport = this.selectedViewport === 'native' ? 'desktop' : 'native';
|
||||
this.buildUrl();
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
this.domtools = await plugins.deesDomtools.DomTools.setupDomTools();
|
||||
|
||||
// Add ESC key handler for fullscreen mode
|
||||
// Add ESC key handler for native mode
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape' && this.isFullscreen) {
|
||||
this.isFullscreen = false;
|
||||
if (event.key === 'Escape' && this.isNative) {
|
||||
this.selectedViewport = 'desktop';
|
||||
this.buildUrl();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -154,37 +223,88 @@ export class WccDashboard extends DeesElement {
|
||||
this.setupScrollListeners();
|
||||
}, 500);
|
||||
|
||||
// Route with demo index (new format)
|
||||
// New route format with section name
|
||||
this.domtools.router.on(
|
||||
'/wcctools-route/:itemType/:itemName/:demoIndex/:viewport/:theme',
|
||||
'/wcctools-route/:sectionName/:itemName/:demoIndex/:viewport/:theme',
|
||||
async (routeInfo) => {
|
||||
this.selectedType = routeInfo.params.itemType as TElementType;
|
||||
const sectionName = decodeURIComponent(routeInfo.params.sectionName);
|
||||
this.selectedSection = this.findSectionByName(sectionName);
|
||||
this.selectedItemName = routeInfo.params.itemName;
|
||||
this.selectedDemoIndex = parseInt(routeInfo.params.demoIndex) || 0;
|
||||
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
|
||||
this.selectedTheme = routeInfo.params.theme as TTheme;
|
||||
if (routeInfo.params.itemType === 'element') {
|
||||
this.selectedItem = this.elements[routeInfo.params.itemName];
|
||||
} else if (routeInfo.params.itemType === 'page') {
|
||||
this.selectedItem = this.pages[routeInfo.params.itemName];
|
||||
|
||||
if (this.selectedSection) {
|
||||
// Find item within the section
|
||||
const entries = getSectionItems(this.selectedSection);
|
||||
const found = entries.find(([name]) => name === routeInfo.params.itemName);
|
||||
if (found) {
|
||||
this.selectedItem = found[1];
|
||||
this.selectedType = this.selectedSection.type === 'elements' ? 'element' : 'page';
|
||||
}
|
||||
} else {
|
||||
// Fallback: try legacy format (element/page as section name)
|
||||
const legacyType = routeInfo.params.sectionName;
|
||||
if (legacyType === 'element' || legacyType === 'page') {
|
||||
this.selectedType = legacyType as TElementType;
|
||||
// Find item in any matching section
|
||||
const result = this.findItemByName(routeInfo.params.itemName);
|
||||
if (result) {
|
||||
this.selectedItem = result.item;
|
||||
this.selectedSection = result.section;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore scroll positions from query parameters
|
||||
// Restore state from query parameters
|
||||
if (routeInfo.queryParams) {
|
||||
const search = routeInfo.queryParams.search;
|
||||
const frameScrollY = routeInfo.queryParams.frameScrollY;
|
||||
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
|
||||
const pinned = routeInfo.queryParams.pinned;
|
||||
const sidebarWidth = routeInfo.queryParams.sidebarWidth;
|
||||
|
||||
if (search) {
|
||||
this.searchQuery = search;
|
||||
} else {
|
||||
this.searchQuery = '';
|
||||
}
|
||||
if (frameScrollY) {
|
||||
this.frameScrollY = parseInt(frameScrollY);
|
||||
}
|
||||
if (sidebarScrollY) {
|
||||
this.sidebarScrollY = parseInt(sidebarScrollY);
|
||||
}
|
||||
if (pinned) {
|
||||
const newPinned = new Set(pinned.split(',').filter(Boolean));
|
||||
// Only update if actually different to avoid update loops
|
||||
if (this.pinnedItems.size !== newPinned.size ||
|
||||
![...newPinned].every(k => this.pinnedItems.has(k))) {
|
||||
this.pinnedItems = newPinned;
|
||||
}
|
||||
} else if (this.pinnedItems.size > 0) {
|
||||
this.pinnedItems = new Set();
|
||||
}
|
||||
if (sidebarWidth) {
|
||||
this.sidebarWidth = parseInt(sidebarWidth, 10);
|
||||
}
|
||||
|
||||
// Apply scroll positions after a short delay to ensure DOM is ready
|
||||
setTimeout(() => {
|
||||
// Apply scroll positions and update frame after a short delay to ensure DOM is ready
|
||||
setTimeout(async () => {
|
||||
this.applyScrollPositions();
|
||||
// Ensure frame gets the sidebarWidth
|
||||
const frame = await this.wccFrame;
|
||||
if (frame) {
|
||||
frame.sidebarWidth = this.sidebarWidth;
|
||||
frame.requestUpdate();
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
this.searchQuery = '';
|
||||
// Only clear if not already empty to avoid update loops
|
||||
if (this.pinnedItems.size > 0) {
|
||||
this.pinnedItems = new Set();
|
||||
}
|
||||
}
|
||||
|
||||
const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
|
||||
@@ -196,35 +316,78 @@ export class WccDashboard extends DeesElement {
|
||||
|
||||
// Legacy route without demo index (for backwards compatibility)
|
||||
this.domtools.router.on(
|
||||
'/wcctools-route/:itemType/:itemName/:viewport/:theme',
|
||||
'/wcctools-route/:sectionName/:itemName/:viewport/:theme',
|
||||
async (routeInfo) => {
|
||||
this.selectedType = routeInfo.params.itemType as TElementType;
|
||||
const sectionName = decodeURIComponent(routeInfo.params.sectionName);
|
||||
this.selectedSection = this.findSectionByName(sectionName);
|
||||
this.selectedItemName = routeInfo.params.itemName;
|
||||
this.selectedDemoIndex = 0; // Default to first demo
|
||||
this.selectedDemoIndex = 0;
|
||||
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
|
||||
this.selectedTheme = routeInfo.params.theme as TTheme;
|
||||
if (routeInfo.params.itemType === 'element') {
|
||||
this.selectedItem = this.elements[routeInfo.params.itemName];
|
||||
} else if (routeInfo.params.itemType === 'page') {
|
||||
this.selectedItem = this.pages[routeInfo.params.itemName];
|
||||
|
||||
if (this.selectedSection) {
|
||||
const entries = getSectionItems(this.selectedSection);
|
||||
const found = entries.find(([name]) => name === routeInfo.params.itemName);
|
||||
if (found) {
|
||||
this.selectedItem = found[1];
|
||||
this.selectedType = this.selectedSection.type === 'elements' ? 'element' : 'page';
|
||||
}
|
||||
} else {
|
||||
// Fallback: try legacy format
|
||||
const legacyType = routeInfo.params.sectionName;
|
||||
if (legacyType === 'element' || legacyType === 'page') {
|
||||
this.selectedType = legacyType as TElementType;
|
||||
const result = this.findItemByName(routeInfo.params.itemName);
|
||||
if (result) {
|
||||
this.selectedItem = result.item;
|
||||
this.selectedSection = result.section;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore scroll positions from query parameters
|
||||
// Restore state from query parameters
|
||||
if (routeInfo.queryParams) {
|
||||
const search = routeInfo.queryParams.search;
|
||||
const frameScrollY = routeInfo.queryParams.frameScrollY;
|
||||
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
|
||||
const pinned = routeInfo.queryParams.pinned;
|
||||
const sidebarWidth = routeInfo.queryParams.sidebarWidth;
|
||||
|
||||
if (search) {
|
||||
this.searchQuery = search;
|
||||
} else {
|
||||
this.searchQuery = '';
|
||||
}
|
||||
if (frameScrollY) {
|
||||
this.frameScrollY = parseInt(frameScrollY);
|
||||
}
|
||||
if (sidebarScrollY) {
|
||||
this.sidebarScrollY = parseInt(sidebarScrollY);
|
||||
}
|
||||
if (pinned) {
|
||||
const newPinned = new Set(pinned.split(',').filter(Boolean));
|
||||
// Only update if actually different to avoid update loops
|
||||
if (this.pinnedItems.size !== newPinned.size ||
|
||||
![...newPinned].every(k => this.pinnedItems.has(k))) {
|
||||
this.pinnedItems = newPinned;
|
||||
}
|
||||
} else if (this.pinnedItems.size > 0) {
|
||||
this.pinnedItems = new Set();
|
||||
}
|
||||
if (sidebarWidth) {
|
||||
this.sidebarWidth = parseInt(sidebarWidth, 10);
|
||||
}
|
||||
|
||||
// Apply scroll positions after a short delay to ensure DOM is ready
|
||||
setTimeout(() => {
|
||||
this.applyScrollPositions();
|
||||
}, 100);
|
||||
} else {
|
||||
this.searchQuery = '';
|
||||
// Only clear if not already empty to avoid update loops
|
||||
if (this.pinnedItems.size > 0) {
|
||||
this.pinnedItems = new Set();
|
||||
}
|
||||
}
|
||||
|
||||
const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
|
||||
@@ -292,15 +455,27 @@ export class WccDashboard extends DeesElement {
|
||||
}
|
||||
|
||||
public buildUrl() {
|
||||
const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
|
||||
const sectionName = this.selectedSection
|
||||
? encodeURIComponent(this.selectedSection.name)
|
||||
: this.selectedType; // Fallback for legacy
|
||||
const baseUrl = `/wcctools-route/${sectionName}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (this.searchQuery) {
|
||||
queryParams.set('search', this.searchQuery);
|
||||
}
|
||||
if (this.frameScrollY > 0) {
|
||||
queryParams.set('frameScrollY', this.frameScrollY.toString());
|
||||
}
|
||||
if (this.sidebarScrollY > 0) {
|
||||
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
|
||||
}
|
||||
if (this.pinnedItems.size > 0) {
|
||||
queryParams.set('pinned', Array.from(this.pinnedItems).join(','));
|
||||
}
|
||||
if (this.sidebarWidth !== 200) {
|
||||
queryParams.set('sidebarWidth', this.sidebarWidth.toString());
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||||
@@ -346,15 +521,27 @@ export class WccDashboard extends DeesElement {
|
||||
}
|
||||
|
||||
private updateUrlWithScrollState() {
|
||||
const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
|
||||
const sectionName = this.selectedSection
|
||||
? encodeURIComponent(this.selectedSection.name)
|
||||
: this.selectedType; // Fallback for legacy
|
||||
const baseUrl = `/wcctools-route/${sectionName}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (this.searchQuery) {
|
||||
queryParams.set('search', this.searchQuery);
|
||||
}
|
||||
if (this.frameScrollY > 0) {
|
||||
queryParams.set('frameScrollY', this.frameScrollY.toString());
|
||||
}
|
||||
if (this.sidebarScrollY > 0) {
|
||||
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
|
||||
}
|
||||
if (this.pinnedItems.size > 0) {
|
||||
queryParams.set('pinned', Array.from(this.pinnedItems).join(','));
|
||||
}
|
||||
if (this.sidebarWidth !== 200) {
|
||||
queryParams.set('sidebarWidth', this.sidebarWidth.toString());
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||||
|
||||
@@ -17,15 +17,20 @@ export class WccFrame extends DeesElement {
|
||||
accessor advancedEditorOpen: boolean = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor isFullscreen: boolean = false;
|
||||
accessor isNative: boolean = false;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor sidebarWidth: number = 200;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor isResizing: boolean = false;
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
border: 10px solid #ffaeaf;
|
||||
border: 10px solid rgba(255, 174, 175, 1);
|
||||
position: absolute;
|
||||
background: ${cssManager.bdTheme('#333', '#000')};
|
||||
left: 200px;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
overflow-y: auto;
|
||||
@@ -46,19 +51,19 @@ export class WccFrame extends DeesElement {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
${this.isFullscreen ? `
|
||||
border: none !important;
|
||||
${this.isNative ? `
|
||||
border: 0px solid rgba(255, 174, 175, 0) !important;
|
||||
left: 0px !important;
|
||||
right: 0px !important;
|
||||
top: 0px !important;
|
||||
bottom: 0px !important;
|
||||
` : `
|
||||
bottom: ${this.advancedEditorOpen ? '400px' : '100px'};
|
||||
border: 10px solid #ffaeaf;
|
||||
left: 200px;
|
||||
border: 10px solid rgba(255, 174, 175, 1);
|
||||
left: ${this.sidebarWidth}px;
|
||||
`}
|
||||
transition: all 0.3s ease;
|
||||
${this.isFullscreen ? 'padding: 0px;' : (() => {
|
||||
transition: ${this.isResizing ? 'none' : 'all 0.3s ease'};
|
||||
${this.isNative ? 'padding: 0px;' : (() => {
|
||||
switch (this.viewport) {
|
||||
case 'desktop':
|
||||
return `
|
||||
@@ -67,19 +72,19 @@ export class WccFrame extends DeesElement {
|
||||
case 'tablet':
|
||||
return `
|
||||
padding: 0px ${
|
||||
(document.body.clientWidth - 200 - domtools.breakpoints.tablet) / 2
|
||||
(document.body.clientWidth - this.sidebarWidth - domtools.breakpoints.tablet) / 2
|
||||
}px;
|
||||
`;
|
||||
case 'phablet':
|
||||
return `
|
||||
padding: 0px ${
|
||||
(document.body.clientWidth - 200 - domtools.breakpoints.phablet) / 2
|
||||
(document.body.clientWidth - this.sidebarWidth - domtools.breakpoints.phablet) / 2
|
||||
}px;
|
||||
`;
|
||||
case 'phone':
|
||||
return `
|
||||
padding: 0px ${
|
||||
(document.body.clientWidth - 200 - domtools.breakpoints.phone) / 2
|
||||
(document.body.clientWidth - this.sidebarWidth - domtools.breakpoints.phone) / 2
|
||||
}px;
|
||||
`;
|
||||
}
|
||||
@@ -87,7 +92,7 @@ export class WccFrame extends DeesElement {
|
||||
}
|
||||
|
||||
.viewport {
|
||||
${!this.isFullscreen && this.viewport !== 'desktop'
|
||||
${!this.isNative && this.viewport !== 'desktop'
|
||||
? html` border-right: 1px dotted #444; border-left: 1px dotted #444; `
|
||||
: html``
|
||||
}
|
||||
|
||||
@@ -35,7 +35,10 @@ export class WccProperties extends DeesElement {
|
||||
accessor warning: string = null;
|
||||
|
||||
@property()
|
||||
accessor isFullscreen: boolean = false;
|
||||
accessor isNative: boolean = false;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor sidebarWidth: number = 200;
|
||||
|
||||
@state()
|
||||
accessor propertyContent: TemplateResult[] = [];
|
||||
@@ -60,6 +63,10 @@ export class WccProperties extends DeesElement {
|
||||
@state()
|
||||
accessor recordingDuration: number = 0;
|
||||
|
||||
// Delayed hide for native mode transition
|
||||
@state()
|
||||
accessor isHidden: boolean = false;
|
||||
|
||||
public editorHeight: number = 300;
|
||||
|
||||
public render(): TemplateResult {
|
||||
@@ -89,18 +96,18 @@ export class WccProperties extends DeesElement {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
left: 200px;
|
||||
left: ${this.sidebarWidth}px;
|
||||
height: ${this.editingProperties.length > 0 ? 100 + this.editorHeight : 100}px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
overflow: hidden;
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
display: ${this.isFullscreen ? 'none' : 'block'};
|
||||
display: ${this.isHidden ? 'none' : 'block'};
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 150px 300px 70px 70px;
|
||||
grid-template-columns: 1fr 150px 350px 100px;
|
||||
height: 100%;
|
||||
}
|
||||
.properties {
|
||||
@@ -197,7 +204,8 @@ export class WccProperties extends DeesElement {
|
||||
}
|
||||
|
||||
.viewportSelector,
|
||||
.themeSelector {
|
||||
.themeSelector,
|
||||
.shareSelector {
|
||||
user-select: none;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
@@ -206,12 +214,22 @@ export class WccProperties extends DeesElement {
|
||||
}
|
||||
.selectorButtons2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
flex: 1;
|
||||
}
|
||||
.selectorButtons4 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
flex: 1;
|
||||
}
|
||||
.selectorButtons5 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
flex: 1;
|
||||
}
|
||||
.selectorButtons1 {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
flex: 1;
|
||||
}
|
||||
.button {
|
||||
@@ -248,6 +266,11 @@ export class WccProperties extends DeesElement {
|
||||
.button .material-symbols-outlined {
|
||||
font-size: 18px;
|
||||
font-variation-settings: 'FILL' 0, 'wght' 300;
|
||||
min-width: 18px;
|
||||
min-height: 18px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.button.selected .material-symbols-outlined {
|
||||
@@ -290,7 +313,7 @@ export class WccProperties extends DeesElement {
|
||||
top: 0.5rem;
|
||||
bottom: 0.5rem;
|
||||
left: 0.5rem;
|
||||
right: calc(520px + 0.5rem);
|
||||
right: calc(600px + 0.5rem);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -623,7 +646,7 @@ export class WccProperties extends DeesElement {
|
||||
</div>
|
||||
<div class="viewportSelector">
|
||||
<div class="panelheading">Viewport</div>
|
||||
<div class="selectorButtons4">
|
||||
<div class="selectorButtons5">
|
||||
<div
|
||||
class="button ${this.selectedViewport === 'phone' ? 'selected' : null}"
|
||||
@click=${() => {
|
||||
@@ -649,29 +672,34 @@ export class WccProperties extends DeesElement {
|
||||
Tablet<i class="material-symbols-outlined">tablet</i>
|
||||
</div>
|
||||
<div
|
||||
class="button ${this.selectedViewport === 'desktop' ||
|
||||
this.selectedViewport === 'native'
|
||||
? 'selected'
|
||||
: null}"
|
||||
class="button ${this.selectedViewport === 'desktop' ? 'selected' : null}"
|
||||
@click=${() => {
|
||||
this.selectViewport('native');
|
||||
this.selectViewport('desktop');
|
||||
}}
|
||||
>
|
||||
Desktop<i class="material-symbols-outlined">desktop_windows</i>
|
||||
</div>
|
||||
<div
|
||||
class="button ${this.selectedViewport === 'native' ? 'selected' : null}"
|
||||
@click=${() => {
|
||||
this.selectViewport('native');
|
||||
}}
|
||||
>
|
||||
Native<i class="material-symbols-outlined">fullscreen</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="docs" @click=${() => this.toggleFullscreen()}>
|
||||
<i class="material-symbols-outlined" style="font-size: 20px;">
|
||||
${this.isFullscreen ? 'fullscreen_exit' : 'fullscreen'}
|
||||
</i>
|
||||
<div class="shareSelector">
|
||||
<div class="panelheading">Share</div>
|
||||
<div class="selectorButtons1">
|
||||
<div
|
||||
class="button ${this.isRecording ? 'selected' : ''}"
|
||||
@click=${() => this.handleRecordButtonClick()}
|
||||
>
|
||||
Record<i class="material-symbols-outlined">${this.isRecording ? 'stop_circle' : 'videocam'}</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Recording Button -->
|
||||
<wcc-record-button
|
||||
.state=${this.isRecording ? 'recording' : 'idle'}
|
||||
.duration=${this.recordingDuration}
|
||||
@record-click=${() => this.handleRecordButtonClick()}
|
||||
></wcc-record-button>
|
||||
</div>
|
||||
${this.warning ? html`<div class="warning">${this.warning}</div>` : null}
|
||||
</div>
|
||||
@@ -904,16 +932,29 @@ export class WccProperties extends DeesElement {
|
||||
this.dashboardRef.buildUrl();
|
||||
}
|
||||
|
||||
public async scheduleUpdate() {
|
||||
try {
|
||||
await this.createProperties();
|
||||
} catch (error) {
|
||||
console.error('Error creating properties:', error);
|
||||
// Clear property content on error to show clean state
|
||||
this.propertyContent = [];
|
||||
protected updated(changedProperties: Map<string, unknown>) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
// Handle delayed hide for native mode transition
|
||||
if (changedProperties.has('isNative')) {
|
||||
if (this.isNative) {
|
||||
// Delay hiding until frame animation completes
|
||||
setTimeout(() => {
|
||||
this.isHidden = true;
|
||||
}, 300);
|
||||
} else {
|
||||
// Show immediately when exiting native mode
|
||||
this.isHidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Only recreate properties when selectedItem changes
|
||||
if (changedProperties.has('selectedItem')) {
|
||||
this.createProperties().catch(error => {
|
||||
console.error('Error creating properties:', error);
|
||||
this.propertyContent = [];
|
||||
});
|
||||
}
|
||||
// Always call super.scheduleUpdate to ensure component updates
|
||||
super.scheduleUpdate();
|
||||
}
|
||||
|
||||
public selectViewport(viewport: TEnvironment) {
|
||||
@@ -1016,9 +1057,9 @@ export class WccProperties extends DeesElement {
|
||||
);
|
||||
}
|
||||
|
||||
private toggleFullscreen() {
|
||||
private toggleNative() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('toggleFullscreen', {
|
||||
new CustomEvent('toggleNative', {
|
||||
bubbles: true
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as plugins from '../wcctools.plugins.js';
|
||||
import { DeesElement, property, html, customElement, type TemplateResult, state } from '@design.estate/dees-element';
|
||||
import { WccDashboard } from './wcc-dashboard.js';
|
||||
import { WccDashboard, getSectionItems } from './wcc-dashboard.js';
|
||||
import type { TTemplateFactory } from './wcctools.helpers.js';
|
||||
import { getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
|
||||
|
||||
export type TElementType = 'element' | 'page';
|
||||
import type { IWccSection, TElementType } from '../wcctools.interfaces.js';
|
||||
import { WccContextmenu } from './wcc-contextmenu.js';
|
||||
|
||||
@customElement('wcc-sidebar')
|
||||
export class WccSidebar extends DeesElement {
|
||||
@@ -18,12 +18,38 @@ export class WccSidebar extends DeesElement {
|
||||
accessor dashboardRef: WccDashboard;
|
||||
|
||||
@property()
|
||||
accessor isFullscreen: boolean = false;
|
||||
accessor isNative: boolean = false;
|
||||
|
||||
// Track which elements are expanded (for multi-demo elements)
|
||||
@state()
|
||||
accessor expandedElements: Set<string> = new Set();
|
||||
|
||||
// Track which sections are collapsed
|
||||
@state()
|
||||
accessor collapsedSections: Set<string> = new Set();
|
||||
|
||||
// Search query for filtering sidebar items
|
||||
@property()
|
||||
accessor searchQuery: string = '';
|
||||
|
||||
// Pinned items as Set of "sectionName::itemName"
|
||||
@property({ attribute: false })
|
||||
accessor pinnedItems: Set<string> = new Set();
|
||||
|
||||
// Sidebar width (resizable)
|
||||
@property({ type: Number })
|
||||
accessor sidebarWidth: number = 200;
|
||||
|
||||
// Track if currently resizing
|
||||
@state()
|
||||
accessor isResizing: boolean = false;
|
||||
|
||||
// Delayed hide for native mode transition
|
||||
@state()
|
||||
accessor isHidden: boolean = false;
|
||||
|
||||
private sectionsInitialized = false;
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet" />
|
||||
@@ -45,14 +71,14 @@ export class WccSidebar extends DeesElement {
|
||||
--ring: #3b82f6;
|
||||
--radius: 4px;
|
||||
|
||||
display: ${this.isFullscreen ? 'none' : 'block'};
|
||||
display: ${this.isHidden ? 'none' : 'block'};
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
width: 200px;
|
||||
width: ${this.sidebarWidth}px;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
overflow-y: auto;
|
||||
@@ -65,7 +91,7 @@ export class WccSidebar extends DeesElement {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
.section-header {
|
||||
padding: 0.3rem 0.75rem;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 500;
|
||||
@@ -77,12 +103,45 @@ export class WccSidebar extends DeesElement {
|
||||
background: rgba(59, 130, 246, 0.03);
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
h3:first-child {
|
||||
.section-header:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.section-header:hover {
|
||||
background: rgba(59, 130, 246, 0.08);
|
||||
}
|
||||
|
||||
.section-header .expand-icon {
|
||||
font-size: 14px;
|
||||
opacity: 0.5;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.section-header.collapsed .expand-icon {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.section-header .section-icon {
|
||||
font-size: 14px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-content.collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-family: 'Material Symbols Outlined';
|
||||
font-weight: normal;
|
||||
@@ -117,7 +176,11 @@ export class WccSidebar extends DeesElement {
|
||||
}
|
||||
|
||||
.selectOption.folder {
|
||||
grid-template-columns: 16px 20px 1fr;
|
||||
grid-template-columns: 16px 1fr;
|
||||
}
|
||||
|
||||
.selectOption.folder .text {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.selectOption .expand-icon {
|
||||
@@ -214,90 +277,429 @@ export class WccSidebar extends DeesElement {
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
</style>
|
||||
<div class="menu">
|
||||
<h3>Pages</h3>
|
||||
${(() => {
|
||||
const pages = Object.keys(this.dashboardRef.pages);
|
||||
return pages.map(pageName => {
|
||||
const item = this.dashboardRef.pages[pageName];
|
||||
return html`
|
||||
<div
|
||||
class="selectOption ${this.selectedItem === item ? 'selected' : null}"
|
||||
@click=${async () => {
|
||||
const domtools = await plugins.deesDomtools.DomTools.setupDomTools();
|
||||
this.selectItem('page', pageName, item, 0);
|
||||
}}
|
||||
>
|
||||
<i class="material-symbols-outlined">insert_drive_file</i>
|
||||
<div class="text">${pageName}</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
})()}
|
||||
<h3>Elements</h3>
|
||||
${(() => {
|
||||
const elements = Object.keys(this.dashboardRef.elements);
|
||||
return elements.map(elementName => {
|
||||
const item = this.dashboardRef.elements[elementName] as any;
|
||||
const demoCount = item.demo ? getDemoCount(item.demo) : 0;
|
||||
const isMultiDemo = item.demo && hasMultipleDemos(item.demo);
|
||||
const isExpanded = this.expandedElements.has(elementName);
|
||||
const isSelected = this.selectedItem === item;
|
||||
|
||||
if (isMultiDemo) {
|
||||
// Multi-demo element - render as expandable folder
|
||||
return html`
|
||||
<div
|
||||
class="selectOption folder ${isExpanded ? 'expanded' : ''} ${isSelected ? 'selected' : ''}"
|
||||
@click=${() => this.toggleExpanded(elementName)}
|
||||
>
|
||||
<i class="material-symbols-outlined expand-icon">chevron_right</i>
|
||||
<i class="material-symbols-outlined">folder</i>
|
||||
<div class="text">${elementName}</div>
|
||||
</div>
|
||||
${isExpanded ? html`
|
||||
<div class="demo-children">
|
||||
${Array.from({ length: demoCount }, (_, i) => {
|
||||
const demoIndex = i;
|
||||
const isThisDemoSelected = isSelected && this.dashboardRef.selectedDemoIndex === demoIndex;
|
||||
return html`
|
||||
<div
|
||||
class="demo-child ${isThisDemoSelected ? 'selected' : ''}"
|
||||
@click=${async () => {
|
||||
await plugins.deesDomtools.DomTools.setupDomTools();
|
||||
this.selectItem('element', elementName, item, demoIndex);
|
||||
}}
|
||||
>
|
||||
<i class="material-symbols-outlined">play_circle</i>
|
||||
<div class="text">demo${demoIndex + 1}</div>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
` : null}
|
||||
`;
|
||||
} else {
|
||||
// Single demo element - render as normal
|
||||
return html`
|
||||
<div
|
||||
class="selectOption ${isSelected ? 'selected' : null}"
|
||||
@click=${async () => {
|
||||
await plugins.deesDomtools.DomTools.setupDomTools();
|
||||
this.selectItem('element', elementName, item, 0);
|
||||
}}
|
||||
>
|
||||
<i class="material-symbols-outlined">featured_video</i>
|
||||
<div class="text">${elementName}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
})()}
|
||||
.search-container {
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: var(--input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: var(--foreground);
|
||||
font-size: 0.75rem;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background: rgba(59, 130, 246, 0.3);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Pinned item highlight in original section */
|
||||
.selectOption.pinned {
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
}
|
||||
|
||||
.selectOption.pinned:hover {
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
}
|
||||
|
||||
.selectOption.pinned.selected {
|
||||
background: rgba(245, 158, 11, 0.18);
|
||||
}
|
||||
|
||||
/* Pinned section styling */
|
||||
.section-header.pinned-section {
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.section-header.pinned-section:hover {
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
}
|
||||
|
||||
.section-header.pinned-section .section-icon {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Section tag pill for pinned items */
|
||||
.section-tag {
|
||||
font-size: 0.5rem;
|
||||
color: #888;
|
||||
margin-left: auto;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 9999px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Group container */
|
||||
.item-group {
|
||||
margin: 0.375rem 0.375rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 6px;
|
||||
padding: 0.25rem 0;
|
||||
background: rgba(255, 255, 255, 0.01);
|
||||
}
|
||||
|
||||
.item-group-legend {
|
||||
font-size: 0.55rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #555;
|
||||
padding: 0.125rem 0.625rem 0.25rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.item-group .selectOption {
|
||||
margin-left: 0.25rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
/* Resize handle */
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
cursor: col-resize;
|
||||
background: transparent;
|
||||
transition: background 0.15s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.resize-handle:hover {
|
||||
background: rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.resize-handle.active {
|
||||
background: var(--primary);
|
||||
}
|
||||
</style>
|
||||
<div class="search-container">
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="Search..."
|
||||
.value=${this.searchQuery}
|
||||
@input=${this.handleSearchInput}
|
||||
/>
|
||||
</div>
|
||||
<div class="menu">
|
||||
${this.renderPinnedSection()}
|
||||
${this.renderSections()}
|
||||
</div>
|
||||
<div
|
||||
class="resize-handle ${this.isResizing ? 'active' : ''}"
|
||||
@mousedown=${this.startResize}
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize collapsed sections from section config
|
||||
*/
|
||||
private initCollapsedSections() {
|
||||
if (this.sectionsInitialized || !this.dashboardRef?.sections) return;
|
||||
|
||||
const collapsed = new Set<string>();
|
||||
for (const section of this.dashboardRef.sections) {
|
||||
if (section.collapsed) {
|
||||
collapsed.add(section.name);
|
||||
}
|
||||
}
|
||||
this.collapsedSections = collapsed;
|
||||
this.sectionsInitialized = true;
|
||||
}
|
||||
|
||||
// ============ Pinning helpers ============
|
||||
|
||||
private getPinKey(sectionName: string, itemName: string): string {
|
||||
return `${sectionName}::${itemName}`;
|
||||
}
|
||||
|
||||
private isPinned(sectionName: string, itemName: string): boolean {
|
||||
return this.pinnedItems.has(this.getPinKey(sectionName, itemName));
|
||||
}
|
||||
|
||||
private togglePin(sectionName: string, itemName: string) {
|
||||
const key = this.getPinKey(sectionName, itemName);
|
||||
const newPinned = new Set(this.pinnedItems);
|
||||
if (newPinned.has(key)) {
|
||||
newPinned.delete(key);
|
||||
} else {
|
||||
newPinned.add(key);
|
||||
}
|
||||
this.pinnedItems = newPinned;
|
||||
this.dispatchEvent(new CustomEvent('pinnedChanged', { detail: newPinned }));
|
||||
}
|
||||
|
||||
private showContextMenu(e: MouseEvent, sectionName: string, itemName: string) {
|
||||
const isPinned = this.isPinned(sectionName, itemName);
|
||||
WccContextmenu.show(e, [
|
||||
{
|
||||
name: isPinned ? 'Unpin' : 'Pin',
|
||||
iconName: isPinned ? 'push_pin' : 'push_pin',
|
||||
action: () => this.togglePin(sectionName, itemName),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the PINNED section (only if there are pinned items)
|
||||
*/
|
||||
private renderPinnedSection() {
|
||||
if (!this.dashboardRef?.sections || this.pinnedItems.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isCollapsed = this.collapsedSections.has('__pinned__');
|
||||
|
||||
// Collect pinned items with their original section info
|
||||
// Pinned items are NOT filtered by search - they always remain visible
|
||||
const pinnedEntries: Array<{ sectionName: string; itemName: string; item: any; section: IWccSection }> = [];
|
||||
|
||||
for (const key of this.pinnedItems) {
|
||||
const [sectionName, itemName] = key.split('::');
|
||||
const section = this.dashboardRef.sections.find(s => s.name === sectionName);
|
||||
if (section) {
|
||||
const entries = getSectionItems(section);
|
||||
const found = entries.find(([name]) => name === itemName);
|
||||
if (found) {
|
||||
pinnedEntries.push({ sectionName, itemName, item: found[1], section });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pinnedEntries.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="section-header pinned-section ${isCollapsed ? 'collapsed' : ''}"
|
||||
@click=${() => this.toggleSectionCollapsed('__pinned__')}
|
||||
>
|
||||
<i class="material-symbols-outlined expand-icon">expand_more</i>
|
||||
<i class="material-symbols-outlined section-icon">push_pin</i>
|
||||
<span>Pinned</span>
|
||||
</div>
|
||||
<div class="section-content ${isCollapsed ? 'collapsed' : ''}">
|
||||
${pinnedEntries.map(({ sectionName, itemName, item, section }) => {
|
||||
const isSelected = this.selectedItem === item;
|
||||
const type = section.type === 'elements' ? 'element' : 'page';
|
||||
const icon = section.type === 'elements' ? 'featured_video' : 'insert_drive_file';
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="selectOption ${isSelected ? 'selected' : ''}"
|
||||
@click=${async () => {
|
||||
await plugins.deesDomtools.DomTools.setupDomTools();
|
||||
this.selectItem(type, itemName, item, 0, section);
|
||||
}}
|
||||
@contextmenu=${(e: MouseEvent) => this.showContextMenu(e, sectionName, itemName)}
|
||||
>
|
||||
<i class="material-symbols-outlined">${icon}</i>
|
||||
<div class="text">${this.highlightMatch(itemName)}</div>
|
||||
<span class="section-tag">${sectionName}</span>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render all sections
|
||||
*/
|
||||
private renderSections() {
|
||||
if (!this.dashboardRef?.sections) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.initCollapsedSections();
|
||||
|
||||
return this.dashboardRef.sections.map((section) => {
|
||||
// Check if section has any matching items
|
||||
const entries = getSectionItems(section);
|
||||
const filteredEntries = entries.filter(([name]) => this.matchesSearch(name));
|
||||
|
||||
// Hide section if no items match the search
|
||||
if (filteredEntries.length === 0 && this.searchQuery) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isCollapsed = this.collapsedSections.has(section.name);
|
||||
const sectionIcon = section.icon || (section.type === 'pages' ? 'insert_drive_file' : 'widgets');
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="section-header ${isCollapsed ? 'collapsed' : ''}"
|
||||
@click=${() => this.toggleSectionCollapsed(section.name)}
|
||||
>
|
||||
<i class="material-symbols-outlined expand-icon">expand_more</i>
|
||||
${section.icon ? html`<i class="material-symbols-outlined section-icon">${section.icon}</i>` : null}
|
||||
<span>${section.name}</span>
|
||||
</div>
|
||||
<div class="section-content ${isCollapsed ? 'collapsed' : ''}">
|
||||
${this.renderSectionItems(section)}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render items for a section
|
||||
*/
|
||||
private renderSectionItems(section: IWccSection) {
|
||||
const entries = getSectionItems(section);
|
||||
// Filter entries by search query
|
||||
const filteredEntries = entries.filter(([name]) => this.matchesSearch(name));
|
||||
|
||||
if (section.type === 'pages') {
|
||||
return filteredEntries.map(([pageName, item]) => {
|
||||
const isPinned = this.isPinned(section.name, pageName);
|
||||
return html`
|
||||
<div
|
||||
class="selectOption ${this.selectedItem === item ? 'selected' : ''} ${isPinned ? 'pinned' : ''}"
|
||||
@click=${async () => {
|
||||
await plugins.deesDomtools.DomTools.setupDomTools();
|
||||
this.selectItem('page', pageName, item, 0, section);
|
||||
}}
|
||||
@contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, pageName)}
|
||||
>
|
||||
<i class="material-symbols-outlined">insert_drive_file</i>
|
||||
<div class="text">${this.highlightMatch(pageName)}</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
// type === 'elements' - group by demoGroup
|
||||
const groupedItems = new Map<string | null, Array<[string, any]>>();
|
||||
|
||||
for (const entry of filteredEntries) {
|
||||
const [, item] = entry;
|
||||
const group = (item as any).demoGroup || null;
|
||||
if (!groupedItems.has(group)) {
|
||||
groupedItems.set(group, []);
|
||||
}
|
||||
groupedItems.get(group)!.push(entry);
|
||||
}
|
||||
|
||||
const result: TemplateResult[] = [];
|
||||
|
||||
// Render ungrouped items first
|
||||
const ungrouped = groupedItems.get(null) || [];
|
||||
for (const entry of ungrouped) {
|
||||
result.push(this.renderElementItem(entry, section));
|
||||
}
|
||||
|
||||
// Render grouped items
|
||||
for (const [groupName, items] of groupedItems) {
|
||||
if (groupName === null) continue;
|
||||
|
||||
result.push(html`
|
||||
<div class="item-group">
|
||||
<span class="item-group-legend">${groupName}</span>
|
||||
${items.map((entry) => this.renderElementItem(entry, section))}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single element item (used by renderSectionItems)
|
||||
*/
|
||||
private renderElementItem(entry: [string, any], section: IWccSection): TemplateResult {
|
||||
const [elementName, item] = entry;
|
||||
const anonItem = item as any;
|
||||
const demoCount = anonItem.demo ? getDemoCount(anonItem.demo) : 0;
|
||||
const isMultiDemo = anonItem.demo && hasMultipleDemos(anonItem.demo);
|
||||
const isExpanded = this.expandedElements.has(elementName);
|
||||
const isSelected = this.selectedItem === item;
|
||||
const isPinned = this.isPinned(section.name, elementName);
|
||||
|
||||
if (isMultiDemo) {
|
||||
// Multi-demo element - render as expandable folder
|
||||
return html`
|
||||
<div
|
||||
class="selectOption folder ${isExpanded ? 'expanded' : ''} ${isSelected ? 'selected' : ''} ${isPinned ? 'pinned' : ''}"
|
||||
@click=${() => this.toggleExpanded(elementName)}
|
||||
@contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, elementName)}
|
||||
>
|
||||
<i class="material-symbols-outlined expand-icon">chevron_right</i>
|
||||
<div class="text">${this.highlightMatch(elementName)}</div>
|
||||
</div>
|
||||
${isExpanded ? html`
|
||||
<div class="demo-children">
|
||||
${Array.from({ length: demoCount }, (_, i) => {
|
||||
const demoIndex = i;
|
||||
const isThisDemoSelected = isSelected && this.dashboardRef.selectedDemoIndex === demoIndex;
|
||||
return html`
|
||||
<div
|
||||
class="demo-child ${isThisDemoSelected ? 'selected' : ''}"
|
||||
@click=${async () => {
|
||||
await plugins.deesDomtools.DomTools.setupDomTools();
|
||||
this.selectItem('element', elementName, item, demoIndex, section);
|
||||
}}
|
||||
>
|
||||
<i class="material-symbols-outlined">play_circle</i>
|
||||
<div class="text">demo${demoIndex + 1}</div>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
` : null}
|
||||
`;
|
||||
} else {
|
||||
// Single demo element
|
||||
return html`
|
||||
<div
|
||||
class="selectOption ${isSelected ? 'selected' : ''} ${isPinned ? 'pinned' : ''}"
|
||||
@click=${async () => {
|
||||
await plugins.deesDomtools.DomTools.setupDomTools();
|
||||
this.selectItem('element', elementName, item, 0, section);
|
||||
}}
|
||||
@contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, elementName)}
|
||||
>
|
||||
<i class="material-symbols-outlined">featured_video</i>
|
||||
<div class="text">${this.highlightMatch(elementName)}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
private toggleSectionCollapsed(sectionName: string) {
|
||||
const newSet = new Set(this.collapsedSections);
|
||||
if (newSet.has(sectionName)) {
|
||||
newSet.delete(sectionName);
|
||||
} else {
|
||||
newSet.add(sectionName);
|
||||
}
|
||||
this.collapsedSections = newSet;
|
||||
}
|
||||
|
||||
private toggleExpanded(elementName: string) {
|
||||
const newSet = new Set(this.expandedElements);
|
||||
if (newSet.has(elementName)) {
|
||||
@@ -308,14 +710,136 @@ export class WccSidebar extends DeesElement {
|
||||
this.expandedElements = newSet;
|
||||
}
|
||||
|
||||
public selectItem(typeArg: TElementType, itemNameArg: string, itemArg: TTemplateFactory | DeesElement, demoIndex: number = 0) {
|
||||
private handleSearchInput(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
this.searchQuery = input.value;
|
||||
this.dispatchEvent(new CustomEvent('searchChanged', { detail: this.searchQuery }));
|
||||
}
|
||||
|
||||
private matchesSearch(name: string): boolean {
|
||||
if (!this.searchQuery) return true;
|
||||
return name.toLowerCase().includes(this.searchQuery.toLowerCase());
|
||||
}
|
||||
|
||||
private highlightMatch(text: string): TemplateResult {
|
||||
if (!this.searchQuery) return html`${text}`;
|
||||
const lowerText = text.toLowerCase();
|
||||
const lowerQuery = this.searchQuery.toLowerCase();
|
||||
const index = lowerText.indexOf(lowerQuery);
|
||||
if (index === -1) return html`${text}`;
|
||||
const before = text.slice(0, index);
|
||||
const match = text.slice(index, index + this.searchQuery.length);
|
||||
const after = text.slice(index + this.searchQuery.length);
|
||||
return html`${before}<span class="highlight">${match}</span>${after}`;
|
||||
}
|
||||
|
||||
protected updated(changedProperties: Map<string, unknown>) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
// Handle delayed hide for native mode transition
|
||||
if (changedProperties.has('isNative')) {
|
||||
if (this.isNative) {
|
||||
// Delay hiding until frame animation completes
|
||||
setTimeout(() => {
|
||||
this.isHidden = true;
|
||||
}, 300);
|
||||
} else {
|
||||
// Show immediately when exiting native mode
|
||||
this.isHidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-expand folder when a multi-demo element is selected
|
||||
if (changedProperties.has('selectedItem') && this.selectedItem && this.dashboardRef?.sections) {
|
||||
// Find the element in any section
|
||||
for (const section of this.dashboardRef.sections) {
|
||||
if (section.type !== 'elements') continue;
|
||||
|
||||
const entries = getSectionItems(section);
|
||||
const found = entries.find(([_, item]) => item === this.selectedItem);
|
||||
if (found) {
|
||||
const [elementName, item] = found;
|
||||
const anonItem = item as any;
|
||||
if (anonItem.demo && hasMultipleDemos(anonItem.demo)) {
|
||||
if (!this.expandedElements.has(elementName)) {
|
||||
const newSet = new Set(this.expandedElements);
|
||||
newSet.add(elementName);
|
||||
this.expandedElements = newSet;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Resize functionality ============
|
||||
|
||||
private startResize = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
this.isResizing = true;
|
||||
const startX = e.clientX;
|
||||
const startWidth = this.sidebarWidth;
|
||||
|
||||
// Cache references once at start
|
||||
const frame = this.dashboardRef?.shadowRoot?.querySelector('wcc-frame') as any;
|
||||
const properties = this.dashboardRef?.shadowRoot?.querySelector('wcc-properties') as any;
|
||||
|
||||
// Disable frame transition during resize
|
||||
if (frame) {
|
||||
frame.isResizing = true;
|
||||
}
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
const newWidth = Math.min(400, Math.max(150, startWidth + (e.clientX - startX)));
|
||||
this.sidebarWidth = newWidth;
|
||||
// Update frame and properties directly
|
||||
if (frame) {
|
||||
frame.sidebarWidth = newWidth;
|
||||
}
|
||||
if (properties) {
|
||||
properties.sidebarWidth = newWidth;
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
this.isResizing = false;
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
// Re-enable frame transition
|
||||
if (frame) {
|
||||
frame.isResizing = false;
|
||||
}
|
||||
// Dispatch event on release for URL persistence
|
||||
this.dispatchEvent(new CustomEvent('widthChanged', { detail: this.sidebarWidth }));
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
|
||||
public selectItem(
|
||||
typeArg: TElementType,
|
||||
itemNameArg: string,
|
||||
itemArg: TTemplateFactory | DeesElement,
|
||||
demoIndex: number = 0,
|
||||
section?: IWccSection
|
||||
) {
|
||||
console.log('selected item');
|
||||
console.log(itemNameArg);
|
||||
console.log(itemArg);
|
||||
console.log('demo index:', demoIndex);
|
||||
console.log('section:', section?.name);
|
||||
|
||||
this.selectedItem = itemArg;
|
||||
this.selectedType = typeArg;
|
||||
this.dashboardRef.selectedDemoIndex = demoIndex;
|
||||
|
||||
// Set the selected section on dashboard
|
||||
if (section) {
|
||||
this.dashboardRef.selectedSection = section;
|
||||
}
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('selectedType', {
|
||||
detail: typeArg
|
||||
@@ -335,7 +859,6 @@ export class WccSidebar extends DeesElement {
|
||||
this.dashboardRef.buildUrl();
|
||||
|
||||
// Force re-render to update demo child selection indicator
|
||||
// (needed when switching between demos of the same element)
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,86 @@
|
||||
import { WccDashboard } from './elements/wcc-dashboard.js';
|
||||
import { LitElement } from 'lit';
|
||||
import type { TTemplateFactory } from './elements/wcctools.helpers.js';
|
||||
import type { IWccConfig, IWccSection } from './wcctools.interfaces.js';
|
||||
|
||||
// Export recording components and service
|
||||
export { RecorderService, type IRecorderEvents, type IRecordingOptions } from './services/recorder.service.js';
|
||||
export { WccRecordButton } from './elements/wcc-record-button.js';
|
||||
export { WccRecordingPanel } from './elements/wcc-recording-panel.js';
|
||||
|
||||
const setupWccTools = (
|
||||
// Export types for external use
|
||||
export type { IWccConfig, IWccSection } from './wcctools.interfaces.js';
|
||||
|
||||
/**
|
||||
* Converts legacy (elements, pages) format to new sections config
|
||||
*/
|
||||
const convertLegacyToConfig = (
|
||||
elementsArg?: { [key: string]: LitElement },
|
||||
pagesArg?: Record<string, TTemplateFactory>
|
||||
): IWccConfig => {
|
||||
const sections: IWccSection[] = [];
|
||||
|
||||
if (pagesArg && Object.keys(pagesArg).length > 0) {
|
||||
sections.push({
|
||||
name: 'Pages',
|
||||
type: 'pages',
|
||||
items: pagesArg,
|
||||
});
|
||||
}
|
||||
|
||||
if (elementsArg && Object.keys(elementsArg).length > 0) {
|
||||
sections.push({
|
||||
name: 'Elements',
|
||||
type: 'elements',
|
||||
items: elementsArg,
|
||||
});
|
||||
}
|
||||
|
||||
return { sections };
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the argument is the new config format
|
||||
*/
|
||||
const isWccConfig = (arg: any): arg is IWccConfig => {
|
||||
return arg && typeof arg === 'object' && 'sections' in arg && Array.isArray(arg.sections);
|
||||
};
|
||||
|
||||
/**
|
||||
* Setup WCC Tools dashboard
|
||||
*
|
||||
* New format (recommended):
|
||||
* ```typescript
|
||||
* setupWccTools({
|
||||
* sections: [
|
||||
* { name: 'Elements', type: 'elements', items: elements },
|
||||
* { name: 'Pages', type: 'pages', items: pages },
|
||||
* ]
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Legacy format (still supported):
|
||||
* ```typescript
|
||||
* setupWccTools(elements, pages);
|
||||
* ```
|
||||
*/
|
||||
const setupWccTools = (
|
||||
configOrElements?: IWccConfig | { [key: string]: LitElement },
|
||||
pagesArg?: Record<string, TTemplateFactory>
|
||||
) => {
|
||||
let config: IWccConfig;
|
||||
|
||||
if (isWccConfig(configOrElements)) {
|
||||
config = configOrElements;
|
||||
} else {
|
||||
config = convertLegacyToConfig(configOrElements, pagesArg);
|
||||
}
|
||||
|
||||
let hasRun = false;
|
||||
const runWccToolsSetup = async () => {
|
||||
if (document.readyState === 'complete' && !hasRun) {
|
||||
hasRun = true;
|
||||
const wccTools = new WccDashboard(elementsArg as any, pagesArg);
|
||||
const wccTools = new WccDashboard(config);
|
||||
document.querySelector('body').append(wccTools);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,6 +14,40 @@ pnpm add -D @design.estate/dees-wcctools
|
||||
|
||||
## Usage
|
||||
|
||||
### Sections-based Configuration (Recommended)
|
||||
|
||||
```typescript
|
||||
import { setupWccTools } from '@design.estate/dees-wcctools';
|
||||
import * as elements from './elements/index.js';
|
||||
import * as views from './views/index.js';
|
||||
import * as pages from './pages/index.js';
|
||||
|
||||
setupWccTools({
|
||||
sections: [
|
||||
{
|
||||
name: 'Pages',
|
||||
type: 'pages',
|
||||
items: pages,
|
||||
},
|
||||
{
|
||||
name: 'Views',
|
||||
type: 'elements',
|
||||
items: views,
|
||||
icon: 'web',
|
||||
},
|
||||
{
|
||||
name: 'Elements',
|
||||
type: 'elements',
|
||||
items: elements,
|
||||
filter: (name) => !name.startsWith('internal-'),
|
||||
sort: ([a], [b]) => a.localeCompare(b),
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Legacy Format (Still Supported)
|
||||
|
||||
```typescript
|
||||
import { setupWccTools } from '@design.estate/dees-wcctools';
|
||||
import { MyButton } from './components/my-button.js';
|
||||
@@ -30,6 +64,8 @@ setupWccTools({
|
||||
| Export | Description |
|
||||
|--------|-------------|
|
||||
| `setupWccTools` | Initialize the component catalogue dashboard |
|
||||
| `IWccConfig` | TypeScript interface for sections configuration |
|
||||
| `IWccSection` | TypeScript interface for individual section |
|
||||
|
||||
### Recording Components
|
||||
|
||||
@@ -41,6 +77,18 @@ setupWccTools({
|
||||
| `IRecorderEvents` | TypeScript interface for recorder callbacks |
|
||||
| `IRecordingOptions` | TypeScript interface for recording options |
|
||||
|
||||
## Section Configuration
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `name` | `string` | Display name for the section header |
|
||||
| `type` | `'elements' \| 'pages'` | Rendering behavior |
|
||||
| `items` | `Record<string, any>` | Element classes or page factories |
|
||||
| `filter` | `(name, item) => boolean` | Optional filter function |
|
||||
| `sort` | `([a, itemA], [b, itemB]) => number` | Optional sort function |
|
||||
| `icon` | `string` | Material Symbols icon name |
|
||||
| `collapsed` | `boolean` | Start section collapsed (default: false) |
|
||||
|
||||
## Internal Components
|
||||
|
||||
The module includes these internal web components:
|
||||
@@ -48,8 +96,8 @@ The module includes these internal web components:
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| `wcc-dashboard` | Main dashboard container with routing |
|
||||
| `wcc-sidebar` | Navigation sidebar with element/page listing |
|
||||
| `wcc-frame` | Iframe viewport with responsive sizing |
|
||||
| `wcc-sidebar` | Navigation sidebar with collapsible sections |
|
||||
| `wcc-frame` | Responsive viewport with size controls |
|
||||
| `wcc-properties` | Property panel with live editing |
|
||||
| `wcc-record-button` | Recording state indicator button |
|
||||
| `wcc-recording-panel` | Recording workflow UI |
|
||||
@@ -72,14 +120,14 @@ const events: IRecorderEvents = {
|
||||
const recorder = new RecorderService(events);
|
||||
|
||||
// Load available microphones
|
||||
const mics = await recorder.loadMicrophones(true); // true = request permission
|
||||
const mics = await recorder.loadMicrophones(true);
|
||||
|
||||
// Start audio level monitoring
|
||||
await recorder.startAudioMonitoring(mics[0].deviceId);
|
||||
|
||||
// Start recording
|
||||
await recorder.startRecording({
|
||||
mode: 'viewport', // or 'screen'
|
||||
mode: 'viewport',
|
||||
audioDeviceId: mics[0].deviceId,
|
||||
viewportElement: document.querySelector('.viewport'),
|
||||
});
|
||||
@@ -99,10 +147,11 @@ recorder.dispose();
|
||||
```
|
||||
ts_web/
|
||||
├── index.ts # Main exports
|
||||
├── wcctools.interfaces.ts # Type definitions
|
||||
├── elements/
|
||||
│ ├── wcc-dashboard.ts # Root dashboard component
|
||||
│ ├── wcc-sidebar.ts # Navigation sidebar
|
||||
│ ├── wcc-frame.ts # Responsive iframe viewport
|
||||
│ ├── wcc-frame.ts # Responsive viewport
|
||||
│ ├── wcc-properties.ts # Property editing panel
|
||||
│ ├── wcc-record-button.ts # Recording button
|
||||
│ ├── wcc-recording-panel.ts # Recording options/preview
|
||||
@@ -116,6 +165,7 @@ ts_web/
|
||||
## Features
|
||||
|
||||
- 🎨 Interactive component preview
|
||||
- 📂 Section-based sidebar with filtering & sorting
|
||||
- 🔧 Real-time property editing with type detection
|
||||
- 🌓 Theme switching (light/dark)
|
||||
- 📱 Responsive viewport testing
|
||||
|
||||
31
ts_web/wcctools.interfaces.ts
Normal file
31
ts_web/wcctools.interfaces.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Configuration for a section in the WCC Tools sidebar
|
||||
*/
|
||||
export interface IWccSection {
|
||||
/** Display name for the section header */
|
||||
name: string;
|
||||
/** How items in this section are rendered - 'elements' show demos, 'pages' render directly */
|
||||
type: 'elements' | 'pages';
|
||||
/** The items in this section - either element classes or page factory functions */
|
||||
items: Record<string, any>;
|
||||
/** Optional filter function to include/exclude items */
|
||||
filter?: (name: string, item: any) => boolean;
|
||||
/** Optional sort function for ordering items */
|
||||
sort?: (a: [string, any], b: [string, any]) => number;
|
||||
/** Optional Material icon name for the section header */
|
||||
icon?: string;
|
||||
/** Whether this section should start collapsed (default: false) */
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration object for setupWccTools
|
||||
*/
|
||||
export interface IWccConfig {
|
||||
sections: IWccSection[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Type for element selection types - now section-based
|
||||
*/
|
||||
export type TElementType = 'element' | 'page';
|
||||
Reference in New Issue
Block a user