@design.estate/wcctools 6.1.0 → 7.0.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 (112) hide show
  1. package/changelog.md +41 -0
  2. package/dist_shell/bundle.js +405 -368
  3. package/dist_shell/bundle.js.map +1 -1
  4. package/dist_shell/bundle.js.third-party-notices.json +52 -0
  5. package/dist_shell/bundle.js.third-party-notices.txt +107 -1
  6. package/dist_shell/index.html +1 -1
  7. package/dist_ts/00_commitinfo_data.js +1 -1
  8. package/dist_ts/capture/classes.capturebrowser.d.ts +65 -0
  9. package/dist_ts/capture/classes.capturebrowser.js +251 -0
  10. package/dist_ts/capture/classes.captureservice.d.ts +39 -0
  11. package/dist_ts/capture/classes.captureservice.js +119 -0
  12. package/dist_ts/capture/classes.screencast.d.ts +47 -0
  13. package/dist_ts/capture/classes.screencast.js +127 -0
  14. package/dist_ts/capture/errors.d.ts +14 -0
  15. package/dist_ts/capture/errors.js +23 -0
  16. package/dist_ts/capture/images.d.ts +3 -0
  17. package/dist_ts/capture/images.js +58 -0
  18. package/dist_ts/capture/index.d.ts +4 -0
  19. package/dist_ts/capture/index.js +5 -0
  20. package/dist_ts/capture/interaction.d.ts +11 -0
  21. package/dist_ts/capture/interaction.js +138 -0
  22. package/dist_ts/capture/navigationguard.d.ts +44 -0
  23. package/dist_ts/capture/navigationguard.js +129 -0
  24. package/dist_ts/capture/previewpage.d.ts +29 -0
  25. package/dist_ts/capture/previewpage.js +96 -0
  26. package/dist_ts/capture/request.d.ts +69 -0
  27. package/dist_ts/capture/request.js +239 -0
  28. package/dist_ts/classes.bundlestatus.d.ts +36 -0
  29. package/dist_ts/classes.bundlestatus.js +110 -0
  30. package/dist_ts/classes.devapi.d.ts +3 -0
  31. package/dist_ts/classes.devapi.js +8 -1
  32. package/dist_ts/classes.devserver.d.ts +65 -14
  33. package/dist_ts/classes.devserver.js +157 -52
  34. package/dist_ts/classes.hostpolicy.d.ts +63 -0
  35. package/dist_ts/classes.hostpolicy.js +171 -0
  36. package/dist_ts/cli.js +28 -3
  37. package/dist_ts/cli.screenshot.d.ts +6 -0
  38. package/dist_ts/cli.screenshot.js +89 -0
  39. package/dist_ts/index.d.ts +0 -2
  40. package/dist_ts/index.js +1 -3
  41. package/dist_ts/plugins.d.ts +6 -2
  42. package/dist_ts/plugins.js +8 -3
  43. package/dist_ts_interfaces/capture.d.ts +151 -0
  44. package/dist_ts_interfaces/capture.js +2 -0
  45. package/dist_ts_interfaces/index.d.ts +1 -0
  46. package/dist_ts_interfaces/index.js +2 -1
  47. package/dist_ts_interfaces/requests.d.ts +59 -0
  48. package/dist_ts_shell/bundlestatus.d.ts +15 -0
  49. package/dist_ts_shell/bundlestatus.js +64 -0
  50. package/dist_ts_shell/elements/wcc-contextmenu.d.ts +3 -0
  51. package/dist_ts_shell/elements/wcc-contextmenu.js +18 -3
  52. package/dist_ts_shell/elements/wcc-preview-frame.d.ts +6 -0
  53. package/dist_ts_shell/elements/wcc-preview-frame.js +50 -2
  54. package/dist_ts_shell/elements/wcc-recording-panel.d.ts +9 -0
  55. package/dist_ts_shell/elements/wcc-recording-panel.js +12 -1
  56. package/dist_ts_shell/elements/wcc-shell.d.ts +15 -0
  57. package/dist_ts_shell/elements/wcc-shell.js +45 -3
  58. package/dist_ts_shell/plugins.d.ts +2 -1
  59. package/dist_ts_shell/plugins.js +3 -2
  60. package/dist_ts_shell/services/framesampler.service.d.ts +23 -0
  61. package/dist_ts_shell/services/framesampler.service.js +101 -0
  62. package/dist_ts_web/00_commitinfo_data.js +1 -1
  63. package/package.json +6 -2
  64. package/readme.md +108 -6
  65. package/ts/00_commitinfo_data.ts +1 -1
  66. package/ts/capture/classes.capturebrowser.ts +290 -0
  67. package/ts/capture/classes.captureservice.ts +152 -0
  68. package/ts/capture/classes.screencast.ts +152 -0
  69. package/ts/capture/errors.ts +25 -0
  70. package/ts/capture/images.ts +61 -0
  71. package/ts/capture/index.ts +4 -0
  72. package/ts/capture/interaction.ts +146 -0
  73. package/ts/capture/navigationguard.ts +147 -0
  74. package/ts/capture/previewpage.ts +130 -0
  75. package/ts/capture/request.ts +304 -0
  76. package/ts/classes.bundlestatus.ts +123 -0
  77. package/ts/classes.devapi.ts +9 -0
  78. package/ts/classes.devserver.ts +172 -50
  79. package/ts/classes.hostpolicy.ts +201 -0
  80. package/ts/cli.screenshot.ts +95 -0
  81. package/ts/cli.ts +28 -2
  82. package/ts/index.ts +0 -2
  83. package/ts/plugins.ts +9 -2
  84. package/ts_interfaces/bridge.ts +95 -0
  85. package/ts_interfaces/capture.ts +144 -0
  86. package/ts_interfaces/catalog.ts +37 -0
  87. package/ts_interfaces/index.ts +5 -0
  88. package/ts_interfaces/plugins.ts +3 -0
  89. package/ts_interfaces/requests.ts +104 -0
  90. package/ts_interfaces/standard.ts +103 -0
  91. package/ts_shared/index.ts +1 -0
  92. package/ts_shared/plugins.ts +3 -0
  93. package/ts_shared/previewroute.ts +58 -0
  94. package/ts_shell/bundlestatus.ts +68 -0
  95. package/ts_shell/elements/wcc-contextmenu.ts +306 -0
  96. package/ts_shell/elements/wcc-preview-frame.ts +167 -0
  97. package/ts_shell/elements/wcc-properties.ts +998 -0
  98. package/ts_shell/elements/wcc-record-button.ts +108 -0
  99. package/ts_shell/elements/wcc-recording-panel.ts +1017 -0
  100. package/ts_shell/elements/wcc-shell.ts +485 -0
  101. package/ts_shell/elements/wcc-sidebar.ts +1419 -0
  102. package/ts_shell/index.html +21 -0
  103. package/ts_shell/index.ts +4 -0
  104. package/ts_shell/plugins.ts +12 -0
  105. package/ts_shell/previewconnection.ts +103 -0
  106. package/ts_shell/services/framesampler.service.ts +112 -0
  107. package/ts_shell/services/recorder.service.ts +451 -0
  108. package/ts_shell/types/dom-mediacapture-stub/index.d.ts +12 -0
  109. package/ts_shell/types/dom-mediacapture-stub/package.json +6 -0
  110. package/ts_shell/types/dom-webcodecs-stub/index.d.ts +2 -0
  111. package/ts_shell/types/dom-webcodecs-stub/package.json +6 -0
  112. package/ts_web/00_commitinfo_data.ts +1 -1
@@ -0,0 +1,998 @@
1
+ import { DeesElement, property, html, customElement, type TemplateResult, state } from '@design.estate/dees-element';
2
+ import * as plugins from '../plugins.js';
3
+ import type { TWccViewport } from './wcc-preview-frame.js';
4
+ import type { WccRecordingPanel } from './wcc-recording-panel.js';
5
+ import './wcc-record-button.js';
6
+ import './wcc-recording-panel.js';
7
+
8
+ /** An open JSON editor for an Object or Array property. */
9
+ interface IPropertyEditor {
10
+ id: string;
11
+ name: string;
12
+ editorValue: string;
13
+ editorError: string;
14
+ }
15
+
16
+ @customElement('wcc-properties')
17
+ export class WccProperties extends DeesElement {
18
+ /** The connected preview's bridge; null while the preview loads. */
19
+ @property({ attribute: false })
20
+ accessor bridge: plugins.interfaces.IWccPreviewBridge | null = null;
21
+
22
+ /** Changes whenever the preview rendered something new, so the panel reads the properties again. */
23
+ @property({ type: Number })
24
+ accessor renderCount: number = 0;
25
+
26
+ /** The element the recorder crops a viewport recording to. */
27
+ @property({ attribute: false })
28
+ accessor captureTarget: HTMLElement | null = null;
29
+
30
+ @property()
31
+ accessor selectedViewport: TWccViewport = 'desktop';
32
+
33
+ @property()
34
+ accessor selectedTheme: plugins.interfaces.TWccTheme = 'dark';
35
+
36
+ @property()
37
+ accessor warning: string = null;
38
+
39
+ @property({ type: Boolean })
40
+ accessor isNative: boolean = false;
41
+
42
+ @property({ type: Number })
43
+ accessor sidebarWidth: number = 200;
44
+
45
+ @state()
46
+ accessor propertyContent: TemplateResult[] = [];
47
+
48
+ @state()
49
+ accessor editingProperties: IPropertyEditor[] = [];
50
+
51
+ // Recording coordination state
52
+ @state()
53
+ accessor showRecordingPanel: boolean = false;
54
+
55
+ @state()
56
+ accessor isRecording: boolean = false;
57
+
58
+ @state()
59
+ accessor recordingDuration: number = 0;
60
+
61
+ // Delayed hide for native mode transition
62
+ @state()
63
+ accessor isHidden: boolean = false;
64
+
65
+ public editorHeight: number = 300;
66
+
67
+ private propertiesGeneration = 0;
68
+ private hideTimeout: ReturnType<typeof setTimeout> | undefined;
69
+
70
+ public async disconnectedCallback() {
71
+ const disconnected = super.disconnectedCallback();
72
+ clearTimeout(this.hideTimeout);
73
+ this.isHidden = this.isNative;
74
+ return disconnected;
75
+ }
76
+
77
+ public render(): TemplateResult {
78
+ return html`
79
+ <style>
80
+ :host {
81
+ /* CSS Variables - Always dark theme */
82
+ --background: #0a0a0a;
83
+ --foreground: #e5e5e5;
84
+ --card: #0f0f0f;
85
+ --card-foreground: #f0f0f0;
86
+ --muted: #1a1a1a;
87
+ --muted-foreground: #666;
88
+ --accent: #222;
89
+ --accent-foreground: #fff;
90
+ --border: rgba(255, 255, 255, 0.06);
91
+ --input: #141414;
92
+ --primary: #3b82f6;
93
+ --primary-foreground: #fff;
94
+ --ring: #3b82f6;
95
+ --radius: 4px;
96
+ --radius-sm: 2px;
97
+ --radius-md: 4px;
98
+ --radius-lg: 6px;
99
+
100
+ /* Base styles */
101
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
102
+ box-sizing: border-box;
103
+ position: absolute;
104
+ left: ${this.sidebarWidth}px;
105
+ height: ${this.editingProperties.length > 0 ? 100 + this.editorHeight : 100}px;
106
+ bottom: 0px;
107
+ right: 0px;
108
+ overflow: hidden;
109
+ background: var(--background);
110
+ color: var(--foreground);
111
+ display: ${this.isHidden ? 'none' : 'block'};
112
+ }
113
+ .grid {
114
+ display: grid;
115
+ grid-template-columns: 1fr 150px 350px 100px;
116
+ height: 100%;
117
+ }
118
+ .properties {
119
+ background: transparent;
120
+ overflow-y: auto;
121
+ display: grid;
122
+ grid-template-columns: repeat(3, 1fr);
123
+ border-right: 1px solid var(--border);
124
+ align-content: start;
125
+ }
126
+
127
+ .material-symbols-outlined {
128
+ font-family: 'Material Symbols Outlined';
129
+ font-weight: normal;
130
+ font-style: normal;
131
+ font-size: 24px; /* Preferred icon size */
132
+ display: inline-block;
133
+ line-height: 1;
134
+ text-transform: none;
135
+ letter-spacing: normal;
136
+ word-wrap: normal;
137
+ white-space: nowrap;
138
+ direction: ltr;
139
+ font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 48;
140
+ }
141
+
142
+ .properties .property {
143
+ padding: 0.4rem;
144
+ background: transparent;
145
+ border-right: 1px solid var(--border);
146
+ border-bottom: 1px solid var(--border);
147
+ transition: all 0.15s ease;
148
+ }
149
+
150
+ .properties .property:hover {
151
+ background: rgba(255, 255, 255, 0.02);
152
+ }
153
+
154
+ .properties .property-label {
155
+ font-size: 0.65rem;
156
+ font-weight: 400;
157
+ color: #888;
158
+ margin-bottom: 0.2rem;
159
+ text-transform: capitalize;
160
+ white-space: nowrap;
161
+ overflow: hidden;
162
+ text-overflow: ellipsis;
163
+ }
164
+
165
+ .properties input[type="text"],
166
+ .properties input[type="number"],
167
+ .properties select {
168
+ display: block;
169
+ width: 100%;
170
+ padding: 0.25rem 0.4rem;
171
+ background: var(--input);
172
+ border: 1px solid transparent;
173
+ color: var(--foreground);
174
+ border-radius: var(--radius-sm);
175
+ font-size: 0.7rem;
176
+ transition: all 0.15s ease;
177
+ outline: none;
178
+ }
179
+
180
+ .properties input[type="text"]:focus,
181
+ .properties input[type="number"]:focus,
182
+ .properties select:focus {
183
+ border-color: var(--primary);
184
+ background: rgba(59, 130, 246, 0.1);
185
+ }
186
+
187
+ .properties input[type="checkbox"] {
188
+ width: 1rem;
189
+ height: 1rem;
190
+ cursor: pointer;
191
+ accent-color: var(--primary);
192
+ }
193
+
194
+ .properties .editor-button {
195
+ padding: 0.25rem 0.5rem;
196
+ background: var(--input);
197
+ border: 1px solid transparent;
198
+ border-radius: var(--radius-sm);
199
+ color: var(--foreground);
200
+ font-size: 0.7rem;
201
+ cursor: pointer;
202
+ transition: all 0.15s ease;
203
+ text-align: center;
204
+ }
205
+
206
+ .properties .editor-button:hover {
207
+ border-color: var(--primary);
208
+ background: rgba(59, 130, 246, 0.1);
209
+ }
210
+
211
+ .viewportSelector,
212
+ .themeSelector,
213
+ .shareSelector {
214
+ user-select: none;
215
+ background: transparent;
216
+ display: flex;
217
+ flex-direction: column;
218
+ border-right: 1px solid var(--border);
219
+ }
220
+ .selectorButtons2 {
221
+ display: grid;
222
+ grid-template-columns: repeat(2, minmax(0, 1fr));
223
+ flex: 1;
224
+ }
225
+ .selectorButtons4 {
226
+ display: grid;
227
+ grid-template-columns: repeat(4, minmax(0, 1fr));
228
+ flex: 1;
229
+ }
230
+ .selectorButtons5 {
231
+ display: grid;
232
+ grid-template-columns: repeat(5, minmax(0, 1fr));
233
+ flex: 1;
234
+ }
235
+ .selectorButtons1 {
236
+ display: grid;
237
+ grid-template-columns: minmax(0, 1fr);
238
+ flex: 1;
239
+ }
240
+ .button {
241
+ appearance: none;
242
+ margin: 0;
243
+ font-family: inherit;
244
+ line-height: 1.2;
245
+ display: flex;
246
+ flex-direction: column;
247
+ align-items: center;
248
+ justify-content: center;
249
+ padding: 0.5rem 0.25rem;
250
+ text-align: center;
251
+ background: transparent;
252
+ border: 1px solid var(--border);
253
+ transition: all 0.15s ease;
254
+ cursor: pointer;
255
+ font-size: 0.65rem;
256
+ gap: 0.2rem;
257
+ color: #999;
258
+ }
259
+
260
+ .button:hover {
261
+ background: rgba(59, 130, 246, 0.05);
262
+ color: #bbb;
263
+ }
264
+
265
+ .button.selected {
266
+ background: rgba(59, 130, 246, 0.15);
267
+ color: var(--primary);
268
+ border-color: rgba(59, 130, 246, 0.3);
269
+ }
270
+
271
+ .button.selected:hover {
272
+ background: rgba(59, 130, 246, 0.2);
273
+ }
274
+
275
+ .button:focus-visible,
276
+ .properties .editor-button:focus-visible,
277
+ .editor-close-all:focus-visible {
278
+ outline: 2px solid var(--ring);
279
+ outline-offset: -2px;
280
+ }
281
+
282
+ .properties input:focus-visible,
283
+ .properties select:focus-visible {
284
+ outline: 2px solid var(--ring);
285
+ outline-offset: 1px;
286
+ }
287
+
288
+ .button .material-symbols-outlined {
289
+ font-size: 18px;
290
+ font-variation-settings: 'FILL' 0, 'wght' 300;
291
+ min-width: 18px;
292
+ min-height: 18px;
293
+ display: inline-flex;
294
+ align-items: center;
295
+ justify-content: center;
296
+ }
297
+
298
+ .button.selected .material-symbols-outlined {
299
+ font-variation-settings: 'FILL' 1, 'wght' 400;
300
+ }
301
+
302
+ .panelheading {
303
+ padding: 0.3rem 0.5rem;
304
+ font-weight: 500;
305
+ font-size: 0.65rem;
306
+ background: rgba(59, 130, 246, 0.03);
307
+ border-bottom: 1px solid var(--border);
308
+ color: #888;
309
+ text-transform: uppercase;
310
+ letter-spacing: 0.08em;
311
+ }
312
+ .docs {
313
+ display: flex;
314
+ align-items: center;
315
+ justify-content: center;
316
+ text-transform: uppercase;
317
+ background: transparent;
318
+ cursor: pointer;
319
+ font-size: 0.65rem;
320
+ font-weight: 500;
321
+ letter-spacing: 0.08em;
322
+ transition: all 0.15s ease;
323
+ color: #666;
324
+ }
325
+
326
+ .docs:hover {
327
+ background: rgba(59, 130, 246, 0.05);
328
+ color: #999;
329
+ }
330
+
331
+ .warning {
332
+ position: absolute;
333
+ background: rgba(20, 20, 20, 0.8);
334
+ color: #888;
335
+ top: 0.5rem;
336
+ bottom: 0.5rem;
337
+ left: 0.5rem;
338
+ right: calc(600px + 0.5rem);
339
+ display: flex;
340
+ align-items: center;
341
+ justify-content: center;
342
+ padding: 1.5rem;
343
+ font-size: 0.85rem;
344
+ border-radius: var(--radius-md);
345
+ border: 1px solid var(--border);
346
+ backdrop-filter: blur(8px);
347
+ }
348
+
349
+ .advanced-editor-container {
350
+ position: absolute;
351
+ left: 0;
352
+ right: 0;
353
+ top: 0;
354
+ height: ${this.editorHeight}px;
355
+ background: #050505;
356
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
357
+ display: flex;
358
+ flex-direction: column;
359
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
360
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
361
+ }
362
+
363
+ .editor-header-bar {
364
+ padding: 0.5rem 0.75rem;
365
+ background: transparent;
366
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
367
+ display: flex;
368
+ justify-content: space-between;
369
+ align-items: center;
370
+ height: 36px;
371
+ }
372
+
373
+ .editor-header-title {
374
+ font-size: 0.7rem;
375
+ font-weight: 500;
376
+ color: #666;
377
+ text-transform: uppercase;
378
+ letter-spacing: 0.08em;
379
+ }
380
+
381
+ .editor-close-all {
382
+ padding: 0.25rem 0.5rem;
383
+ background: transparent;
384
+ border: 1px solid rgba(255, 255, 255, 0.08);
385
+ border-radius: var(--radius-sm);
386
+ color: #999;
387
+ font-size: 0.65rem;
388
+ font-weight: 500;
389
+ cursor: pointer;
390
+ transition: all 0.15s ease;
391
+ }
392
+
393
+ .editor-close-all:hover {
394
+ background: rgba(255, 255, 255, 0.05);
395
+ border-color: rgba(255, 255, 255, 0.12);
396
+ color: #f87171;
397
+ }
398
+
399
+ .editors-container {
400
+ flex: 1;
401
+ display: flex;
402
+ overflow-x: auto;
403
+ overflow-y: hidden;
404
+ gap: 0;
405
+ background: rgba(255, 255, 255, 0.02);
406
+ padding: 0.75rem;
407
+ }
408
+
409
+ .editors-container::-webkit-scrollbar {
410
+ height: 8px;
411
+ }
412
+
413
+ .editors-container::-webkit-scrollbar-track {
414
+ background: rgba(255, 255, 255, 0.02);
415
+ border-radius: 4px;
416
+ }
417
+
418
+ .editors-container::-webkit-scrollbar-thumb {
419
+ background: rgba(255, 255, 255, 0.08);
420
+ border-radius: 4px;
421
+ }
422
+
423
+ .editors-container::-webkit-scrollbar-thumb:hover {
424
+ background: rgba(255, 255, 255, 0.12);
425
+ }
426
+
427
+ .editor-instance {
428
+ min-width: 320px;
429
+ flex: 1;
430
+ max-width: 480px;
431
+ background: rgba(10, 10, 10, 0.6);
432
+ display: flex;
433
+ flex-direction: column;
434
+ border-radius: var(--radius);
435
+ overflow: hidden;
436
+ margin-right: 0.75rem;
437
+ border: 1px solid rgba(255, 255, 255, 0.06);
438
+ transition: all 0.2s ease;
439
+ }
440
+
441
+ .editor-instance:hover {
442
+ border-color: rgba(255, 255, 255, 0.1);
443
+ }
444
+
445
+ .editor-instance:last-child {
446
+ margin-right: 0;
447
+ }
448
+
449
+ .editor-header {
450
+ padding: 0.5rem 0.75rem;
451
+ background: rgba(255, 255, 255, 0.02);
452
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
453
+ display: flex;
454
+ justify-content: space-between;
455
+ align-items: center;
456
+ height: 36px;
457
+ }
458
+
459
+ .editor-title {
460
+ font-size: 0.75rem;
461
+ font-weight: 500;
462
+ color: #999;
463
+ overflow: hidden;
464
+ text-overflow: ellipsis;
465
+ white-space: nowrap;
466
+ font-family: 'Consolas', 'Monaco', monospace;
467
+ }
468
+
469
+ .editor-actions {
470
+ display: flex;
471
+ gap: 0.25rem;
472
+ }
473
+
474
+ .editor-button {
475
+ width: 24px;
476
+ height: 24px;
477
+ padding: 0;
478
+ background: transparent;
479
+ border: none;
480
+ color: #666;
481
+ font-size: 1rem;
482
+ cursor: pointer;
483
+ transition: all 0.15s ease;
484
+ border-radius: var(--radius-sm);
485
+ display: flex;
486
+ align-items: center;
487
+ justify-content: center;
488
+ }
489
+
490
+ .editor-button:hover {
491
+ background: rgba(255, 255, 255, 0.05);
492
+ color: #999;
493
+ }
494
+
495
+ .editor-button.primary {
496
+ color: #4ade80;
497
+ }
498
+
499
+ .editor-button.primary:hover {
500
+ background: rgba(74, 222, 128, 0.1);
501
+ }
502
+
503
+ .editor-content {
504
+ flex: 1;
505
+ display: flex;
506
+ flex-direction: column;
507
+ overflow: hidden;
508
+ min-height: 0;
509
+ position: relative;
510
+ }
511
+
512
+ .editor-textarea {
513
+ width: 100%;
514
+ height: 100%;
515
+ background: transparent;
516
+ border: none;
517
+ color: #d0d0d0;
518
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
519
+ font-size: 0.8125rem;
520
+ line-height: 1.6;
521
+ padding: 0.75rem;
522
+ resize: none;
523
+ outline: none;
524
+ transition: all 0.15s ease;
525
+ overflow: auto;
526
+ }
527
+
528
+ .editor-textarea:focus {
529
+ background: rgba(255, 255, 255, 0.01);
530
+ }
531
+
532
+ .editor-textarea::selection {
533
+ background: rgba(59, 130, 246, 0.3);
534
+ }
535
+
536
+ .editor-error {
537
+ position: absolute;
538
+ bottom: 0;
539
+ left: 0;
540
+ right: 0;
541
+ padding: 0.5rem 0.75rem;
542
+ background: rgba(239, 68, 68, 0.9);
543
+ backdrop-filter: blur(4px);
544
+ color: #fff;
545
+ font-size: 0.7rem;
546
+ font-weight: 500;
547
+ display: flex;
548
+ align-items: center;
549
+ gap: 0.375rem;
550
+ border-top: 1px solid rgba(239, 68, 68, 0.5);
551
+ }
552
+
553
+ .editor-error::before {
554
+ content: '!';
555
+ display: inline-flex;
556
+ width: 16px;
557
+ height: 16px;
558
+ align-items: center;
559
+ justify-content: center;
560
+ background: rgba(255, 255, 255, 0.2);
561
+ border-radius: 50%;
562
+ font-size: 0.65rem;
563
+ font-weight: bold;
564
+ }
565
+
566
+ .main-content {
567
+ position: absolute;
568
+ left: 0;
569
+ right: 0;
570
+ bottom: 0;
571
+ height: 100px;
572
+ }
573
+
574
+ @container wccShell (max-width: 600px) {
575
+ :host {
576
+ left: 0;
577
+ height: ${this.editingProperties.length > 0 ? 160 + this.editorHeight : 160}px;
578
+ }
579
+
580
+ .main-content {
581
+ height: 160px;
582
+ }
583
+
584
+ .grid {
585
+ grid-template-columns: minmax(90px, 2fr) minmax(0, 5fr) minmax(52px, 1fr);
586
+ grid-template-rows: 60px 100px;
587
+ }
588
+
589
+ .properties {
590
+ grid-column: 1 / -1;
591
+ grid-row: 1;
592
+ display: flex;
593
+ overflow-x: auto;
594
+ overflow-y: hidden;
595
+ border-right: 0;
596
+ border-bottom: 1px solid var(--border);
597
+ }
598
+
599
+ .properties .property {
600
+ flex: 0 0 120px;
601
+ min-width: 0;
602
+ }
603
+
604
+ .themeSelector {
605
+ grid-column: 1;
606
+ grid-row: 2;
607
+ }
608
+
609
+ .viewportSelector {
610
+ grid-column: 2;
611
+ grid-row: 2;
612
+ }
613
+
614
+ .shareSelector {
615
+ grid-column: 3;
616
+ grid-row: 2;
617
+ }
618
+
619
+ .themeSelector,
620
+ .viewportSelector,
621
+ .shareSelector {
622
+ min-width: 0;
623
+ }
624
+
625
+ .button {
626
+ min-width: 0;
627
+ overflow: hidden;
628
+ padding: 0.25rem 0.1rem;
629
+ font-size: 0.55rem;
630
+ }
631
+
632
+ .panelheading {
633
+ overflow: hidden;
634
+ padding: 0.3rem 0.25rem;
635
+ text-overflow: ellipsis;
636
+ white-space: nowrap;
637
+ }
638
+
639
+ .warning {
640
+ top: 0.25rem;
641
+ right: 0.25rem;
642
+ bottom: calc(100px + 0.25rem);
643
+ left: 0.25rem;
644
+ padding: 0.5rem;
645
+ }
646
+ }
647
+ </style>
648
+ ${this.editingProperties.length > 0 ? html`
649
+ <div class="advanced-editor-container">
650
+ <div class="editor-header-bar">
651
+ <div class="editor-header-title">Property Editors</div>
652
+ <button class="editor-close-all" @click=${this.closeAllEditors}>
653
+ Close All
654
+ </button>
655
+ </div>
656
+ <div class="editors-container">
657
+ ${this.editingProperties.length === 0 ? html`
658
+ <div style="
659
+ flex: 1;
660
+ display: flex;
661
+ align-items: center;
662
+ justify-content: center;
663
+ color: #666;
664
+ font-size: 0.875rem;
665
+ text-align: center;
666
+ padding: 2rem;
667
+ ">
668
+ <div>
669
+ <div style="margin-bottom: 0.5rem; font-size: 1.5rem; opacity: 0.5;">{ }</div>
670
+ <div>No properties being edited</div>
671
+ <div style="font-size: 0.75rem; margin-top: 0.25rem; opacity: 0.7;">Click "Edit Object/Array" buttons to start editing</div>
672
+ </div>
673
+ </div>
674
+ ` : null}
675
+ ${this.editingProperties.map(prop => html`
676
+ <div class="editor-instance">
677
+ <div class="editor-header">
678
+ <div class="editor-title">${prop.name}</div>
679
+ <div class="editor-actions">
680
+ <button class="editor-button" @click=${() => this.handleEditorCancel(prop.id)}>✕</button>
681
+ <button class="editor-button primary" @click=${() => this.handleEditorSave(prop.id)}>✓</button>
682
+ </div>
683
+ </div>
684
+ <div class="editor-content">
685
+ <textarea
686
+ class="editor-textarea"
687
+ .value=${prop.editorValue}
688
+ @input=${(e: InputEvent) => {
689
+ const editor = this.editingProperties.find(p => p.id === prop.id);
690
+ if (editor) {
691
+ editor.editorValue = (e.target as HTMLTextAreaElement).value;
692
+ editor.editorError = '';
693
+ this.requestUpdate();
694
+ }
695
+ }}
696
+ @keydown=${(e: KeyboardEvent) => {
697
+ if (e.key === 'Tab') {
698
+ e.preventDefault();
699
+ const target = e.target as HTMLTextAreaElement;
700
+ const start = target.selectionStart;
701
+ const end = target.selectionEnd;
702
+ const value = target.value;
703
+ target.value = value.substring(0, start) + ' ' + value.substring(end);
704
+ target.selectionStart = target.selectionEnd = start + 2;
705
+ }
706
+ }}
707
+ ></textarea>
708
+ ${prop.editorError ? html`
709
+ <div class="editor-error">${prop.editorError}</div>
710
+ ` : null}
711
+ </div>
712
+ </div>
713
+ `)}
714
+ </div>
715
+ </div>
716
+ ` : null}
717
+ <div class="main-content">
718
+ <div class="grid">
719
+ <div class="properties" role="group" aria-label="Properties">
720
+ ${this.propertyContent}
721
+ </div>
722
+ <div class="themeSelector" role="group" aria-labelledby="wcc-theme-heading">
723
+ <div class="panelheading" id="wcc-theme-heading">Theme</div>
724
+ <div class="selectorButtons2">
725
+ ${([
726
+ ['dark', 'Dark', 'brightness_3'],
727
+ ['bright', 'Bright', 'flare'],
728
+ ] as Array<[plugins.interfaces.TWccTheme, string, string]>).map(([theme, label, icon]) => html`
729
+ <button
730
+ type="button"
731
+ class="button ${this.selectedTheme === theme ? 'selected' : ''}"
732
+ aria-pressed=${this.selectedTheme === theme ? 'true' : 'false'}
733
+ @click=${() => this.selectTheme(theme)}
734
+ >
735
+ ${label}<i class="material-symbols-outlined" aria-hidden="true">${icon}</i>
736
+ </button>
737
+ `)}
738
+ </div>
739
+ </div>
740
+ <div class="viewportSelector" role="group" aria-labelledby="wcc-viewport-heading">
741
+ <div class="panelheading" id="wcc-viewport-heading">Viewport</div>
742
+ <div class="selectorButtons5">
743
+ ${([
744
+ ['phone', 'Phone', 'smartphone'],
745
+ ['phablet', 'Phablet', 'smartphone'],
746
+ ['tablet', 'Tablet', 'tablet'],
747
+ ['desktop', 'Desktop', 'desktop_windows'],
748
+ ['native', 'Native', 'fullscreen'],
749
+ ] as Array<[TWccViewport, string, string]>).map(([viewport, label, icon]) => html`
750
+ <button
751
+ type="button"
752
+ class="button ${this.selectedViewport === viewport ? 'selected' : ''}"
753
+ aria-pressed=${this.selectedViewport === viewport ? 'true' : 'false'}
754
+ @click=${() => this.selectViewport(viewport)}
755
+ >
756
+ ${label}<i class="material-symbols-outlined" aria-hidden="true">${icon}</i>
757
+ </button>
758
+ `)}
759
+ </div>
760
+ </div>
761
+ <div class="shareSelector" role="group" aria-labelledby="wcc-share-heading">
762
+ <div class="panelheading" id="wcc-share-heading">Share</div>
763
+ <div class="selectorButtons1">
764
+ <button
765
+ type="button"
766
+ class="button ${this.isRecording ? 'selected' : ''}"
767
+ aria-expanded=${this.showRecordingPanel ? 'true' : 'false'}
768
+ @click=${() => this.handleRecordButtonClick()}
769
+ >
770
+ ${this.isRecording ? 'Stop' : 'Record'}<i class="material-symbols-outlined" aria-hidden="true">${this.isRecording ? 'stop_circle' : 'videocam'}</i>
771
+ </button>
772
+ </div>
773
+ </div>
774
+ </div>
775
+ ${this.warning ? html`<div class="warning">${this.warning}</div>` : null}
776
+ </div>
777
+
778
+ <!-- Recording Panel (options + preview) -->
779
+ ${this.showRecordingPanel ? html`
780
+ <wcc-recording-panel
781
+ .captureTarget=${this.captureTarget}
782
+ @recording-start=${() => { this.isRecording = true; }}
783
+ @recording-stop=${() => { this.isRecording = false; }}
784
+ @duration-update=${(e: CustomEvent) => { this.recordingDuration = e.detail.duration; }}
785
+ @close=${() => { this.showRecordingPanel = false; this.isRecording = false; this.recordingDuration = 0; }}
786
+ ></wcc-recording-panel>
787
+ ` : null}
788
+ `;
789
+ }
790
+
791
+ /** Read the rendered element's properties from the preview and build their editors. */
792
+ public async createProperties() {
793
+ const generation = ++this.propertiesGeneration;
794
+ const bridge = this.bridge;
795
+ if (!bridge) {
796
+ this.propertyContent = [];
797
+ this.warning = 'Waiting for the preview to load.';
798
+ return;
799
+ }
800
+ const listing = await bridge.getProperties();
801
+ if (generation !== this.propertiesGeneration) {
802
+ return;
803
+ }
804
+ if (listing.kind === 'unavailable') {
805
+ this.propertyContent = [];
806
+ this.warning = listing.reason;
807
+ return;
808
+ }
809
+ this.warning = null;
810
+ this.propertyContent = listing.properties.map((descriptor) => this.renderPropertyEditor(bridge, descriptor));
811
+ }
812
+
813
+ private renderPropertyEditor(
814
+ bridgeArg: plugins.interfaces.IWccPreviewBridge,
815
+ descriptorArg: plugins.interfaces.IWccPropertyDescriptor,
816
+ ): TemplateResult {
817
+ const { name, kind } = descriptorArg;
818
+ const setValue = (valueArg: plugins.interfaces.TWccPropertyScalar) => {
819
+ const result = bridgeArg.setProperty(name, valueArg);
820
+ this.warning = 'error' in result ? result.error : null;
821
+ };
822
+ const editor = (() => {
823
+ switch (kind) {
824
+ case 'Boolean':
825
+ return html`<input
826
+ aria-label=${name}
827
+ type="checkbox"
828
+ ?checked=${descriptorArg.value === true}
829
+ @input="${(eventArg: Event) => setValue((eventArg.target as HTMLInputElement).checked)}"
830
+ />`;
831
+ case 'String':
832
+ return html`<input
833
+ aria-label=${name}
834
+ type="text"
835
+ .value=${descriptorArg.value == null ? '' : String(descriptorArg.value)}
836
+ @input="${(eventArg: Event) => setValue((eventArg.target as HTMLInputElement).value)}"
837
+ />`;
838
+ case 'Number':
839
+ return html`<input
840
+ aria-label=${name}
841
+ type="number"
842
+ .value=${descriptorArg.value == null ? '' : String(descriptorArg.value)}
843
+ @input="${(eventArg: Event) => setValue(parseFloat((eventArg.target as HTMLInputElement).value) || 0)}"
844
+ />`;
845
+ case 'Enum':
846
+ return html`<select
847
+ aria-label=${name}
848
+ .value=${descriptorArg.value == null ? '' : String(descriptorArg.value)}
849
+ @change="${(eventArg: Event) => setValue((eventArg.target as HTMLSelectElement).value)}"
850
+ >
851
+ ${(descriptorArg.enumValues ?? []).map((valueArg) => html`<option value="${valueArg}">${valueArg}</option>`)}
852
+ </select>`;
853
+ case 'Object':
854
+ case 'Array':
855
+ return descriptorArg.json === undefined
856
+ ? html`<div style="color: #666; font-size: 0.7rem;">Not representable as JSON</div>`
857
+ : html`<button
858
+ type="button"
859
+ aria-label=${`Edit ${name}`}
860
+ class="editor-button"
861
+ style="width: 100%; margin-top: 0.25rem;"
862
+ @click="${() => this.openAdvancedEditor(name, descriptorArg.json)}"
863
+ >
864
+ Edit ${kind}
865
+ </button>`;
866
+ }
867
+ })();
868
+ return html`
869
+ <div class="property">
870
+ <div class="property-label">${name} (${kind})</div>
871
+ ${editor}
872
+ </div>
873
+ `;
874
+ }
875
+
876
+ public selectTheme(themeArg: plugins.interfaces.TWccTheme) {
877
+ this.selectedTheme = themeArg;
878
+ this.dispatchEvent(
879
+ new CustomEvent('selectedTheme', {
880
+ detail: themeArg,
881
+ })
882
+ );
883
+ }
884
+
885
+ protected updated(changedProperties: Map<string, unknown>) {
886
+ super.updated(changedProperties);
887
+
888
+ // Handle delayed hide for native mode transition
889
+ if (changedProperties.has('isNative')) {
890
+ clearTimeout(this.hideTimeout);
891
+ if (this.isNative && this.isConnected) {
892
+ // Delay hiding until frame animation completes
893
+ this.hideTimeout = setTimeout(() => {
894
+ this.isHidden = true;
895
+ }, 300);
896
+ } else {
897
+ // Show immediately when exiting native mode
898
+ this.isHidden = this.isNative;
899
+ }
900
+ }
901
+
902
+ // Read the properties again for a new preview or a new render
903
+ if (changedProperties.has('bridge') || changedProperties.has('renderCount')) {
904
+ if (changedProperties.has('bridge')) {
905
+ this.closeAllEditors();
906
+ }
907
+ this.createProperties().catch((error) => {
908
+ console.error('wcctools: reading the preview properties failed', error);
909
+ this.propertyContent = [];
910
+ });
911
+ }
912
+ }
913
+
914
+ public selectViewport(viewport: TWccViewport) {
915
+ this.selectedViewport = viewport;
916
+ this.dispatchEvent(
917
+ new CustomEvent('selectedViewport', {
918
+ detail: viewport,
919
+ })
920
+ );
921
+ }
922
+
923
+ private openAdvancedEditor(propertyName: string, jsonArg: string) {
924
+ // Check if this property is already being edited
925
+ if (this.editingProperties.some((editor) => editor.name === propertyName)) {
926
+ return;
927
+ }
928
+
929
+ const newEditor: IPropertyEditor = {
930
+ id: `${propertyName}-${Date.now()}`,
931
+ name: propertyName,
932
+ editorValue: jsonArg,
933
+ editorError: '',
934
+ };
935
+
936
+ this.editingProperties = [...this.editingProperties, newEditor];
937
+
938
+ // Notify parent to resize frame if this is the first editor
939
+ if (this.editingProperties.length === 1) {
940
+ this.dispatchEditorState(true);
941
+ }
942
+ }
943
+
944
+ private handleEditorSave(editorId: string) {
945
+ const editor = this.editingProperties.find(p => p.id === editorId);
946
+ if (!editor || !this.bridge) return;
947
+
948
+ const result = this.bridge.setPropertyJson(editor.name, editor.editorValue);
949
+ if ('error' in result) {
950
+ editor.editorError = result.error;
951
+ this.requestUpdate();
952
+ return;
953
+ }
954
+ this.handleEditorCancel(editorId);
955
+ // Refresh properties display
956
+ this.createProperties();
957
+ }
958
+
959
+ private handleEditorCancel(editorId: string) {
960
+ // Remove this editor from the list
961
+ this.editingProperties = this.editingProperties.filter(p => p.id !== editorId);
962
+
963
+ // If no more editors, notify parent to resize frame
964
+ if (this.editingProperties.length === 0) {
965
+ this.dispatchEditorState(false);
966
+ }
967
+ }
968
+
969
+ private closeAllEditors() {
970
+ if (this.editingProperties.length === 0) {
971
+ return;
972
+ }
973
+ this.editingProperties = [];
974
+ // Notify parent to resize frame back
975
+ this.dispatchEditorState(false);
976
+ }
977
+
978
+ private dispatchEditorState(isOpenArg: boolean) {
979
+ this.dispatchEvent(
980
+ new CustomEvent('editorStateChanged', {
981
+ detail: { isOpen: isOpenArg },
982
+ bubbles: true
983
+ })
984
+ );
985
+ }
986
+
987
+ // ==================== Recording Methods ====================
988
+
989
+ private handleRecordButtonClick() {
990
+ if (this.isRecording) {
991
+ // Stop recording through the panel
992
+ this.shadowRoot?.querySelector<WccRecordingPanel>('wcc-recording-panel')?.stopRecording();
993
+ } else {
994
+ // Toggle the recording panel
995
+ this.showRecordingPanel = !this.showRecordingPanel;
996
+ }
997
+ }
998
+ }