Compare commits

..

11 Commits

Author SHA1 Message Date
36dd6b5064 v3.1.0
Some checks failed
Default (tags) / security (push) Failing after 15s
Default (tags) / test (push) Failing after 11s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-19 09:21:30 +00:00
ddecfcdb4c feat(wcc-properties): add Share selector with inline Record button and adjust properties panel grid layout 2025-12-19 09:21:30 +00:00
8f0f8606a1 v3.0.0
Some checks failed
Default (tags) / security (push) Failing after 15s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-19 09:08:08 +00:00
7dca519d9a BREAKING CHANGE(ts_web): Replace fullscreen boolean with native viewport mode across components, add native viewport selector and toggle, and update dev deps and npmextra config 2025-12-19 09:08:08 +00:00
d48cd063c4 v2.0.1
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Failing after 18s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-11 19:26:59 +00:00
bb04895be8 fix(@git.zone/tswatch): Bump @git.zone/tswatch devDependency to ^2.3.12 2025-12-11 19:26:59 +00:00
54b34b6faa v2.0.0
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Failing after 17s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-11 19:02:02 +00:00
12c85fa4cb BREAKING CHANGE(recorder): Remove FFmpeg-based MP4 conversion; simplify recorder/export to WebM and improve recorder/editor robustness 2025-12-11 19:02:02 +00:00
d90df9717b update 2025-12-11 18:03:46 +00:00
d4b161437b update 2025-12-11 16:58:04 +00:00
ad033c8104 update 2025-12-11 15:49:04 +00:00
16 changed files with 1295 additions and 1080 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -1,5 +1,43 @@
# Changelog # Changelog
## 2025-12-19 - 3.1.0 - feat(wcc-properties)
add Share selector with inline Record button and adjust properties panel grid layout
- Increase rightmost column width from 70px to 100px in properties grid
- Introduce .shareSelector and .selectorButtons1 CSS classes and markup
- Replace <wcc-record-button> with inline Record button that toggles icon based on isRecording
- Add Share panel heading and integrate record control into selector area
## 2025-12-19 - 3.0.0 - BREAKING CHANGE(ts_web)
Replace fullscreen boolean with native viewport mode across components, add native viewport selector and toggle, and update dev deps and npmextra config
- Introduce isNative (derived from selectedViewport === 'native') replacing isFullscreen on WccDashboard, WccFrame, WccProperties and WccSidebar (property and styling changes).
- WccDashboard: remove isFullscreen property, add isNative getter, implement toggleNative to switch selectedViewport between 'native' and 'desktop', update ESC handler to exit native mode and call buildUrl().
- WccProperties: add Native button to viewport selector, adjust grid columns and selector layout, replace fullscreen toggle with toggleNative (dispatches 'toggleNative' event).
- WccFrame: rename isFullscreen -> isNative and update style/markup branching to use isNative.
- WccSidebar: visibility now driven by isNative instead of isFullscreen.
- API/event changes: 'toggleFullscreen' event renamed to 'toggleNative' — this is an incompatible change for consumers relying on the old event/property.
- package.json: bump devDependencies @git.zone/tsbuild -> ^4.0.2, @git.zone/tsrun -> ^2.0.1, @git.zone/tswatch -> ^2.3.13, @types/node -> ^25.0.3.
- npmextra.json: rename keys (e.g. "gitzone" -> "@git.zone/cli", "tsdoc" -> "@git.zone/tsdoc"), add @ship.zone/szci entry and add release registries/accessLevel for @git.zone/cli.
## 2025-12-11 - 2.0.1 - fix(@git.zone/tswatch)
Bump @git.zone/tswatch devDependency to ^2.3.12
- Updated devDependency @git.zone/tswatch from ^2.3.11 to ^2.3.12 in package.json
## 2025-12-11 - 2.0.0 - BREAKING CHANGE(recorder)
Remove FFmpeg-based MP4 conversion; simplify recorder/export to WebM and improve recorder/editor robustness
- Removed FFmpegService and all client-side MP4 conversion logic — exports are now WebM-only (MP4 conversion and related UI/controls removed).
- ts_web/elements/wcc-recording-panel: dropped outputFormat and conversion states/UI; download flow simplified to always export WebM.
- ts_web/index.ts: removed FFmpegService exports and conversion types from public API.
- package.json: removed @ffmpeg/* dependencies.
- RecorderService: handleRecordingComplete is now async and fixes recorded blob assignment and cleanup timing.
- wcc-properties: improved element detection and robustness — recursive search through light/shadow DOM with retry/delay, plus an advanced JSON editor for Object/Array props (supports multiple open editors and frame resize events).
- wcc-sidebar: force re-render after selecting demos to ensure child demo selection indicators update correctly.
- dees-demowrapper: ensure slotted content is rendered before calling runAfterRender (small timing/stability improvements).
- Test update: demo definitions can be arrays (multiple demos) — test-demoelement updated to use multiple demo entries.
## 2025-12-11 - 1.3.0 - feat(recording-panel) ## 2025-12-11 - 1.3.0 - feat(recording-panel)
Add demo wrapper utilities, improve recording trim behavior, and harden property panel element detection; update documentation Add demo wrapper utilities, improve recording trim behavior, and harden property panel element detection; update documentation

View File

@@ -1,5 +1,5 @@
{ {
"gitzone": { "@git.zone/cli": {
"projectType": "wcc", "projectType": "wcc",
"module": { "module": {
"githost": "code.foss.global", "githost": "code.foss.global",
@@ -21,13 +21,19 @@
"element testing", "element testing",
"page development" "page development"
] ]
},
"release": {
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public"
} }
}, },
"npmci": { "@git.zone/tsdoc": {
"npmGlobalTools": [],
"npmAccessLevel": "public"
},
"tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n" "legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
},
"@ship.zone/szci": {
"npmGlobalTools": []
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@design.estate/dees-wcctools", "name": "@design.estate/dees-wcctools",
"version": "1.3.0", "version": "3.1.0",
"private": false, "private": false,
"description": "A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.", "description": "A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.",
"exports": { "exports": {
@@ -24,13 +24,13 @@
}, },
"devDependencies": { "devDependencies": {
"@api.global/typedserver": "^7.11.1", "@api.global/typedserver": "^7.11.1",
"@git.zone/tsbuild": "^3.1.2", "@git.zone/tsbuild": "^4.0.2",
"@git.zone/tsbundle": "^2.6.3", "@git.zone/tsbundle": "^2.6.3",
"@git.zone/tsrun": "^2.0.0", "@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.1.3", "@git.zone/tstest": "^3.1.3",
"@git.zone/tswatch": "^2.3.10", "@git.zone/tswatch": "^2.3.13",
"@push.rocks/projectinfo": "^5.0.2", "@push.rocks/projectinfo": "^5.0.2",
"@types/node": "^25.0.0" "@types/node": "^25.0.3"
}, },
"files": [ "files": [
"ts/**/*", "ts/**/*",

1916
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,11 @@ enum ETestEnum {
@customElement('test-demoelement') @customElement('test-demoelement')
export class TestDemoelement extends DeesElement { export class TestDemoelement extends DeesElement {
public static demo = () => html`<test-demoelement>This is a slot text</test-demoelement>`; public static demo = [
() => html`<test-demoelement>This is demo 1</test-demoelement>`,
() => html`<test-demoelement>This is demo 2</test-demoelement>`,
() => html`<test-demoelement>This is demo 2</test-demoelement>`,
]
@property() @property()
accessor notTyped = 'hello'; accessor notTyped = 'hello';

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@design.estate/dees-wcctools', name: '@design.estate/dees-wcctools',
version: '1.3.0', version: '3.1.0',
description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.' description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.'
} }

View File

@@ -1,5 +1,5 @@
import { DeesElement, property, html, customElement, type TemplateResult, queryAsync, render, domtools } from '@design.estate/dees-element'; import { DeesElement, property, html, customElement, type TemplateResult, queryAsync, render, domtools } from '@design.estate/dees-element';
import { resolveTemplateFactory } from './wcctools.helpers.js'; import { resolveTemplateFactory, getDemoAtIndex, getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
import type { TTemplateFactory } from './wcctools.helpers.js'; import type { TTemplateFactory } from './wcctools.helpers.js';
import * as plugins from '../wcctools.plugins.js'; import * as plugins from '../wcctools.plugins.js';
@@ -25,14 +25,19 @@ export class WccDashboard extends DeesElement {
@property() @property()
accessor selectedItem: TTemplateFactory | DeesElement; accessor selectedItem: TTemplateFactory | DeesElement;
@property({ type: Number })
accessor selectedDemoIndex: number = 0;
@property() @property()
accessor selectedViewport: plugins.deesDomtools.breakpoints.TViewport = 'desktop'; accessor selectedViewport: plugins.deesDomtools.breakpoints.TViewport = 'desktop';
@property() @property()
accessor selectedTheme: TTheme = 'dark'; accessor selectedTheme: TTheme = 'dark';
@property() // Derived from selectedViewport - no need for separate property
accessor isFullscreen: boolean = false; public get isNative(): boolean {
return this.selectedViewport === 'native';
}
@property() @property()
accessor pages: Record<string, TTemplateFactory> = {}; accessor pages: Record<string, TTemplateFactory> = {};
@@ -81,7 +86,7 @@ export class WccDashboard extends DeesElement {
<wcc-sidebar <wcc-sidebar
.dashboardRef=${this} .dashboardRef=${this}
.selectedItem=${this.selectedItem} .selectedItem=${this.selectedItem}
.isFullscreen=${this.isFullscreen} .isNative=${this.isNative}
@selectedType=${(eventArg) => { @selectedType=${(eventArg) => {
this.selectedType = eventArg.detail; this.selectedType = eventArg.detail;
}} }}
@@ -98,7 +103,7 @@ export class WccDashboard extends DeesElement {
.selectedItem=${this.selectedItem} .selectedItem=${this.selectedItem}
.selectedViewport=${this.selectedViewport} .selectedViewport=${this.selectedViewport}
.selectedTheme=${this.selectedTheme} .selectedTheme=${this.selectedTheme}
.isFullscreen=${this.isFullscreen} .isNative=${this.isNative}
@selectedViewport=${(eventArg) => { @selectedViewport=${(eventArg) => {
this.selectedViewport = eventArg.detail; this.selectedViewport = eventArg.detail;
this.scheduleUpdate(); this.scheduleUpdate();
@@ -113,11 +118,11 @@ export class WccDashboard extends DeesElement {
frame.requestUpdate(); frame.requestUpdate();
} }
}} }}
@toggleFullscreen=${() => { @toggleNative=${() => {
this.toggleFullscreen(); this.toggleNative();
}} }}
></wcc-properties> ></wcc-properties>
<wcc-frame id="wccFrame" viewport=${this.selectedViewport} .isFullscreen=${this.isFullscreen}> <wcc-frame id="wccFrame" viewport=${this.selectedViewport} .isNative=${this.isNative}>
</wcc-frame> </wcc-frame>
`; `;
} }
@@ -132,17 +137,20 @@ export class WccDashboard extends DeesElement {
} }
} }
public toggleFullscreen() { public toggleNative() {
this.isFullscreen = !this.isFullscreen; // Toggle between 'native' and 'desktop' viewports
this.selectedViewport = this.selectedViewport === 'native' ? 'desktop' : 'native';
this.buildUrl();
} }
public async firstUpdated() { public async firstUpdated() {
this.domtools = await plugins.deesDomtools.DomTools.setupDomTools(); this.domtools = await plugins.deesDomtools.DomTools.setupDomTools();
// Add ESC key handler for fullscreen mode // Add ESC key handler for native mode
document.addEventListener('keydown', (event) => { document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && this.isFullscreen) { if (event.key === 'Escape' && this.isNative) {
this.isFullscreen = false; this.selectedViewport = 'desktop';
this.buildUrl();
} }
}); });
@@ -151,11 +159,13 @@ export class WccDashboard extends DeesElement {
this.setupScrollListeners(); this.setupScrollListeners();
}, 500); }, 500);
// Route with demo index (new format)
this.domtools.router.on( this.domtools.router.on(
'/wcctools-route/:itemType/:itemName/:viewport/:theme', '/wcctools-route/:itemType/:itemName/:demoIndex/:viewport/:theme',
async (routeInfo) => { async (routeInfo) => {
this.selectedType = routeInfo.params.itemType as TElementType; this.selectedType = routeInfo.params.itemType as TElementType;
this.selectedItemName = routeInfo.params.itemName; this.selectedItemName = routeInfo.params.itemName;
this.selectedDemoIndex = parseInt(routeInfo.params.demoIndex) || 0;
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport; this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
this.selectedTheme = routeInfo.params.theme as TTheme; this.selectedTheme = routeInfo.params.theme as TTheme;
if (routeInfo.params.itemType === 'element') { if (routeInfo.params.itemType === 'element') {
@@ -163,25 +173,65 @@ export class WccDashboard extends DeesElement {
} else if (routeInfo.params.itemType === 'page') { } else if (routeInfo.params.itemType === 'page') {
this.selectedItem = this.pages[routeInfo.params.itemName]; this.selectedItem = this.pages[routeInfo.params.itemName];
} }
// Restore scroll positions from query parameters // Restore scroll positions from query parameters
if (routeInfo.queryParams) { if (routeInfo.queryParams) {
const frameScrollY = routeInfo.queryParams.frameScrollY; const frameScrollY = routeInfo.queryParams.frameScrollY;
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY; const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
if (frameScrollY) { if (frameScrollY) {
this.frameScrollY = parseInt(frameScrollY); this.frameScrollY = parseInt(frameScrollY);
} }
if (sidebarScrollY) { if (sidebarScrollY) {
this.sidebarScrollY = parseInt(sidebarScrollY); this.sidebarScrollY = parseInt(sidebarScrollY);
} }
// Apply scroll positions after a short delay to ensure DOM is ready // Apply scroll positions after a short delay to ensure DOM is ready
setTimeout(() => { setTimeout(() => {
this.applyScrollPositions(); this.applyScrollPositions();
}, 100); }, 100);
} }
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(); const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
this.selectedTheme === 'bright' this.selectedTheme === 'bright'
? domtoolsInstance.themeManager.goBright() ? domtoolsInstance.themeManager.goBright()
@@ -218,33 +268,48 @@ export class WccDashboard extends DeesElement {
this.setWarning(`component ${anonItem.name} does not expose a demo property.`); this.setWarning(`component ${anonItem.name} does not expose a demo property.`);
return; return;
} }
if (!(typeof anonItem.demo === 'function')) {
// Support both single demo (function) and multiple demos (array)
const isArray = Array.isArray(anonItem.demo);
const isFunction = typeof anonItem.demo === 'function';
if (!isArray && !isFunction) {
this.setWarning( this.setWarning(
`component ${anonItem.name} has demo property, but it is not of type function` `component ${anonItem.name} has demo property, but it is not a function or array of functions`
); );
return; return;
} }
// Get the specific demo to render
const demoFactory = getDemoAtIndex(anonItem.demo, this.selectedDemoIndex);
if (!demoFactory) {
this.setWarning(
`component ${anonItem.name} does not have a demo at index ${this.selectedDemoIndex + 1}`
);
return;
}
this.setWarning(null); this.setWarning(null);
const viewport = await wccFrame.getViewportElement(); const viewport = await wccFrame.getViewportElement();
const demoTemplate = await resolveTemplateFactory(() => anonItem.demo()); const demoTemplate = await resolveTemplateFactory(demoFactory);
render(demoTemplate, viewport); render(demoTemplate, viewport);
} }
} }
public buildUrl() { public buildUrl() {
const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedViewport}/${this.selectedTheme}`; const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
const queryParams = new URLSearchParams(); const queryParams = new URLSearchParams();
if (this.frameScrollY > 0) { if (this.frameScrollY > 0) {
queryParams.set('frameScrollY', this.frameScrollY.toString()); queryParams.set('frameScrollY', this.frameScrollY.toString());
} }
if (this.sidebarScrollY > 0) { if (this.sidebarScrollY > 0) {
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString()); queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
} }
const queryString = queryParams.toString(); const queryString = queryParams.toString();
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl; const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
this.domtools.router.pushUrl(fullUrl); this.domtools.router.pushUrl(fullUrl);
} }
@@ -286,19 +351,19 @@ export class WccDashboard extends DeesElement {
} }
private updateUrlWithScrollState() { 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(); const queryParams = new URLSearchParams();
if (this.frameScrollY > 0) { if (this.frameScrollY > 0) {
queryParams.set('frameScrollY', this.frameScrollY.toString()); queryParams.set('frameScrollY', this.frameScrollY.toString());
} }
if (this.sidebarScrollY > 0) { if (this.sidebarScrollY > 0) {
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString()); queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
} }
const queryString = queryParams.toString(); const queryString = queryParams.toString();
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl; const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
// Use replaceState to update URL without navigation // Use replaceState to update URL without navigation
window.history.replaceState(null, '', fullUrl); window.history.replaceState(null, '', fullUrl);
} }

View File

@@ -17,7 +17,7 @@ export class WccFrame extends DeesElement {
accessor advancedEditorOpen: boolean = false; accessor advancedEditorOpen: boolean = false;
@property({ type: Boolean }) @property({ type: Boolean })
accessor isFullscreen: boolean = false; accessor isNative: boolean = false;
public static styles = [ public static styles = [
css` css`
@@ -46,7 +46,7 @@ export class WccFrame extends DeesElement {
return html` return html`
<style> <style>
:host { :host {
${this.isFullscreen ? ` ${this.isNative ? `
border: none !important; border: none !important;
left: 0px !important; left: 0px !important;
right: 0px !important; right: 0px !important;
@@ -58,7 +58,7 @@ export class WccFrame extends DeesElement {
left: 200px; left: 200px;
`} `}
transition: all 0.3s ease; transition: all 0.3s ease;
${this.isFullscreen ? 'padding: 0px;' : (() => { ${this.isNative ? 'padding: 0px;' : (() => {
switch (this.viewport) { switch (this.viewport) {
case 'desktop': case 'desktop':
return ` return `
@@ -87,7 +87,7 @@ export class WccFrame extends DeesElement {
} }
.viewport { .viewport {
${!this.isFullscreen && this.viewport !== 'desktop' ${!this.isNative && this.viewport !== 'desktop'
? html` border-right: 1px dotted #444; border-left: 1px dotted #444; ` ? html` border-right: 1px dotted #444; border-left: 1px dotted #444; `
: html`` : html``
} }

View File

@@ -35,7 +35,7 @@ export class WccProperties extends DeesElement {
accessor warning: string = null; accessor warning: string = null;
@property() @property()
accessor isFullscreen: boolean = false; accessor isNative: boolean = false;
@state() @state()
accessor propertyContent: TemplateResult[] = []; accessor propertyContent: TemplateResult[] = [];
@@ -96,11 +96,11 @@ export class WccProperties extends DeesElement {
overflow: hidden; overflow: hidden;
background: var(--background); background: var(--background);
color: var(--foreground); color: var(--foreground);
display: ${this.isFullscreen ? 'none' : 'block'}; display: ${this.isNative ? 'none' : 'block'};
} }
.grid { .grid {
display: grid; display: grid;
grid-template-columns: 1fr 150px 300px 70px 70px; grid-template-columns: 1fr 150px 350px 100px;
height: 100%; height: 100%;
} }
.properties { .properties {
@@ -197,7 +197,8 @@ export class WccProperties extends DeesElement {
} }
.viewportSelector, .viewportSelector,
.themeSelector { .themeSelector,
.shareSelector {
user-select: none; user-select: none;
background: transparent; background: transparent;
display: flex; display: flex;
@@ -214,6 +215,16 @@ export class WccProperties extends DeesElement {
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
flex: 1; flex: 1;
} }
.selectorButtons5 {
display: grid;
grid-template-columns: repeat(5, 1fr);
flex: 1;
}
.selectorButtons1 {
display: grid;
grid-template-columns: 1fr;
flex: 1;
}
.button { .button {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -623,7 +634,7 @@ export class WccProperties extends DeesElement {
</div> </div>
<div class="viewportSelector"> <div class="viewportSelector">
<div class="panelheading">Viewport</div> <div class="panelheading">Viewport</div>
<div class="selectorButtons4"> <div class="selectorButtons5">
<div <div
class="button ${this.selectedViewport === 'phone' ? 'selected' : null}" class="button ${this.selectedViewport === 'phone' ? 'selected' : null}"
@click=${() => { @click=${() => {
@@ -649,29 +660,34 @@ export class WccProperties extends DeesElement {
Tablet<i class="material-symbols-outlined">tablet</i> Tablet<i class="material-symbols-outlined">tablet</i>
</div> </div>
<div <div
class="button ${this.selectedViewport === 'desktop' || class="button ${this.selectedViewport === 'desktop' ? 'selected' : null}"
this.selectedViewport === 'native'
? 'selected'
: null}"
@click=${() => { @click=${() => {
this.selectViewport('native'); this.selectViewport('desktop');
}} }}
> >
Desktop<i class="material-symbols-outlined">desktop_windows</i> Desktop<i class="material-symbols-outlined">desktop_windows</i>
</div> </div>
<div
class="button ${this.selectedViewport === 'native' ? 'selected' : null}"
@click=${() => {
this.selectViewport('native');
}}
>
Native<i class="material-symbols-outlined">fullscreen</i>
</div>
</div> </div>
</div> </div>
<div class="docs" @click=${() => this.toggleFullscreen()}> <div class="shareSelector">
<i class="material-symbols-outlined" style="font-size: 20px;"> <div class="panelheading">Share</div>
${this.isFullscreen ? 'fullscreen_exit' : 'fullscreen'} <div class="selectorButtons1">
</i> <div
class="button ${this.isRecording ? 'selected' : ''}"
@click=${() => this.handleRecordButtonClick()}
>
Record<i class="material-symbols-outlined">${this.isRecording ? 'stop_circle' : 'videocam'}</i>
</div>
</div>
</div> </div>
<!-- Recording Button -->
<wcc-record-button
.state=${this.isRecording ? 'recording' : 'idle'}
.duration=${this.recordingDuration}
@record-click=${() => this.handleRecordButtonClick()}
></wcc-record-button>
</div> </div>
${this.warning ? html`<div class="warning">${this.warning}</div>` : null} ${this.warning ? html`<div class="warning">${this.warning}</div>` : null}
</div> </div>
@@ -1016,9 +1032,9 @@ export class WccProperties extends DeesElement {
); );
} }
private toggleFullscreen() { private toggleNative() {
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('toggleFullscreen', { new CustomEvent('toggleNative', {
bubbles: true bubbles: true
}) })
); );

View File

@@ -552,6 +552,7 @@ export class WccRecordingPanel extends DeesElement {
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
` `
]; ];
@@ -706,6 +707,7 @@ export class WccRecordingPanel extends DeesElement {
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<div class="preview-modal-actions"> <div class="preview-modal-actions">
<button class="preview-btn secondary" @click=${() => this.discardRecording()}>Discard</button> <button class="preview-btn secondary" @click=${() => this.discardRecording()}>Discard</button>
@@ -714,7 +716,7 @@ export class WccRecordingPanel extends DeesElement {
?disabled=${this.isExporting} ?disabled=${this.isExporting}
@click=${() => this.downloadRecording()} @click=${() => this.downloadRecording()}
> >
${this.isExporting ? html`<span class="export-spinner"></span>Exporting...` : 'Download'} ${this.isExporting ? html`<span class="export-spinner"></span>Exporting...` : 'Download WebM'}
</button> </button>
</div> </div>
</div> </div>
@@ -815,6 +817,7 @@ export class WccRecordingPanel extends DeesElement {
try { try {
let blobToDownload: Blob; let blobToDownload: Blob;
// Handle trimming if needed
const needsTrim = this.trimStart > 0.1 || this.trimEnd < this.videoDuration - 0.1; const needsTrim = this.trimStart > 0.1 || this.trimEnd < this.videoDuration - 0.1;
if (needsTrim) { if (needsTrim) {
@@ -828,6 +831,7 @@ export class WccRecordingPanel extends DeesElement {
blobToDownload = recordedBlob; blobToDownload = recordedBlob;
} }
// Trigger download
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
const filename = `wcctools-recording-${timestamp}.webm`; const filename = `wcctools-recording-${timestamp}.webm`;

View File

@@ -1,7 +1,8 @@
import * as plugins from '../wcctools.plugins.js'; import * as plugins from '../wcctools.plugins.js';
import { DeesElement, property, html, customElement, type TemplateResult } from '@design.estate/dees-element'; import { DeesElement, property, html, customElement, type TemplateResult, state } from '@design.estate/dees-element';
import { WccDashboard } from './wcc-dashboard.js'; import { WccDashboard } from './wcc-dashboard.js';
import type { TTemplateFactory } from './wcctools.helpers.js'; import type { TTemplateFactory } from './wcctools.helpers.js';
import { getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
export type TElementType = 'element' | 'page'; export type TElementType = 'element' | 'page';
@@ -17,7 +18,11 @@ export class WccSidebar extends DeesElement {
accessor dashboardRef: WccDashboard; accessor dashboardRef: WccDashboard;
@property() @property()
accessor isFullscreen: boolean = false; accessor isNative: boolean = false;
// Track which elements are expanded (for multi-demo elements)
@state()
accessor expandedElements: Set<string> = new Set();
public render(): TemplateResult { public render(): TemplateResult {
return html` return html`
@@ -40,7 +45,7 @@ export class WccSidebar extends DeesElement {
--ring: #3b82f6; --ring: #3b82f6;
--radius: 4px; --radius: 4px;
display: ${this.isFullscreen ? 'none' : 'block'}; display: ${this.isNative ? 'none' : 'block'};
border-right: 1px solid rgba(255, 255, 255, 0.08); border-right: 1px solid rgba(255, 255, 255, 0.08);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
font-size: 14px; font-size: 14px;
@@ -110,7 +115,21 @@ export class WccSidebar extends DeesElement {
color: #999; color: #999;
background: transparent; background: transparent;
} }
.selectOption.folder {
grid-template-columns: 16px 20px 1fr;
}
.selectOption .expand-icon {
font-size: 14px;
opacity: 0.5;
transition: transform 0.2s ease;
}
.selectOption.expanded .expand-icon {
transform: rotate(90deg);
}
.selectOption:hover { .selectOption:hover {
background: rgba(59, 130, 246, 0.05); background: rgba(59, 130, 246, 0.05);
color: #bbb; color: #bbb;
@@ -143,6 +162,42 @@ export class WccSidebar extends DeesElement {
font-weight: 400; font-weight: 400;
} }
.demo-children {
margin-left: 1rem;
overflow: hidden;
}
.demo-child {
user-select: none;
position: relative;
margin: 0.125rem 0.5rem;
padding: 0.35rem 0.75rem;
transition: all 0.15s ease;
display: grid;
grid-template-columns: 16px 1fr;
align-items: center;
gap: 0.5rem;
border-radius: var(--radius);
cursor: pointer;
font-size: 0.7rem;
color: #777;
background: transparent;
}
.demo-child:hover {
background: rgba(59, 130, 246, 0.05);
color: #bbb;
}
.demo-child.selected {
background: rgba(59, 130, 246, 0.15);
color: var(--primary);
}
.demo-child .material-symbols-outlined {
font-size: 14px;
}
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 8px;
} }
@@ -171,7 +226,7 @@ export class WccSidebar extends DeesElement {
class="selectOption ${this.selectedItem === item ? 'selected' : null}" class="selectOption ${this.selectedItem === item ? 'selected' : null}"
@click=${async () => { @click=${async () => {
const domtools = await plugins.deesDomtools.DomTools.setupDomTools(); 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> <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); const elements = Object.keys(this.dashboardRef.elements);
return elements.map(elementName => { return elements.map(elementName => {
const item = this.dashboardRef.elements[elementName]; const item = this.dashboardRef.elements[elementName] as any;
return html` const demoCount = item.demo ? getDemoCount(item.demo) : 0;
<div const isMultiDemo = item.demo && hasMultipleDemos(item.demo);
class="selectOption ${this.selectedItem === item ? 'selected' : null}" const isExpanded = this.expandedElements.has(elementName);
@click=${async () => { const isSelected = this.selectedItem === item;
const domtools = await plugins.deesDomtools.DomTools.setupDomTools();
this.selectItem('element', elementName, item); if (isMultiDemo) {
}} // Multi-demo element - render as expandable folder
> return html`
<i class="material-symbols-outlined">featured_video</i> <div
<div class="text">${elementName}</div> class="selectOption folder ${isExpanded ? 'expanded' : ''} ${isSelected ? 'selected' : ''}"
</div> @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> </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('selected item');
console.log(itemNameArg); console.log(itemNameArg);
console.log(itemArg); console.log(itemArg);
console.log('demo index:', demoIndex);
this.selectedItem = itemArg; this.selectedItem = itemArg;
this.selectedType = typeArg; this.selectedType = typeArg;
this.dashboardRef.selectedDemoIndex = demoIndex;
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('selectedType', { new CustomEvent('selectedType', {
detail: typeArg detail: typeArg
@@ -224,7 +331,11 @@ export class WccSidebar extends DeesElement {
detail: itemArg detail: itemArg
}) })
); );
this.dashboardRef.buildUrl(); this.dashboardRef.buildUrl();
// Force re-render to update demo child selection indicator
// (needed when switching between demos of the same element)
this.requestUpdate();
} }
} }

View File

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

View File

@@ -235,9 +235,11 @@ export class RecorderService {
} }
} }
private handleRecordingComplete(): void { private async handleRecordingComplete(): Promise<void> {
// Create blob from recorded chunks // Create blob from recorded chunks
this._recordedBlob = new Blob(this.recordedChunks, { type: 'video/webm' }); const blob = new Blob(this.recordedChunks, { type: 'video/webm' });
this._recordedBlob = blob;
// Stop all tracks // Stop all tracks
if (this.currentStream) { if (this.currentStream) {