import { DeesElement, property, html, customElement, type TemplateResult, queryAsync, render, domtools } from '@design.estate/dees-element';
import { resolveTemplateFactory, getDemoAtIndex, getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
import type { TTemplateFactory } from './wcctools.helpers.js';
import type { IWccConfig, IWccSection, TElementType } from '../wcctools.interfaces.js';
import * as plugins from '../wcctools.plugins.js';
// wcc tools
import './wcc-frame.js';
import './wcc-sidebar.js';
import './wcc-properties.js';
import { type TTheme } from './wcc-properties.js';
import { breakpoints } from '@design.estate/dees-domtools';
import { WccFrame } from './wcc-frame.js';
/**
* Get filtered and sorted items from a section
*/
export const getSectionItems = (section: IWccSection): Array<[string, any]> => {
let entries = Object.entries(section.items);
// Apply filter if provided
if (section.filter) {
entries = entries.filter(([name, item]) => section.filter(name, item));
}
// Apply sort if provided
if (section.sort) {
entries.sort(section.sort);
}
return entries;
};
@customElement('wcc-dashboard')
export class WccDashboard extends DeesElement {
@property()
accessor sections: IWccSection[] = [];
@property()
accessor selectedSection: IWccSection | null = null;
@property()
accessor selectedType: TElementType;
@property()
accessor selectedItemName: string;
@property()
accessor selectedItem: TTemplateFactory | DeesElement;
@property({ type: Number })
accessor selectedDemoIndex: number = 0;
@property()
accessor selectedViewport: plugins.deesDomtools.breakpoints.TViewport = 'desktop';
@property()
accessor selectedTheme: TTheme = 'dark';
@property()
accessor searchQuery: string = '';
// Derived from selectedViewport - no need for separate property
public get isNative(): boolean {
return this.selectedViewport === 'native';
}
@property()
accessor warning: string = null;
private frameScrollY: number = 0;
private sidebarScrollY: number = 0;
private scrollPositionsApplied: boolean = false;
@queryAsync('wcc-frame')
accessor wccFrame: Promise;
constructor(config?: IWccConfig) {
super();
if (config && config.sections) {
this.sections = config.sections;
console.log('got sections:', this.sections.map(s => s.name));
}
}
/**
* Find an item by name across all sections, returns the item and its section
*/
public findItemByName(name: string): { item: any; section: IWccSection } | null {
for (const section of this.sections) {
const entries = getSectionItems(section);
const found = entries.find(([itemName]) => itemName === name);
if (found) {
return { item: found[1], section };
}
}
return null;
}
/**
* Find a section by name (URL-decoded)
*/
public findSectionByName(name: string): IWccSection | null {
return this.sections.find(s => s.name === name) || null;
}
public render(): TemplateResult {
return html`
{
this.selectedType = eventArg.detail;
}}
@selectedItemName=${(eventArg) => {
this.selectedItemName = eventArg.detail;
}}
@selectedItem=${(eventArg) => {
this.selectedItem = eventArg.detail;
}}
@searchChanged=${(eventArg: CustomEvent) => {
this.searchQuery = eventArg.detail;
this.updateUrlWithScrollState();
}}
>
{
this.selectedViewport = eventArg.detail;
this.scheduleUpdate();
}}
@selectedTheme=${(eventArg) => {
this.selectedTheme = eventArg.detail;
}}
@editorStateChanged=${async (eventArg) => {
const frame = await this.wccFrame;
if (frame) {
frame.advancedEditorOpen = eventArg.detail.isOpen;
frame.requestUpdate();
}
}}
@toggleNative=${() => {
this.toggleNative();
}}
>
`;
}
public setWarning(warningTextArg: string) {
if (this.warning !== warningTextArg) {
console.log(warningTextArg);
this.warning = warningTextArg;
setTimeout(() => {
this.scheduleUpdate();
}, 0);
}
}
public toggleNative() {
// Toggle between 'native' and 'desktop' viewports
this.selectedViewport = this.selectedViewport === 'native' ? 'desktop' : 'native';
this.buildUrl();
}
public async firstUpdated() {
this.domtools = await plugins.deesDomtools.DomTools.setupDomTools();
// Add ESC key handler for native mode
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && this.isNative) {
this.selectedViewport = 'desktop';
this.buildUrl();
}
});
// Set up scroll listeners after DOM is ready
setTimeout(() => {
this.setupScrollListeners();
}, 500);
// New route format with section name
this.domtools.router.on(
'/wcctools-route/:sectionName/:itemName/:demoIndex/:viewport/:theme',
async (routeInfo) => {
const sectionName = decodeURIComponent(routeInfo.params.sectionName);
this.selectedSection = this.findSectionByName(sectionName);
this.selectedItemName = routeInfo.params.itemName;
this.selectedDemoIndex = parseInt(routeInfo.params.demoIndex) || 0;
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
this.selectedTheme = routeInfo.params.theme as TTheme;
if (this.selectedSection) {
// Find item within the section
const entries = getSectionItems(this.selectedSection);
const found = entries.find(([name]) => name === routeInfo.params.itemName);
if (found) {
this.selectedItem = found[1];
this.selectedType = this.selectedSection.type === 'elements' ? 'element' : 'page';
}
} else {
// Fallback: try legacy format (element/page as section name)
const legacyType = routeInfo.params.sectionName;
if (legacyType === 'element' || legacyType === 'page') {
this.selectedType = legacyType as TElementType;
// Find item in any matching section
const result = this.findItemByName(routeInfo.params.itemName);
if (result) {
this.selectedItem = result.item;
this.selectedSection = result.section;
}
}
}
// Restore state from query parameters
if (routeInfo.queryParams) {
const search = routeInfo.queryParams.search;
const frameScrollY = routeInfo.queryParams.frameScrollY;
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
if (search) {
this.searchQuery = search;
} else {
this.searchQuery = '';
}
if (frameScrollY) {
this.frameScrollY = parseInt(frameScrollY);
}
if (sidebarScrollY) {
this.sidebarScrollY = parseInt(sidebarScrollY);
}
// Apply scroll positions after a short delay to ensure DOM is ready
setTimeout(() => {
this.applyScrollPositions();
}, 100);
} else {
this.searchQuery = '';
}
const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
this.selectedTheme === 'bright'
? domtoolsInstance.themeManager.goBright()
: domtoolsInstance.themeManager.goDark();
}
);
// Legacy route without demo index (for backwards compatibility)
this.domtools.router.on(
'/wcctools-route/:sectionName/:itemName/:viewport/:theme',
async (routeInfo) => {
const sectionName = decodeURIComponent(routeInfo.params.sectionName);
this.selectedSection = this.findSectionByName(sectionName);
this.selectedItemName = routeInfo.params.itemName;
this.selectedDemoIndex = 0;
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
this.selectedTheme = routeInfo.params.theme as TTheme;
if (this.selectedSection) {
const entries = getSectionItems(this.selectedSection);
const found = entries.find(([name]) => name === routeInfo.params.itemName);
if (found) {
this.selectedItem = found[1];
this.selectedType = this.selectedSection.type === 'elements' ? 'element' : 'page';
}
} else {
// Fallback: try legacy format
const legacyType = routeInfo.params.sectionName;
if (legacyType === 'element' || legacyType === 'page') {
this.selectedType = legacyType as TElementType;
const result = this.findItemByName(routeInfo.params.itemName);
if (result) {
this.selectedItem = result.item;
this.selectedSection = result.section;
}
}
}
// Restore state from query parameters
if (routeInfo.queryParams) {
const search = routeInfo.queryParams.search;
const frameScrollY = routeInfo.queryParams.frameScrollY;
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
if (search) {
this.searchQuery = search;
} else {
this.searchQuery = '';
}
if (frameScrollY) {
this.frameScrollY = parseInt(frameScrollY);
}
if (sidebarScrollY) {
this.sidebarScrollY = parseInt(sidebarScrollY);
}
// Apply scroll positions after a short delay to ensure DOM is ready
setTimeout(() => {
this.applyScrollPositions();
}, 100);
} else {
this.searchQuery = '';
}
const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
this.selectedTheme === 'bright'
? domtoolsInstance.themeManager.goBright()
: domtoolsInstance.themeManager.goDark();
}
);
}
public async updated(changedPropertiesArg: Map) {
this.domtools = await plugins.deesDomtools.DomTools.setupDomTools();
await this.domtools.router._handleRouteState();
const wccFrame: WccFrame = this.shadowRoot.querySelector('wcc-frame');
if (changedPropertiesArg.has('selectedItemName')) {
document.title = this.selectedItemName;
};
if (this.selectedType === 'page' && this.selectedItem) {
if (typeof this.selectedItem === 'function') {
console.log('slotting page.');
const viewport = await wccFrame.getViewportElement();
const pageFactory = this.selectedItem as TTemplateFactory;
const pageTemplate = await resolveTemplateFactory(pageFactory);
render(pageTemplate, viewport);
console.log('rendered page.');
} else {
console.error('The selected item looks strange:');
console.log(this.selectedItem);
}
} else if (this.selectedType === 'element' && this.selectedItem) {
console.log('slotting element.');
const anonItem: any = this.selectedItem;
if (!anonItem.demo) {
this.setWarning(`component ${anonItem.name} does not expose a demo property.`);
return;
}
// 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 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(demoFactory);
render(demoTemplate, viewport);
}
}
public buildUrl() {
const sectionName = this.selectedSection
? encodeURIComponent(this.selectedSection.name)
: this.selectedType; // Fallback for legacy
const baseUrl = `/wcctools-route/${sectionName}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
const queryParams = new URLSearchParams();
if (this.searchQuery) {
queryParams.set('search', this.searchQuery);
}
if (this.frameScrollY > 0) {
queryParams.set('frameScrollY', this.frameScrollY.toString());
}
if (this.sidebarScrollY > 0) {
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
}
const queryString = queryParams.toString();
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
this.domtools.router.pushUrl(fullUrl);
}
private scrollUpdateTimeout: NodeJS.Timeout;
private scrollListenersAttached: boolean = false;
public async setupScrollListeners() {
// Prevent duplicate listeners
if (this.scrollListenersAttached) {
return;
}
const wccFrame = await this.wccFrame;
const wccSidebar = this.shadowRoot.querySelector('wcc-sidebar');
if (wccFrame) {
// The frame element itself is the scrollable container
wccFrame.addEventListener('scroll', () => {
this.frameScrollY = wccFrame.scrollTop;
this.debouncedScrollUpdate();
});
this.scrollListenersAttached = true;
}
if (wccSidebar) {
// The sidebar element itself is the scrollable container
wccSidebar.addEventListener('scroll', () => {
this.sidebarScrollY = wccSidebar.scrollTop;
this.debouncedScrollUpdate();
});
}
}
private debouncedScrollUpdate() {
clearTimeout(this.scrollUpdateTimeout);
this.scrollUpdateTimeout = setTimeout(() => {
this.updateUrlWithScrollState();
}, 300);
}
private updateUrlWithScrollState() {
const sectionName = this.selectedSection
? encodeURIComponent(this.selectedSection.name)
: this.selectedType; // Fallback for legacy
const baseUrl = `/wcctools-route/${sectionName}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
const queryParams = new URLSearchParams();
if (this.searchQuery) {
queryParams.set('search', this.searchQuery);
}
if (this.frameScrollY > 0) {
queryParams.set('frameScrollY', this.frameScrollY.toString());
}
if (this.sidebarScrollY > 0) {
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
}
const queryString = queryParams.toString();
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
// Use replaceState to update URL without navigation
window.history.replaceState(null, '', fullUrl);
}
public async applyScrollPositions() {
// Only apply scroll positions once to avoid interfering with user scrolling
if (this.scrollPositionsApplied) {
return;
}
const wccFrame = await this.wccFrame;
const wccSidebar = this.shadowRoot.querySelector('wcc-sidebar');
if (wccFrame && this.frameScrollY > 0) {
// The frame element itself is the scrollable container
wccFrame.scrollTop = this.frameScrollY;
}
if (wccSidebar && this.sidebarScrollY > 0) {
// The sidebar element itself is the scrollable container
wccSidebar.scrollTop = this.sidebarScrollY;
}
this.scrollPositionsApplied = true;
}
}