Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 399ef3d508 | |||
| e0f176b221 | |||
| e625fe9ba6 | |||
| fe62278d74 | |||
| 3ee8afcdae | |||
| ab517b6ba8 | |||
| 2e4cbd911c | |||
| 6e14ebde03 | |||
| 28d1227d30 | |||
| 8c60d3bea3 | |||
| 9ed614994f | |||
| 61b79aa4dc | |||
| 1134cba575 | |||
| 29c0df489e | |||
| 53df62a9fd | |||
| 4fe17f5afd | |||
| 63dd6a27b3 | |||
| 287cc4d1c3 | |||
| 14e63738b7 | |||
| dd151bdad8 | |||
| bd409745e6 | |||
| eb7f482b75 | |||
| d16d482120 | |||
| 5ba4e55011 | |||
| 0068c0749d |
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 |
100
changelog.md
100
changelog.md
@@ -1,5 +1,105 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-01-04 - 3.6.2 - fix(wcc-sidebar)
|
||||||
|
use sidebar's internal .menu element for scroll management and expose scrollableContainer getter
|
||||||
|
|
||||||
|
- Add public scrollableContainer getter to wcc-sidebar that returns the .menu element for external scroll control
|
||||||
|
- Update wcc-dashboard to query wcc-sidebar as WccSidebar and attach scroll listeners to sidebar.scrollableContainer instead of the host element
|
||||||
|
- Restore sidebar scroll position by setting scrollTop on the scrollableContainer when applying saved positions
|
||||||
|
- TypeScript casting added to avoid nullable/implicit any issues when querying the sidebar element
|
||||||
|
|
||||||
|
## 2026-01-04 - 3.6.1 - fix(wcc-sidebar)
|
||||||
|
sort sidebar items alphabetically and unify grouped and ungrouped items for consistent ordering
|
||||||
|
|
||||||
|
- Unifies ungrouped elements and groups into a single render list using a RenderItem type with a sortKey.
|
||||||
|
- Sorts all top-level items alphabetically by element name (case-insensitive via toLowerCase) so sidebar order is deterministic.
|
||||||
|
- Groups are ordered by the first element's name; individual items inside a group preserve their original order.
|
||||||
|
- Replaces previous separate rendering paths with a single sorted render pass that returns TemplateResult array.
|
||||||
|
|
||||||
|
## 2026-01-04 - 3.6.0 - feat(sidebar)
|
||||||
|
restructure sidebar layout, add search clear button, and improve scrolling behavior
|
||||||
|
|
||||||
|
- Change sidebar root to a flex column layout and add a .sidebar-header to separate header content from the scrollable menu
|
||||||
|
- Move pinned section into the header and make .menu flex: 1 with min-height: 0 so the menu becomes the scrollable area
|
||||||
|
- Replace overflow-y on the root with overflow:hidden to avoid double scrolling and constrain scrolling to .menu
|
||||||
|
- Add a clear button for the search input (.search-clear) with positioning, hover styles, and a clearSearch() method to reset the query and emit searchChanged
|
||||||
|
- Adjust search input padding and make .search-container position: relative to correctly position the clear button
|
||||||
|
|
||||||
|
## 2026-01-04 - 3.5.3 - fix(deps)
|
||||||
|
bump dependency versions: @design.estate/dees-domtools to ^2.3.7, @design.estate/dees-element to ^2.1.5, lit to ^3.3.2; update devDependencies @api.global/typedserver to ^8.1.0 and @git.zone/tstest to ^3.1.4
|
||||||
|
|
||||||
|
- Updated runtime dependencies: @design.estate/dees-domtools ^2.3.7, @design.estate/dees-element ^2.1.5, lit ^3.3.2.
|
||||||
|
- Updated devDependencies: @api.global/typedserver ^8.1.0 (major bump), @git.zone/tstest ^3.1.4.
|
||||||
|
- @api.global/typedserver major bump affects development tooling only (devDependency), not runtime API.
|
||||||
|
- Current package version is 3.5.2; recommend a patch release to 3.5.3.
|
||||||
|
|
||||||
|
## 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)
|
## 2025-12-19 - 3.1.0 - feat(wcc-properties)
|
||||||
add Share selector with inline Record button and adjust properties panel grid layout
|
add Share selector with inline Record button and adjust properties panel grid layout
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,32 @@
|
|||||||
import * as deesWccTools from '../ts_web/index.js';
|
import * as deesWccTools from '../ts_web/index.js';
|
||||||
import * as deesDomTools from '@design.estate/dees-domtools';
|
import * as deesDomTools from '@design.estate/dees-domtools';
|
||||||
|
|
||||||
// elements and pages
|
// elements, views and pages
|
||||||
import * as elements from '../test/elements/index.js';
|
import * as elements from '../test/elements/index.js';
|
||||||
|
import * as views from '../test/views/index.js';
|
||||||
import * as pages from '../test/pages/index.js';
|
import * as pages from '../test/pages/index.js';
|
||||||
|
|
||||||
deesWccTools.setupWccTools(elements as any, pages);
|
// Sections-based API with Views
|
||||||
|
deesWccTools.setupWccTools({
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
name: 'Pages',
|
||||||
|
type: 'pages',
|
||||||
|
items: pages,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Views',
|
||||||
|
type: 'elements',
|
||||||
|
items: views,
|
||||||
|
icon: 'web',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Elements',
|
||||||
|
type: 'elements',
|
||||||
|
items: elements,
|
||||||
|
sort: ([a], [b]) => a.localeCompare(b),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
deesDomTools.elementBasic.setup();
|
deesDomTools.elementBasic.setup();
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@design.estate/dees-wcctools",
|
"name": "@design.estate/dees-wcctools",
|
||||||
"version": "3.1.0",
|
"version": "3.6.2",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.",
|
"description": "A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -17,17 +17,17 @@
|
|||||||
"author": "Lossless GmbH",
|
"author": "Lossless GmbH",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@design.estate/dees-domtools": "^2.3.6",
|
"@design.estate/dees-domtools": "^2.3.7",
|
||||||
"@design.estate/dees-element": "^2.1.3",
|
"@design.estate/dees-element": "^2.1.5",
|
||||||
"@push.rocks/smartdelay": "^3.0.5",
|
"@push.rocks/smartdelay": "^3.0.5",
|
||||||
"lit": "^3.3.1"
|
"lit": "^3.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@api.global/typedserver": "^7.11.1",
|
"@api.global/typedserver": "^8.1.0",
|
||||||
"@git.zone/tsbuild": "^4.0.2",
|
"@git.zone/tsbuild": "^4.0.2",
|
||||||
"@git.zone/tsbundle": "^2.6.3",
|
"@git.zone/tsbundle": "^2.6.3",
|
||||||
"@git.zone/tsrun": "^2.0.1",
|
"@git.zone/tsrun": "^2.0.1",
|
||||||
"@git.zone/tstest": "^3.1.3",
|
"@git.zone/tstest": "^3.1.4",
|
||||||
"@git.zone/tswatch": "^2.3.13",
|
"@git.zone/tswatch": "^2.3.13",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@types/node": "^25.0.3"
|
"@types/node": "^25.0.3"
|
||||||
|
|||||||
959
pnpm-lock.yaml
generated
959
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,67 @@
|
|||||||
# Project Hints and Findings
|
# Project Hints and Findings
|
||||||
|
|
||||||
|
## Section-based Configuration API (2025-12-27)
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
Refactored `setupWccTools` to accept a section-based configuration object instead of separate elements/pages arguments. This allows multiple custom sections with filtering, sorting, and collapsible headers.
|
||||||
|
|
||||||
|
### New API
|
||||||
|
```typescript
|
||||||
|
import * as deesWccTools from '@design.estate/dees-wcctools';
|
||||||
|
|
||||||
|
deesWccTools.setupWccTools({
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
name: 'Pages',
|
||||||
|
type: 'pages',
|
||||||
|
items: pages,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Elements',
|
||||||
|
type: 'elements',
|
||||||
|
items: elements,
|
||||||
|
filter: (name, item) => !name.startsWith('internal-'),
|
||||||
|
sort: ([a], [b]) => a.localeCompare(b),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Views',
|
||||||
|
type: 'elements',
|
||||||
|
items: elements,
|
||||||
|
filter: (name, item) => name.startsWith('view-'),
|
||||||
|
icon: 'web',
|
||||||
|
collapsed: true, // Start collapsed
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Section Properties
|
||||||
|
- `name`: Display name for the section header
|
||||||
|
- `type`: `'elements'` (shows demos) or `'pages'` (renders directly)
|
||||||
|
- `items`: Record of items (element classes or page factories)
|
||||||
|
- `filter`: Optional `(name, item) => boolean` to filter items
|
||||||
|
- `sort`: Optional `([name, item], [name, item]) => number` for ordering
|
||||||
|
- `icon`: Optional Material icon name for section header
|
||||||
|
- `collapsed`: Optional boolean to start section collapsed
|
||||||
|
|
||||||
|
### Backwards Compatibility
|
||||||
|
Legacy format is still supported:
|
||||||
|
```typescript
|
||||||
|
deesWccTools.setupWccTools(elements, pages); // Still works
|
||||||
|
```
|
||||||
|
|
||||||
|
### URL Routing
|
||||||
|
Changed from `/wcctools-route/:itemType/:itemName/...` to `/wcctools-route/:sectionName/:itemName/...`
|
||||||
|
Section names are URL-encoded. Legacy routes (`element`/`page` as section name) still work for backwards compatibility.
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
- `ts_web/wcctools.interfaces.ts` - New types: `IWccSection`, `IWccConfig`
|
||||||
|
- `ts_web/index.ts` - Updated `setupWccTools` with backwards compat detection
|
||||||
|
- `ts_web/elements/wcc-dashboard.ts` - Uses sections, updated routing
|
||||||
|
- `ts_web/elements/wcc-sidebar.ts` - Dynamic section rendering
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## UI Redesign with Shadcn-like Styles (2025-06-27)
|
## UI Redesign with Shadcn-like Styles (2025-06-27)
|
||||||
|
|
||||||
### Changes Made
|
### Changes Made
|
||||||
|
|||||||
229
readme.md
229
readme.md
@@ -6,12 +6,13 @@
|
|||||||
|
|
||||||
`@design.estate/dees-wcctools` provides a comprehensive development environment for web components, featuring:
|
`@design.estate/dees-wcctools` provides a comprehensive development environment for web components, featuring:
|
||||||
|
|
||||||
- 🎨 **Interactive Component Catalogue** — Live preview with sidebar navigation
|
- 🎨 **Interactive Component Catalogue** — Live preview with customizable sidebar sections
|
||||||
- 🔧 **Real-time Property Editing** — Modify component props on the fly with auto-detected editors
|
- 🔧 **Real-time Property Editing** — Modify component props on the fly with auto-detected editors
|
||||||
- 🌓 **Theme Switching** — Test light/dark modes instantly
|
- 🌓 **Theme Switching** — Test light/dark modes instantly
|
||||||
- 📱 **Responsive Viewport Testing** — Phone, phablet, tablet, and desktop views
|
- 📱 **Responsive Viewport Testing** — Phone, phablet, tablet, and desktop views
|
||||||
- 🎬 **Screen Recording** — Record component demos with audio support and video trimming
|
- 🎬 **Screen Recording** — Record component demos with audio support and video trimming
|
||||||
- 🧪 **Advanced Demo Tools** — Post-render hooks for interactive testing
|
- 🧪 **Advanced Demo Tools** — Post-render hooks for interactive testing
|
||||||
|
- 📂 **Section-based Organization** — Group components into custom sections with filtering and sorting
|
||||||
- 🚀 **Zero-config Setup** — TypeScript and Lit support out of the box
|
- 🚀 **Zero-config Setup** — TypeScript and Lit support out of the box
|
||||||
|
|
||||||
## Issue Reporting and Security
|
## Issue Reporting and Security
|
||||||
@@ -57,29 +58,22 @@ export class MyButton extends DeesElement {
|
|||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 14px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
}
|
||||||
button.primary {
|
button.primary {
|
||||||
background: #007bff;
|
background: #3b82f6;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
button.secondary {
|
button.secondary {
|
||||||
background: #6c757d;
|
background: #6b7280;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
button:hover {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
`
|
`
|
||||||
];
|
];
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return html`
|
return html`
|
||||||
<button class="${this.variant}">
|
<button class="${this.variant}">${this.label}</button>
|
||||||
${this.label}
|
|
||||||
</button>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,33 +87,32 @@ import { setupWccTools } from '@design.estate/dees-wcctools';
|
|||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
|
|
||||||
// Import your components
|
// Import your components
|
||||||
import { MyButton } from './components/my-button.js';
|
import * as elements from './components/index.js';
|
||||||
import { MyCard } from './components/my-card.js';
|
import * as views from './views/index.js';
|
||||||
|
import * as pages from './pages/index.js';
|
||||||
|
|
||||||
// Define elements for the catalogue
|
// Initialize with sections-based configuration
|
||||||
const elements = {
|
setupWccTools({
|
||||||
'my-button': MyButton,
|
sections: [
|
||||||
'my-card': MyCard,
|
{
|
||||||
};
|
name: 'Pages',
|
||||||
|
type: 'pages',
|
||||||
// Optionally define pages
|
items: pages,
|
||||||
const pages = {
|
},
|
||||||
'home': () => html`
|
{
|
||||||
<div style="padding: 20px;">
|
name: 'Views',
|
||||||
<h1>Welcome to My Component Library</h1>
|
type: 'elements',
|
||||||
<p>Browse components using the sidebar.</p>
|
items: views,
|
||||||
</div>
|
icon: 'web',
|
||||||
`,
|
},
|
||||||
'getting-started': () => html`
|
{
|
||||||
<div style="padding: 20px;">
|
name: 'Elements',
|
||||||
<h2>Getting Started</h2>
|
type: 'elements',
|
||||||
<p>Installation and usage instructions...</p>
|
items: elements,
|
||||||
</div>
|
sort: ([a], [b]) => a.localeCompare(b),
|
||||||
`,
|
},
|
||||||
};
|
],
|
||||||
|
});
|
||||||
// Initialize the catalogue
|
|
||||||
setupWccTools(elements, pages);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Create an HTML Entry Point
|
### 3. Create an HTML Entry Point
|
||||||
@@ -137,6 +130,69 @@ setupWccTools(elements, pages);
|
|||||||
</html>
|
</html>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 📂 Sections Configuration
|
||||||
|
|
||||||
|
The sections-based API gives you full control over how components are organized in the sidebar.
|
||||||
|
|
||||||
|
### Section Properties
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
|----------|------|-------------|
|
||||||
|
| `name` | `string` | Display name for the section header |
|
||||||
|
| `type` | `'elements' \| 'pages'` | How items render (`elements` show demos, `pages` render directly) |
|
||||||
|
| `items` | `Record<string, any>` | Object containing element classes or page factories |
|
||||||
|
| `filter` | `(name, item) => boolean` | Optional filter function to include/exclude items |
|
||||||
|
| `sort` | `([a, itemA], [b, itemB]) => number` | Optional sort function for ordering items |
|
||||||
|
| `icon` | `string` | Optional Material Symbols icon name |
|
||||||
|
| `collapsed` | `boolean` | Start section collapsed (default: `false`) |
|
||||||
|
|
||||||
|
### Advanced Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { setupWccTools } from '@design.estate/dees-wcctools';
|
||||||
|
import * as allElements from './elements/index.js';
|
||||||
|
import * as pages from './pages/index.js';
|
||||||
|
|
||||||
|
setupWccTools({
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
name: 'Pages',
|
||||||
|
type: 'pages',
|
||||||
|
items: pages,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Form Controls',
|
||||||
|
type: 'elements',
|
||||||
|
items: allElements,
|
||||||
|
icon: 'edit_note',
|
||||||
|
filter: (name) => name.startsWith('form-') || name.includes('input'),
|
||||||
|
sort: ([a], [b]) => a.localeCompare(b),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Layout',
|
||||||
|
type: 'elements',
|
||||||
|
items: allElements,
|
||||||
|
icon: 'dashboard',
|
||||||
|
filter: (name) => name.startsWith('layout-') || name.startsWith('grid-'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Legacy',
|
||||||
|
type: 'elements',
|
||||||
|
items: allElements,
|
||||||
|
filter: (name) => name.startsWith('legacy-'),
|
||||||
|
collapsed: true, // Start collapsed
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Legacy API (Still Supported)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// The old format still works for simple use cases
|
||||||
|
setupWccTools(elements, pages);
|
||||||
|
```
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### 🎯 Live Property Editing
|
### 🎯 Live Property Editing
|
||||||
@@ -162,19 +218,7 @@ Test your components across different screen sizes:
|
|||||||
|
|
||||||
### 🌓 Theme Support
|
### 🌓 Theme Support
|
||||||
|
|
||||||
Components automatically adapt to light/dark themes using the `goBright` property:
|
Components automatically adapt to light/dark themes. Use CSS custom properties with the theme manager:
|
||||||
|
|
||||||
```typescript
|
|
||||||
public render() {
|
|
||||||
return html`
|
|
||||||
<div class="${this.goBright ? 'light-theme' : 'dark-theme'}">
|
|
||||||
<!-- Your component content -->
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Or use CSS custom properties with the theme manager:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { cssManager } from '@design.estate/dees-element';
|
import { cssManager } from '@design.estate/dees-element';
|
||||||
@@ -182,8 +226,8 @@ import { cssManager } from '@design.estate/dees-element';
|
|||||||
public static styles = [
|
public static styles = [
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
color: ${cssManager.bdTheme('#1a1a1a', '#e5e5e5')};
|
||||||
background: ${cssManager.bdTheme('#fff', '#000')};
|
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
];
|
];
|
||||||
@@ -191,7 +235,7 @@ public static styles = [
|
|||||||
|
|
||||||
### 🎬 Screen Recording
|
### 🎬 Screen Recording
|
||||||
|
|
||||||
Record component demos directly from the catalogue! The built-in recorder supports:
|
Record component demos directly from the catalogue:
|
||||||
|
|
||||||
- **Viewport Recording** — Record just the component viewport
|
- **Viewport Recording** — Record just the component viewport
|
||||||
- **Full Screen Recording** — Capture the entire screen
|
- **Full Screen Recording** — Capture the entire screen
|
||||||
@@ -232,9 +276,26 @@ export class MyComponent extends DeesElement {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 🎭 Multiple Demos
|
||||||
|
|
||||||
|
Components can expose multiple demo variations:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@customElement('my-button')
|
||||||
|
export class MyButton extends DeesElement {
|
||||||
|
public static demo = [
|
||||||
|
() => html`<my-button variant="primary">Primary</my-button>`,
|
||||||
|
() => html`<my-button variant="secondary">Secondary</my-button>`,
|
||||||
|
() => html`<my-button variant="danger">Danger</my-button>`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each demo appears as a numbered item in an expandable folder in the sidebar.
|
||||||
|
|
||||||
### ⏳ Async Demos
|
### ⏳ Async Demos
|
||||||
|
|
||||||
Return a `Promise` from `demo` for async setup. The dashboard waits for resolution:
|
Return a `Promise` from `demo` for async setup:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
public static demo = async () => {
|
public static demo = async () => {
|
||||||
@@ -243,7 +304,7 @@ public static demo = async () => {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🎭 Container Queries
|
### 🎯 Container Queries
|
||||||
|
|
||||||
Components can respond to their container size using the `wccToolsViewport` container:
|
Components can respond to their container size using the `wccToolsViewport` container:
|
||||||
|
|
||||||
@@ -269,7 +330,7 @@ public static styles = [
|
|||||||
|
|
||||||
### Required for Catalogue Display
|
### Required for Catalogue Display
|
||||||
|
|
||||||
1. Components must expose a static `demo` property returning a Lit template
|
1. Components must expose a static `demo` property returning a Lit template (or array of templates)
|
||||||
2. Use `@property()` decorators with the `accessor` keyword for editable properties
|
2. Use `@property()` decorators with the `accessor` keyword for editable properties
|
||||||
3. Export component classes for proper detection
|
3. Export component classes for proper detection
|
||||||
|
|
||||||
@@ -278,7 +339,7 @@ public static styles = [
|
|||||||
```typescript
|
```typescript
|
||||||
@customElement('best-practice-component')
|
@customElement('best-practice-component')
|
||||||
export class BestPracticeComponent extends DeesElement {
|
export class BestPracticeComponent extends DeesElement {
|
||||||
// ✅ Static demo property
|
// ✅ Static demo property (single or array)
|
||||||
public static demo = () => html`
|
public static demo = () => html`
|
||||||
<best-practice-component
|
<best-practice-component
|
||||||
.complexProp=${{ key: 'value' }}
|
.complexProp=${{ key: 'value' }}
|
||||||
@@ -305,23 +366,40 @@ export class BestPracticeComponent extends DeesElement {
|
|||||||
The catalogue uses URL routing for deep linking:
|
The catalogue uses URL routing for deep linking:
|
||||||
|
|
||||||
```
|
```
|
||||||
/wcctools-route/:type/:name/:viewport/:theme
|
/wcctools-route/:sectionName/:itemName/:demoIndex/:viewport/:theme
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
/wcctools-route/element/my-button/desktop/dark
|
/wcctools-route/Elements/my-button/0/desktop/dark
|
||||||
/wcctools-route/page/home/tablet/bright
|
/wcctools-route/Views/view-dashboard/0/tablet/bright
|
||||||
|
/wcctools-route/Pages/home/0/desktop/dark
|
||||||
```
|
```
|
||||||
|
|
||||||
## API Reference
|
## API Reference
|
||||||
|
|
||||||
### `setupWccTools(elements, pages?)`
|
### `setupWccTools(config)`
|
||||||
|
|
||||||
Initialize the WCC Tools dashboard.
|
Initialize the WCC Tools dashboard with sections configuration.
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
```typescript
|
||||||
|-----------|------|-------------|
|
interface IWccSection {
|
||||||
| `elements` | `Record<string, typeof LitElement>` | Map of element names to classes |
|
name: string;
|
||||||
| `pages` | `Record<string, TTemplateFactory>` | Optional map of page names to template functions |
|
type: 'elements' | 'pages';
|
||||||
|
items: Record<string, any>;
|
||||||
|
filter?: (name: string, item: any) => boolean;
|
||||||
|
sort?: (a: [string, any], b: [string, any]) => number;
|
||||||
|
icon?: string;
|
||||||
|
collapsed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IWccConfig {
|
||||||
|
sections: IWccSection[];
|
||||||
|
}
|
||||||
|
|
||||||
|
setupWccTools(config: IWccConfig): void;
|
||||||
|
|
||||||
|
// Legacy (still supported)
|
||||||
|
setupWccTools(elements: Record<string, any>, pages?: Record<string, TTemplateFactory>): void;
|
||||||
|
```
|
||||||
|
|
||||||
### `DeesDemoWrapper`
|
### `DeesDemoWrapper`
|
||||||
|
|
||||||
@@ -357,14 +435,21 @@ recorder.stopRecording();
|
|||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
my-components/
|
my-component-library/
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── components/
|
│ ├── elements/ # UI components
|
||||||
│ │ ├── my-button.ts
|
│ │ ├── my-button.ts
|
||||||
│ │ └── my-card.ts
|
│ │ ├── my-card.ts
|
||||||
│ └── catalogue.ts
|
│ │ └── index.ts
|
||||||
├── dist/
|
│ ├── views/ # Full-page layouts
|
||||||
├── index.html
|
│ │ ├── view-dashboard.ts
|
||||||
|
│ │ └── index.ts
|
||||||
|
│ ├── pages/ # Documentation pages
|
||||||
|
│ │ ├── home.ts
|
||||||
|
│ │ └── index.ts
|
||||||
|
│ └── catalogue.ts # WCC Tools setup
|
||||||
|
├── html/
|
||||||
|
│ └── index.html
|
||||||
└── package.json
|
└── package.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -4,3 +4,10 @@ export * from './test-complextypes.js';
|
|||||||
export * from './test-withwrapper.js';
|
export * from './test-withwrapper.js';
|
||||||
export * from './test-edgecases.js';
|
export * from './test-edgecases.js';
|
||||||
export * from './test-nested.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 = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-wcctools',
|
name: '@design.estate/dees-wcctools',
|
||||||
version: '3.1.0',
|
version: '3.6.2',
|
||||||
description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.'
|
description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.'
|
||||||
}
|
}
|
||||||
|
|||||||
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 { DeesElement, property, html, customElement, type TemplateResult, queryAsync, render, domtools } from '@design.estate/dees-element';
|
||||||
import { resolveTemplateFactory, getDemoAtIndex, getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
|
import { resolveTemplateFactory, getDemoAtIndex, getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
|
||||||
import type { TTemplateFactory } from './wcctools.helpers.js';
|
import type { TTemplateFactory } from './wcctools.helpers.js';
|
||||||
|
import type { IWccConfig, IWccSection, TElementType } from '../wcctools.interfaces.js';
|
||||||
|
|
||||||
import * as plugins from '../wcctools.plugins.js';
|
import * as plugins from '../wcctools.plugins.js';
|
||||||
|
|
||||||
@@ -9,13 +10,38 @@ import './wcc-frame.js';
|
|||||||
import './wcc-sidebar.js';
|
import './wcc-sidebar.js';
|
||||||
import './wcc-properties.js';
|
import './wcc-properties.js';
|
||||||
import { type TTheme } from './wcc-properties.js';
|
import { type TTheme } from './wcc-properties.js';
|
||||||
import { type TElementType } from './wcc-sidebar.js';
|
|
||||||
import { breakpoints } from '@design.estate/dees-domtools';
|
import { breakpoints } from '@design.estate/dees-domtools';
|
||||||
import { WccFrame } from './wcc-frame.js';
|
import { WccFrame } from './wcc-frame.js';
|
||||||
|
import { WccSidebar } from './wcc-sidebar.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get filtered and sorted items from a section
|
||||||
|
*/
|
||||||
|
export const getSectionItems = (section: IWccSection): Array<[string, any]> => {
|
||||||
|
let entries = Object.entries(section.items);
|
||||||
|
|
||||||
|
// Apply filter if provided
|
||||||
|
if (section.filter) {
|
||||||
|
entries = entries.filter(([name, item]) => section.filter(name, item));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply sort if provided
|
||||||
|
if (section.sort) {
|
||||||
|
entries.sort(section.sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
};
|
||||||
|
|
||||||
@customElement('wcc-dashboard')
|
@customElement('wcc-dashboard')
|
||||||
export class WccDashboard extends DeesElement {
|
export class WccDashboard extends DeesElement {
|
||||||
|
|
||||||
|
@property()
|
||||||
|
accessor sections: IWccSection[] = [];
|
||||||
|
|
||||||
|
@property()
|
||||||
|
accessor selectedSection: IWccSection | null = null;
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
accessor selectedType: TElementType;
|
accessor selectedType: TElementType;
|
||||||
|
|
||||||
@@ -34,41 +60,59 @@ export class WccDashboard extends DeesElement {
|
|||||||
@property()
|
@property()
|
||||||
accessor selectedTheme: TTheme = 'dark';
|
accessor selectedTheme: TTheme = 'dark';
|
||||||
|
|
||||||
|
@property()
|
||||||
|
accessor searchQuery: string = '';
|
||||||
|
|
||||||
|
// Pinned items as Set of "sectionName::itemName"
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor pinnedItems: Set<string> = new Set();
|
||||||
|
|
||||||
|
// Sidebar width (resizable)
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor sidebarWidth: number = 200;
|
||||||
|
|
||||||
// Derived from selectedViewport - no need for separate property
|
// Derived from selectedViewport - no need for separate property
|
||||||
public get isNative(): boolean {
|
public get isNative(): boolean {
|
||||||
return this.selectedViewport === 'native';
|
return this.selectedViewport === 'native';
|
||||||
}
|
}
|
||||||
|
|
||||||
@property()
|
|
||||||
accessor pages: Record<string, TTemplateFactory> = {};
|
|
||||||
|
|
||||||
@property()
|
|
||||||
accessor elements: { [key: string]: DeesElement } = {};
|
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
accessor warning: string = null;
|
accessor warning: string = null;
|
||||||
|
|
||||||
private frameScrollY: number = 0;
|
private frameScrollY: number = 0;
|
||||||
private sidebarScrollY: number = 0;
|
private sidebarScrollY: number = 0;
|
||||||
private scrollPositionsApplied: boolean = false;
|
private scrollPositionsApplied: boolean = false;
|
||||||
|
|
||||||
@queryAsync('wcc-frame')
|
@queryAsync('wcc-frame')
|
||||||
accessor wccFrame: Promise<WccFrame>;
|
accessor wccFrame: Promise<WccFrame>;
|
||||||
|
|
||||||
constructor(
|
constructor(config?: IWccConfig) {
|
||||||
elementsArg?: { [key: string]: DeesElement },
|
|
||||||
pagesArg?: Record<string, TTemplateFactory>
|
|
||||||
) {
|
|
||||||
super();
|
super();
|
||||||
if (elementsArg) {
|
if (config && config.sections) {
|
||||||
this.elements = elementsArg;
|
this.sections = config.sections;
|
||||||
console.log('got elements:');
|
console.log('got sections:', this.sections.map(s => s.name));
|
||||||
console.log(this.elements);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (pagesArg) {
|
/**
|
||||||
this.pages = pagesArg;
|
* Find an item by name across all sections, returns the item and its section
|
||||||
|
*/
|
||||||
|
public findItemByName(name: string): { item: any; section: IWccSection } | null {
|
||||||
|
for (const section of this.sections) {
|
||||||
|
const entries = getSectionItems(section);
|
||||||
|
const found = entries.find(([itemName]) => itemName === name);
|
||||||
|
if (found) {
|
||||||
|
return { item: found[1], section };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a section by name (URL-decoded)
|
||||||
|
*/
|
||||||
|
public findSectionByName(name: string): IWccSection | null {
|
||||||
|
return this.sections.find(s => s.name === name) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
@@ -86,6 +130,9 @@ export class WccDashboard extends DeesElement {
|
|||||||
<wcc-sidebar
|
<wcc-sidebar
|
||||||
.dashboardRef=${this}
|
.dashboardRef=${this}
|
||||||
.selectedItem=${this.selectedItem}
|
.selectedItem=${this.selectedItem}
|
||||||
|
.searchQuery=${this.searchQuery}
|
||||||
|
.pinnedItems=${this.pinnedItems}
|
||||||
|
.sidebarWidth=${this.sidebarWidth}
|
||||||
.isNative=${this.isNative}
|
.isNative=${this.isNative}
|
||||||
@selectedType=${(eventArg) => {
|
@selectedType=${(eventArg) => {
|
||||||
this.selectedType = eventArg.detail;
|
this.selectedType = eventArg.detail;
|
||||||
@@ -96,6 +143,23 @@ export class WccDashboard extends DeesElement {
|
|||||||
@selectedItem=${(eventArg) => {
|
@selectedItem=${(eventArg) => {
|
||||||
this.selectedItem = eventArg.detail;
|
this.selectedItem = eventArg.detail;
|
||||||
}}
|
}}
|
||||||
|
@searchChanged=${(eventArg: CustomEvent) => {
|
||||||
|
this.searchQuery = eventArg.detail;
|
||||||
|
this.updateUrlWithScrollState();
|
||||||
|
}}
|
||||||
|
@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-sidebar>
|
||||||
<wcc-properties
|
<wcc-properties
|
||||||
.dashboardRef=${this}
|
.dashboardRef=${this}
|
||||||
@@ -104,6 +168,7 @@ export class WccDashboard extends DeesElement {
|
|||||||
.selectedViewport=${this.selectedViewport}
|
.selectedViewport=${this.selectedViewport}
|
||||||
.selectedTheme=${this.selectedTheme}
|
.selectedTheme=${this.selectedTheme}
|
||||||
.isNative=${this.isNative}
|
.isNative=${this.isNative}
|
||||||
|
.sidebarWidth=${this.sidebarWidth}
|
||||||
@selectedViewport=${(eventArg) => {
|
@selectedViewport=${(eventArg) => {
|
||||||
this.selectedViewport = eventArg.detail;
|
this.selectedViewport = eventArg.detail;
|
||||||
this.scheduleUpdate();
|
this.scheduleUpdate();
|
||||||
@@ -122,7 +187,7 @@ export class WccDashboard extends DeesElement {
|
|||||||
this.toggleNative();
|
this.toggleNative();
|
||||||
}}
|
}}
|
||||||
></wcc-properties>
|
></wcc-properties>
|
||||||
<wcc-frame id="wccFrame" viewport=${this.selectedViewport} .isNative=${this.isNative}>
|
<wcc-frame id="wccFrame" viewport=${this.selectedViewport} .isNative=${this.isNative} .sidebarWidth=${this.sidebarWidth}>
|
||||||
</wcc-frame>
|
</wcc-frame>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -159,37 +224,88 @@ export class WccDashboard extends DeesElement {
|
|||||||
this.setupScrollListeners();
|
this.setupScrollListeners();
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
// Route with demo index (new format)
|
// New route format with section name
|
||||||
this.domtools.router.on(
|
this.domtools.router.on(
|
||||||
'/wcctools-route/:itemType/:itemName/:demoIndex/:viewport/:theme',
|
'/wcctools-route/:sectionName/:itemName/:demoIndex/:viewport/:theme',
|
||||||
async (routeInfo) => {
|
async (routeInfo) => {
|
||||||
this.selectedType = routeInfo.params.itemType as TElementType;
|
const sectionName = decodeURIComponent(routeInfo.params.sectionName);
|
||||||
|
this.selectedSection = this.findSectionByName(sectionName);
|
||||||
this.selectedItemName = routeInfo.params.itemName;
|
this.selectedItemName = routeInfo.params.itemName;
|
||||||
this.selectedDemoIndex = parseInt(routeInfo.params.demoIndex) || 0;
|
this.selectedDemoIndex = parseInt(routeInfo.params.demoIndex) || 0;
|
||||||
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
|
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
|
||||||
this.selectedTheme = routeInfo.params.theme as TTheme;
|
this.selectedTheme = routeInfo.params.theme as TTheme;
|
||||||
if (routeInfo.params.itemType === 'element') {
|
|
||||||
this.selectedItem = this.elements[routeInfo.params.itemName];
|
if (this.selectedSection) {
|
||||||
} else if (routeInfo.params.itemType === 'page') {
|
// Find item within the section
|
||||||
this.selectedItem = this.pages[routeInfo.params.itemName];
|
const entries = getSectionItems(this.selectedSection);
|
||||||
|
const found = entries.find(([name]) => name === routeInfo.params.itemName);
|
||||||
|
if (found) {
|
||||||
|
this.selectedItem = found[1];
|
||||||
|
this.selectedType = this.selectedSection.type === 'elements' ? 'element' : 'page';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: try legacy format (element/page as section name)
|
||||||
|
const legacyType = routeInfo.params.sectionName;
|
||||||
|
if (legacyType === 'element' || legacyType === 'page') {
|
||||||
|
this.selectedType = legacyType as TElementType;
|
||||||
|
// Find item in any matching section
|
||||||
|
const result = this.findItemByName(routeInfo.params.itemName);
|
||||||
|
if (result) {
|
||||||
|
this.selectedItem = result.item;
|
||||||
|
this.selectedSection = result.section;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore scroll positions from query parameters
|
// Restore state from query parameters
|
||||||
if (routeInfo.queryParams) {
|
if (routeInfo.queryParams) {
|
||||||
|
const search = routeInfo.queryParams.search;
|
||||||
const frameScrollY = routeInfo.queryParams.frameScrollY;
|
const frameScrollY = routeInfo.queryParams.frameScrollY;
|
||||||
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
|
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
|
||||||
|
const pinned = routeInfo.queryParams.pinned;
|
||||||
|
const sidebarWidth = routeInfo.queryParams.sidebarWidth;
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
this.searchQuery = search;
|
||||||
|
} else {
|
||||||
|
this.searchQuery = '';
|
||||||
|
}
|
||||||
if (frameScrollY) {
|
if (frameScrollY) {
|
||||||
this.frameScrollY = parseInt(frameScrollY);
|
this.frameScrollY = parseInt(frameScrollY);
|
||||||
}
|
}
|
||||||
if (sidebarScrollY) {
|
if (sidebarScrollY) {
|
||||||
this.sidebarScrollY = parseInt(sidebarScrollY);
|
this.sidebarScrollY = parseInt(sidebarScrollY);
|
||||||
}
|
}
|
||||||
|
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
|
// Apply scroll positions and update frame after a short delay to ensure DOM is ready
|
||||||
setTimeout(() => {
|
setTimeout(async () => {
|
||||||
this.applyScrollPositions();
|
this.applyScrollPositions();
|
||||||
|
// Ensure frame gets the sidebarWidth
|
||||||
|
const frame = await this.wccFrame;
|
||||||
|
if (frame) {
|
||||||
|
frame.sidebarWidth = this.sidebarWidth;
|
||||||
|
frame.requestUpdate();
|
||||||
|
}
|
||||||
}, 100);
|
}, 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();
|
const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
|
||||||
@@ -201,35 +317,78 @@ export class WccDashboard extends DeesElement {
|
|||||||
|
|
||||||
// Legacy route without demo index (for backwards compatibility)
|
// Legacy route without demo index (for backwards compatibility)
|
||||||
this.domtools.router.on(
|
this.domtools.router.on(
|
||||||
'/wcctools-route/:itemType/:itemName/:viewport/:theme',
|
'/wcctools-route/:sectionName/:itemName/:viewport/:theme',
|
||||||
async (routeInfo) => {
|
async (routeInfo) => {
|
||||||
this.selectedType = routeInfo.params.itemType as TElementType;
|
const sectionName = decodeURIComponent(routeInfo.params.sectionName);
|
||||||
|
this.selectedSection = this.findSectionByName(sectionName);
|
||||||
this.selectedItemName = routeInfo.params.itemName;
|
this.selectedItemName = routeInfo.params.itemName;
|
||||||
this.selectedDemoIndex = 0; // Default to first demo
|
this.selectedDemoIndex = 0;
|
||||||
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
|
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
|
||||||
this.selectedTheme = routeInfo.params.theme as TTheme;
|
this.selectedTheme = routeInfo.params.theme as TTheme;
|
||||||
if (routeInfo.params.itemType === 'element') {
|
|
||||||
this.selectedItem = this.elements[routeInfo.params.itemName];
|
if (this.selectedSection) {
|
||||||
} else if (routeInfo.params.itemType === 'page') {
|
const entries = getSectionItems(this.selectedSection);
|
||||||
this.selectedItem = this.pages[routeInfo.params.itemName];
|
const found = entries.find(([name]) => name === routeInfo.params.itemName);
|
||||||
|
if (found) {
|
||||||
|
this.selectedItem = found[1];
|
||||||
|
this.selectedType = this.selectedSection.type === 'elements' ? 'element' : 'page';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: try legacy format
|
||||||
|
const legacyType = routeInfo.params.sectionName;
|
||||||
|
if (legacyType === 'element' || legacyType === 'page') {
|
||||||
|
this.selectedType = legacyType as TElementType;
|
||||||
|
const result = this.findItemByName(routeInfo.params.itemName);
|
||||||
|
if (result) {
|
||||||
|
this.selectedItem = result.item;
|
||||||
|
this.selectedSection = result.section;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore scroll positions from query parameters
|
// Restore state from query parameters
|
||||||
if (routeInfo.queryParams) {
|
if (routeInfo.queryParams) {
|
||||||
|
const search = routeInfo.queryParams.search;
|
||||||
const frameScrollY = routeInfo.queryParams.frameScrollY;
|
const frameScrollY = routeInfo.queryParams.frameScrollY;
|
||||||
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
|
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
|
||||||
|
const pinned = routeInfo.queryParams.pinned;
|
||||||
|
const sidebarWidth = routeInfo.queryParams.sidebarWidth;
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
this.searchQuery = search;
|
||||||
|
} else {
|
||||||
|
this.searchQuery = '';
|
||||||
|
}
|
||||||
if (frameScrollY) {
|
if (frameScrollY) {
|
||||||
this.frameScrollY = parseInt(frameScrollY);
|
this.frameScrollY = parseInt(frameScrollY);
|
||||||
}
|
}
|
||||||
if (sidebarScrollY) {
|
if (sidebarScrollY) {
|
||||||
this.sidebarScrollY = parseInt(sidebarScrollY);
|
this.sidebarScrollY = parseInt(sidebarScrollY);
|
||||||
}
|
}
|
||||||
|
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
|
// Apply scroll positions after a short delay to ensure DOM is ready
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.applyScrollPositions();
|
this.applyScrollPositions();
|
||||||
}, 100);
|
}, 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();
|
const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
|
||||||
@@ -297,15 +456,27 @@ export class WccDashboard extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public buildUrl() {
|
public buildUrl() {
|
||||||
const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
|
const sectionName = this.selectedSection
|
||||||
|
? encodeURIComponent(this.selectedSection.name)
|
||||||
|
: this.selectedType; // Fallback for legacy
|
||||||
|
const baseUrl = `/wcctools-route/${sectionName}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
|
||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
|
|
||||||
|
if (this.searchQuery) {
|
||||||
|
queryParams.set('search', this.searchQuery);
|
||||||
|
}
|
||||||
if (this.frameScrollY > 0) {
|
if (this.frameScrollY > 0) {
|
||||||
queryParams.set('frameScrollY', this.frameScrollY.toString());
|
queryParams.set('frameScrollY', this.frameScrollY.toString());
|
||||||
}
|
}
|
||||||
if (this.sidebarScrollY > 0) {
|
if (this.sidebarScrollY > 0) {
|
||||||
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
|
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
|
||||||
}
|
}
|
||||||
|
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 queryString = queryParams.toString();
|
||||||
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||||||
@@ -321,10 +492,10 @@ export class WccDashboard extends DeesElement {
|
|||||||
if (this.scrollListenersAttached) {
|
if (this.scrollListenersAttached) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wccFrame = await this.wccFrame;
|
const wccFrame = await this.wccFrame;
|
||||||
const wccSidebar = this.shadowRoot.querySelector('wcc-sidebar');
|
const wccSidebar = this.shadowRoot.querySelector('wcc-sidebar') as WccSidebar | null;
|
||||||
|
|
||||||
if (wccFrame) {
|
if (wccFrame) {
|
||||||
// The frame element itself is the scrollable container
|
// The frame element itself is the scrollable container
|
||||||
wccFrame.addEventListener('scroll', () => {
|
wccFrame.addEventListener('scroll', () => {
|
||||||
@@ -335,11 +506,14 @@ export class WccDashboard extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (wccSidebar) {
|
if (wccSidebar) {
|
||||||
// The sidebar element itself is the scrollable container
|
// Use the sidebar's scrollable container (.menu element)
|
||||||
wccSidebar.addEventListener('scroll', () => {
|
const scrollContainer = wccSidebar.scrollableContainer;
|
||||||
this.sidebarScrollY = wccSidebar.scrollTop;
|
if (scrollContainer) {
|
||||||
this.debouncedScrollUpdate();
|
scrollContainer.addEventListener('scroll', () => {
|
||||||
});
|
this.sidebarScrollY = scrollContainer.scrollTop;
|
||||||
|
this.debouncedScrollUpdate();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,15 +525,27 @@ export class WccDashboard extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateUrlWithScrollState() {
|
private updateUrlWithScrollState() {
|
||||||
const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
|
const sectionName = this.selectedSection
|
||||||
|
? encodeURIComponent(this.selectedSection.name)
|
||||||
|
: this.selectedType; // Fallback for legacy
|
||||||
|
const baseUrl = `/wcctools-route/${sectionName}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
|
||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
|
|
||||||
|
if (this.searchQuery) {
|
||||||
|
queryParams.set('search', this.searchQuery);
|
||||||
|
}
|
||||||
if (this.frameScrollY > 0) {
|
if (this.frameScrollY > 0) {
|
||||||
queryParams.set('frameScrollY', this.frameScrollY.toString());
|
queryParams.set('frameScrollY', this.frameScrollY.toString());
|
||||||
}
|
}
|
||||||
if (this.sidebarScrollY > 0) {
|
if (this.sidebarScrollY > 0) {
|
||||||
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
|
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
|
||||||
}
|
}
|
||||||
|
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 queryString = queryParams.toString();
|
||||||
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||||||
@@ -373,18 +559,21 @@ export class WccDashboard extends DeesElement {
|
|||||||
if (this.scrollPositionsApplied) {
|
if (this.scrollPositionsApplied) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wccFrame = await this.wccFrame;
|
const wccFrame = await this.wccFrame;
|
||||||
const wccSidebar = this.shadowRoot.querySelector('wcc-sidebar');
|
const wccSidebar = this.shadowRoot.querySelector('wcc-sidebar') as WccSidebar | null;
|
||||||
|
|
||||||
if (wccFrame && this.frameScrollY > 0) {
|
if (wccFrame && this.frameScrollY > 0) {
|
||||||
// The frame element itself is the scrollable container
|
// The frame element itself is the scrollable container
|
||||||
wccFrame.scrollTop = this.frameScrollY;
|
wccFrame.scrollTop = this.frameScrollY;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wccSidebar && this.sidebarScrollY > 0) {
|
if (wccSidebar && this.sidebarScrollY > 0) {
|
||||||
// The sidebar element itself is the scrollable container
|
// Use the sidebar's scrollable container (.menu element)
|
||||||
wccSidebar.scrollTop = this.sidebarScrollY;
|
const scrollContainer = wccSidebar.scrollableContainer;
|
||||||
|
if (scrollContainer) {
|
||||||
|
scrollContainer.scrollTop = this.sidebarScrollY;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.scrollPositionsApplied = true;
|
this.scrollPositionsApplied = true;
|
||||||
|
|||||||
@@ -19,13 +19,18 @@ export class WccFrame extends DeesElement {
|
|||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
accessor isNative: boolean = false;
|
accessor isNative: boolean = false;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor sidebarWidth: number = 200;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
accessor isResizing: boolean = false;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
border: 10px solid #ffaeaf;
|
border: 10px solid rgba(255, 174, 175, 1);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: ${cssManager.bdTheme('#333', '#000')};
|
background: ${cssManager.bdTheme('#333', '#000')};
|
||||||
left: 200px;
|
|
||||||
right: 0px;
|
right: 0px;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -47,17 +52,17 @@ export class WccFrame extends DeesElement {
|
|||||||
<style>
|
<style>
|
||||||
:host {
|
:host {
|
||||||
${this.isNative ? `
|
${this.isNative ? `
|
||||||
border: none !important;
|
border: 0px solid rgba(255, 174, 175, 0) !important;
|
||||||
left: 0px !important;
|
left: 0px !important;
|
||||||
right: 0px !important;
|
right: 0px !important;
|
||||||
top: 0px !important;
|
top: 0px !important;
|
||||||
bottom: 0px !important;
|
bottom: 0px !important;
|
||||||
` : `
|
` : `
|
||||||
bottom: ${this.advancedEditorOpen ? '400px' : '100px'};
|
bottom: ${this.advancedEditorOpen ? '400px' : '100px'};
|
||||||
border: 10px solid #ffaeaf;
|
border: 10px solid rgba(255, 174, 175, 1);
|
||||||
left: 200px;
|
left: ${this.sidebarWidth}px;
|
||||||
`}
|
`}
|
||||||
transition: all 0.3s ease;
|
transition: ${this.isResizing ? 'none' : 'all 0.3s ease'};
|
||||||
${this.isNative ? 'padding: 0px;' : (() => {
|
${this.isNative ? 'padding: 0px;' : (() => {
|
||||||
switch (this.viewport) {
|
switch (this.viewport) {
|
||||||
case 'desktop':
|
case 'desktop':
|
||||||
@@ -67,19 +72,19 @@ export class WccFrame extends DeesElement {
|
|||||||
case 'tablet':
|
case 'tablet':
|
||||||
return `
|
return `
|
||||||
padding: 0px ${
|
padding: 0px ${
|
||||||
(document.body.clientWidth - 200 - domtools.breakpoints.tablet) / 2
|
(document.body.clientWidth - this.sidebarWidth - domtools.breakpoints.tablet) / 2
|
||||||
}px;
|
}px;
|
||||||
`;
|
`;
|
||||||
case 'phablet':
|
case 'phablet':
|
||||||
return `
|
return `
|
||||||
padding: 0px ${
|
padding: 0px ${
|
||||||
(document.body.clientWidth - 200 - domtools.breakpoints.phablet) / 2
|
(document.body.clientWidth - this.sidebarWidth - domtools.breakpoints.phablet) / 2
|
||||||
}px;
|
}px;
|
||||||
`;
|
`;
|
||||||
case 'phone':
|
case 'phone':
|
||||||
return `
|
return `
|
||||||
padding: 0px ${
|
padding: 0px ${
|
||||||
(document.body.clientWidth - 200 - domtools.breakpoints.phone) / 2
|
(document.body.clientWidth - this.sidebarWidth - domtools.breakpoints.phone) / 2
|
||||||
}px;
|
}px;
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ export class WccProperties extends DeesElement {
|
|||||||
@property()
|
@property()
|
||||||
accessor isNative: boolean = false;
|
accessor isNative: boolean = false;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor sidebarWidth: number = 200;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor propertyContent: TemplateResult[] = [];
|
accessor propertyContent: TemplateResult[] = [];
|
||||||
|
|
||||||
@@ -60,6 +63,10 @@ export class WccProperties extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
accessor recordingDuration: number = 0;
|
accessor recordingDuration: number = 0;
|
||||||
|
|
||||||
|
// Delayed hide for native mode transition
|
||||||
|
@state()
|
||||||
|
accessor isHidden: boolean = false;
|
||||||
|
|
||||||
public editorHeight: number = 300;
|
public editorHeight: number = 300;
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
@@ -89,14 +96,14 @@ export class WccProperties extends DeesElement {
|
|||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 200px;
|
left: ${this.sidebarWidth}px;
|
||||||
height: ${this.editingProperties.length > 0 ? 100 + this.editorHeight : 100}px;
|
height: ${this.editingProperties.length > 0 ? 100 + this.editorHeight : 100}px;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
display: ${this.isNative ? 'none' : 'block'};
|
display: ${this.isHidden ? 'none' : 'block'};
|
||||||
}
|
}
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -207,22 +214,22 @@ export class WccProperties extends DeesElement {
|
|||||||
}
|
}
|
||||||
.selectorButtons2 {
|
.selectorButtons2 {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.selectorButtons4 {
|
.selectorButtons4 {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.selectorButtons5 {
|
.selectorButtons5 {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, 1fr);
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.selectorButtons1 {
|
.selectorButtons1 {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: minmax(0, 1fr);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.button {
|
.button {
|
||||||
@@ -259,6 +266,11 @@ export class WccProperties extends DeesElement {
|
|||||||
.button .material-symbols-outlined {
|
.button .material-symbols-outlined {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-variation-settings: 'FILL' 0, 'wght' 300;
|
font-variation-settings: 'FILL' 0, 'wght' 300;
|
||||||
|
min-width: 18px;
|
||||||
|
min-height: 18px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.selected .material-symbols-outlined {
|
.button.selected .material-symbols-outlined {
|
||||||
@@ -301,7 +313,7 @@ export class WccProperties extends DeesElement {
|
|||||||
top: 0.5rem;
|
top: 0.5rem;
|
||||||
bottom: 0.5rem;
|
bottom: 0.5rem;
|
||||||
left: 0.5rem;
|
left: 0.5rem;
|
||||||
right: calc(520px + 0.5rem);
|
right: calc(600px + 0.5rem);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -920,16 +932,29 @@ export class WccProperties extends DeesElement {
|
|||||||
this.dashboardRef.buildUrl();
|
this.dashboardRef.buildUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async scheduleUpdate() {
|
protected updated(changedProperties: Map<string, unknown>) {
|
||||||
try {
|
super.updated(changedProperties);
|
||||||
await this.createProperties();
|
|
||||||
} catch (error) {
|
// Handle delayed hide for native mode transition
|
||||||
console.error('Error creating properties:', error);
|
if (changedProperties.has('isNative')) {
|
||||||
// Clear property content on error to show clean state
|
if (this.isNative) {
|
||||||
this.propertyContent = [];
|
// 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) {
|
public selectViewport(viewport: TEnvironment) {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import * as plugins from '../wcctools.plugins.js';
|
import * as plugins from '../wcctools.plugins.js';
|
||||||
import { DeesElement, property, html, customElement, type TemplateResult, state } from '@design.estate/dees-element';
|
import { DeesElement, property, html, customElement, type TemplateResult, state } from '@design.estate/dees-element';
|
||||||
import { WccDashboard } from './wcc-dashboard.js';
|
import { WccDashboard, getSectionItems } from './wcc-dashboard.js';
|
||||||
import type { TTemplateFactory } from './wcctools.helpers.js';
|
import type { TTemplateFactory } from './wcctools.helpers.js';
|
||||||
import { getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
|
import { getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
|
||||||
|
import type { IWccSection, TElementType } from '../wcctools.interfaces.js';
|
||||||
export type TElementType = 'element' | 'page';
|
import { WccContextmenu } from './wcc-contextmenu.js';
|
||||||
|
|
||||||
@customElement('wcc-sidebar')
|
@customElement('wcc-sidebar')
|
||||||
export class WccSidebar extends DeesElement {
|
export class WccSidebar extends DeesElement {
|
||||||
@@ -24,6 +24,39 @@ export class WccSidebar extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
accessor expandedElements: Set<string> = new Set();
|
accessor expandedElements: Set<string> = new Set();
|
||||||
|
|
||||||
|
// Track which sections are collapsed
|
||||||
|
@state()
|
||||||
|
accessor collapsedSections: Set<string> = new Set();
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the scrollable container element (.menu) for external scroll management
|
||||||
|
*/
|
||||||
|
public get scrollableContainer(): HTMLElement | null {
|
||||||
|
return this.shadowRoot?.querySelector('.menu') as HTMLElement | null;
|
||||||
|
}
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet" />
|
||||||
@@ -45,27 +78,35 @@ export class WccSidebar extends DeesElement {
|
|||||||
--ring: #3b82f6;
|
--ring: #3b82f6;
|
||||||
--radius: 4px;
|
--radius: 4px;
|
||||||
|
|
||||||
display: ${this.isNative ? 'none' : 'block'};
|
display: ${this.isHidden ? 'none' : 'flex'};
|
||||||
|
flex-direction: column;
|
||||||
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
width: 200px;
|
width: ${this.sidebarWidth}px;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
overflow-y: auto;
|
overflow: hidden;
|
||||||
overflow-x: hidden;
|
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.menu {
|
.menu {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
.section-header {
|
||||||
padding: 0.3rem 0.75rem;
|
padding: 0.3rem 0.75rem;
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -77,12 +118,45 @@ export class WccSidebar extends DeesElement {
|
|||||||
background: rgba(59, 130, 246, 0.03);
|
background: rgba(59, 130, 246, 0.03);
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: all 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3:first-child {
|
.section-header:first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-header:hover {
|
||||||
|
background: rgba(59, 130, 246, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header .expand-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header.collapsed .expand-icon {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header .section-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content.collapsed {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.material-symbols-outlined {
|
.material-symbols-outlined {
|
||||||
font-family: 'Material Symbols Outlined';
|
font-family: 'Material Symbols Outlined';
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
@@ -117,7 +191,11 @@ export class WccSidebar extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.selectOption.folder {
|
.selectOption.folder {
|
||||||
grid-template-columns: 16px 20px 1fr;
|
grid-template-columns: 16px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectOption.folder .text {
|
||||||
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectOption .expand-icon {
|
.selectOption .expand-icon {
|
||||||
@@ -214,90 +292,480 @@ export class WccSidebar extends DeesElement {
|
|||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
</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) {
|
.search-container {
|
||||||
// Multi-demo element - render as expandable folder
|
padding: 0.5rem;
|
||||||
return html`
|
border-bottom: 1px solid var(--border);
|
||||||
<div
|
position: relative;
|
||||||
class="selectOption folder ${isExpanded ? 'expanded' : ''} ${isSelected ? 'selected' : ''}"
|
}
|
||||||
@click=${() => this.toggleExpanded(elementName)}
|
|
||||||
>
|
.search-input {
|
||||||
<i class="material-symbols-outlined expand-icon">chevron_right</i>
|
width: 100%;
|
||||||
<i class="material-symbols-outlined">folder</i>
|
box-sizing: border-box;
|
||||||
<div class="text">${elementName}</div>
|
background: var(--input);
|
||||||
</div>
|
border: 1px solid var(--border);
|
||||||
${isExpanded ? html`
|
border-radius: var(--radius);
|
||||||
<div class="demo-children">
|
padding: 0.5rem 1.75rem 0.5rem 0.75rem;
|
||||||
${Array.from({ length: demoCount }, (_, i) => {
|
color: var(--foreground);
|
||||||
const demoIndex = i;
|
font-size: 0.75rem;
|
||||||
const isThisDemoSelected = isSelected && this.dashboardRef.selectedDemoIndex === demoIndex;
|
font-family: inherit;
|
||||||
return html`
|
outline: none;
|
||||||
<div
|
transition: border-color 0.15s ease;
|
||||||
class="demo-child ${isThisDemoSelected ? 'selected' : ''}"
|
}
|
||||||
@click=${async () => {
|
|
||||||
await plugins.deesDomtools.DomTools.setupDomTools();
|
.search-input:focus {
|
||||||
this.selectItem('element', elementName, item, demoIndex);
|
border-color: var(--primary);
|
||||||
}}
|
}
|
||||||
>
|
|
||||||
<i class="material-symbols-outlined">play_circle</i>
|
.search-input::placeholder {
|
||||||
<div class="text">demo${demoIndex + 1}</div>
|
color: var(--muted-foreground);
|
||||||
</div>
|
}
|
||||||
`;
|
|
||||||
})}
|
.search-clear {
|
||||||
</div>
|
position: absolute;
|
||||||
` : null}
|
right: 0.75rem;
|
||||||
`;
|
top: 50%;
|
||||||
} else {
|
transform: translateY(-50%);
|
||||||
// Single demo element - render as normal
|
background: none;
|
||||||
return html`
|
border: none;
|
||||||
<div
|
padding: 0.25rem;
|
||||||
class="selectOption ${isSelected ? 'selected' : null}"
|
cursor: pointer;
|
||||||
@click=${async () => {
|
color: var(--muted-foreground);
|
||||||
await plugins.deesDomtools.DomTools.setupDomTools();
|
display: flex;
|
||||||
this.selectItem('element', elementName, item, 0);
|
align-items: center;
|
||||||
}}
|
justify-content: center;
|
||||||
>
|
border-radius: 2px;
|
||||||
<i class="material-symbols-outlined">featured_video</i>
|
transition: color 0.15s ease, background 0.15s ease;
|
||||||
<div class="text">${elementName}</div>
|
}
|
||||||
</div>
|
|
||||||
`;
|
.search-clear:hover {
|
||||||
}
|
color: var(--foreground);
|
||||||
});
|
background: rgba(255, 255, 255, 0.1);
|
||||||
})()}
|
}
|
||||||
|
|
||||||
|
.search-clear .material-symbols-outlined {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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="sidebar-header">
|
||||||
|
<div class="search-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="search-input"
|
||||||
|
placeholder="Search..."
|
||||||
|
.value=${this.searchQuery}
|
||||||
|
@input=${this.handleSearchInput}
|
||||||
|
/>
|
||||||
|
${this.searchQuery ? html`
|
||||||
|
<button class="search-clear" @click=${this.clearSearch}>
|
||||||
|
<i class="material-symbols-outlined">close</i>
|
||||||
|
</button>
|
||||||
|
` : null}
|
||||||
|
</div>
|
||||||
|
${this.renderPinnedSection()}
|
||||||
|
</div>
|
||||||
|
<div class="menu">
|
||||||
|
${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>
|
</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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a unified list of render items (ungrouped elements and groups)
|
||||||
|
// Each item has a sortKey (element name or first element name of group)
|
||||||
|
type RenderItem =
|
||||||
|
| { type: 'element'; entry: [string, any]; sortKey: string }
|
||||||
|
| { type: 'group'; groupName: string; items: Array<[string, any]>; sortKey: string };
|
||||||
|
|
||||||
|
const renderItems: RenderItem[] = [];
|
||||||
|
|
||||||
|
// Add ungrouped items
|
||||||
|
const ungrouped = groupedItems.get(null) || [];
|
||||||
|
for (const entry of ungrouped) {
|
||||||
|
renderItems.push({ type: 'element', entry, sortKey: entry[0].toLowerCase() });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add groups (sorted by their first element's name)
|
||||||
|
for (const [groupName, items] of groupedItems) {
|
||||||
|
if (groupName === null) continue;
|
||||||
|
const firstElementName = items[0]?.[0] || '';
|
||||||
|
renderItems.push({ type: 'group', groupName, items, sortKey: firstElementName.toLowerCase() });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort all items alphabetically by sortKey
|
||||||
|
renderItems.sort((a, b) => a.sortKey.localeCompare(b.sortKey));
|
||||||
|
|
||||||
|
// Render in sorted order
|
||||||
|
return renderItems.map((item) => {
|
||||||
|
if (item.type === 'element') {
|
||||||
|
return this.renderElementItem(item.entry, section);
|
||||||
|
} else {
|
||||||
|
return html`
|
||||||
|
<div class="item-group">
|
||||||
|
<span class="item-group-legend">${item.groupName}</span>
|
||||||
|
${item.items.map((entry) => this.renderElementItem(entry, section))}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
private toggleExpanded(elementName: string) {
|
||||||
const newSet = new Set(this.expandedElements);
|
const newSet = new Set(this.expandedElements);
|
||||||
if (newSet.has(elementName)) {
|
if (newSet.has(elementName)) {
|
||||||
@@ -308,14 +776,141 @@ export class WccSidebar extends DeesElement {
|
|||||||
this.expandedElements = newSet;
|
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 clearSearch() {
|
||||||
|
this.searchQuery = '';
|
||||||
|
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('selected item');
|
||||||
console.log(itemNameArg);
|
console.log(itemNameArg);
|
||||||
console.log(itemArg);
|
console.log(itemArg);
|
||||||
console.log('demo index:', demoIndex);
|
console.log('demo index:', demoIndex);
|
||||||
|
console.log('section:', section?.name);
|
||||||
|
|
||||||
this.selectedItem = itemArg;
|
this.selectedItem = itemArg;
|
||||||
this.selectedType = typeArg;
|
this.selectedType = typeArg;
|
||||||
this.dashboardRef.selectedDemoIndex = demoIndex;
|
this.dashboardRef.selectedDemoIndex = demoIndex;
|
||||||
|
|
||||||
|
// Set the selected section on dashboard
|
||||||
|
if (section) {
|
||||||
|
this.dashboardRef.selectedSection = section;
|
||||||
|
}
|
||||||
|
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('selectedType', {
|
new CustomEvent('selectedType', {
|
||||||
detail: typeArg
|
detail: typeArg
|
||||||
@@ -335,7 +930,6 @@ export class WccSidebar extends DeesElement {
|
|||||||
this.dashboardRef.buildUrl();
|
this.dashboardRef.buildUrl();
|
||||||
|
|
||||||
// Force re-render to update demo child selection indicator
|
// Force re-render to update demo child selection indicator
|
||||||
// (needed when switching between demos of the same element)
|
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,86 @@
|
|||||||
import { WccDashboard } from './elements/wcc-dashboard.js';
|
import { WccDashboard } from './elements/wcc-dashboard.js';
|
||||||
import { LitElement } from 'lit';
|
import { LitElement } from 'lit';
|
||||||
import type { TTemplateFactory } from './elements/wcctools.helpers.js';
|
import type { TTemplateFactory } from './elements/wcctools.helpers.js';
|
||||||
|
import type { IWccConfig, IWccSection } from './wcctools.interfaces.js';
|
||||||
|
|
||||||
// Export recording components and service
|
// Export recording components and service
|
||||||
export { RecorderService, type IRecorderEvents, type IRecordingOptions } from './services/recorder.service.js';
|
export { RecorderService, type IRecorderEvents, type IRecordingOptions } from './services/recorder.service.js';
|
||||||
export { WccRecordButton } from './elements/wcc-record-button.js';
|
export { WccRecordButton } from './elements/wcc-record-button.js';
|
||||||
export { WccRecordingPanel } from './elements/wcc-recording-panel.js';
|
export { WccRecordingPanel } from './elements/wcc-recording-panel.js';
|
||||||
|
|
||||||
const setupWccTools = (
|
// Export types for external use
|
||||||
|
export type { IWccConfig, IWccSection } from './wcctools.interfaces.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts legacy (elements, pages) format to new sections config
|
||||||
|
*/
|
||||||
|
const convertLegacyToConfig = (
|
||||||
elementsArg?: { [key: string]: LitElement },
|
elementsArg?: { [key: string]: LitElement },
|
||||||
pagesArg?: Record<string, TTemplateFactory>
|
pagesArg?: Record<string, TTemplateFactory>
|
||||||
|
): IWccConfig => {
|
||||||
|
const sections: IWccSection[] = [];
|
||||||
|
|
||||||
|
if (pagesArg && Object.keys(pagesArg).length > 0) {
|
||||||
|
sections.push({
|
||||||
|
name: 'Pages',
|
||||||
|
type: 'pages',
|
||||||
|
items: pagesArg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elementsArg && Object.keys(elementsArg).length > 0) {
|
||||||
|
sections.push({
|
||||||
|
name: 'Elements',
|
||||||
|
type: 'elements',
|
||||||
|
items: elementsArg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { sections };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the argument is the new config format
|
||||||
|
*/
|
||||||
|
const isWccConfig = (arg: any): arg is IWccConfig => {
|
||||||
|
return arg && typeof arg === 'object' && 'sections' in arg && Array.isArray(arg.sections);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup WCC Tools dashboard
|
||||||
|
*
|
||||||
|
* New format (recommended):
|
||||||
|
* ```typescript
|
||||||
|
* setupWccTools({
|
||||||
|
* sections: [
|
||||||
|
* { name: 'Elements', type: 'elements', items: elements },
|
||||||
|
* { name: 'Pages', type: 'pages', items: pages },
|
||||||
|
* ]
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Legacy format (still supported):
|
||||||
|
* ```typescript
|
||||||
|
* setupWccTools(elements, pages);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
const setupWccTools = (
|
||||||
|
configOrElements?: IWccConfig | { [key: string]: LitElement },
|
||||||
|
pagesArg?: Record<string, TTemplateFactory>
|
||||||
) => {
|
) => {
|
||||||
|
let config: IWccConfig;
|
||||||
|
|
||||||
|
if (isWccConfig(configOrElements)) {
|
||||||
|
config = configOrElements;
|
||||||
|
} else {
|
||||||
|
config = convertLegacyToConfig(configOrElements, pagesArg);
|
||||||
|
}
|
||||||
|
|
||||||
let hasRun = false;
|
let hasRun = false;
|
||||||
const runWccToolsSetup = async () => {
|
const runWccToolsSetup = async () => {
|
||||||
if (document.readyState === 'complete' && !hasRun) {
|
if (document.readyState === 'complete' && !hasRun) {
|
||||||
hasRun = true;
|
hasRun = true;
|
||||||
const wccTools = new WccDashboard(elementsArg as any, pagesArg);
|
const wccTools = new WccDashboard(config);
|
||||||
document.querySelector('body').append(wccTools);
|
document.querySelector('body').append(wccTools);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,40 @@ pnpm add -D @design.estate/dees-wcctools
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
### Sections-based Configuration (Recommended)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { setupWccTools } from '@design.estate/dees-wcctools';
|
||||||
|
import * as elements from './elements/index.js';
|
||||||
|
import * as views from './views/index.js';
|
||||||
|
import * as pages from './pages/index.js';
|
||||||
|
|
||||||
|
setupWccTools({
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
name: 'Pages',
|
||||||
|
type: 'pages',
|
||||||
|
items: pages,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Views',
|
||||||
|
type: 'elements',
|
||||||
|
items: views,
|
||||||
|
icon: 'web',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Elements',
|
||||||
|
type: 'elements',
|
||||||
|
items: elements,
|
||||||
|
filter: (name) => !name.startsWith('internal-'),
|
||||||
|
sort: ([a], [b]) => a.localeCompare(b),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Legacy Format (Still Supported)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { setupWccTools } from '@design.estate/dees-wcctools';
|
import { setupWccTools } from '@design.estate/dees-wcctools';
|
||||||
import { MyButton } from './components/my-button.js';
|
import { MyButton } from './components/my-button.js';
|
||||||
@@ -30,6 +64,8 @@ setupWccTools({
|
|||||||
| Export | Description |
|
| Export | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `setupWccTools` | Initialize the component catalogue dashboard |
|
| `setupWccTools` | Initialize the component catalogue dashboard |
|
||||||
|
| `IWccConfig` | TypeScript interface for sections configuration |
|
||||||
|
| `IWccSection` | TypeScript interface for individual section |
|
||||||
|
|
||||||
### Recording Components
|
### Recording Components
|
||||||
|
|
||||||
@@ -41,6 +77,18 @@ setupWccTools({
|
|||||||
| `IRecorderEvents` | TypeScript interface for recorder callbacks |
|
| `IRecorderEvents` | TypeScript interface for recorder callbacks |
|
||||||
| `IRecordingOptions` | TypeScript interface for recording options |
|
| `IRecordingOptions` | TypeScript interface for recording options |
|
||||||
|
|
||||||
|
## Section Configuration
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
|----------|------|-------------|
|
||||||
|
| `name` | `string` | Display name for the section header |
|
||||||
|
| `type` | `'elements' \| 'pages'` | Rendering behavior |
|
||||||
|
| `items` | `Record<string, any>` | Element classes or page factories |
|
||||||
|
| `filter` | `(name, item) => boolean` | Optional filter function |
|
||||||
|
| `sort` | `([a, itemA], [b, itemB]) => number` | Optional sort function |
|
||||||
|
| `icon` | `string` | Material Symbols icon name |
|
||||||
|
| `collapsed` | `boolean` | Start section collapsed (default: false) |
|
||||||
|
|
||||||
## Internal Components
|
## Internal Components
|
||||||
|
|
||||||
The module includes these internal web components:
|
The module includes these internal web components:
|
||||||
@@ -48,8 +96,8 @@ The module includes these internal web components:
|
|||||||
| Component | Description |
|
| Component | Description |
|
||||||
|-----------|-------------|
|
|-----------|-------------|
|
||||||
| `wcc-dashboard` | Main dashboard container with routing |
|
| `wcc-dashboard` | Main dashboard container with routing |
|
||||||
| `wcc-sidebar` | Navigation sidebar with element/page listing |
|
| `wcc-sidebar` | Navigation sidebar with collapsible sections |
|
||||||
| `wcc-frame` | Iframe viewport with responsive sizing |
|
| `wcc-frame` | Responsive viewport with size controls |
|
||||||
| `wcc-properties` | Property panel with live editing |
|
| `wcc-properties` | Property panel with live editing |
|
||||||
| `wcc-record-button` | Recording state indicator button |
|
| `wcc-record-button` | Recording state indicator button |
|
||||||
| `wcc-recording-panel` | Recording workflow UI |
|
| `wcc-recording-panel` | Recording workflow UI |
|
||||||
@@ -72,14 +120,14 @@ const events: IRecorderEvents = {
|
|||||||
const recorder = new RecorderService(events);
|
const recorder = new RecorderService(events);
|
||||||
|
|
||||||
// Load available microphones
|
// Load available microphones
|
||||||
const mics = await recorder.loadMicrophones(true); // true = request permission
|
const mics = await recorder.loadMicrophones(true);
|
||||||
|
|
||||||
// Start audio level monitoring
|
// Start audio level monitoring
|
||||||
await recorder.startAudioMonitoring(mics[0].deviceId);
|
await recorder.startAudioMonitoring(mics[0].deviceId);
|
||||||
|
|
||||||
// Start recording
|
// Start recording
|
||||||
await recorder.startRecording({
|
await recorder.startRecording({
|
||||||
mode: 'viewport', // or 'screen'
|
mode: 'viewport',
|
||||||
audioDeviceId: mics[0].deviceId,
|
audioDeviceId: mics[0].deviceId,
|
||||||
viewportElement: document.querySelector('.viewport'),
|
viewportElement: document.querySelector('.viewport'),
|
||||||
});
|
});
|
||||||
@@ -99,10 +147,11 @@ recorder.dispose();
|
|||||||
```
|
```
|
||||||
ts_web/
|
ts_web/
|
||||||
├── index.ts # Main exports
|
├── index.ts # Main exports
|
||||||
|
├── wcctools.interfaces.ts # Type definitions
|
||||||
├── elements/
|
├── elements/
|
||||||
│ ├── wcc-dashboard.ts # Root dashboard component
|
│ ├── wcc-dashboard.ts # Root dashboard component
|
||||||
│ ├── wcc-sidebar.ts # Navigation sidebar
|
│ ├── wcc-sidebar.ts # Navigation sidebar
|
||||||
│ ├── wcc-frame.ts # Responsive iframe viewport
|
│ ├── wcc-frame.ts # Responsive viewport
|
||||||
│ ├── wcc-properties.ts # Property editing panel
|
│ ├── wcc-properties.ts # Property editing panel
|
||||||
│ ├── wcc-record-button.ts # Recording button
|
│ ├── wcc-record-button.ts # Recording button
|
||||||
│ ├── wcc-recording-panel.ts # Recording options/preview
|
│ ├── wcc-recording-panel.ts # Recording options/preview
|
||||||
@@ -116,6 +165,7 @@ ts_web/
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 🎨 Interactive component preview
|
- 🎨 Interactive component preview
|
||||||
|
- 📂 Section-based sidebar with filtering & sorting
|
||||||
- 🔧 Real-time property editing with type detection
|
- 🔧 Real-time property editing with type detection
|
||||||
- 🌓 Theme switching (light/dark)
|
- 🌓 Theme switching (light/dark)
|
||||||
- 📱 Responsive viewport testing
|
- 📱 Responsive viewport testing
|
||||||
|
|||||||
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