@design.estate/wcctools 6.1.0 → 6.2.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 (63) hide show
  1. package/changelog.md +20 -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/classes.bundlestatus.d.ts +36 -0
  9. package/dist_ts/classes.bundlestatus.js +110 -0
  10. package/dist_ts/classes.devapi.d.ts +3 -0
  11. package/dist_ts/classes.devapi.js +8 -1
  12. package/dist_ts/classes.devserver.d.ts +45 -10
  13. package/dist_ts/classes.devserver.js +123 -49
  14. package/dist_ts/cli.js +5 -2
  15. package/dist_ts/index.d.ts +0 -2
  16. package/dist_ts/index.js +1 -3
  17. package/dist_ts_interfaces/requests.d.ts +35 -0
  18. package/dist_ts_shell/bundlestatus.d.ts +15 -0
  19. package/dist_ts_shell/bundlestatus.js +64 -0
  20. package/dist_ts_shell/elements/wcc-contextmenu.d.ts +3 -0
  21. package/dist_ts_shell/elements/wcc-contextmenu.js +18 -3
  22. package/dist_ts_shell/elements/wcc-preview-frame.d.ts +6 -0
  23. package/dist_ts_shell/elements/wcc-preview-frame.js +50 -2
  24. package/dist_ts_shell/elements/wcc-shell.d.ts +15 -0
  25. package/dist_ts_shell/elements/wcc-shell.js +45 -3
  26. package/dist_ts_shell/plugins.d.ts +2 -1
  27. package/dist_ts_shell/plugins.js +3 -2
  28. package/dist_ts_web/00_commitinfo_data.js +1 -1
  29. package/package.json +5 -2
  30. package/readme.md +6 -2
  31. package/ts/00_commitinfo_data.ts +1 -1
  32. package/ts/classes.bundlestatus.ts +123 -0
  33. package/ts/classes.devapi.ts +9 -0
  34. package/ts/classes.devserver.ts +127 -47
  35. package/ts/cli.ts +4 -1
  36. package/ts/index.ts +0 -2
  37. package/ts_interfaces/bridge.ts +95 -0
  38. package/ts_interfaces/catalog.ts +37 -0
  39. package/ts_interfaces/index.ts +4 -0
  40. package/ts_interfaces/plugins.ts +3 -0
  41. package/ts_interfaces/requests.ts +65 -0
  42. package/ts_interfaces/standard.ts +103 -0
  43. package/ts_shared/index.ts +1 -0
  44. package/ts_shared/plugins.ts +3 -0
  45. package/ts_shared/previewroute.ts +58 -0
  46. package/ts_shell/bundlestatus.ts +68 -0
  47. package/ts_shell/elements/wcc-contextmenu.ts +306 -0
  48. package/ts_shell/elements/wcc-preview-frame.ts +167 -0
  49. package/ts_shell/elements/wcc-properties.ts +998 -0
  50. package/ts_shell/elements/wcc-record-button.ts +108 -0
  51. package/ts_shell/elements/wcc-recording-panel.ts +1005 -0
  52. package/ts_shell/elements/wcc-shell.ts +485 -0
  53. package/ts_shell/elements/wcc-sidebar.ts +1419 -0
  54. package/ts_shell/index.html +21 -0
  55. package/ts_shell/index.ts +4 -0
  56. package/ts_shell/plugins.ts +12 -0
  57. package/ts_shell/previewconnection.ts +103 -0
  58. package/ts_shell/services/recorder.service.ts +451 -0
  59. package/ts_shell/types/dom-mediacapture-stub/index.d.ts +12 -0
  60. package/ts_shell/types/dom-mediacapture-stub/package.json +6 -0
  61. package/ts_shell/types/dom-webcodecs-stub/index.d.ts +2 -0
  62. package/ts_shell/types/dom-webcodecs-stub/package.json +6 -0
  63. package/ts_web/00_commitinfo_data.ts +1 -1
@@ -0,0 +1,68 @@
1
+ import * as plugins from './plugins.js';
2
+
3
+ /** The first and the longest pause before a failed status request is sent again. */
4
+ const retryDelayMinMs = 1_000;
5
+ const retryDelayMaxMs = 10_000;
6
+
7
+ /** Resolves after `msArg`, or at once when the signal aborts. */
8
+ const pause = (msArg: number, signalArg: AbortSignal): Promise<void> =>
9
+ new Promise((resolve) => {
10
+ const done = () => {
11
+ clearTimeout(timer);
12
+ signalArg.removeEventListener('abort', done);
13
+ resolve();
14
+ };
15
+ const timer = setTimeout(done, msArg);
16
+ signalArg.addEventListener('abort', done, { once: true });
17
+ });
18
+
19
+ /**
20
+ * Follows the dev server's bundle status: one `getBundleStatus` request at a time, each answered
21
+ * when the status changes. A failed request (the server restarting, for example) is sent again
22
+ * after a growing pause. `dispose()` aborts the request in flight and ends the loop.
23
+ */
24
+ export class WccBundleStatusClient {
25
+ private abortController: AbortController | null = null;
26
+
27
+ constructor(
28
+ private readonly apiPath: string,
29
+ private readonly onStatus: (snapshotArg: plugins.interfaces.IWccBundleStatusSnapshot) => void,
30
+ ) {}
31
+
32
+ public start() {
33
+ if (this.abortController) {
34
+ return;
35
+ }
36
+ const abortController = new AbortController();
37
+ this.abortController = abortController;
38
+ void this.follow(abortController.signal);
39
+ }
40
+
41
+ public dispose() {
42
+ this.abortController?.abort();
43
+ this.abortController = null;
44
+ }
45
+
46
+ private async follow(signalArg: AbortSignal) {
47
+ const request = new plugins.typedrequest.TypedRequest<plugins.interfaces.IReq_GetBundleStatus>(this.apiPath, 'getBundleStatus');
48
+ let revision: number | undefined;
49
+ let retryDelay = retryDelayMinMs;
50
+ while (!signalArg.aborted) {
51
+ try {
52
+ const snapshot = await request.fire({ afterRevision: revision }, { abortSignal: signalArg });
53
+ retryDelay = retryDelayMinMs;
54
+ if (!signalArg.aborted && snapshot.revision !== revision) {
55
+ revision = snapshot.revision;
56
+ this.onStatus(snapshot);
57
+ }
58
+ } catch (error) {
59
+ if (signalArg.aborted) {
60
+ return;
61
+ }
62
+ console.warn('wcctools: reading the bundle status failed; retrying', error);
63
+ await pause(retryDelay, signalArg);
64
+ retryDelay = Math.min(retryDelay * 2, retryDelayMaxMs);
65
+ }
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,306 @@
1
+ import { DeesElement, property, html, customElement, type TemplateResult, state, css, cssManager } from '@design.estate/dees-element';
2
+
3
+ export interface IContextMenuItem {
4
+ name: string;
5
+ iconName?: string;
6
+ action: () => void | Promise<void>;
7
+ disabled?: boolean;
8
+ }
9
+
10
+ export interface IContextMenuOptions {
11
+ /** The element the menu belongs to: focus returns to it when the menu closes */
12
+ ownerElement?: HTMLElement;
13
+ }
14
+
15
+ @customElement('wcc-contextmenu')
16
+ export class WccContextmenu extends DeesElement {
17
+ /**
18
+ * Show a context menu at the event's position. The menu takes focus (its first item),
19
+ * is operated with the arrow keys, Home/End, Enter/Space and Escape/Tab, and returns
20
+ * focus to `ownerElement` when it closes from the keyboard or after an action.
21
+ */
22
+ public static async show(
23
+ event: MouseEvent,
24
+ menuItems: IContextMenuItem[],
25
+ options: IContextMenuOptions = {}
26
+ ): Promise<void> {
27
+ event.preventDefault();
28
+ event.stopPropagation();
29
+
30
+ // Remove any existing context menu
31
+ const existing = document.querySelector('wcc-contextmenu');
32
+ if (existing) {
33
+ existing.remove();
34
+ }
35
+
36
+ const menu = new WccContextmenu();
37
+ menu.menuItems = menuItems;
38
+ menu.x = event.clientX;
39
+ menu.y = event.clientY;
40
+ menu.ownerElement = options.ownerElement ?? null;
41
+
42
+ document.body.appendChild(menu);
43
+
44
+ // Wait for render then adjust position if needed
45
+ await menu.updateComplete;
46
+ menu.adjustPosition();
47
+ menu.focusItem(0);
48
+ }
49
+
50
+ /** Focus returns here when the menu closes from the keyboard or after an action */
51
+ private ownerElement: HTMLElement | null = null;
52
+ private closing = false;
53
+
54
+ @property({ type: Array })
55
+ accessor menuItems: IContextMenuItem[] = [];
56
+
57
+ @property({ type: Number })
58
+ accessor x: number = 0;
59
+
60
+ @property({ type: Number })
61
+ accessor y: number = 0;
62
+
63
+ @state()
64
+ accessor visible: boolean = false;
65
+
66
+ private boundHandleOutsideClick = this.handleOutsideClick.bind(this);
67
+ private boundHandleKeydown = this.handleKeydown.bind(this);
68
+ /** The pending frame that adds the document listeners; cancelled on removal */
69
+ private listenerFrame: number | null = null;
70
+
71
+ public static styles = [
72
+ css`
73
+ :host {
74
+ position: fixed;
75
+ z-index: 10000;
76
+ opacity: 0;
77
+ transform: scale(0.95) translateY(-5px);
78
+ transition: opacity 0.15s ease, transform 0.15s ease;
79
+ pointer-events: none;
80
+ }
81
+
82
+ :host(.visible) {
83
+ opacity: 1;
84
+ transform: scale(1) translateY(0);
85
+ pointer-events: auto;
86
+ }
87
+
88
+ .menu {
89
+ min-width: 160px;
90
+ background: #0f0f0f;
91
+ border: 1px solid rgba(255, 255, 255, 0.1);
92
+ border-radius: 6px;
93
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
94
+ padding: 4px 0;
95
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
96
+ font-size: 12px;
97
+ }
98
+
99
+ .menu-item {
100
+ display: flex;
101
+ align-items: center;
102
+ gap: 8px;
103
+ padding: 8px 12px;
104
+ color: #ccc;
105
+ cursor: pointer;
106
+ transition: background 0.1s ease;
107
+ user-select: none;
108
+ }
109
+
110
+ .menu {
111
+ outline: none;
112
+ }
113
+
114
+ .menu-item:hover,
115
+ .menu-item:focus {
116
+ background: rgba(59, 130, 246, 0.15);
117
+ color: #fff;
118
+ outline: none;
119
+ }
120
+
121
+ .menu-item:focus-visible {
122
+ box-shadow: inset 0 0 0 2px #3b82f6;
123
+ }
124
+
125
+ .menu-item.disabled {
126
+ opacity: 0.4;
127
+ cursor: not-allowed;
128
+ pointer-events: none;
129
+ }
130
+
131
+ .menu-item .icon {
132
+ font-family: 'Material Symbols Outlined';
133
+ font-size: 16px;
134
+ font-weight: normal;
135
+ font-style: normal;
136
+ line-height: 1;
137
+ letter-spacing: normal;
138
+ text-transform: none;
139
+ white-space: nowrap;
140
+ word-wrap: normal;
141
+ direction: ltr;
142
+ font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 24;
143
+ opacity: 0.7;
144
+ }
145
+
146
+ .menu-item:hover .icon {
147
+ opacity: 1;
148
+ }
149
+
150
+ .menu-item .label {
151
+ flex: 1;
152
+ }
153
+ `
154
+ ];
155
+
156
+ public render(): TemplateResult {
157
+ return html`
158
+ <div class="menu" role="menu" tabindex="-1" @keydown=${this.handleMenuKeydown}>
159
+ ${this.menuItems.map(item => html`
160
+ <div
161
+ class="menu-item ${item.disabled ? 'disabled' : ''}"
162
+ role="menuitem"
163
+ tabindex="-1"
164
+ aria-disabled=${item.disabled ? 'true' : 'false'}
165
+ @click=${() => this.handleItemClick(item)}
166
+ >
167
+ ${item.iconName ? html`<span class="icon" aria-hidden="true">${item.iconName}</span>` : null}
168
+ <span class="label">${item.name}</span>
169
+ </div>
170
+ `)}
171
+ </div>
172
+ `;
173
+ }
174
+
175
+ async connectedCallback() {
176
+ await super.connectedCallback();
177
+ // Removed again while connecting: there is nothing to listen for
178
+ if (!this.isConnected) return;
179
+ // Delay adding listeners to avoid immediate close
180
+ this.cancelListenerFrame();
181
+ this.listenerFrame = requestAnimationFrame(() => {
182
+ this.listenerFrame = null;
183
+ document.addEventListener('click', this.boundHandleOutsideClick);
184
+ document.addEventListener('contextmenu', this.boundHandleOutsideClick);
185
+ document.addEventListener('keydown', this.boundHandleKeydown);
186
+ this.classList.add('visible');
187
+ });
188
+ }
189
+
190
+ async disconnectedCallback() {
191
+ // A frame still pending would add the listeners after removal
192
+ this.cancelListenerFrame();
193
+ document.removeEventListener('click', this.boundHandleOutsideClick);
194
+ document.removeEventListener('contextmenu', this.boundHandleOutsideClick);
195
+ document.removeEventListener('keydown', this.boundHandleKeydown);
196
+ await super.disconnectedCallback();
197
+ }
198
+
199
+ private cancelListenerFrame() {
200
+ if (this.listenerFrame !== null) {
201
+ cancelAnimationFrame(this.listenerFrame);
202
+ this.listenerFrame = null;
203
+ }
204
+ }
205
+
206
+ private adjustPosition() {
207
+ const rect = this.getBoundingClientRect();
208
+ const windowWidth = window.innerWidth;
209
+ const windowHeight = window.innerHeight;
210
+
211
+ let x = this.x;
212
+ let y = this.y;
213
+
214
+ // Adjust if menu goes off right edge
215
+ if (x + rect.width > windowWidth - 10) {
216
+ x = windowWidth - rect.width - 10;
217
+ }
218
+
219
+ // Adjust if menu goes off bottom edge
220
+ if (y + rect.height > windowHeight - 10) {
221
+ y = windowHeight - rect.height - 10;
222
+ }
223
+
224
+ // Ensure not off left or top
225
+ if (x < 10) x = 10;
226
+ if (y < 10) y = 10;
227
+
228
+ this.style.left = `${x}px`;
229
+ this.style.top = `${y}px`;
230
+ }
231
+
232
+ private handleOutsideClick(e: Event) {
233
+ const path = e.composedPath();
234
+ if (!path.includes(this)) {
235
+ this.close();
236
+ }
237
+ }
238
+
239
+ private handleKeydown(e: KeyboardEvent) {
240
+ if (e.key === 'Escape') {
241
+ this.close(true);
242
+ }
243
+ }
244
+
245
+ private getEnabledItems(): HTMLElement[] {
246
+ return Array.from(this.shadowRoot?.querySelectorAll<HTMLElement>('.menu-item:not(.disabled)') ?? []);
247
+ }
248
+
249
+ /** Focus an enabled item by index (wrapping); the menu itself if there is none */
250
+ private focusItem(indexArg: number) {
251
+ const items = this.getEnabledItems();
252
+ if (items.length === 0) {
253
+ (this.shadowRoot?.querySelector('.menu') as HTMLElement | null)?.focus({ preventScroll: true });
254
+ return;
255
+ }
256
+ const index = ((indexArg % items.length) + items.length) % items.length;
257
+ items[index].focus({ preventScroll: true });
258
+ }
259
+
260
+ private handleMenuKeydown = (e: KeyboardEvent) => {
261
+ const items = this.getEnabledItems();
262
+ const current = items.indexOf(this.shadowRoot?.activeElement as HTMLElement);
263
+ switch (e.key) {
264
+ case 'ArrowDown':
265
+ this.focusItem(current + 1);
266
+ break;
267
+ case 'ArrowUp':
268
+ this.focusItem(current < 0 ? -1 : current - 1);
269
+ break;
270
+ case 'Home':
271
+ this.focusItem(0);
272
+ break;
273
+ case 'End':
274
+ this.focusItem(-1);
275
+ break;
276
+ case 'Enter':
277
+ case ' ':
278
+ if (current >= 0) items[current].click();
279
+ break;
280
+ case 'Escape':
281
+ case 'Tab':
282
+ this.close(true);
283
+ break;
284
+ default:
285
+ return;
286
+ }
287
+ e.preventDefault();
288
+ e.stopPropagation();
289
+ };
290
+
291
+ private async handleItemClick(item: IContextMenuItem) {
292
+ if (item.disabled) return;
293
+ await item.action();
294
+ this.close(true);
295
+ }
296
+
297
+ private close(returnFocus = false) {
298
+ if (this.closing) return;
299
+ this.closing = true;
300
+ if (returnFocus && this.ownerElement?.isConnected) {
301
+ this.ownerElement.focus({ preventScroll: true });
302
+ }
303
+ this.classList.remove('visible');
304
+ setTimeout(() => this.remove(), 150);
305
+ }
306
+ }
@@ -0,0 +1,167 @@
1
+ import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager } from '@design.estate/dees-element';
2
+
3
+ import * as plugins from '../plugins.js';
4
+
5
+ export type TWccViewport = 'native' | 'desktop' | 'tablet' | 'phablet' | 'phone';
6
+
7
+ declare global {
8
+ interface HTMLElementTagNameMap {
9
+ 'wcc-preview-frame': WccPreviewFrame;
10
+ }
11
+ }
12
+
13
+ /** The width of the preview document per viewport; desktop and native fill the frame. */
14
+ const getViewportWidth = (viewportArg: TWccViewport): string => {
15
+ switch (viewportArg) {
16
+ case 'tablet':
17
+ return `${plugins.deesDomtools.breakpoints.tablet}px`;
18
+ case 'phablet':
19
+ return `${plugins.deesDomtools.breakpoints.phablet}px`;
20
+ case 'phone':
21
+ return `${plugins.deesDomtools.breakpoints.phone}px`;
22
+ default:
23
+ return '100%';
24
+ }
25
+ };
26
+
27
+ /**
28
+ * Hosts the preview document in an iframe as wide as the selected viewport, so the preview's
29
+ * width media queries respond to it.
30
+ */
31
+ @customElement('wcc-preview-frame')
32
+ export class WccPreviewFrame extends DeesElement {
33
+ @property()
34
+ accessor viewport: TWccViewport = 'desktop';
35
+
36
+ /** The preview document's address. Set once: changing it navigates the preview. */
37
+ @property()
38
+ accessor previewSrc: string = '';
39
+
40
+ @property({ type: Boolean })
41
+ accessor advancedEditorOpen: boolean = false;
42
+
43
+ @property({ type: Boolean })
44
+ accessor isNative: boolean = false;
45
+
46
+ @property({ type: Number })
47
+ accessor sidebarWidth: number = 200;
48
+
49
+ @property({ type: Boolean })
50
+ accessor isResizing: boolean = false;
51
+
52
+ /**
53
+ * Catalog bundles whose latest run failed: shown over the preview, which shows an outdated
54
+ * bundle or none, until a run finishes and the preview reloads.
55
+ */
56
+ @property({ attribute: false })
57
+ accessor bundleFailures: plugins.interfaces.IWccBundleStatus[] = [];
58
+
59
+ public static styles = [
60
+ css`
61
+ :host {
62
+ border: 10px solid rgba(255, 174, 175, 1);
63
+ box-sizing: border-box;
64
+ position: absolute;
65
+ background: ${cssManager.bdTheme('#333', '#000')};
66
+ right: 0px;
67
+ overflow-x: auto;
68
+ overflow-y: hidden;
69
+ overscroll-behavior: contain;
70
+ }
71
+
72
+ .preview {
73
+ display: block;
74
+ box-sizing: border-box;
75
+ height: 100%;
76
+ margin: 0 auto;
77
+ border: 0;
78
+ background: transparent;
79
+ }
80
+
81
+ .bundle-failure {
82
+ position: absolute;
83
+ inset: 0;
84
+ box-sizing: border-box;
85
+ overflow: auto;
86
+ padding: 24px;
87
+ background: rgba(20, 0, 0, 0.92);
88
+ color: #ffd7d7;
89
+ font-family: 'Geist Sans', 'Inter', sans-serif;
90
+ font-size: 14px;
91
+ }
92
+
93
+ .bundle-failure h2 {
94
+ margin: 0 0 8px;
95
+ font-size: 16px;
96
+ font-weight: 600;
97
+ color: #ff8a8a;
98
+ }
99
+
100
+ .bundle-failure pre {
101
+ margin: 0 0 24px;
102
+ white-space: pre-wrap;
103
+ overflow-wrap: anywhere;
104
+ font-family: 'Geist Mono', 'Intel One Mono', monospace;
105
+ font-size: 13px;
106
+ line-height: 1.5;
107
+ }
108
+ `,
109
+ ];
110
+
111
+ public get iframe(): HTMLIFrameElement | null {
112
+ return this.shadowRoot?.querySelector('iframe') ?? null;
113
+ }
114
+
115
+ public render(): TemplateResult {
116
+ return html`
117
+ <style>
118
+ :host {
119
+ ${this.isNative ? `
120
+ border: 0px solid rgba(255, 174, 175, 0) !important;
121
+ left: 0px !important;
122
+ right: 0px !important;
123
+ top: 0px !important;
124
+ bottom: 0px !important;
125
+ ` : `
126
+ bottom: ${this.advancedEditorOpen ? '400px' : '100px'};
127
+ border: 10px solid rgba(255, 174, 175, 1);
128
+ left: ${this.sidebarWidth}px;
129
+ top: 0;
130
+ `}
131
+ transition: ${this.isResizing ? 'none' : 'all 0.3s ease'};
132
+ padding: 0;
133
+ }
134
+
135
+ .preview {
136
+ width: ${this.isNative ? '100%' : getViewportWidth(this.viewport)};
137
+ pointer-events: ${this.isResizing ? 'none' : 'auto'};
138
+ ${!this.isNative && this.viewport !== 'desktop'
139
+ ? 'border-right: 1px dotted #444; border-left: 1px dotted #444;'
140
+ : ''
141
+ }
142
+ }
143
+
144
+ ${!this.isNative ? `
145
+ @container wccShell (max-width: 600px) {
146
+ :host {
147
+ left: 0;
148
+ top: 120px;
149
+ bottom: ${this.advancedEditorOpen ? '460px' : '160px'};
150
+ }
151
+ }
152
+ ` : ''}
153
+ </style>
154
+ ${this.previewSrc ? html`
155
+ <iframe class="preview" title="Preview" src=${this.previewSrc}></iframe>
156
+ ` : null}
157
+ ${this.bundleFailures.length > 0 ? html`
158
+ <div class="bundle-failure" role="alert">
159
+ ${this.bundleFailures.map((bundleArg) => html`
160
+ <h2>bundle failed: ${bundleArg.name}</h2>
161
+ <pre>${bundleArg.errorMessage}</pre>
162
+ `)}
163
+ </div>
164
+ ` : null}
165
+ `;
166
+ }
167
+ }