@webstir-io/webstir 0.1.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 (123) hide show
  1. package/README.md +69 -0
  2. package/assets/features/client_nav/client_nav.ts +469 -0
  3. package/assets/features/content_nav/content_nav.css +170 -0
  4. package/assets/features/content_nav/content_nav.ts +358 -0
  5. package/assets/features/router/router-types.ts +6 -0
  6. package/assets/features/router/router.ts +118 -0
  7. package/assets/features/search/search.css +204 -0
  8. package/assets/features/search/search.ts +627 -0
  9. package/assets/templates/api/src/backend/index.ts +13 -0
  10. package/assets/templates/api/src/backend/tsconfig.json +15 -0
  11. package/assets/templates/api/src/shared/router-types.ts +23 -0
  12. package/assets/templates/api/src/shared/tsconfig.json +10 -0
  13. package/assets/templates/api/src/shared/types/index.ts +4 -0
  14. package/assets/templates/full/src/backend/index.ts +13 -0
  15. package/assets/templates/full/src/backend/tsconfig.json +15 -0
  16. package/assets/templates/full/src/frontend/app/app.css +65 -0
  17. package/assets/templates/full/src/frontend/app/app.html +13 -0
  18. package/assets/templates/full/src/frontend/app/app.ts +188 -0
  19. package/assets/templates/full/src/frontend/app/error.ts +127 -0
  20. package/assets/templates/full/src/frontend/app/hmr.js +355 -0
  21. package/assets/templates/full/src/frontend/app/navigation.ts +8 -0
  22. package/assets/templates/full/src/frontend/app/refresh.js +114 -0
  23. package/assets/templates/full/src/frontend/app/router.ts +126 -0
  24. package/assets/templates/full/src/frontend/app/styles/base.css +2 -0
  25. package/assets/templates/full/src/frontend/app/styles/reset.css +48 -0
  26. package/assets/templates/full/src/frontend/pages/home/index.css +21 -0
  27. package/assets/templates/full/src/frontend/pages/home/index.html +10 -0
  28. package/assets/templates/full/src/frontend/pages/home/index.ts +18 -0
  29. package/assets/templates/full/src/frontend/pages/home/tests/home.test.ts +21 -0
  30. package/assets/templates/full/src/frontend/tsconfig.json +20 -0
  31. package/assets/templates/full/src/shared/router-types.ts +23 -0
  32. package/assets/templates/full/src/shared/tsconfig.json +10 -0
  33. package/assets/templates/full/src/shared/types/index.ts +4 -0
  34. package/assets/templates/shared/Errors.404.html +23 -0
  35. package/assets/templates/shared/Errors.500.html +23 -0
  36. package/assets/templates/shared/Errors.default.html +23 -0
  37. package/assets/templates/shared/types/global.d.ts +32 -0
  38. package/assets/templates/shared/types.global.d.ts +32 -0
  39. package/assets/templates/spa/src/frontend/app/app.css +65 -0
  40. package/assets/templates/spa/src/frontend/app/app.html +13 -0
  41. package/assets/templates/spa/src/frontend/app/app.ts +188 -0
  42. package/assets/templates/spa/src/frontend/app/error.ts +127 -0
  43. package/assets/templates/spa/src/frontend/app/hmr.js +355 -0
  44. package/assets/templates/spa/src/frontend/app/navigation.ts +8 -0
  45. package/assets/templates/spa/src/frontend/app/refresh.js +114 -0
  46. package/assets/templates/spa/src/frontend/app/router.ts +126 -0
  47. package/assets/templates/spa/src/frontend/app/styles/base.css +2 -0
  48. package/assets/templates/spa/src/frontend/app/styles/reset.css +48 -0
  49. package/assets/templates/spa/src/frontend/pages/home/index.css +21 -0
  50. package/assets/templates/spa/src/frontend/pages/home/index.html +10 -0
  51. package/assets/templates/spa/src/frontend/pages/home/index.ts +18 -0
  52. package/assets/templates/spa/src/frontend/pages/home/tests/home.test.ts +21 -0
  53. package/assets/templates/spa/src/frontend/tsconfig.json +20 -0
  54. package/assets/templates/spa/src/shared/router-types.ts +23 -0
  55. package/assets/templates/spa/src/shared/tsconfig.json +10 -0
  56. package/assets/templates/spa/src/shared/types/index.ts +4 -0
  57. package/assets/templates/ssg/src/frontend/app/app.css +12 -0
  58. package/assets/templates/ssg/src/frontend/app/app.html +43 -0
  59. package/assets/templates/ssg/src/frontend/app/app.ts +190 -0
  60. package/assets/templates/ssg/src/frontend/app/error.ts +127 -0
  61. package/assets/templates/ssg/src/frontend/app/hmr.js +370 -0
  62. package/assets/templates/ssg/src/frontend/app/refresh.js +163 -0
  63. package/assets/templates/ssg/src/frontend/app/scripts/components/drawer.ts +183 -0
  64. package/assets/templates/ssg/src/frontend/app/scripts/components/menu.ts +75 -0
  65. package/assets/templates/ssg/src/frontend/app/styles/base.css +77 -0
  66. package/assets/templates/ssg/src/frontend/app/styles/components/buttons.css +108 -0
  67. package/assets/templates/ssg/src/frontend/app/styles/components/drawer.css +12 -0
  68. package/assets/templates/ssg/src/frontend/app/styles/components/header.css +164 -0
  69. package/assets/templates/ssg/src/frontend/app/styles/components/markdown.css +25 -0
  70. package/assets/templates/ssg/src/frontend/app/styles/layout.css +41 -0
  71. package/assets/templates/ssg/src/frontend/app/styles/reset.css +56 -0
  72. package/assets/templates/ssg/src/frontend/app/styles/tokens.css +72 -0
  73. package/assets/templates/ssg/src/frontend/app/styles/utilities.css +14 -0
  74. package/assets/templates/ssg/src/frontend/content/_sidebar.json +14 -0
  75. package/assets/templates/ssg/src/frontend/content/content-pipeline.md +82 -0
  76. package/assets/templates/ssg/src/frontend/content/css-playbook.md +68 -0
  77. package/assets/templates/ssg/src/frontend/content/hosting.md +48 -0
  78. package/assets/templates/ssg/src/frontend/pages/about/index.css +33 -0
  79. package/assets/templates/ssg/src/frontend/pages/about/index.html +60 -0
  80. package/assets/templates/ssg/src/frontend/pages/docs/index.css +505 -0
  81. package/assets/templates/ssg/src/frontend/pages/docs/index.html +52 -0
  82. package/assets/templates/ssg/src/frontend/pages/docs/index.ts +495 -0
  83. package/assets/templates/ssg/src/frontend/pages/home/index.css +91 -0
  84. package/assets/templates/ssg/src/frontend/pages/home/index.html +38 -0
  85. package/assets/templates/ssg/src/frontend/pages/home/tests/home.test.ts +24 -0
  86. package/assets/templates/ssg/src/frontend/tsconfig.json +13 -0
  87. package/package.json +41 -0
  88. package/scripts/pack-standalone.mjs +127 -0
  89. package/scripts/sync-assets.mjs +87 -0
  90. package/src/add-backend.ts +164 -0
  91. package/src/add.ts +112 -0
  92. package/src/api-watch.ts +84 -0
  93. package/src/backend-inspect.ts +45 -0
  94. package/src/backend-runtime.ts +286 -0
  95. package/src/build-plan.ts +12 -0
  96. package/src/build.ts +10 -0
  97. package/src/cli.ts +569 -0
  98. package/src/compile-tests.ts +61 -0
  99. package/src/dev-server.ts +393 -0
  100. package/src/enable-assets.ts +196 -0
  101. package/src/enable.ts +477 -0
  102. package/src/execute.ts +85 -0
  103. package/src/format.ts +254 -0
  104. package/src/frontend-watch.ts +145 -0
  105. package/src/full-watch.ts +80 -0
  106. package/src/index.ts +20 -0
  107. package/src/init-assets.ts +96 -0
  108. package/src/init.ts +339 -0
  109. package/src/paths.ts +26 -0
  110. package/src/providers.ts +88 -0
  111. package/src/publish.ts +8 -0
  112. package/src/refresh.ts +56 -0
  113. package/src/repair.ts +414 -0
  114. package/src/runtime.ts +48 -0
  115. package/src/smoke.ts +161 -0
  116. package/src/stop-signal.ts +26 -0
  117. package/src/test.ts +215 -0
  118. package/src/types.ts +29 -0
  119. package/src/watch-daemon-client.ts +171 -0
  120. package/src/watch-events.ts +195 -0
  121. package/src/watch.ts +66 -0
  122. package/src/workspace-watcher.ts +251 -0
  123. package/src/workspace.ts +55 -0
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # webstir
2
+
3
+ Bun-first CLI for Webstir.
4
+
5
+ Primary command name: `webstir`
6
+
7
+ Current command surface:
8
+
9
+ - `init`
10
+ - `refresh`
11
+ - `repair`
12
+ - `enable`
13
+ - `add-page`
14
+ - `add-test`
15
+ - `add-route`
16
+ - `add-job`
17
+ - `backend-inspect`
18
+ - `build`
19
+ - `publish`
20
+ - `watch`
21
+ - `test`
22
+ - `smoke`
23
+
24
+ Repo-local use:
25
+
26
+ ```bash
27
+ bun run webstir -- --help
28
+ bun run webstir -- build --workspace /absolute/path/to/workspace
29
+ ```
30
+
31
+ Local machine install for external workspaces:
32
+
33
+ ```bash
34
+ cd /Users/iamce/dev/webstir-io/webstir/orchestrators/bun
35
+ bun link
36
+
37
+ mkdir -p ~/tmp/webstir-check
38
+ cd ~/tmp/webstir-check
39
+ bun link @webstir-io/webstir
40
+
41
+ ./node_modules/.bin/webstir init ssg site
42
+ cd site
43
+ bun install
44
+ ../node_modules/.bin/webstir build --workspace "$PWD"
45
+ ```
46
+
47
+ For a tarball install, pack the CLI locally with:
48
+
49
+ ```bash
50
+ cd /Users/iamce/dev/webstir-io/webstir/orchestrators/bun
51
+ bun run pack:local
52
+ ```
53
+
54
+ For a machine-local standalone tarball that bundles the current Webstir packages:
55
+
56
+ ```bash
57
+ cd /Users/iamce/dev/webstir-io/webstir/orchestrators/bun
58
+ bun run pack:standalone
59
+
60
+ mkdir -p ~/tmp/webstir-standalone
61
+ cd ~/tmp/webstir-standalone
62
+ printf '{"name":"webstir-local","private":true}\n' > package.json
63
+ bun add /Users/iamce/dev/webstir-io/webstir/orchestrators/bun/artifacts/webstir-io-webstir-0.1.0-standalone.tgz
64
+
65
+ ./node_modules/.bin/webstir init ssg site
66
+ cd site
67
+ bun install
68
+ ../node_modules/.bin/webstir build --workspace "$PWD"
69
+ ```
@@ -0,0 +1,469 @@
1
+ export {};
2
+
3
+ /**
4
+ * Minimal PJAX-style navigation: swaps the <main> content, updates title/URL,
5
+ * and restores scroll/focus.
6
+ *
7
+ * Opt out per-link with:
8
+ * - data-no-client-nav
9
+ * - data-client-nav="off"
10
+ */
11
+ export function enableClientNav(): void {
12
+ document.addEventListener('click', async (event) => {
13
+ const target = event.target;
14
+ if (!(target instanceof Element)) {
15
+ return;
16
+ }
17
+ if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
18
+ return;
19
+ }
20
+
21
+ const link = target.closest('a');
22
+ if (!link || !(link instanceof HTMLAnchorElement)) {
23
+ return;
24
+ }
25
+
26
+ const setting = link.getAttribute('data-client-nav');
27
+ const optOut = link.hasAttribute('data-no-client-nav')
28
+ || setting === 'off'
29
+ || setting === 'false';
30
+ if (optOut) {
31
+ return;
32
+ }
33
+
34
+ const isExternal = link.origin !== window.location.origin;
35
+ const opensInNewTab = link.getAttribute('target') === '_blank';
36
+ const isDownload = link.hasAttribute('download');
37
+ if (isExternal || opensInNewTab || isDownload) {
38
+ return;
39
+ }
40
+
41
+ const isSameDocumentAnchor = link.hash
42
+ && link.pathname === window.location.pathname
43
+ && link.search === window.location.search;
44
+ if (isSameDocumentAnchor) {
45
+ return;
46
+ }
47
+
48
+ event.preventDefault();
49
+ await renderUrl(link.href, { pushHistory: true });
50
+ });
51
+
52
+ window.addEventListener('popstate', async () => {
53
+ await renderUrl(window.location.href, { pushHistory: false });
54
+ });
55
+ }
56
+
57
+ let activeRequestId = 0;
58
+ let activeController: AbortController | null = null;
59
+ const DYNAMIC_ATTR = 'data-webstir-dynamic';
60
+ const DYNAMIC_VALUE = 'client-nav';
61
+ const BASE_PATH = resolveBasePath();
62
+
63
+ function resolveBasePath(): string {
64
+ const raw = document.documentElement?.getAttribute('data-webstir-base') ?? '';
65
+ return normalizeBasePath(raw);
66
+ }
67
+
68
+ function normalizeBasePath(value: string): string {
69
+ const trimmed = value.trim();
70
+ if (!trimmed || trimmed === '/') {
71
+ return '';
72
+ }
73
+ if (!trimmed.startsWith('/')) {
74
+ return `/${trimmed}`;
75
+ }
76
+ return trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed;
77
+ }
78
+
79
+ function withBasePath(value: string): string {
80
+ if (!BASE_PATH) {
81
+ return value;
82
+ }
83
+ if (!value.startsWith('/') || value.startsWith('//')) {
84
+ return value;
85
+ }
86
+ if (value === BASE_PATH || value.startsWith(`${BASE_PATH}/`) || value.startsWith(`${BASE_PATH}?`) || value.startsWith(`${BASE_PATH}#`)) {
87
+ return value;
88
+ }
89
+ return `${BASE_PATH}${value}`;
90
+ }
91
+
92
+ function stripBasePath(value: string): string {
93
+ if (!BASE_PATH || !value.startsWith('/')) {
94
+ return value;
95
+ }
96
+ if (value === BASE_PATH) {
97
+ return '/';
98
+ }
99
+ if (value.startsWith(`${BASE_PATH}/`) || value.startsWith(`${BASE_PATH}?`) || value.startsWith(`${BASE_PATH}#`)) {
100
+ return value.slice(BASE_PATH.length);
101
+ }
102
+ return value;
103
+ }
104
+
105
+ async function renderUrl(url: string, { pushHistory }: { pushHistory: boolean }): Promise<void> {
106
+ activeRequestId += 1;
107
+ const requestId = activeRequestId;
108
+
109
+ if (activeController) {
110
+ activeController.abort();
111
+ }
112
+
113
+ const controller = new AbortController();
114
+ activeController = controller;
115
+
116
+ let response: Response;
117
+ try {
118
+ response = await fetch(url, {
119
+ headers: { 'X-Webstir-Client-Nav': '1' },
120
+ signal: controller.signal
121
+ });
122
+ } catch {
123
+ if (controller.signal.aborted) {
124
+ return;
125
+ }
126
+
127
+ window.location.href = url;
128
+ return;
129
+ }
130
+
131
+ if (!response.ok) {
132
+ window.location.href = url;
133
+ return;
134
+ }
135
+
136
+ const html = await response.text();
137
+ if (requestId !== activeRequestId) {
138
+ return;
139
+ }
140
+
141
+ const parser = new DOMParser();
142
+ const doc = parser.parseFromString(html, 'text/html');
143
+
144
+ await syncHead(doc, url);
145
+
146
+ const newMain = doc.querySelector('main');
147
+ const currentMain = document.querySelector('main');
148
+ if (newMain && currentMain) {
149
+ currentMain.replaceWith(newMain);
150
+ }
151
+
152
+ const newTitle = doc.querySelector('title');
153
+ if (newTitle && newTitle.textContent) {
154
+ document.title = newTitle.textContent;
155
+ }
156
+
157
+ if (pushHistory) {
158
+ window.history.pushState({}, '', url);
159
+ }
160
+ window.scrollTo({ top: 0, behavior: 'smooth' });
161
+ const focusTarget = document.querySelector('[autofocus]');
162
+ if (focusTarget instanceof HTMLElement) {
163
+ focusTarget.focus();
164
+ }
165
+
166
+ executeScripts(document.querySelector('main'));
167
+ window.dispatchEvent(new CustomEvent('webstir:client-nav', { detail: { url } }));
168
+ }
169
+
170
+ enableClientNav();
171
+
172
+ async function syncHead(doc: Document, url: string): Promise<void> {
173
+ const head = document.head;
174
+ const newHead = doc.head;
175
+ if (!head || !newHead) {
176
+ return;
177
+ }
178
+
179
+ const preservedClientNav = head.querySelector('script[data-webstir="client-nav"]');
180
+ const preservedAppCss = Array.from(head.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"]'))
181
+ .find((link) => isAppStylesheetHref(link.getAttribute('href'))) ?? null;
182
+
183
+ for (const element of Array.from(head.querySelectorAll(`script[${DYNAMIC_ATTR}="${DYNAMIC_VALUE}"]`))) {
184
+ element.remove();
185
+ }
186
+
187
+ for (const script of Array.from(head.querySelectorAll('script[src]'))) {
188
+ const src = script.getAttribute('src') ?? '';
189
+ const normalizedSrc = stripBasePath(src);
190
+ if (script === preservedClientNav) {
191
+ continue;
192
+ }
193
+ if (normalizedSrc === '/hmr.js' || normalizedSrc === '/refresh.js') {
194
+ continue;
195
+ }
196
+ if (normalizedSrc.startsWith('/pages/')) {
197
+ script.remove();
198
+ }
199
+ }
200
+
201
+ const desiredStyles = new Map<string, string>();
202
+ for (const link of Array.from(newHead.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"]'))) {
203
+ const href = link.getAttribute('href');
204
+ if (!href) {
205
+ continue;
206
+ }
207
+ const resolved = resolveUrl(href, url);
208
+ if (!resolved) {
209
+ continue;
210
+ }
211
+ const key = stripBasePath(stripQueryAndHash(resolved));
212
+ const finalHref = key === '/app/app.css' && preservedAppCss
213
+ ? (preservedAppCss.getAttribute('href') ?? resolved)
214
+ : withBasePath(resolved);
215
+ desiredStyles.set(key, finalHref);
216
+ }
217
+
218
+ if (preservedAppCss) {
219
+ const appHref = preservedAppCss.getAttribute('href') ?? withBasePath('/app/app.css');
220
+ desiredStyles.set('/app/app.css', appHref);
221
+ }
222
+
223
+ const existingStyles = new Map<string, HTMLLinkElement>();
224
+ const staleStyles: HTMLLinkElement[] = [];
225
+ for (const link of Array.from(head.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"]'))) {
226
+ const key = normalizeStylesheetKey(link.getAttribute('href'), window.location.href);
227
+ if (!key) {
228
+ link.remove();
229
+ continue;
230
+ }
231
+ if (desiredStyles.has(key)) {
232
+ if (!existingStyles.has(key)) {
233
+ existingStyles.set(key, link);
234
+ }
235
+ continue;
236
+ }
237
+ staleStyles.push(link);
238
+ }
239
+
240
+ const pendingStyles: HTMLLinkElement[] = [];
241
+ for (const [key, href] of desiredStyles.entries()) {
242
+ if (existingStyles.has(key)) {
243
+ continue;
244
+ }
245
+ const next = document.createElement('link');
246
+ next.rel = 'stylesheet';
247
+ next.href = href;
248
+ head.appendChild(next);
249
+ existingStyles.set(key, next);
250
+ pendingStyles.push(next);
251
+ }
252
+
253
+ const stylesReady = pendingStyles.length > 0
254
+ ? waitForStylesheets(pendingStyles)
255
+ : Promise.resolve();
256
+ if (staleStyles.length > 0) {
257
+ void stylesReady.then(() => {
258
+ requestAnimationFrame(() => {
259
+ for (const link of staleStyles) {
260
+ link.remove();
261
+ }
262
+ });
263
+ });
264
+ }
265
+
266
+ syncCriticalStyles(head, newHead);
267
+
268
+ for (const script of Array.from(newHead.querySelectorAll('script[src]'))) {
269
+ const src = script.getAttribute('src');
270
+ if (!src) {
271
+ continue;
272
+ }
273
+ if (src === '/clientNav.js' || src.endsWith('/clientNav.js')) {
274
+ continue;
275
+ }
276
+ if (src === '/hmr.js' || src === '/refresh.js') {
277
+ continue;
278
+ }
279
+
280
+ const resolved = resolveUrl(src, url);
281
+ if (!resolved) {
282
+ continue;
283
+ }
284
+
285
+ const next = document.createElement('script');
286
+ const type = script.getAttribute('type');
287
+ if (type) {
288
+ next.type = type;
289
+ }
290
+ next.src = resolved;
291
+ next.setAttribute(DYNAMIC_ATTR, DYNAMIC_VALUE);
292
+ head.appendChild(next);
293
+ }
294
+
295
+ if (preservedClientNav && !head.contains(preservedClientNav)) {
296
+ head.appendChild(preservedClientNav);
297
+ }
298
+
299
+ await stylesReady;
300
+ }
301
+
302
+ function executeScripts(container: Element | null): void {
303
+ if (!container) {
304
+ return;
305
+ }
306
+
307
+ const scripts = Array.from(container.querySelectorAll('script'));
308
+ for (const script of scripts) {
309
+ const src = script.getAttribute('src');
310
+ const type = script.getAttribute('type');
311
+
312
+ const normalizedSrc = src ? stripBasePath(src) : '';
313
+ if (normalizedSrc && (normalizedSrc === '/clientNav.js' || normalizedSrc.endsWith('/clientNav.js'))) {
314
+ script.remove();
315
+ continue;
316
+ }
317
+ if (normalizedSrc === '/hmr.js' || normalizedSrc === '/refresh.js') {
318
+ script.remove();
319
+ continue;
320
+ }
321
+
322
+ const next = document.createElement('script');
323
+ if (type) {
324
+ next.type = type;
325
+ }
326
+
327
+ if (src) {
328
+ const resolved = resolveUrl(src, window.location.href);
329
+ if (resolved) {
330
+ next.src = resolved;
331
+ }
332
+ } else if (script.textContent) {
333
+ next.textContent = script.textContent;
334
+ }
335
+
336
+ script.replaceWith(next);
337
+ }
338
+ }
339
+
340
+ function resolveUrl(value: string, baseUrl: string): string | null {
341
+ try {
342
+ const trimmed = String(value ?? '').trim();
343
+ const [path, suffix] = splitPathSuffix(trimmed);
344
+ if (path && !path.startsWith('/') && !path.startsWith('http:') && !path.startsWith('https:')) {
345
+ if (path === 'index.js' || path === 'index.css') {
346
+ const pageName = getPageNameFromUrl(baseUrl);
347
+ return withBasePath(`/pages/${pageName}/${path}${suffix}`);
348
+ }
349
+ }
350
+
351
+ const resolved = new URL(value, baseUrl);
352
+ return withBasePath(resolved.pathname + resolved.search + resolved.hash);
353
+ } catch {
354
+ return null;
355
+ }
356
+ }
357
+
358
+ function normalizeStylesheetKey(href: string | null, baseUrl: string): string | null {
359
+ const resolved = resolveUrl(href ?? '', baseUrl);
360
+ if (!resolved) {
361
+ return null;
362
+ }
363
+ return stripBasePath(stripQueryAndHash(resolved));
364
+ }
365
+
366
+ function stripQueryAndHash(value: string): string {
367
+ return value.split(/[?#]/)[0] ?? value;
368
+ }
369
+
370
+ function waitForStylesheets(links: HTMLLinkElement[], timeoutMs = 2000): Promise<void> {
371
+ if (links.length === 0) {
372
+ return Promise.resolve();
373
+ }
374
+
375
+ return new Promise((resolve) => {
376
+ let remaining = links.length;
377
+ let done = false;
378
+ const finish = () => {
379
+ if (done) {
380
+ return;
381
+ }
382
+ done = true;
383
+ resolve();
384
+ };
385
+
386
+ const timer = window.setTimeout(finish, timeoutMs);
387
+ const handle = () => {
388
+ if (done) {
389
+ return;
390
+ }
391
+ remaining -= 1;
392
+ if (remaining <= 0) {
393
+ window.clearTimeout(timer);
394
+ finish();
395
+ }
396
+ };
397
+
398
+ for (const link of links) {
399
+ if (link.sheet) {
400
+ handle();
401
+ continue;
402
+ }
403
+ link.addEventListener('load', handle, { once: true });
404
+ link.addEventListener('error', handle, { once: true });
405
+ }
406
+ });
407
+ }
408
+
409
+ function syncCriticalStyles(head: HTMLHeadElement, newHead: HTMLHeadElement): void {
410
+ for (const style of Array.from(head.querySelectorAll<HTMLStyleElement>('style[data-critical]'))) {
411
+ style.remove();
412
+ }
413
+
414
+ for (const style of Array.from(newHead.querySelectorAll<HTMLStyleElement>('style[data-critical]'))) {
415
+ const next = document.createElement('style');
416
+ for (const attribute of Array.from(style.attributes)) {
417
+ next.setAttribute(attribute.name, attribute.value);
418
+ }
419
+ if (style.textContent) {
420
+ next.textContent = style.textContent;
421
+ }
422
+ head.appendChild(next);
423
+ }
424
+ }
425
+
426
+ function splitPathSuffix(value: string): [string, string] {
427
+ const [path, suffix = ''] = value.split(/(?=[?#])/);
428
+ return [path ?? '', suffix ?? ''];
429
+ }
430
+
431
+ function isAppStylesheetHref(href: string | null): boolean {
432
+ if (!href) {
433
+ return false;
434
+ }
435
+
436
+ try {
437
+ const normalized = stripBasePath(new URL(href, window.location.origin).pathname);
438
+ return normalized === '/app/app.css';
439
+ } catch {
440
+ const trimmed = href.trim();
441
+ if (!trimmed) {
442
+ return false;
443
+ }
444
+ const [path] = trimmed.split(/[?#]/);
445
+ return stripBasePath(path) === '/app/app.css';
446
+ }
447
+ }
448
+
449
+ function getPageNameFromUrl(url: string): string {
450
+ try {
451
+ const pathname = stripBasePath(new URL(url, window.location.href).pathname);
452
+ const trimmed = pathname.replace(/^\/+|\/+$/g, '');
453
+ if (!trimmed) {
454
+ return 'home';
455
+ }
456
+
457
+ const firstSegment = trimmed.split('/')[0];
458
+ return firstSegment || 'home';
459
+ } catch {
460
+ return 'home';
461
+ }
462
+ }
463
+
464
+ function cssEscape(value: string): string {
465
+ if (typeof CSS !== 'undefined' && typeof (CSS as { escape?: (value: string) => string }).escape === 'function') {
466
+ return (CSS as { escape: (value: string) => string }).escape(value);
467
+ }
468
+ return value.replace(/[\"\\\\]/g, '\\\\$&');
469
+ }
@@ -0,0 +1,170 @@
1
+ @layer features {
2
+ .docs-layout[data-content-nav="true"] .docs-sidebar {
3
+ position: fixed;
4
+ top: var(--ws-header-sticky-offset, 0px);
5
+ bottom: 0;
6
+ left: 0;
7
+ width: var(--ws-docs-sidebar-width, 16rem);
8
+ border: 0;
9
+ border-right: 1px solid var(--ws-border);
10
+ border-radius: 0;
11
+ background: var(--ws-surface-1);
12
+ padding: var(--ws-space-5) var(--ws-space-6);
13
+ }
14
+
15
+ .docs-panel__header {
16
+ display: flex;
17
+ align-items: center;
18
+ justify-content: space-between;
19
+ gap: var(--ws-space-3);
20
+ margin-bottom: var(--ws-space-3);
21
+ }
22
+
23
+ .docs-panel__title {
24
+ font-size: 0.75rem;
25
+ text-transform: uppercase;
26
+ letter-spacing: 0.08em;
27
+ font-weight: 700;
28
+ color: var(--ws-muted);
29
+ }
30
+
31
+ .docs-panel__link {
32
+ font-size: 1rem;
33
+ letter-spacing: 0;
34
+ font-weight: 600;
35
+ color: var(--ws-fg);
36
+ text-decoration: none;
37
+ }
38
+
39
+ .docs-panel__link:hover {
40
+ color: var(--ws-accent);
41
+ }
42
+
43
+ .docs-panel__link:focus-visible {
44
+ outline: 0.1875rem solid var(--ws-focus);
45
+ outline-offset: 0.125rem;
46
+ border-radius: var(--ws-radius-2);
47
+ }
48
+
49
+ .docs-toolbar {
50
+ display: flex;
51
+ align-items: center;
52
+ gap: var(--ws-space-3);
53
+ flex-wrap: wrap;
54
+ margin-bottom: var(--ws-space-4);
55
+ }
56
+
57
+ .docs-breadcrumb {
58
+ flex: 1 1 auto;
59
+ min-width: 0;
60
+ }
61
+
62
+ .docs-breadcrumb__list {
63
+ list-style: none;
64
+ margin: 0;
65
+ padding: 0;
66
+ display: flex;
67
+ flex-wrap: wrap;
68
+ align-items: center;
69
+ gap: var(--ws-space-2);
70
+ color: var(--ws-muted);
71
+ font-size: 0.9rem;
72
+ }
73
+
74
+ .docs-breadcrumb__item {
75
+ display: inline-flex;
76
+ align-items: center;
77
+ gap: var(--ws-space-2);
78
+ }
79
+
80
+ .docs-breadcrumb__item:not(:first-child)::before {
81
+ content: "/";
82
+ color: var(--ws-muted);
83
+ }
84
+
85
+ .docs-breadcrumb__link {
86
+ color: inherit;
87
+ text-decoration: none;
88
+ }
89
+
90
+ .docs-breadcrumb__link:hover {
91
+ color: var(--ws-accent);
92
+ }
93
+
94
+ .docs-nav__list--nested {
95
+ margin-top: var(--ws-space-2);
96
+ padding-left: var(--ws-space-4);
97
+ border-left: 1px solid var(--ws-border);
98
+ }
99
+
100
+ .docs-nav__link,
101
+ .docs-nav__label {
102
+ color: inherit;
103
+ text-decoration: none;
104
+ font-weight: 600;
105
+ line-height: 1.4;
106
+ }
107
+
108
+ .docs-nav__label {
109
+ display: inline-flex;
110
+ color: var(--ws-muted);
111
+ }
112
+
113
+ .docs-nav__link {
114
+ color: var(--ws-fg);
115
+ font-size: 0.95rem;
116
+ padding-left: var(--ws-space-3);
117
+ }
118
+
119
+ .docs-nav__link:hover {
120
+ color: var(--ws-accent);
121
+ }
122
+
123
+ .docs-nav__item[data-active="true"] > .docs-nav__link,
124
+ .docs-nav__item[data-active-branch="true"] > .docs-nav__label {
125
+ color: var(--ws-accent);
126
+ }
127
+
128
+ .app-nav__docs {
129
+ display: none;
130
+ }
131
+
132
+ .app-nav__docs > .docs-nav__list {
133
+ padding-left: var(--ws-space-3);
134
+ }
135
+
136
+ .app-nav__docs .docs-nav__link {
137
+ color: var(--ws-fg);
138
+ font-weight: 400;
139
+ }
140
+
141
+ .app-nav__docs .docs-nav__link:focus-visible {
142
+ outline: 0.1875rem solid var(--ws-focus);
143
+ outline-offset: 0.125rem;
144
+ border-radius: var(--ws-radius-1);
145
+ }
146
+
147
+ .app-nav__docs .docs-nav__list--nested {
148
+ padding-left: var(--ws-space-2);
149
+ }
150
+
151
+ @media (--ws-sm) {
152
+ .docs-layout[data-content-nav="true"] .docs-sidebar {
153
+ display: none;
154
+ }
155
+
156
+ .docs-toolbar {
157
+ margin-bottom: var(--ws-space-2);
158
+ }
159
+
160
+ .app-nav__docs {
161
+ display: grid;
162
+ gap: var(--ws-space-1);
163
+ }
164
+
165
+ .app-menu.is-open .app-nav__docs .docs-nav__link {
166
+ width: 100%;
167
+ justify-content: space-between;
168
+ }
169
+ }
170
+ }