This commit is contained in:
2025-12-11 15:49:04 +00:00
parent d7d6d650bc
commit ad033c8104
3 changed files with 233 additions and 35 deletions

View File

@@ -1,5 +1,5 @@
import { DeesElement, property, html, customElement, type TemplateResult, queryAsync, render, domtools } from '@design.estate/dees-element';
import { resolveTemplateFactory } from './wcctools.helpers.js';
import { resolveTemplateFactory, getDemoAtIndex, getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
import type { TTemplateFactory } from './wcctools.helpers.js';
import * as plugins from '../wcctools.plugins.js';
@@ -25,6 +25,9 @@ export class WccDashboard extends DeesElement {
@property()
accessor selectedItem: TTemplateFactory | DeesElement;
@property({ type: Number })
accessor selectedDemoIndex: number = 0;
@property()
accessor selectedViewport: plugins.deesDomtools.breakpoints.TViewport = 'desktop';
@@ -151,11 +154,13 @@ export class WccDashboard extends DeesElement {
this.setupScrollListeners();
}, 500);
// Route with demo index (new format)
this.domtools.router.on(
'/wcctools-route/:itemType/:itemName/:viewport/:theme',
'/wcctools-route/:itemType/:itemName/:demoIndex/:viewport/:theme',
async (routeInfo) => {
this.selectedType = routeInfo.params.itemType as TElementType;
this.selectedItemName = routeInfo.params.itemName;
this.selectedDemoIndex = parseInt(routeInfo.params.demoIndex) || 0;
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
this.selectedTheme = routeInfo.params.theme as TTheme;
if (routeInfo.params.itemType === 'element') {
@@ -163,25 +168,65 @@ export class WccDashboard extends DeesElement {
} else if (routeInfo.params.itemType === 'page') {
this.selectedItem = this.pages[routeInfo.params.itemName];
}
// Restore scroll positions from query parameters
if (routeInfo.queryParams) {
const frameScrollY = routeInfo.queryParams.frameScrollY;
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
if (frameScrollY) {
this.frameScrollY = parseInt(frameScrollY);
}
if (sidebarScrollY) {
this.sidebarScrollY = parseInt(sidebarScrollY);
}
// Apply scroll positions after a short delay to ensure DOM is ready
setTimeout(() => {
this.applyScrollPositions();
}, 100);
}
const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
this.selectedTheme === 'bright'
? domtoolsInstance.themeManager.goBright()
: domtoolsInstance.themeManager.goDark();
}
);
// Legacy route without demo index (for backwards compatibility)
this.domtools.router.on(
'/wcctools-route/:itemType/:itemName/:viewport/:theme',
async (routeInfo) => {
this.selectedType = routeInfo.params.itemType as TElementType;
this.selectedItemName = routeInfo.params.itemName;
this.selectedDemoIndex = 0; // Default to first demo
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
this.selectedTheme = routeInfo.params.theme as TTheme;
if (routeInfo.params.itemType === 'element') {
this.selectedItem = this.elements[routeInfo.params.itemName];
} else if (routeInfo.params.itemType === 'page') {
this.selectedItem = this.pages[routeInfo.params.itemName];
}
// Restore scroll positions from query parameters
if (routeInfo.queryParams) {
const frameScrollY = routeInfo.queryParams.frameScrollY;
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
if (frameScrollY) {
this.frameScrollY = parseInt(frameScrollY);
}
if (sidebarScrollY) {
this.sidebarScrollY = parseInt(sidebarScrollY);
}
// Apply scroll positions after a short delay to ensure DOM is ready
setTimeout(() => {
this.applyScrollPositions();
}, 100);
}
const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
this.selectedTheme === 'bright'
? domtoolsInstance.themeManager.goBright()
@@ -218,33 +263,48 @@ export class WccDashboard extends DeesElement {
this.setWarning(`component ${anonItem.name} does not expose a demo property.`);
return;
}
if (!(typeof anonItem.demo === 'function')) {
// Support both single demo (function) and multiple demos (array)
const isArray = Array.isArray(anonItem.demo);
const isFunction = typeof anonItem.demo === 'function';
if (!isArray && !isFunction) {
this.setWarning(
`component ${anonItem.name} has demo property, but it is not of type function`
`component ${anonItem.name} has demo property, but it is not a function or array of functions`
);
return;
}
// Get the specific demo to render
const demoFactory = getDemoAtIndex(anonItem.demo, this.selectedDemoIndex);
if (!demoFactory) {
this.setWarning(
`component ${anonItem.name} does not have a demo at index ${this.selectedDemoIndex + 1}`
);
return;
}
this.setWarning(null);
const viewport = await wccFrame.getViewportElement();
const demoTemplate = await resolveTemplateFactory(() => anonItem.demo());
const demoTemplate = await resolveTemplateFactory(demoFactory);
render(demoTemplate, viewport);
}
}
public buildUrl() {
const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedViewport}/${this.selectedTheme}`;
const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
const queryParams = new URLSearchParams();
if (this.frameScrollY > 0) {
queryParams.set('frameScrollY', this.frameScrollY.toString());
}
if (this.sidebarScrollY > 0) {
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
}
const queryString = queryParams.toString();
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
this.domtools.router.pushUrl(fullUrl);
}
@@ -286,19 +346,19 @@ export class WccDashboard extends DeesElement {
}
private updateUrlWithScrollState() {
const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedViewport}/${this.selectedTheme}`;
const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
const queryParams = new URLSearchParams();
if (this.frameScrollY > 0) {
queryParams.set('frameScrollY', this.frameScrollY.toString());
}
if (this.sidebarScrollY > 0) {
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
}
const queryString = queryParams.toString();
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
// Use replaceState to update URL without navigation
window.history.replaceState(null, '', fullUrl);
}

View File

@@ -1,7 +1,8 @@
import * as plugins from '../wcctools.plugins.js';
import { DeesElement, property, html, customElement, type TemplateResult } from '@design.estate/dees-element';
import { DeesElement, property, html, customElement, type TemplateResult, state } from '@design.estate/dees-element';
import { WccDashboard } from './wcc-dashboard.js';
import type { TTemplateFactory } from './wcctools.helpers.js';
import { getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
export type TElementType = 'element' | 'page';
@@ -19,6 +20,10 @@ export class WccSidebar extends DeesElement {
@property()
accessor isFullscreen: boolean = false;
// Track which elements are expanded (for multi-demo elements)
@state()
accessor expandedElements: Set<string> = new Set();
public render(): TemplateResult {
return html`
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet" />
@@ -110,7 +115,21 @@ export class WccSidebar extends DeesElement {
color: #999;
background: transparent;
}
.selectOption.folder {
grid-template-columns: 16px 20px 1fr;
}
.selectOption .expand-icon {
font-size: 14px;
opacity: 0.5;
transition: transform 0.2s ease;
}
.selectOption.expanded .expand-icon {
transform: rotate(90deg);
}
.selectOption:hover {
background: rgba(59, 130, 246, 0.05);
color: #bbb;
@@ -143,6 +162,42 @@ export class WccSidebar extends DeesElement {
font-weight: 400;
}
.demo-children {
margin-left: 1rem;
overflow: hidden;
}
.demo-child {
user-select: none;
position: relative;
margin: 0.125rem 0.5rem;
padding: 0.35rem 0.75rem;
transition: all 0.15s ease;
display: grid;
grid-template-columns: 16px 1fr;
align-items: center;
gap: 0.5rem;
border-radius: var(--radius);
cursor: pointer;
font-size: 0.7rem;
color: #777;
background: transparent;
}
.demo-child:hover {
background: rgba(59, 130, 246, 0.05);
color: #bbb;
}
.demo-child.selected {
background: rgba(59, 130, 246, 0.15);
color: var(--primary);
}
.demo-child .material-symbols-outlined {
font-size: 14px;
}
::-webkit-scrollbar {
width: 8px;
}
@@ -171,7 +226,7 @@ export class WccSidebar extends DeesElement {
class="selectOption ${this.selectedItem === item ? 'selected' : null}"
@click=${async () => {
const domtools = await plugins.deesDomtools.DomTools.setupDomTools();
this.selectItem('page', pageName, item);
this.selectItem('page', pageName, item, 0);
}}
>
<i class="material-symbols-outlined">insert_drive_file</i>
@@ -184,31 +239,83 @@ export class WccSidebar extends DeesElement {
${(() => {
const elements = Object.keys(this.dashboardRef.elements);
return elements.map(elementName => {
const item = this.dashboardRef.elements[elementName];
return html`
<div
class="selectOption ${this.selectedItem === item ? 'selected' : null}"
@click=${async () => {
const domtools = await plugins.deesDomtools.DomTools.setupDomTools();
this.selectItem('element', elementName, item);
}}
>
<i class="material-symbols-outlined">featured_video</i>
<div class="text">${elementName}</div>
</div>
`;
const item = this.dashboardRef.elements[elementName] as any;
const demoCount = item.demo ? getDemoCount(item.demo) : 0;
const isMultiDemo = item.demo && hasMultipleDemos(item.demo);
const isExpanded = this.expandedElements.has(elementName);
const isSelected = this.selectedItem === item;
if (isMultiDemo) {
// Multi-demo element - render as expandable folder
return html`
<div
class="selectOption folder ${isExpanded ? 'expanded' : ''} ${isSelected ? 'selected' : ''}"
@click=${() => this.toggleExpanded(elementName)}
>
<i class="material-symbols-outlined expand-icon">chevron_right</i>
<i class="material-symbols-outlined">folder</i>
<div class="text">${elementName}</div>
</div>
${isExpanded ? html`
<div class="demo-children">
${Array.from({ length: demoCount }, (_, i) => {
const demoIndex = i;
const isThisDemoSelected = isSelected && this.dashboardRef.selectedDemoIndex === demoIndex;
return html`
<div
class="demo-child ${isThisDemoSelected ? 'selected' : ''}"
@click=${async () => {
await plugins.deesDomtools.DomTools.setupDomTools();
this.selectItem('element', elementName, item, demoIndex);
}}
>
<i class="material-symbols-outlined">play_circle</i>
<div class="text">demo${demoIndex + 1}</div>
</div>
`;
})}
</div>
` : null}
`;
} else {
// Single demo element - render as normal
return html`
<div
class="selectOption ${isSelected ? 'selected' : null}"
@click=${async () => {
await plugins.deesDomtools.DomTools.setupDomTools();
this.selectItem('element', elementName, item, 0);
}}
>
<i class="material-symbols-outlined">featured_video</i>
<div class="text">${elementName}</div>
</div>
`;
}
});
})()}
</div>
`;
}
public selectItem(typeArg: TElementType, itemNameArg: string, itemArg: TTemplateFactory | DeesElement) {
private toggleExpanded(elementName: string) {
const newSet = new Set(this.expandedElements);
if (newSet.has(elementName)) {
newSet.delete(elementName);
} else {
newSet.add(elementName);
}
this.expandedElements = newSet;
}
public selectItem(typeArg: TElementType, itemNameArg: string, itemArg: TTemplateFactory | DeesElement, demoIndex: number = 0) {
console.log('selected item');
console.log(itemNameArg);
console.log(itemArg);
console.log('demo index:', demoIndex);
this.selectedItem = itemArg;
this.selectedType = typeArg;
this.dashboardRef.selectedDemoIndex = demoIndex;
this.dispatchEvent(
new CustomEvent('selectedType', {
detail: typeArg
@@ -224,7 +331,7 @@ export class WccSidebar extends DeesElement {
detail: itemArg
})
);
this.dashboardRef.buildUrl();
}
}

View File

@@ -2,8 +2,39 @@ import type { TemplateResult } from 'lit';
export type TTemplateFactory = () => TemplateResult | Promise<TemplateResult>;
// Demo can be a single function or an array of functions
export type TDemoDefinition = TTemplateFactory | TTemplateFactory[];
export const resolveTemplateFactory = async (
factoryArg: TTemplateFactory
): Promise<TemplateResult> => {
return await Promise.resolve(factoryArg());
};
/**
* Get the number of demos for an element
*/
export const getDemoCount = (demo: TDemoDefinition): number => {
if (Array.isArray(demo)) {
return demo.length;
}
return 1;
};
/**
* Get a specific demo by index (0-based internally, displayed as 1-based)
*/
export const getDemoAtIndex = (demo: TDemoDefinition, index: number): TTemplateFactory | null => {
if (Array.isArray(demo)) {
return demo[index] ?? null;
}
// Single demo - only index 0 is valid
return index === 0 ? demo : null;
};
/**
* Check if an element has multiple demos
*/
export const hasMultipleDemos = (demo: TDemoDefinition): boolean => {
return Array.isArray(demo) && demo.length > 1;
};