@design.estate/wcctools 6.0.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 (202) hide show
  1. package/changelog.md +33 -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 +12 -3
  15. package/dist_ts/index.d.ts +0 -2
  16. package/dist_ts/index.js +1 -3
  17. package/dist_ts/plugins.d.ts +7 -2
  18. package/dist_ts/plugins.js +9 -3
  19. package/dist_ts/standards/baselines.d.ts +27 -0
  20. package/dist_ts/standards/baselines.js +83 -0
  21. package/dist_ts/standards/checker.d.ts +21 -0
  22. package/dist_ts/standards/checker.js +88 -0
  23. package/dist_ts/standards/classes.gitrepo.d.ts +13 -0
  24. package/dist_ts/standards/classes.gitrepo.js +43 -0
  25. package/dist_ts/standards/classes.sourceedits.d.ts +15 -0
  26. package/dist_ts/standards/classes.sourceedits.js +54 -0
  27. package/dist_ts/standards/classes.standardfixer.d.ts +40 -0
  28. package/dist_ts/standards/classes.standardfixer.js +210 -0
  29. package/dist_ts/standards/classes.standardproject.d.ts +80 -0
  30. package/dist_ts/standards/classes.standardproject.js +195 -0
  31. package/dist_ts/standards/classes.standardtree.d.ts +56 -0
  32. package/dist_ts/standards/classes.standardtree.js +207 -0
  33. package/dist_ts/standards/cli.d.ts +7 -0
  34. package/dist_ts/standards/cli.js +89 -0
  35. package/dist_ts/standards/config.d.ts +33 -0
  36. package/dist_ts/standards/config.js +139 -0
  37. package/dist_ts/standards/errors.d.ts +9 -0
  38. package/dist_ts/standards/errors.js +17 -0
  39. package/dist_ts/standards/helpers.ast.d.ts +56 -0
  40. package/dist_ts/standards/helpers.ast.js +172 -0
  41. package/dist_ts/standards/helpers.paths.d.ts +36 -0
  42. package/dist_ts/standards/helpers.paths.js +86 -0
  43. package/dist_ts/standards/index.d.ts +14 -0
  44. package/dist_ts/standards/index.js +15 -0
  45. package/dist_ts/standards/output.d.ts +7 -0
  46. package/dist_ts/standards/output.js +57 -0
  47. package/dist_ts/standards/ratchet.d.ts +15 -0
  48. package/dist_ts/standards/ratchet.js +45 -0
  49. package/dist_ts/standards/rule.d.ts +50 -0
  50. package/dist_ts/standards/rule.js +9 -0
  51. package/dist_ts/standards/rules/barrel.element-index.d.ts +7 -0
  52. package/dist_ts/standards/rules/barrel.element-index.js +108 -0
  53. package/dist_ts/standards/rules/barrel.no-demo-export.d.ts +3 -0
  54. package/dist_ts/standards/rules/barrel.no-demo-export.js +29 -0
  55. package/dist_ts/standards/rules/demo.fixture-not-exported.d.ts +3 -0
  56. package/dist_ts/standards/rules/demo.fixture-not-exported.js +29 -0
  57. package/dist_ts/standards/rules/demo.groups.d.ts +6 -0
  58. package/dist_ts/standards/rules/demo.groups.js +72 -0
  59. package/dist_ts/standards/rules/demo.required.d.ts +3 -0
  60. package/dist_ts/standards/rules/demo.required.js +48 -0
  61. package/dist_ts/standards/rules/demo.separate-file.d.ts +3 -0
  62. package/dist_ts/standards/rules/demo.separate-file.js +38 -0
  63. package/dist_ts/standards/rules/demo.shape.d.ts +3 -0
  64. package/dist_ts/standards/rules/demo.shape.js +38 -0
  65. package/dist_ts/standards/rules/docs.component-index.d.ts +8 -0
  66. package/dist_ts/standards/rules/docs.component-index.js +84 -0
  67. package/dist_ts/standards/rules/element.exported.d.ts +3 -0
  68. package/dist_ts/standards/rules/element.exported.js +33 -0
  69. package/dist_ts/standards/rules/element.literal-tag.d.ts +3 -0
  70. package/dist_ts/standards/rules/element.literal-tag.js +30 -0
  71. package/dist_ts/standards/rules/element.reachable.d.ts +3 -0
  72. package/dist_ts/standards/rules/element.reachable.js +48 -0
  73. package/dist_ts/standards/rules/element.tag-map.d.ts +3 -0
  74. package/dist_ts/standards/rules/element.tag-map.js +83 -0
  75. package/dist_ts/standards/rules/i18n.literals.d.ts +3 -0
  76. package/dist_ts/standards/rules/i18n.literals.js +52 -0
  77. package/dist_ts/standards/rules/index.d.ts +12 -0
  78. package/dist_ts/standards/rules/index.js +83 -0
  79. package/dist_ts/standards/rules/layout.element-folder.d.ts +3 -0
  80. package/dist_ts/standards/rules/layout.element-folder.js +47 -0
  81. package/dist_ts/standards/rules/layout.entry-file-name.d.ts +3 -0
  82. package/dist_ts/standards/rules/layout.entry-file-name.js +71 -0
  83. package/dist_ts/standards/rules/layout.entry.d.ts +3 -0
  84. package/dist_ts/standards/rules/layout.entry.js +38 -0
  85. package/dist_ts/standards/rules/layout.group-folder.d.ts +3 -0
  86. package/dist_ts/standards/rules/layout.group-folder.js +22 -0
  87. package/dist_ts/standards/rules/layout.shared-00.d.ts +3 -0
  88. package/dist_ts/standards/rules/layout.shared-00.js +36 -0
  89. package/dist_ts/standards/rules/naming.class-matches-tag.d.ts +3 -0
  90. package/dist_ts/standards/rules/naming.class-matches-tag.js +26 -0
  91. package/dist_ts/standards/rules/naming.lowercase-files.d.ts +3 -0
  92. package/dist_ts/standards/rules/naming.lowercase-files.js +26 -0
  93. package/dist_ts/standards/rules/naming.tag-prefix.d.ts +3 -0
  94. package/dist_ts/standards/rules/naming.tag-prefix.js +23 -0
  95. package/dist_ts/standards/rules/props.accessor.d.ts +3 -0
  96. package/dist_ts/standards/rules/props.accessor.js +35 -0
  97. package/dist_ts/standards/rules/props.object-no-attribute.d.ts +3 -0
  98. package/dist_ts/standards/rules/props.object-no-attribute.js +42 -0
  99. package/dist_ts/standards/rules/props.reflect-primitive.d.ts +5 -0
  100. package/dist_ts/standards/rules/props.reflect-primitive.js +41 -0
  101. package/dist_ts/standards/rules/props.typed.d.ts +6 -0
  102. package/dist_ts/standards/rules/props.typed.js +64 -0
  103. package/dist_ts/standards/rules/shared.d.ts +38 -0
  104. package/dist_ts/standards/rules/shared.js +103 -0
  105. package/dist_ts/standards/rules/style.bdtheme.d.ts +5 -0
  106. package/dist_ts/standards/rules/style.bdtheme.js +28 -0
  107. package/dist_ts_interfaces/index.d.ts +1 -0
  108. package/dist_ts_interfaces/index.js +2 -1
  109. package/dist_ts_interfaces/requests.d.ts +35 -0
  110. package/dist_ts_interfaces/standard.d.ts +93 -0
  111. package/dist_ts_interfaces/standard.js +6 -0
  112. package/dist_ts_shell/bundlestatus.d.ts +15 -0
  113. package/dist_ts_shell/bundlestatus.js +64 -0
  114. package/dist_ts_shell/elements/wcc-contextmenu.d.ts +3 -0
  115. package/dist_ts_shell/elements/wcc-contextmenu.js +18 -3
  116. package/dist_ts_shell/elements/wcc-preview-frame.d.ts +6 -0
  117. package/dist_ts_shell/elements/wcc-preview-frame.js +50 -2
  118. package/dist_ts_shell/elements/wcc-shell.d.ts +15 -0
  119. package/dist_ts_shell/elements/wcc-shell.js +45 -3
  120. package/dist_ts_shell/plugins.d.ts +2 -1
  121. package/dist_ts_shell/plugins.js +3 -2
  122. package/dist_ts_web/00_commitinfo_data.js +1 -1
  123. package/package.json +8 -3
  124. package/readme.md +40 -3
  125. package/ts/00_commitinfo_data.ts +1 -1
  126. package/ts/classes.bundlestatus.ts +123 -0
  127. package/ts/classes.devapi.ts +9 -0
  128. package/ts/classes.devserver.ts +127 -47
  129. package/ts/cli.ts +12 -2
  130. package/ts/index.ts +0 -2
  131. package/ts/plugins.ts +10 -2
  132. package/ts/standards/baselines.ts +100 -0
  133. package/ts/standards/checker.ts +114 -0
  134. package/ts/standards/classes.gitrepo.ts +48 -0
  135. package/ts/standards/classes.sourceedits.ts +58 -0
  136. package/ts/standards/classes.standardfixer.ts +234 -0
  137. package/ts/standards/classes.standardproject.ts +250 -0
  138. package/ts/standards/classes.standardtree.ts +220 -0
  139. package/ts/standards/cli.ts +86 -0
  140. package/ts/standards/config.ts +160 -0
  141. package/ts/standards/errors.ts +18 -0
  142. package/ts/standards/helpers.ast.ts +208 -0
  143. package/ts/standards/helpers.paths.ts +119 -0
  144. package/ts/standards/index.ts +22 -0
  145. package/ts/standards/output.ts +65 -0
  146. package/ts/standards/ratchet.ts +63 -0
  147. package/ts/standards/rule.ts +58 -0
  148. package/ts/standards/rules/barrel.element-index.ts +112 -0
  149. package/ts/standards/rules/barrel.no-demo-export.ts +30 -0
  150. package/ts/standards/rules/demo.fixture-not-exported.ts +30 -0
  151. package/ts/standards/rules/demo.groups.ts +74 -0
  152. package/ts/standards/rules/demo.required.ts +49 -0
  153. package/ts/standards/rules/demo.separate-file.ts +40 -0
  154. package/ts/standards/rules/demo.shape.ts +42 -0
  155. package/ts/standards/rules/docs.component-index.ts +88 -0
  156. package/ts/standards/rules/element.exported.ts +35 -0
  157. package/ts/standards/rules/element.literal-tag.ts +30 -0
  158. package/ts/standards/rules/element.reachable.ts +49 -0
  159. package/ts/standards/rules/element.tag-map.ts +87 -0
  160. package/ts/standards/rules/i18n.literals.ts +58 -0
  161. package/ts/standards/rules/index.ts +87 -0
  162. package/ts/standards/rules/layout.element-folder.ts +49 -0
  163. package/ts/standards/rules/layout.entry-file-name.ts +73 -0
  164. package/ts/standards/rules/layout.entry.ts +40 -0
  165. package/ts/standards/rules/layout.group-folder.ts +23 -0
  166. package/ts/standards/rules/layout.shared-00.ts +37 -0
  167. package/ts/standards/rules/naming.class-matches-tag.ts +27 -0
  168. package/ts/standards/rules/naming.lowercase-files.ts +27 -0
  169. package/ts/standards/rules/naming.tag-prefix.ts +24 -0
  170. package/ts/standards/rules/props.accessor.ts +37 -0
  171. package/ts/standards/rules/props.object-no-attribute.ts +45 -0
  172. package/ts/standards/rules/props.reflect-primitive.ts +44 -0
  173. package/ts/standards/rules/props.typed.ts +70 -0
  174. package/ts/standards/rules/shared.ts +127 -0
  175. package/ts/standards/rules/style.bdtheme.ts +31 -0
  176. package/ts_interfaces/bridge.ts +95 -0
  177. package/ts_interfaces/catalog.ts +37 -0
  178. package/ts_interfaces/index.ts +4 -0
  179. package/ts_interfaces/plugins.ts +3 -0
  180. package/ts_interfaces/requests.ts +65 -0
  181. package/ts_interfaces/standard.ts +103 -0
  182. package/ts_shared/index.ts +1 -0
  183. package/ts_shared/plugins.ts +3 -0
  184. package/ts_shared/previewroute.ts +58 -0
  185. package/ts_shell/bundlestatus.ts +68 -0
  186. package/ts_shell/elements/wcc-contextmenu.ts +306 -0
  187. package/ts_shell/elements/wcc-preview-frame.ts +167 -0
  188. package/ts_shell/elements/wcc-properties.ts +998 -0
  189. package/ts_shell/elements/wcc-record-button.ts +108 -0
  190. package/ts_shell/elements/wcc-recording-panel.ts +1005 -0
  191. package/ts_shell/elements/wcc-shell.ts +485 -0
  192. package/ts_shell/elements/wcc-sidebar.ts +1419 -0
  193. package/ts_shell/index.html +21 -0
  194. package/ts_shell/index.ts +4 -0
  195. package/ts_shell/plugins.ts +12 -0
  196. package/ts_shell/previewconnection.ts +103 -0
  197. package/ts_shell/services/recorder.service.ts +451 -0
  198. package/ts_shell/types/dom-mediacapture-stub/index.d.ts +12 -0
  199. package/ts_shell/types/dom-mediacapture-stub/package.json +6 -0
  200. package/ts_shell/types/dom-webcodecs-stub/index.d.ts +2 -0
  201. package/ts_shell/types/dom-webcodecs-stub/package.json +6 -0
  202. package/ts_web/00_commitinfo_data.ts +1 -1
@@ -0,0 +1,485 @@
1
+ import { DeesElement, property, state, html, customElement, type TemplateResult } from '@design.estate/dees-element';
2
+ import * as plugins from '../plugins.js';
3
+ import { WccPreviewConnection } from '../previewconnection.js';
4
+ import { WccBundleStatusClient } from '../bundlestatus.js';
5
+ import type { TWccViewport, WccPreviewFrame } from './wcc-preview-frame.js';
6
+ import type { WccSidebar } from './wcc-sidebar.js';
7
+ import './wcc-preview-frame.js';
8
+ import './wcc-sidebar.js';
9
+ import './wcc-properties.js';
10
+
11
+ /** Parameters of a `/wcctools-route/...` URL. */
12
+ export interface IWccShellRouteParams {
13
+ sectionName: string;
14
+ itemName: string;
15
+ demoIndex?: string;
16
+ viewport: string;
17
+ theme: string;
18
+ }
19
+
20
+ const viewports: TWccViewport[] = ['native', 'desktop', 'tablet', 'phablet', 'phone'];
21
+ const defaultSidebarWidth = 200;
22
+
23
+ const selectionKey = (selectionArg: plugins.interfaces.IWccSelection | null): string | null =>
24
+ selectionArg ? `${selectionArg.sectionName}::${selectionArg.itemName}::${selectionArg.demoIndex}` : null;
25
+
26
+ declare global {
27
+ interface HTMLElementTagNameMap {
28
+ 'wcc-shell': WccShell;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * The wcctools UI: catalog sidebar, properties and preview controls, and the preview frame
34
+ * that shows the catalog's own document. The shell owns the `/wcctools-route/...` URL and
35
+ * drives the preview through its bridge; a reloaded preview reconnects without the shell
36
+ * reloading.
37
+ */
38
+ @customElement('wcc-shell')
39
+ export class WccShell extends DeesElement {
40
+ /** The path the server serves the preview document on. */
41
+ @property({ attribute: 'preview-path' })
42
+ accessor previewPath: string = plugins.shared.previewRoutePath;
43
+
44
+ /**
45
+ * The dev server's typed API. With it, the shell follows the bundle status and shows a failed
46
+ * catalog bundle over the preview; without it (the shell outside `wcctools dev`), it does not.
47
+ * Read when the shell connects.
48
+ */
49
+ @property({ attribute: 'api-path' })
50
+ accessor apiPath: string = '';
51
+
52
+ @property({ attribute: false })
53
+ accessor selection: plugins.interfaces.IWccSelection | null = null;
54
+
55
+ @property()
56
+ accessor selectedViewport: TWccViewport = 'desktop';
57
+
58
+ @property()
59
+ accessor selectedTheme: plugins.interfaces.TWccTheme = 'dark';
60
+
61
+ @property()
62
+ accessor searchQuery: string = '';
63
+
64
+ // Pinned items as Set of "sectionName::itemName"
65
+ @property({ attribute: false })
66
+ accessor pinnedItems: Set<string> = new Set();
67
+
68
+ @property({ type: Number })
69
+ accessor sidebarWidth: number = defaultSidebarWidth;
70
+
71
+ /** The catalog as the connected preview describes it; kept while the preview reloads. */
72
+ @state()
73
+ accessor manifest: plugins.interfaces.IWccCatalogManifest | null = null;
74
+
75
+ /** The connected preview's bridge; null while the preview loads. */
76
+ @state()
77
+ accessor bridge: plugins.interfaces.IWccPreviewBridge | null = null;
78
+
79
+ /** Counts the renders of the preview, so the properties panel reads them. */
80
+ @state()
81
+ accessor renderCount: number = 0;
82
+
83
+ @state()
84
+ accessor previewSrc: string = '';
85
+
86
+ @state()
87
+ accessor advancedEditorOpen: boolean = false;
88
+
89
+ @state()
90
+ accessor isResizing: boolean = false;
91
+
92
+ @state()
93
+ accessor captureTarget: HTMLElement | null = null;
94
+
95
+ /** The catalog bundles whose latest completed run failed, as the dev server reports them. */
96
+ @state()
97
+ accessor bundleFailures: plugins.interfaces.IWccBundleStatus[] = [];
98
+
99
+ public get isNative(): boolean {
100
+ return this.selectedViewport === 'native';
101
+ }
102
+
103
+ private frameScrollY: number = 0;
104
+ private sidebarScrollY: number = 0;
105
+ private scrollPositionsRestored: boolean = false;
106
+ /** The selection the preview shows, as far as the shell asked it to. */
107
+ private previewSelectionKey: string | null = null;
108
+
109
+ private connection = new WccPreviewConnection(
110
+ () => this.shadowRoot?.querySelector<WccPreviewFrame>('wcc-preview-frame')?.iframe ?? null,
111
+ {
112
+ onReady: (bridgeArg) => {
113
+ this.handlePreviewReady(bridgeArg).catch((error) => {
114
+ console.error('wcctools: connecting the preview failed', error);
115
+ });
116
+ },
117
+ onDisconnect: () => {
118
+ this.bridge = null;
119
+ },
120
+ onEscape: () => this.leaveNative(),
121
+ onScroll: (scrollYArg) => {
122
+ this.frameScrollY = scrollYArg;
123
+ this.debouncedScrollUpdate();
124
+ },
125
+ },
126
+ );
127
+ private bundleStatusClient: WccBundleStatusClient | null = null;
128
+ private routeDisposers: Array<() => void> = [];
129
+ private scrollDisposers: Array<() => void> = [];
130
+ private scrollUpdateTimeout: ReturnType<typeof setTimeout> | undefined;
131
+
132
+ public render(): TemplateResult {
133
+ return html`
134
+ <style>
135
+ :host {
136
+ background: #fcfcfc;
137
+ display: block;
138
+ box-sizing: border-box;
139
+ position: fixed;
140
+ inset: 0;
141
+ min-width: 0;
142
+ overflow: hidden;
143
+ container-type: inline-size;
144
+ container-name: wccShell;
145
+ }
146
+ :host([hidden]) {
147
+ display: none;
148
+ }
149
+ </style>
150
+ <wcc-sidebar
151
+ .manifest=${this.manifest}
152
+ .selection=${this.selection}
153
+ .searchQuery=${this.searchQuery}
154
+ .pinnedItems=${this.pinnedItems}
155
+ .sidebarWidth=${this.sidebarWidth}
156
+ .isNative=${this.isNative}
157
+ @selectionChanged=${(eventArg: CustomEvent<plugins.interfaces.IWccSelection>) => {
158
+ this.selection = eventArg.detail;
159
+ this.buildUrl();
160
+ }}
161
+ @searchChanged=${(eventArg: CustomEvent<string>) => {
162
+ this.searchQuery = eventArg.detail;
163
+ this.updateUrlWithScrollState();
164
+ }}
165
+ @pinnedChanged=${(eventArg: CustomEvent<Set<string>>) => {
166
+ this.pinnedItems = eventArg.detail;
167
+ this.updateUrlWithScrollState();
168
+ }}
169
+ @widthChanged=${(eventArg: CustomEvent<number>) => {
170
+ this.sidebarWidth = eventArg.detail;
171
+ }}
172
+ @widthCommitted=${() => this.updateUrlWithScrollState()}
173
+ @resizingChanged=${(eventArg: CustomEvent<boolean>) => {
174
+ this.isResizing = eventArg.detail;
175
+ }}
176
+ ></wcc-sidebar>
177
+ <wcc-properties
178
+ role="region"
179
+ aria-label="Properties and preview controls"
180
+ .bridge=${this.bridge}
181
+ .renderCount=${this.renderCount}
182
+ .captureTarget=${this.captureTarget}
183
+ .selectedViewport=${this.selectedViewport}
184
+ .selectedTheme=${this.selectedTheme}
185
+ .isNative=${this.isNative}
186
+ .sidebarWidth=${this.sidebarWidth}
187
+ @selectedViewport=${(eventArg: CustomEvent<TWccViewport>) => {
188
+ this.selectedViewport = eventArg.detail;
189
+ this.buildUrl();
190
+ }}
191
+ @selectedTheme=${(eventArg: CustomEvent<plugins.interfaces.TWccTheme>) => {
192
+ this.selectedTheme = eventArg.detail;
193
+ this.buildUrl();
194
+ }}
195
+ @editorStateChanged=${(eventArg: CustomEvent<{ isOpen: boolean }>) => {
196
+ this.advancedEditorOpen = eventArg.detail.isOpen;
197
+ }}
198
+ ></wcc-properties>
199
+ <wcc-preview-frame
200
+ role="main"
201
+ aria-label="Preview"
202
+ .viewport=${this.selectedViewport}
203
+ .previewSrc=${this.previewSrc}
204
+ .isNative=${this.isNative}
205
+ .sidebarWidth=${this.sidebarWidth}
206
+ .advancedEditorOpen=${this.advancedEditorOpen}
207
+ .isResizing=${this.isResizing}
208
+ .bundleFailures=${this.bundleFailures}
209
+ ></wcc-preview-frame>
210
+ `;
211
+ }
212
+
213
+ private leaveNative() {
214
+ if (this.isNative) {
215
+ this.selectedViewport = 'desktop';
216
+ this.buildUrl();
217
+ }
218
+ }
219
+
220
+ private handleDocumentKeydown = (event: KeyboardEvent) => {
221
+ if (event.key === 'Escape') {
222
+ this.leaveNative();
223
+ }
224
+ };
225
+
226
+ public async connectedCallback() {
227
+ const connected = super.connectedCallback();
228
+ document.addEventListener('keydown', this.handleDocumentKeydown);
229
+ this.connection.start();
230
+ this.startBundleStatus();
231
+ if (this.hasUpdated) {
232
+ await this.setupSidebarScrollListener();
233
+ }
234
+ await this.connectRoutes();
235
+ return connected;
236
+ }
237
+
238
+ public async disconnectedCallback() {
239
+ const disconnected = super.disconnectedCallback();
240
+ document.removeEventListener('keydown', this.handleDocumentKeydown);
241
+ this.connection.dispose();
242
+ this.bundleStatusClient?.dispose();
243
+ this.bundleStatusClient = null;
244
+ this.bridge = null;
245
+ for (const dispose of this.routeDisposers) {
246
+ dispose();
247
+ }
248
+ this.routeDisposers = [];
249
+ this.releaseScrollListeners();
250
+ clearTimeout(this.scrollUpdateTimeout);
251
+ return disconnected;
252
+ }
253
+
254
+ private startBundleStatus() {
255
+ if (!this.apiPath || this.bundleStatusClient) {
256
+ return;
257
+ }
258
+ this.bundleStatusClient = new WccBundleStatusClient(this.apiPath, (snapshotArg) => this.applyBundleStatus(snapshotArg));
259
+ this.bundleStatusClient.start();
260
+ }
261
+
262
+ /**
263
+ * Apply the dev server's bundle status: a bundle whose latest completed run failed is shown
264
+ * over the preview until a run of it finishes, which also reloads the preview.
265
+ */
266
+ public applyBundleStatus(snapshotArg: plugins.interfaces.IWccBundleStatusSnapshot) {
267
+ this.bundleFailures = snapshotArg.bundles.filter((bundleArg) => bundleArg.errorMessage !== undefined);
268
+ }
269
+
270
+ public async firstUpdated() {
271
+ await this.setupSidebarScrollListener();
272
+ }
273
+
274
+ private async connectRoutes() {
275
+ const domtools = await this.domtoolsPromise;
276
+ if (!this.isConnected || this.routeDisposers.length > 0) {
277
+ return;
278
+ }
279
+ this.domtools = domtools;
280
+ const handleRoute = async (routeInfo: { params: Record<string, string>; queryParams: Record<string, string> }) => {
281
+ const { sectionName, itemName, demoIndex, viewport, theme } = routeInfo.params;
282
+ this.applyRoute({ sectionName, itemName, demoIndex, viewport, theme }, routeInfo.queryParams);
283
+ };
284
+ this.routeDisposers = [
285
+ domtools.router.on('/wcctools-route/:sectionName/:itemName/:demoIndex/:viewport/:theme', handleRoute),
286
+ // Route without demo index (kept for existing links)
287
+ domtools.router.on('/wcctools-route/:sectionName/:itemName/:viewport/:theme', handleRoute),
288
+ ];
289
+ await domtools.router._handleRouteState();
290
+ if (this.isConnected && !this.previewSrc) {
291
+ // Loaded once: later selections render through the bridge without reloading the preview
292
+ this.previewSrc = `${this.previewPath}${plugins.shared.formatPreviewQuery({ selection: this.selection, theme: this.selectedTheme })}`;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Apply a `/wcctools-route/...` location to the shell state.
298
+ */
299
+ public applyRoute(routeArg: IWccShellRouteParams, queryParamsArg: Record<string, string> = {}) {
300
+ this.selection = {
301
+ sectionName: decodeURIComponent(routeArg.sectionName),
302
+ itemName: routeArg.itemName,
303
+ demoIndex: parseInt(routeArg.demoIndex ?? '0', 10) || 0,
304
+ };
305
+ this.selectedViewport = viewports.includes(routeArg.viewport as TWccViewport)
306
+ ? routeArg.viewport as TWccViewport
307
+ : 'desktop';
308
+ this.selectedTheme = routeArg.theme === 'bright' ? 'bright' : 'dark';
309
+
310
+ // Restore state from query parameters
311
+ this.searchQuery = queryParamsArg.search || '';
312
+ if (queryParamsArg.frameScrollY) {
313
+ this.frameScrollY = parseInt(queryParamsArg.frameScrollY, 10);
314
+ }
315
+ if (queryParamsArg.sidebarScrollY) {
316
+ this.sidebarScrollY = parseInt(queryParamsArg.sidebarScrollY, 10);
317
+ }
318
+ if (queryParamsArg.pinned) {
319
+ const newPinned = new Set(queryParamsArg.pinned.split(',').filter(Boolean));
320
+ // Only update if actually different to avoid update loops
321
+ if (this.pinnedItems.size !== newPinned.size ||
322
+ ![...newPinned].every(k => this.pinnedItems.has(k))) {
323
+ this.pinnedItems = newPinned;
324
+ }
325
+ } else if (this.pinnedItems.size > 0) {
326
+ this.pinnedItems = new Set();
327
+ }
328
+ this.sidebarWidth = queryParamsArg.sidebarWidth
329
+ ? parseInt(queryParamsArg.sidebarWidth, 10)
330
+ : defaultSidebarWidth;
331
+ }
332
+
333
+ public async updated(changedPropertiesArg: Map<string, unknown>) {
334
+ super.updated(changedPropertiesArg);
335
+ if (changedPropertiesArg.has('selection') && this.selection) {
336
+ document.title = this.selection.itemName;
337
+ }
338
+ if (changedPropertiesArg.has('selectedTheme')) {
339
+ await this.applyTheme();
340
+ }
341
+ if (changedPropertiesArg.has('selection')) {
342
+ await this.renderSelectionInPreview();
343
+ }
344
+ }
345
+
346
+ /** The shell's own elements and the preview follow the selected theme. */
347
+ private async applyTheme() {
348
+ const theme = this.selectedTheme;
349
+ const domtools = await plugins.deesDomtools.elementBasic.setup();
350
+ theme === 'bright' ? domtools.themeManager.goBright() : domtools.themeManager.goDark();
351
+ if (this.bridge && this.bridge.getTheme() !== theme) {
352
+ await this.bridge.setTheme(theme);
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Render the selection in the preview. Only a change of the selected entry or demo
358
+ * re-renders; theme, viewport, search, pins and sidebar width leave the rendered demo
359
+ * (and its internal state) untouched.
360
+ */
361
+ private async renderSelectionInPreview() {
362
+ const bridge = this.bridge;
363
+ const key = selectionKey(this.selection);
364
+ if (!bridge || !this.selection || key === this.previewSelectionKey) {
365
+ return;
366
+ }
367
+ this.previewSelectionKey = key;
368
+ await bridge.render(this.selection);
369
+ if (this.bridge === bridge) {
370
+ this.renderCount++;
371
+ }
372
+ }
373
+
374
+ /** A preview document is ready: read its catalog and bring it to the shell's state. */
375
+ private async handlePreviewReady(bridgeArg: plugins.interfaces.IWccPreviewBridge) {
376
+ this.bridge = bridgeArg;
377
+ this.manifest = bridgeArg.getManifest();
378
+ this.captureTarget = this.shadowRoot?.querySelector<WccPreviewFrame>('wcc-preview-frame')?.iframe ?? null;
379
+ if (bridgeArg.getTheme() !== this.selectedTheme) {
380
+ await bridgeArg.setTheme(this.selectedTheme);
381
+ }
382
+ // A reloaded preview restores what it showed from its own URL
383
+ this.previewSelectionKey = selectionKey(bridgeArg.getSelection());
384
+ await this.renderSelectionInPreview();
385
+ if (this.bridge !== bridgeArg) {
386
+ return;
387
+ }
388
+ this.renderCount++;
389
+ await this.restoreScrollPositions();
390
+ }
391
+
392
+ /** Scroll the sidebar and the preview to the positions of the loaded URL, once. */
393
+ private async restoreScrollPositions() {
394
+ if (this.scrollPositionsRestored) {
395
+ return;
396
+ }
397
+ this.scrollPositionsRestored = true;
398
+ const sidebar = this.shadowRoot?.querySelector<WccSidebar>('wcc-sidebar');
399
+ await this.updateComplete;
400
+ await sidebar?.updateComplete;
401
+ if (sidebar?.scrollableContainer && this.sidebarScrollY > 0) {
402
+ sidebar.scrollableContainer.scrollTop = this.sidebarScrollY;
403
+ }
404
+ if (this.frameScrollY > 0) {
405
+ this.connection.scrollPreviewTo(this.frameScrollY);
406
+ }
407
+ }
408
+
409
+ private getRouteUrl(): string | null {
410
+ if (!this.selection) {
411
+ return null;
412
+ }
413
+ const sectionName = encodeURIComponent(this.selection.sectionName);
414
+ const baseUrl = `/wcctools-route/${sectionName}/${this.selection.itemName}/${this.selection.demoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
415
+ const queryParams = new URLSearchParams();
416
+
417
+ if (this.searchQuery) {
418
+ queryParams.set('search', this.searchQuery);
419
+ }
420
+ if (this.frameScrollY > 0) {
421
+ queryParams.set('frameScrollY', this.frameScrollY.toString());
422
+ }
423
+ if (this.sidebarScrollY > 0) {
424
+ queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
425
+ }
426
+ if (this.pinnedItems.size > 0) {
427
+ queryParams.set('pinned', Array.from(this.pinnedItems).join(','));
428
+ }
429
+ if (this.sidebarWidth !== defaultSidebarWidth) {
430
+ queryParams.set('sidebarWidth', this.sidebarWidth.toString());
431
+ }
432
+
433
+ const queryString = queryParams.toString();
434
+ return queryString ? `${baseUrl}?${queryString}` : baseUrl;
435
+ }
436
+
437
+ /** Push the current state as a new history entry; without a selection there is no route to push. */
438
+ public buildUrl() {
439
+ const url = this.getRouteUrl();
440
+ if (url && this.domtools) {
441
+ this.domtools.router.pushUrl(url);
442
+ }
443
+ }
444
+
445
+ private updateUrlWithScrollState() {
446
+ const url = this.getRouteUrl();
447
+ if (url) {
448
+ // Use replaceState to update URL without navigation
449
+ window.history.replaceState(null, '', url);
450
+ }
451
+ }
452
+
453
+ private handleSidebarScroll = (event: Event) => {
454
+ this.sidebarScrollY = (event.currentTarget as HTMLElement).scrollTop;
455
+ this.debouncedScrollUpdate();
456
+ };
457
+
458
+ private async setupSidebarScrollListener() {
459
+ const sidebar = this.shadowRoot?.querySelector<WccSidebar>('wcc-sidebar');
460
+ await sidebar?.updateComplete;
461
+ // Prevent duplicate listeners and listeners on a removed shell
462
+ if (!this.isConnected || this.scrollDisposers.length > 0) {
463
+ return;
464
+ }
465
+ const scrollContainer = sidebar?.scrollableContainer;
466
+ if (scrollContainer) {
467
+ scrollContainer.addEventListener('scroll', this.handleSidebarScroll);
468
+ this.scrollDisposers.push(() => scrollContainer.removeEventListener('scroll', this.handleSidebarScroll));
469
+ }
470
+ }
471
+
472
+ private releaseScrollListeners() {
473
+ for (const dispose of this.scrollDisposers) {
474
+ dispose();
475
+ }
476
+ this.scrollDisposers = [];
477
+ }
478
+
479
+ private debouncedScrollUpdate() {
480
+ clearTimeout(this.scrollUpdateTimeout);
481
+ this.scrollUpdateTimeout = setTimeout(() => {
482
+ this.updateUrlWithScrollState();
483
+ }, 300);
484
+ }
485
+ }