@design.estate/dees-wcctools 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist_bundle/bundle.js +1581 -198
  2. package/dist_bundle/bundle.js.map +4 -4
  3. package/dist_ts_demotools/demotools.d.ts +1 -1
  4. package/dist_ts_demotools/demotools.js +86 -38
  5. package/dist_ts_web/00_commitinfo_data.js +1 -1
  6. package/dist_ts_web/elements/wcc-dashboard.d.ts +10 -10
  7. package/dist_ts_web/elements/wcc-dashboard.js +317 -245
  8. package/dist_ts_web/elements/wcc-frame.d.ts +3 -3
  9. package/dist_ts_web/elements/wcc-frame.js +108 -57
  10. package/dist_ts_web/elements/wcc-properties.d.ts +14 -8
  11. package/dist_ts_web/elements/wcc-properties.js +442 -323
  12. package/dist_ts_web/elements/wcc-record-button.d.ts +12 -0
  13. package/dist_ts_web/elements/wcc-record-button.js +165 -0
  14. package/dist_ts_web/elements/wcc-recording-panel.d.ts +42 -0
  15. package/dist_ts_web/elements/wcc-recording-panel.js +1063 -0
  16. package/dist_ts_web/elements/wcc-sidebar.d.ts +4 -4
  17. package/dist_ts_web/elements/wcc-sidebar.js +125 -71
  18. package/dist_ts_web/index.d.ts +3 -0
  19. package/dist_ts_web/index.js +5 -1
  20. package/dist_ts_web/services/recorder.service.d.ts +44 -0
  21. package/dist_ts_web/services/recorder.service.js +306 -0
  22. package/dist_watch/bundle.js +1939 -521
  23. package/dist_watch/bundle.js.map +4 -4
  24. package/package.json +8 -8
  25. package/readme.md +133 -141
  26. package/ts_web/00_commitinfo_data.ts +1 -1
  27. package/ts_web/elements/wcc-dashboard.ts +10 -10
  28. package/ts_web/elements/wcc-frame.ts +3 -3
  29. package/ts_web/elements/wcc-properties.ts +53 -9
  30. package/ts_web/elements/wcc-record-button.ts +108 -0
  31. package/ts_web/elements/wcc-recording-panel.ts +974 -0
  32. package/ts_web/elements/wcc-sidebar.ts +4 -4
  33. package/ts_web/index.ts +5 -0
  34. package/ts_web/readme.md +123 -0
  35. package/ts_web/services/recorder.service.ts +391 -0
@@ -8,16 +8,16 @@ export type TElementType = 'element' | 'page';
8
8
  @customElement('wcc-sidebar')
9
9
  export class WccSidebar extends DeesElement {
10
10
  @property({ attribute: false })
11
- public selectedItem: DeesElement | TTemplateFactory;
11
+ accessor selectedItem: DeesElement | TTemplateFactory;
12
12
 
13
13
  @property({ attribute: false })
14
- public selectedType: TElementType;
14
+ accessor selectedType: TElementType;
15
15
 
16
16
  @property()
17
- public dashboardRef: WccDashboard;
17
+ accessor dashboardRef: WccDashboard;
18
18
 
19
19
  @property()
20
- public isFullscreen: boolean = false;
20
+ accessor isFullscreen: boolean = false;
21
21
 
22
22
  public render(): TemplateResult {
23
23
  return html`
package/ts_web/index.ts CHANGED
@@ -2,6 +2,11 @@ import { WccDashboard } from './elements/wcc-dashboard.js';
2
2
  import { LitElement } from 'lit';
3
3
  import type { TTemplateFactory } from './elements/wcctools.helpers.js';
4
4
 
5
+ // Export recording components and service
6
+ export { RecorderService, type IRecorderEvents, type IRecordingOptions } from './services/recorder.service.js';
7
+ export { WccRecordButton } from './elements/wcc-record-button.js';
8
+ export { WccRecordingPanel } from './elements/wcc-recording-panel.js';
9
+
5
10
  const setupWccTools = (
6
11
  elementsArg?: { [key: string]: LitElement },
7
12
  pagesArg?: Record<string, TTemplateFactory>
@@ -0,0 +1,123 @@
1
+ # @design.estate/dees-wcctools
2
+
3
+ 🛠️ **Web Component Catalogue Tools** — The core dashboard and UI components for building interactive component catalogues
4
+
5
+ ## Overview
6
+
7
+ This is the main module of `@design.estate/dees-wcctools`, providing the complete dashboard experience for developing, testing, and documenting web components.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pnpm add -D @design.estate/dees-wcctools
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```typescript
18
+ import { setupWccTools } from '@design.estate/dees-wcctools';
19
+ import { MyButton } from './components/my-button.js';
20
+
21
+ setupWccTools({
22
+ 'my-button': MyButton,
23
+ });
24
+ ```
25
+
26
+ ## Exports
27
+
28
+ ### Main Entry Point
29
+
30
+ | Export | Description |
31
+ |--------|-------------|
32
+ | `setupWccTools` | Initialize the component catalogue dashboard |
33
+
34
+ ### Recording Components
35
+
36
+ | Export | Description |
37
+ |--------|-------------|
38
+ | `RecorderService` | Service class for screen/viewport recording |
39
+ | `WccRecordButton` | Record button UI component |
40
+ | `WccRecordingPanel` | Recording options and preview panel |
41
+ | `IRecorderEvents` | TypeScript interface for recorder callbacks |
42
+ | `IRecordingOptions` | TypeScript interface for recording options |
43
+
44
+ ## Internal Components
45
+
46
+ The module includes these internal web components:
47
+
48
+ | Component | Description |
49
+ |-----------|-------------|
50
+ | `wcc-dashboard` | Main dashboard container with routing |
51
+ | `wcc-sidebar` | Navigation sidebar with element/page listing |
52
+ | `wcc-frame` | Iframe viewport with responsive sizing |
53
+ | `wcc-properties` | Property panel with live editing |
54
+ | `wcc-record-button` | Recording state indicator button |
55
+ | `wcc-recording-panel` | Recording workflow UI |
56
+
57
+ ## RecorderService API
58
+
59
+ For programmatic recording control:
60
+
61
+ ```typescript
62
+ import { RecorderService, type IRecorderEvents } from '@design.estate/dees-wcctools';
63
+
64
+ const events: IRecorderEvents = {
65
+ onDurationUpdate: (duration) => console.log(`Recording: ${duration}s`),
66
+ onRecordingComplete: (blob) => saveBlob(blob),
67
+ onAudioLevelUpdate: (level) => updateMeter(level),
68
+ onError: (error) => console.error(error),
69
+ onStreamEnded: () => console.log('User stopped sharing'),
70
+ };
71
+
72
+ const recorder = new RecorderService(events);
73
+
74
+ // Load available microphones
75
+ const mics = await recorder.loadMicrophones(true); // true = request permission
76
+
77
+ // Start audio level monitoring
78
+ await recorder.startAudioMonitoring(mics[0].deviceId);
79
+
80
+ // Start recording
81
+ await recorder.startRecording({
82
+ mode: 'viewport', // or 'screen'
83
+ audioDeviceId: mics[0].deviceId,
84
+ viewportElement: document.querySelector('.viewport'),
85
+ });
86
+
87
+ // Stop recording
88
+ recorder.stopRecording();
89
+
90
+ // Export trimmed video
91
+ const trimmedBlob = await recorder.exportTrimmedVideo(videoElement, startTime, endTime);
92
+
93
+ // Cleanup
94
+ recorder.dispose();
95
+ ```
96
+
97
+ ## Architecture
98
+
99
+ ```
100
+ ts_web/
101
+ ├── index.ts # Main exports
102
+ ├── elements/
103
+ │ ├── wcc-dashboard.ts # Root dashboard component
104
+ │ ├── wcc-sidebar.ts # Navigation sidebar
105
+ │ ├── wcc-frame.ts # Responsive iframe viewport
106
+ │ ├── wcc-properties.ts # Property editing panel
107
+ │ ├── wcc-record-button.ts # Recording button
108
+ │ ├── wcc-recording-panel.ts # Recording options/preview
109
+ │ └── wcctools.helpers.ts # Shared utilities
110
+ ├── services/
111
+ │ └── recorder.service.ts # MediaRecorder abstraction
112
+ └── pages/
113
+ └── index.ts # Built-in pages
114
+ ```
115
+
116
+ ## Features
117
+
118
+ - 🎨 Interactive component preview
119
+ - 🔧 Real-time property editing with type detection
120
+ - 🌓 Theme switching (light/dark)
121
+ - 📱 Responsive viewport testing
122
+ - 🎬 Screen recording with trimming
123
+ - 🔗 URL-based deep linking
@@ -0,0 +1,391 @@
1
+ /**
2
+ * RecorderService - Handles all MediaRecorder, audio monitoring, and video export logic
3
+ */
4
+
5
+ export interface IRecorderEvents {
6
+ onDurationUpdate?: (duration: number) => void;
7
+ onRecordingComplete?: (blob: Blob) => void;
8
+ onAudioLevelUpdate?: (level: number) => void;
9
+ onError?: (error: Error) => void;
10
+ onStreamEnded?: () => void;
11
+ }
12
+
13
+ export interface IRecordingOptions {
14
+ mode: 'viewport' | 'screen';
15
+ audioDeviceId?: string;
16
+ viewportElement?: HTMLElement;
17
+ }
18
+
19
+ export class RecorderService {
20
+ // Recording state
21
+ private mediaRecorder: MediaRecorder | null = null;
22
+ private recordedChunks: Blob[] = [];
23
+ private durationInterval: number | null = null;
24
+ private _duration: number = 0;
25
+ private _recordedBlob: Blob | null = null;
26
+ private _isRecording: boolean = false;
27
+
28
+ // Audio monitoring state
29
+ private audioContext: AudioContext | null = null;
30
+ private audioAnalyser: AnalyserNode | null = null;
31
+ private audioMonitoringInterval: number | null = null;
32
+ private monitoringStream: MediaStream | null = null;
33
+
34
+ // Current recording stream
35
+ private currentStream: MediaStream | null = null;
36
+
37
+ // Event callbacks
38
+ private events: IRecorderEvents = {};
39
+
40
+ constructor(events?: IRecorderEvents) {
41
+ if (events) {
42
+ this.events = events;
43
+ }
44
+ }
45
+
46
+ // Public getters
47
+ get isRecording(): boolean {
48
+ return this._isRecording;
49
+ }
50
+
51
+ get duration(): number {
52
+ return this._duration;
53
+ }
54
+
55
+ get recordedBlob(): Blob | null {
56
+ return this._recordedBlob;
57
+ }
58
+
59
+ // Update event callbacks
60
+ setEvents(events: IRecorderEvents): void {
61
+ this.events = { ...this.events, ...events };
62
+ }
63
+
64
+ // ==================== Microphone Management ====================
65
+
66
+ async loadMicrophones(requestPermission: boolean = false): Promise<MediaDeviceInfo[]> {
67
+ try {
68
+ if (requestPermission) {
69
+ // Request permission by getting a temporary stream
70
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
71
+ stream.getTracks().forEach(track => track.stop());
72
+ }
73
+
74
+ const devices = await navigator.mediaDevices.enumerateDevices();
75
+ return devices.filter(d => d.kind === 'audioinput');
76
+ } catch (error) {
77
+ console.error('Error loading microphones:', error);
78
+ return [];
79
+ }
80
+ }
81
+
82
+ async startAudioMonitoring(deviceId: string): Promise<void> {
83
+ this.stopAudioMonitoring();
84
+
85
+ if (!deviceId) return;
86
+
87
+ try {
88
+ const stream = await navigator.mediaDevices.getUserMedia({
89
+ audio: { deviceId: { exact: deviceId } }
90
+ });
91
+
92
+ this.monitoringStream = stream;
93
+ this.audioContext = new AudioContext();
94
+ const source = this.audioContext.createMediaStreamSource(stream);
95
+ this.audioAnalyser = this.audioContext.createAnalyser();
96
+ this.audioAnalyser.fftSize = 256;
97
+ source.connect(this.audioAnalyser);
98
+
99
+ const dataArray = new Uint8Array(this.audioAnalyser.frequencyBinCount);
100
+
101
+ this.audioMonitoringInterval = window.setInterval(() => {
102
+ if (this.audioAnalyser) {
103
+ this.audioAnalyser.getByteFrequencyData(dataArray);
104
+ const average = dataArray.reduce((a, b) => a + b) / dataArray.length;
105
+ const level = Math.min(100, (average / 128) * 100);
106
+ this.events.onAudioLevelUpdate?.(level);
107
+ }
108
+ }, 50);
109
+ } catch (error) {
110
+ console.error('Error starting audio monitoring:', error);
111
+ this.events.onAudioLevelUpdate?.(0);
112
+ }
113
+ }
114
+
115
+ stopAudioMonitoring(): void {
116
+ if (this.audioMonitoringInterval) {
117
+ clearInterval(this.audioMonitoringInterval);
118
+ this.audioMonitoringInterval = null;
119
+ }
120
+ if (this.audioContext) {
121
+ this.audioContext.close();
122
+ this.audioContext = null;
123
+ }
124
+ if (this.monitoringStream) {
125
+ this.monitoringStream.getTracks().forEach(track => track.stop());
126
+ this.monitoringStream = null;
127
+ }
128
+ this.audioAnalyser = null;
129
+ }
130
+
131
+ // ==================== Recording Control ====================
132
+
133
+ async startRecording(options: IRecordingOptions): Promise<void> {
134
+ try {
135
+ // Stop audio monitoring before recording
136
+ this.stopAudioMonitoring();
137
+
138
+ // Get video stream based on mode
139
+ const displayMediaOptions: DisplayMediaStreamOptions = {
140
+ video: {
141
+ displaySurface: options.mode === 'viewport' ? 'browser' : 'monitor'
142
+ } as MediaTrackConstraints,
143
+ audio: false
144
+ };
145
+
146
+ // Add preferCurrentTab hint for viewport mode
147
+ if (options.mode === 'viewport') {
148
+ (displayMediaOptions as any).preferCurrentTab = true;
149
+ }
150
+
151
+ const videoStream = await navigator.mediaDevices.getDisplayMedia(displayMediaOptions);
152
+
153
+ // If viewport mode, try to crop to viewport element using Element Capture API
154
+ if (options.mode === 'viewport' && options.viewportElement) {
155
+ try {
156
+ if ('CropTarget' in window) {
157
+ const cropTarget = await (window as any).CropTarget.fromElement(options.viewportElement);
158
+ const [videoTrack] = videoStream.getVideoTracks();
159
+ await (videoTrack as any).cropTo(cropTarget);
160
+ }
161
+ } catch (e) {
162
+ console.warn('Element Capture not supported, recording full tab:', e);
163
+ }
164
+ }
165
+
166
+ // Combine video with audio if enabled
167
+ let combinedStream = videoStream;
168
+ if (options.audioDeviceId) {
169
+ try {
170
+ const audioStream = await navigator.mediaDevices.getUserMedia({
171
+ audio: { deviceId: { exact: options.audioDeviceId } }
172
+ });
173
+ combinedStream = new MediaStream([
174
+ ...videoStream.getVideoTracks(),
175
+ ...audioStream.getAudioTracks()
176
+ ]);
177
+ } catch (audioError) {
178
+ console.warn('Could not add audio:', audioError);
179
+ }
180
+ }
181
+
182
+ // Store stream for cleanup
183
+ this.currentStream = combinedStream;
184
+
185
+ // Create MediaRecorder
186
+ const mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9')
187
+ ? 'video/webm;codecs=vp9'
188
+ : 'video/webm';
189
+
190
+ this.mediaRecorder = new MediaRecorder(combinedStream, { mimeType });
191
+ this.recordedChunks = [];
192
+
193
+ this.mediaRecorder.ondataavailable = (e) => {
194
+ if (e.data.size > 0) {
195
+ this.recordedChunks.push(e.data);
196
+ }
197
+ };
198
+
199
+ this.mediaRecorder.onstop = () => this.handleRecordingComplete();
200
+
201
+ // Handle stream ending (user clicks "Stop sharing")
202
+ videoStream.getVideoTracks()[0].onended = () => {
203
+ if (this._isRecording) {
204
+ this.stopRecording();
205
+ this.events.onStreamEnded?.();
206
+ }
207
+ };
208
+
209
+ this.mediaRecorder.start(1000); // Capture in 1-second chunks
210
+
211
+ // Start duration timer
212
+ this._duration = 0;
213
+ this.durationInterval = window.setInterval(() => {
214
+ this._duration++;
215
+ this.events.onDurationUpdate?.(this._duration);
216
+ }, 1000);
217
+
218
+ this._isRecording = true;
219
+ } catch (error) {
220
+ console.error('Error starting recording:', error);
221
+ this._isRecording = false;
222
+ this.events.onError?.(error as Error);
223
+ throw error;
224
+ }
225
+ }
226
+
227
+ stopRecording(): void {
228
+ if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
229
+ this.mediaRecorder.stop();
230
+ }
231
+
232
+ if (this.durationInterval) {
233
+ clearInterval(this.durationInterval);
234
+ this.durationInterval = null;
235
+ }
236
+ }
237
+
238
+ private handleRecordingComplete(): void {
239
+ // Create blob from recorded chunks
240
+ this._recordedBlob = new Blob(this.recordedChunks, { type: 'video/webm' });
241
+
242
+ // Stop all tracks
243
+ if (this.currentStream) {
244
+ this.currentStream.getTracks().forEach(track => track.stop());
245
+ this.currentStream = null;
246
+ }
247
+
248
+ this._isRecording = false;
249
+ this.events.onRecordingComplete?.(this._recordedBlob);
250
+ }
251
+
252
+ // ==================== Trim & Export ====================
253
+
254
+ async exportTrimmedVideo(
255
+ videoElement: HTMLVideoElement,
256
+ trimStart: number,
257
+ trimEnd: number
258
+ ): Promise<Blob> {
259
+ return new Promise((resolve, reject) => {
260
+ // Create a canvas for capturing frames
261
+ const canvas = document.createElement('canvas');
262
+ canvas.width = videoElement.videoWidth || 1280;
263
+ canvas.height = videoElement.videoHeight || 720;
264
+ const ctx = canvas.getContext('2d');
265
+
266
+ if (!ctx) {
267
+ reject(new Error('Could not get canvas context'));
268
+ return;
269
+ }
270
+
271
+ // Create canvas stream for video
272
+ const canvasStream = canvas.captureStream(30);
273
+
274
+ // Try to capture audio from video element
275
+ let combinedStream: MediaStream;
276
+
277
+ try {
278
+ // Create audio context to capture video's audio
279
+ const audioCtx = new AudioContext();
280
+ const source = audioCtx.createMediaElementSource(videoElement);
281
+ const destination = audioCtx.createMediaStreamDestination();
282
+ source.connect(destination);
283
+ source.connect(audioCtx.destination); // Also play through speakers
284
+
285
+ // Combine video (from canvas) and audio (from video element)
286
+ combinedStream = new MediaStream([
287
+ ...canvasStream.getVideoTracks(),
288
+ ...destination.stream.getAudioTracks()
289
+ ]);
290
+
291
+ // Store audioCtx for cleanup
292
+ const cleanup = () => {
293
+ audioCtx.close();
294
+ };
295
+
296
+ this.recordTrimmedStream(videoElement, canvas, ctx, combinedStream, trimStart, trimEnd, cleanup, resolve, reject);
297
+ } catch (audioError) {
298
+ console.warn('Could not capture audio, recording video only:', audioError);
299
+ combinedStream = canvasStream;
300
+ this.recordTrimmedStream(videoElement, canvas, ctx, combinedStream, trimStart, trimEnd, () => {}, resolve, reject);
301
+ }
302
+ });
303
+ }
304
+
305
+ private recordTrimmedStream(
306
+ video: HTMLVideoElement,
307
+ canvas: HTMLCanvasElement,
308
+ ctx: CanvasRenderingContext2D,
309
+ stream: MediaStream,
310
+ trimStart: number,
311
+ trimEnd: number,
312
+ cleanup: () => void,
313
+ resolve: (blob: Blob) => void,
314
+ reject: (error: Error) => void
315
+ ): void {
316
+ const mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9')
317
+ ? 'video/webm;codecs=vp9'
318
+ : 'video/webm';
319
+
320
+ const recorder = new MediaRecorder(stream, { mimeType });
321
+ const chunks: Blob[] = [];
322
+
323
+ recorder.ondataavailable = (e) => {
324
+ if (e.data.size > 0) {
325
+ chunks.push(e.data);
326
+ }
327
+ };
328
+
329
+ recorder.onstop = () => {
330
+ cleanup();
331
+ resolve(new Blob(chunks, { type: 'video/webm' }));
332
+ };
333
+
334
+ recorder.onerror = (e) => {
335
+ cleanup();
336
+ reject(new Error('Recording error: ' + e));
337
+ };
338
+
339
+ // Seek to trim start
340
+ video.currentTime = trimStart;
341
+
342
+ video.onseeked = () => {
343
+ // Start recording
344
+ recorder.start(100);
345
+
346
+ // Start playing
347
+ video.play();
348
+
349
+ // Draw frames to canvas
350
+ const drawFrame = () => {
351
+ if (video.currentTime >= trimEnd || video.paused || video.ended) {
352
+ video.pause();
353
+ video.onseeked = null;
354
+
355
+ // Give a small delay before stopping to ensure last frame is captured
356
+ setTimeout(() => {
357
+ if (recorder.state === 'recording') {
358
+ recorder.stop();
359
+ }
360
+ }, 100);
361
+ return;
362
+ }
363
+
364
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
365
+ requestAnimationFrame(drawFrame);
366
+ };
367
+
368
+ drawFrame();
369
+ };
370
+ }
371
+
372
+ // ==================== Cleanup ====================
373
+
374
+ reset(): void {
375
+ this._recordedBlob = null;
376
+ this.recordedChunks = [];
377
+ this._duration = 0;
378
+ this._isRecording = false;
379
+ }
380
+
381
+ dispose(): void {
382
+ this.stopRecording();
383
+ this.stopAudioMonitoring();
384
+ this.reset();
385
+
386
+ if (this.currentStream) {
387
+ this.currentStream.getTracks().forEach(track => track.stop());
388
+ this.currentStream = null;
389
+ }
390
+ }
391
+ }