feat(recording-panel): Add demo wrapper utilities, improve recording trim behavior, and harden property panel element detection; update documentation

This commit is contained in:
2025-12-11 12:16:48 +00:00
parent 6cbfd714eb
commit 53c5d839ca
6 changed files with 418 additions and 145 deletions

View File

@@ -1,5 +1,14 @@
# Changelog # Changelog
## 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 dees-demowrapper (ts_demotools) with runAfterRender callback to run post-render demo logic (supports async callbacks).
- Improve recording UI and trimming: handle WebM files with Infinity/NaN durations by falling back to tracked recording duration; replace numeric handle positioning with CSS calc strings for responsive trim handles.
- Harden property extraction: implement recursive element search (including shadowRoots), add an initial delay and retry loop to wait for demo rendering, and add an advanced JSON editor for Object/Array properties with open/save/cancel and per-editor error reporting.
- Add and expand documentation: new ts_web/ and ts_demotools/ READMEs, reorganized main README with clearer feature list, usage examples, and API reference.
- Minor exports and module/docs housekeeping (index exports, readme reorder, examples updated to import classes).
## 2025-11-16 - 1.2.1 - fix(dependencies) ## 2025-11-16 - 1.2.1 - fix(dependencies)
Bump dependencies and developer tooling versions Bump dependencies and developer tooling versions

246
readme.md
View File

@@ -1,14 +1,18 @@
# @design.estate/dees-wcctools # @design.estate/dees-wcctools
Web Component Development Tools - A powerful framework for building, testing, and documenting web components
🛠️ **Web Component Development Tools** — A powerful framework for building, testing, documenting, and recording web components
## Overview ## Overview
`@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 with live preview
- 🔧 Real-time property editing - 🎨 **Interactive Component Catalogue** — Live preview with sidebar navigation
- 🌓 Theme switching (light/dark modes) - 🔧 **Real-time Property Editing** — Modify component props on the fly with auto-detected editors
- 📱 Responsive viewport testing - 🌓 **Theme Switching** — Test light/dark modes instantly
- 🧪 Advanced demo tools for component testing - 📱 **Responsive Viewport Testing** — Phone, phablet, tablet, and desktop views
- 🚀 Zero-config setup with TypeScript and Lit support - 🎬 **Screen Recording** — Record component demos with audio support and video trimming
- 🧪 **Advanced Demo Tools** — Post-render hooks for interactive testing
- 🚀 **Zero-config Setup** — TypeScript and Lit support out of the box
## Issue Reporting and Security ## Issue Reporting and Security
@@ -17,11 +21,11 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
## Installation ## Installation
```bash ```bash
# Using npm
npm install @design.estate/dees-wcctools --save-dev
# Using pnpm (recommended) # Using pnpm (recommended)
pnpm add -D @design.estate/dees-wcctools pnpm add -D @design.estate/dees-wcctools
# Using npm
npm install @design.estate/dees-wcctools --save-dev
``` ```
## Quick Start ## Quick Start
@@ -89,8 +93,8 @@ import { setupWccTools } from '@design.estate/dees-wcctools';
import { html } from 'lit'; import { html } from 'lit';
// Import your components // Import your components
import './components/my-button.js'; import { MyButton } from './components/my-button.js';
import './components/my-card.js'; import { MyCard } from './components/my-card.js';
// Define elements for the catalogue // Define elements for the catalogue
const elements = { const elements = {
@@ -136,21 +140,28 @@ setupWccTools(elements, pages);
## Features ## Features
### 🎯 Live Property Editing ### 🎯 Live Property Editing
The properties panel automatically detects and allows editing of: The properties panel automatically detects and allows editing of:
- **String** properties with text inputs
- **Number** properties with number inputs | Property Type | Editor |
- **Boolean** properties with checkboxes |--------------|--------|
- **Enum** properties with select dropdowns | **String** | Text input |
- **Object** and **Array** properties (read-only display) | **Number** | Number input |
| **Boolean** | Checkbox |
| **Enum** | Select dropdown |
| **Object/Array** | JSON editor modal |
### 📱 Viewport Testing ### 📱 Viewport Testing
Test your components across different screen sizes: Test your components across different screen sizes:
- **Phone** (320px width)
- **Phablet** (600px width) - **Phone** — 320px width
- **Tablet** (768px width) - **Phablet** — 600px width
- **Desktop** (full width) - **Tablet** — 768px width
- **Desktop** — Full width (native)
### 🌓 Theme Support ### 🌓 Theme Support
Components automatically adapt to light/dark themes using the `goBright` property: Components automatically adapt to light/dark themes using the `goBright` property:
```typescript ```typescript
@@ -163,7 +174,8 @@ public render() {
} }
``` ```
Or use CSS custom properties: 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';
@@ -177,39 +189,44 @@ public static styles = [
]; ];
``` ```
### 🧪 Advanced Demo Tools ### 🎬 Screen Recording
The demo tools provide enhanced testing capabilities: Record component demos directly from the catalogue! The built-in recorder supports:
- **Viewport Recording** — Record just the component viewport
- **Full Screen Recording** — Capture the entire screen
- **Audio Support** — Add microphone commentary with live level monitoring
- **Video Trimming** — Trim start/end before export with visual timeline
- **WebM Export** — High-quality video output
Click the red record button in the bottom toolbar to start.
### 🧪 Demo Tools
The demotools module provides enhanced testing capabilities with `dees-demowrapper`:
```typescript ```typescript
import * as demoTools from '@design.estate/dees-wcctools/demotools'; import '@design.estate/dees-wcctools/demotools';
@customElement('my-component') @customElement('my-component')
export class MyComponent extends DeesElement { export class MyComponent extends DeesElement {
public static demo = () => html` public static demo = () => html`
<dees-demowrapper .runAfterRender=${async (wrapper) => { <dees-demowrapper .runAfterRender=${async (wrapper) => {
// Use querySelector to find specific elements // Find elements using standard DOM APIs
const myComponent = wrapper.querySelector('my-component') as MyComponent; const myComponent = wrapper.querySelector('my-component');
console.log('Component found:', myComponent);
// Access all children via wrapper.children
console.log('Total children:', wrapper.children.length);
// Use querySelectorAll for multiple elements
const allDivs = wrapper.querySelectorAll('div');
console.log('Found divs:', allDivs.length);
// Simulate user interactions // Simulate user interactions
myComponent.value = 'Test value'; myComponent.value = 'Test value';
await myComponent.updateComplete; await myComponent.updateComplete;
// Work with all children // Work with multiple elements
Array.from(wrapper.children).forEach((child, index) => { wrapper.querySelectorAll('.item').forEach((el, i) => {
console.log(`Child ${index}:`, child.tagName); console.log(`Item ${i}:`, el.textContent);
}); });
}}> }}>
<my-component></my-component> <my-component></my-component>
<div>Additional content</div> <div class="item">Item 1</div>
<div class="item">Item 2</div>
</dees-demowrapper> </dees-demowrapper>
`; `;
} }
@@ -217,20 +234,18 @@ export class MyComponent extends DeesElement {
### ⏳ Async Demos ### ⏳ Async Demos
If your catalogue needs additional setup before rendering, return a `Promise` from the `demo` function. The dashboard waits for the result before inserting it into the viewport: Return a `Promise` from `demo` for async setup. The dashboard waits for resolution:
```typescript ```typescript
public static demo = async () => { public static demo = async () => {
await Promise.resolve(); // e.g. fetch data, load fixtures, or await wrappers const data = await fetchSomeData();
return html`<my-component .value=${'Loaded asynchronously'}></my-component>`; return html`<my-component .data=${data}></my-component>`;
}; };
``` ```
The same pattern works for page factories you pass into `setupWccTools`, enabling asynchronous data preparation across the entire demo surface. ### 🎭 Container Queries
### 🎭 Container Queries Support Components can respond to their container size using the `wccToolsViewport` container:
Components can respond to their container size:
```typescript ```typescript
public static styles = [ public static styles = [
@@ -240,7 +255,7 @@ public static styles = [
flex-direction: row; flex-direction: row;
} }
} }
@container wccToolsViewport (max-width: 767px) { @container wccToolsViewport (max-width: 767px) {
:host { :host {
flex-direction: column; flex-direction: column;
@@ -253,11 +268,13 @@ public static styles = [
## Component Guidelines ## Component Guidelines
### 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
2. Use `@property()` decorators for properties you want to be editable 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
### Best Practices ### Best Practices
```typescript ```typescript
@customElement('best-practice-component') @customElement('best-practice-component')
export class BestPracticeComponent extends DeesElement { export class BestPracticeComponent extends DeesElement {
@@ -269,7 +286,7 @@ export class BestPracticeComponent extends DeesElement {
></best-practice-component> ></best-practice-component>
`; `;
// ✅ Typed properties with defaults (TC39 decorator syntax with accessor) // ✅ Typed properties with defaults (TC39 decorators)
@property({ type: String }) @property({ type: String })
accessor title: string = 'Default Title'; accessor title: string = 'Default Title';
@@ -286,28 +303,59 @@ export class BestPracticeComponent extends DeesElement {
## URL Routing ## URL Routing
The catalogue uses URL routing for deep linking: The catalogue uses URL routing for deep linking:
``` ```
/wcctools-route/:type/:name/:viewport/:theme /wcctools-route/:type/:name/:viewport/:theme
Example: Examples:
/wcctools-route/element/my-button/desktop/dark /wcctools-route/element/my-button/desktop/dark
/wcctools-route/page/home/tablet/bright /wcctools-route/page/home/tablet/bright
``` ```
## Development Workflow ## API Reference
### Build and Watch ### `setupWccTools(elements, pages?)`
```json
{ Initialize the WCC Tools dashboard.
"scripts": {
"build": "tsbuild tsfolders --allowimplicitany && tsbundle element", | Parameter | Type | Description |
"watch": "tswatch element", |-----------|------|-------------|
"serve": "http-server ./dist" | `elements` | `Record<string, typeof LitElement>` | Map of element names to classes |
} | `pages` | `Record<string, TTemplateFactory>` | Optional map of page names to template functions |
}
### `DeesDemoWrapper`
Component for wrapping demos with post-render logic.
| Property | Type | Description |
|----------|------|-------------|
| `runAfterRender` | `(wrapper) => void \| Promise<void>` | Callback after wrapped elements render |
The wrapper provides full DOM API access:
- `wrapper.querySelector()` — Find single element
- `wrapper.querySelectorAll()` — Find multiple elements
- `wrapper.children` — Access child elements directly
### Recording Components (Advanced)
For custom recording integrations:
```typescript
import { RecorderService } from '@design.estate/dees-wcctools';
const recorder = new RecorderService({
onDurationUpdate: (duration) => console.log(`${duration}s`),
onRecordingComplete: (blob) => console.log('Recording done!', blob),
onAudioLevelUpdate: (level) => console.log(`Audio: ${level}%`),
});
await recorder.startRecording({ mode: 'viewport' });
// ... later
recorder.stopRecording();
``` ```
### Project Structure ## Project Structure
``` ```
my-components/ my-components/
├── src/ ├── src/
@@ -320,74 +368,12 @@ my-components/
└── package.json └── package.json
``` ```
## Advanced Features
### Custom Property Handlers
For complex property types, implement custom logic in your demo:
```typescript
public static demo = () => html`
<dees-demowrapper .runAfterRender=${(wrapper) => {
// Use querySelector to target specific elements
const component = wrapper.querySelector('my-component');
if (component) {
component.addEventListener('property-change', (e) => {
console.log('Property changed:', e.detail);
});
}
// Or handle all elements of a type
wrapper.querySelectorAll('my-component').forEach(el => {
el.addEventListener('click', () => console.log('Clicked!'));
});
}}>
<my-component></my-component>
</dees-demowrapper>
`;
```
### Responsive Testing Helpers
```typescript
import * as domtools from '@design.estate/dees-domtools';
public static styles = [
// Media query helpers
domtools.breakpoints.cssForPhone(css`
:host { font-size: 14px; }
`),
domtools.breakpoints.cssForTablet(css`
:host { font-size: 16px; }
`),
domtools.breakpoints.cssForDesktop(css`
:host { font-size: 18px; }
`)
];
```
## API Reference
### setupWccTools(elements, pages?)
Initialize the WCC Tools dashboard.
- `elements`: Object mapping element names to element classes
- `pages`: Optional object mapping page names to template functions
### DeesDemoWrapper
Component for wrapping demos with post-render logic.
- `runAfterRender`: Function called after the wrapped elements render
- Receives the wrapper element itself, providing full DOM API access
- Use `wrapper.querySelector()` and `wrapper.querySelectorAll()` for element selection
- Access children via `wrapper.children` property
- Supports async operations
## Browser Support ## Browser Support
- Chrome/Edge (latest)
- Firefox (latest) - ✅ Chrome/Edge (latest)
- Safari (latest) - ✅ Firefox (latest)
- Mobile browsers with Web Components support - ✅ Safari (latest)
- ✅ Mobile browsers with Web Components support
## License and Legal Information ## License and Legal Information

147
ts_demotools/readme.md Normal file
View File

@@ -0,0 +1,147 @@
# @design.estate/dees-wcctools/demotools
🧪 **Demo Wrapper Utilities** — Enhanced testing tools for web component demos
## Overview
The demotools module provides `dees-demowrapper`, a utility component for executing post-render logic in component demos. Perfect for simulating user interactions, setting up test data, or validating component state.
## Installation
This module is included with `@design.estate/dees-wcctools`:
```bash
pnpm add -D @design.estate/dees-wcctools
```
## Usage
Import the demotools subpath:
```typescript
import '@design.estate/dees-wcctools/demotools';
```
## DeesDemoWrapper
A wrapper component that executes a callback after its slotted content renders.
### Properties
| Property | Type | Description |
|----------|------|-------------|
| `runAfterRender` | `(wrapper: DeesDemoWrapper) => void \| Promise<void>` | Callback executed after content renders |
### Example: Basic Usage
```typescript
import { html } from 'lit';
import '@design.estate/dees-wcctools/demotools';
public static demo = () => html`
<dees-demowrapper .runAfterRender=${(wrapper) => {
const button = wrapper.querySelector('my-button');
console.log('Button found:', button);
}}>
<my-button>Click Me</my-button>
</dees-demowrapper>
`;
```
### Example: Async Operations
```typescript
public static demo = () => html`
<dees-demowrapper .runAfterRender=${async (wrapper) => {
const form = wrapper.querySelector('my-form');
// Wait for component initialization
await form.updateComplete;
// Simulate user input
form.values = { name: 'Test User', email: 'test@example.com' };
// Trigger validation
await form.validate();
console.log('Form state:', form.isValid);
}}>
<my-form></my-form>
</dees-demowrapper>
`;
```
### Example: Multiple Elements
```typescript
public static demo = () => html`
<dees-demowrapper .runAfterRender=${(wrapper) => {
// Find all cards
const cards = wrapper.querySelectorAll('my-card');
console.log(`Found ${cards.length} cards`);
// Access by index
Array.from(wrapper.children).forEach((child, i) => {
console.log(`Child ${i}:`, child.tagName);
});
// Add event listeners
wrapper.querySelectorAll('button').forEach(btn => {
btn.addEventListener('click', () => console.log('Clicked!'));
});
}}>
<my-card title="Card 1"></my-card>
<my-card title="Card 2"></my-card>
<my-card title="Card 3"></my-card>
</dees-demowrapper>
`;
```
### Example: Component State Manipulation
```typescript
public static demo = () => html`
<dees-demowrapper .runAfterRender=${async (wrapper) => {
const tabs = wrapper.querySelector('my-tabs');
// Programmatically switch tabs
tabs.activeTab = 'settings';
await tabs.updateComplete;
// Verify content updated
const content = tabs.shadowRoot.querySelector('.tab-content');
console.log('Active content:', content.textContent);
}}>
<my-tabs>
<div slot="home">Home Content</div>
<div slot="settings">Settings Content</div>
</my-tabs>
</dees-demowrapper>
`;
```
## How It Works
1. The wrapper renders its slot content immediately
2. After a brief delay (50ms) to allow slotted content to initialize
3. The `runAfterRender` callback is invoked with the wrapper element
4. You have full DOM API access to query and manipulate children
## Key Features
- 📦 **Light DOM Access** — Slotted elements remain accessible via standard DOM APIs
- ⏱️ **Async Support** — Return a Promise for async operations
- 🎯 **Full DOM API** — Use `querySelector`, `querySelectorAll`, `children`, etc.
- 🛡️ **Error Handling** — Errors in callbacks are caught and logged
## CSS Behavior
The wrapper uses `display: contents` so it doesn't affect layout:
```css
:host {
display: contents;
}
```
This means the wrapper is "invisible" in the layout — its children render as if they were direct children of the wrapper's parent.

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@design.estate/dees-wcctools', name: '@design.estate/dees-wcctools',
version: '1.2.1', version: '1.3.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

@@ -678,16 +678,16 @@ export class WccRecordingPanel extends DeesElement {
<div class="trim-track"></div> <div class="trim-track"></div>
<div <div
class="trim-selected" class="trim-selected"
style="left: ${this.getHandlePosition(this.trimStart)}px; right: ${this.getHandlePositionFromEnd(this.trimEnd)}px;" style="left: ${this.getHandlePositionStyle(this.trimStart)}; right: ${this.getHandlePositionFromEndStyle(this.trimEnd)};"
></div> ></div>
<div <div
class="trim-handle start-handle" class="trim-handle start-handle"
style="left: ${this.getHandlePosition(this.trimStart)}px;" style="left: ${this.getHandlePositionStyle(this.trimStart)};"
@mousedown=${(e: MouseEvent) => { e.stopPropagation(); this.isDraggingTrim = 'start'; }} @mousedown=${(e: MouseEvent) => { e.stopPropagation(); this.isDraggingTrim = 'start'; }}
></div> ></div>
<div <div
class="trim-handle end-handle" class="trim-handle end-handle"
style="left: ${this.getHandlePosition(this.trimEnd)}px;" style="left: ${this.getHandlePositionStyle(this.trimEnd)};"
@mousedown=${(e: MouseEvent) => { e.stopPropagation(); this.isDraggingTrim = 'end'; }} @mousedown=${(e: MouseEvent) => { e.stopPropagation(); this.isDraggingTrim = 'end'; }}
></div> ></div>
</div> </div>
@@ -850,9 +850,12 @@ export class WccRecordingPanel extends DeesElement {
// ==================== Trim Methods ==================== // ==================== Trim Methods ====================
private handleVideoLoaded(video: HTMLVideoElement): void { private handleVideoLoaded(video: HTMLVideoElement): void {
this.videoDuration = video.duration; // WebM files from MediaRecorder may have Infinity/NaN duration
// Fall back to the tracked recording duration
const duration = Number.isFinite(video.duration) ? video.duration : this.recordingDuration;
this.videoDuration = duration;
this.trimStart = 0; this.trimStart = 0;
this.trimEnd = video.duration; this.trimEnd = duration;
} }
private formatDuration(seconds: number): string { private formatDuration(seconds: number): string {
@@ -861,18 +864,23 @@ export class WccRecordingPanel extends DeesElement {
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
} }
private getHandlePosition(time: number): number { private getHandlePositionStyle(time: number): string {
if (this.videoDuration === 0) return 12; if (this.videoDuration === 0) return '12px';
const percentage = time / this.videoDuration; const percentage = time / this.videoDuration;
const trackWidth = 336; // Formula: 12px padding + percentage of remaining width (total - 24px padding)
return 12 + (percentage * trackWidth); // At 0%: 12px (left edge of track)
// At 100%: calc(100% - 12px) (right edge of track)
return `calc(12px + ${(percentage * 100).toFixed(2)}% - ${(percentage * 24).toFixed(2)}px)`;
} }
private getHandlePositionFromEnd(time: number): number { private getHandlePositionFromEndStyle(time: number): string {
if (this.videoDuration === 0) return 12; if (this.videoDuration === 0) return '12px';
const percentage = (this.videoDuration - time) / this.videoDuration; const percentage = time / this.videoDuration;
const trackWidth = 336; const remainingPercentage = 1 - percentage;
return 12 + (percentage * trackWidth); // For CSS 'right' property: distance from right edge
// At trimEnd = 100%: right = 12px (at right edge of track)
// At trimEnd = 0%: right = calc(100% - 12px) (at left edge of track)
return `calc(12px + ${(remainingPercentage * 100).toFixed(2)}% - ${(remainingPercentage * 24).toFixed(2)}px)`;
} }
private handleTimelineClick(e: MouseEvent): void { private handleTimelineClick(e: MouseEvent): void {

123
ts_web/readme.md Normal file
View File

@@ -0,0 +1,123 @@
# @design.estate/dees-wcctools
🛠️ **Web Component Catalogue Tools** — The core dashboard and UI components for building interactive component catalogues
## Overview
This is the main module of `@design.estate/dees-wcctools`, providing the complete dashboard experience for developing, testing, and documenting web components.
## Installation
```bash
pnpm add -D @design.estate/dees-wcctools
```
## Usage
```typescript
import { setupWccTools } from '@design.estate/dees-wcctools';
import { MyButton } from './components/my-button.js';
setupWccTools({
'my-button': MyButton,
});
```
## Exports
### Main Entry Point
| Export | Description |
|--------|-------------|
| `setupWccTools` | Initialize the component catalogue dashboard |
### Recording Components
| Export | Description |
|--------|-------------|
| `RecorderService` | Service class for screen/viewport recording |
| `WccRecordButton` | Record button UI component |
| `WccRecordingPanel` | Recording options and preview panel |
| `IRecorderEvents` | TypeScript interface for recorder callbacks |
| `IRecordingOptions` | TypeScript interface for recording options |
## Internal Components
The module includes these internal web components:
| Component | Description |
|-----------|-------------|
| `wcc-dashboard` | Main dashboard container with routing |
| `wcc-sidebar` | Navigation sidebar with element/page listing |
| `wcc-frame` | Iframe viewport with responsive sizing |
| `wcc-properties` | Property panel with live editing |
| `wcc-record-button` | Recording state indicator button |
| `wcc-recording-panel` | Recording workflow UI |
## RecorderService API
For programmatic recording control:
```typescript
import { RecorderService, type IRecorderEvents } from '@design.estate/dees-wcctools';
const events: IRecorderEvents = {
onDurationUpdate: (duration) => console.log(`Recording: ${duration}s`),
onRecordingComplete: (blob) => saveBlob(blob),
onAudioLevelUpdate: (level) => updateMeter(level),
onError: (error) => console.error(error),
onStreamEnded: () => console.log('User stopped sharing'),
};
const recorder = new RecorderService(events);
// Load available microphones
const mics = await recorder.loadMicrophones(true); // true = request permission
// Start audio level monitoring
await recorder.startAudioMonitoring(mics[0].deviceId);
// Start recording
await recorder.startRecording({
mode: 'viewport', // or 'screen'
audioDeviceId: mics[0].deviceId,
viewportElement: document.querySelector('.viewport'),
});
// Stop recording
recorder.stopRecording();
// Export trimmed video
const trimmedBlob = await recorder.exportTrimmedVideo(videoElement, startTime, endTime);
// Cleanup
recorder.dispose();
```
## Architecture
```
ts_web/
├── index.ts # Main exports
├── elements/
│ ├── wcc-dashboard.ts # Root dashboard component
│ ├── wcc-sidebar.ts # Navigation sidebar
│ ├── wcc-frame.ts # Responsive iframe viewport
│ ├── wcc-properties.ts # Property editing panel
│ ├── wcc-record-button.ts # Recording button
│ ├── wcc-recording-panel.ts # Recording options/preview
│ └── wcctools.helpers.ts # Shared utilities
├── services/
│ └── recorder.service.ts # MediaRecorder abstraction
└── pages/
└── index.ts # Built-in pages
```
## Features
- 🎨 Interactive component preview
- 🔧 Real-time property editing with type detection
- 🌓 Theme switching (light/dark)
- 📱 Responsive viewport testing
- 🎬 Screen recording with trimming
- 🔗 URL-based deep linking