@stacksjs/stx 0.2.118 → 0.2.120

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 (60) hide show
  1. package/dist/builtins/attrs.d.ts +16 -0
  2. package/dist/builtins/attrs.js +16 -0
  3. package/dist/builtins/safe-image.js +2 -9
  4. package/dist/builtins/stx-image.js +2 -9
  5. package/dist/builtins/stx-link.js +2 -9
  6. package/dist/cli.js +1869 -824
  7. package/dist/client-script-bundler.d.ts +0 -11
  8. package/dist/client-script-bundler.js +26 -4
  9. package/dist/client-script.d.ts +24 -0
  10. package/dist/client-script.js +59 -5
  11. package/dist/component-registry.d.ts +1 -0
  12. package/dist/component-renderer.js +13 -5
  13. package/dist/composables/index.d.ts +1 -0
  14. package/dist/composables/index.js +1 -0
  15. package/dist/composables/use-scroll-lock.d.ts +13 -0
  16. package/dist/composables/use-scroll-lock.js +55 -0
  17. package/dist/composables/use-timer.d.ts +1 -1
  18. package/dist/composables/use-timer.js +13 -3
  19. package/dist/composition-api.d.ts +1 -1
  20. package/dist/composition-api.js +13 -4
  21. package/dist/craft-bridge.d.ts +80 -1
  22. package/dist/craft-bridge.js +80 -1
  23. package/dist/craft-components.d.ts +77 -3
  24. package/dist/craft-components.js +138 -9
  25. package/dist/craft.js +481 -73
  26. package/dist/dev-server/crosswind.d.ts +15 -1
  27. package/dist/dev-server/crosswind.js +12 -3
  28. package/dist/dev-server/serve-app.js +3 -3
  29. package/dist/document-shell.d.ts +17 -0
  30. package/dist/document-shell.js +72 -2
  31. package/dist/expressions.js +14 -7
  32. package/dist/head.d.ts +8 -0
  33. package/dist/head.js +11 -8
  34. package/dist/html-masking.d.ts +12 -0
  35. package/dist/html-masking.js +50 -15
  36. package/dist/index.d.ts +9 -1
  37. package/dist/index.js +9 -1
  38. package/dist/process.js +11 -3
  39. package/dist/pwa.js +1172 -431
  40. package/dist/runtime-globals.d.ts +7 -4
  41. package/dist/runtime-globals.js +10 -2
  42. package/dist/runtime-injection.js +15 -1
  43. package/dist/signal-processing.js +19 -28
  44. package/dist/signals-api.d.ts +2 -0
  45. package/dist/signals-api.js +9 -0
  46. package/dist/signals.js +802 -62
  47. package/dist/ssg.js +1161 -420
  48. package/dist/state-management.d.ts +1 -1
  49. package/dist/state-management.js +1 -1
  50. package/dist/strict-lint.d.ts +1 -1
  51. package/dist/strict-lint.js +1 -1
  52. package/dist/stx.d.ts +6 -0
  53. package/dist/style-scoping.js +10 -0
  54. package/dist/utils.js +5 -3
  55. package/dist/variable-extractor.js +14 -22
  56. package/dist/vue-template.js +3 -2
  57. package/dist/x-element.d.ts +2 -2
  58. package/dist/x-element.js +9 -8
  59. package/package.json +5 -5
  60. package/stx.d.ts +6 -0
@@ -9,17 +9,6 @@
9
9
  * - `import { ... } from '@composables'` (handled by transformStoreImports)
10
10
  */
11
11
  export declare function hasUserImports(code: string): boolean;
12
- /**
13
- * Bundle a client script using Bun.build.
14
- *
15
- * Resolves local imports (@/, ./), npm packages, and tree-shakes unused exports.
16
- * stx runtime, @stores, and @composables are marked as external.
17
- *
18
- * @param code - The script content (may contain import statements)
19
- * @param filePath - The .stx template file path (for relative import resolution)
20
- * @param options - Bundling options
21
- * @returns Bundled code with all imports resolved and inlined
22
- */
23
12
  export declare function bundleClientScript(code: string, filePath: string, options?: {
24
13
  projectRoot?: string
25
14
  minify?: boolean
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
  import fs from "node:fs";
3
3
  import { getPublicEnvDefine } from "./public-env.js";
4
4
  import { stateDir } from "./state-dir.js";
5
- const BUNDLE_CACHE_VERSION = 4, EXTERNAL_PATTERNS = [
5
+ const BUNDLE_CACHE_VERSION = 5, BUNDLE_CACHE_METADATA_VERSION = 1, EXTERNAL_PATTERNS = [
6
6
  /^stx$/,
7
7
  /^@stacksjs\/stx$/,
8
8
  /^@stores$/,
@@ -101,6 +101,7 @@ function createBundlePlugin(projectRoot, templateDir, tmpEntry, inputFiles) {
101
101
  }
102
102
  };
103
103
  }
104
+ const inFlightBundles = new Map;
104
105
  export async function bundleClientScript(code, filePath, options = {}) {
105
106
  const projectRoot = options.projectRoot || process.cwd(), minify = options.minify ?? !1, cacheDir = options.cacheDir || stateDir(projectRoot, "bundle-cache"), hasher = new Bun.CryptoHasher("md5");
106
107
  hasher.update(`${BUNDLE_CACHE_VERSION}\x00${code}\x00${filePath}`);
@@ -111,7 +112,9 @@ export async function bundleClientScript(code, filePath, options = {}) {
111
112
  if (await depsFile.exists())
112
113
  try {
113
114
  const stored = JSON.parse(await depsFile.text());
114
- for (const dep of stored.files)
115
+ if (stored.metadataVersion !== BUNDLE_CACHE_METADATA_VERSION || !Array.isArray(stored.files))
116
+ depsValid = !1;
117
+ for (const dep of Array.isArray(stored.files) ? stored.files : [])
115
118
  try {
116
119
  if (fs.statSync(dep.path).mtimeMs !== dep.mtimeMs) {
117
120
  depsValid = !1;
@@ -130,7 +133,19 @@ export async function bundleClientScript(code, filePath, options = {}) {
130
133
  }
131
134
  console.log("[stx:bundler] cache invalidated by dep change:", hash);
132
135
  }
133
- const tmpDir = stateDir(projectRoot, "bundle-tmp"), tmpEntry = path.join(tmpDir, `${hash}.ts`), tmpOutDir = path.join(tmpDir, "out", hash);
136
+ const running = inFlightBundles.get(hash);
137
+ if (running)
138
+ return await running;
139
+ const build = buildBundle(code, filePath, { projectRoot, minify, cacheDir, hash, cachePath, depsPath });
140
+ inFlightBundles.set(hash, build);
141
+ try {
142
+ return await build;
143
+ } finally {
144
+ inFlightBundles.delete(hash);
145
+ }
146
+ }
147
+ async function buildBundle(code, filePath, options) {
148
+ const { projectRoot, minify, cacheDir, hash, cachePath, depsPath } = options, tmpDir = stateDir(projectRoot, "bundle-tmp"), tmpEntry = path.join(tmpDir, `${hash}.ts`), tmpOutDir = path.join(tmpDir, "out", hash);
134
149
  fs.mkdirSync(tmpDir, { recursive: !0 });
135
150
  fs.mkdirSync(tmpOutDir, { recursive: !0 });
136
151
  const templateDir = path.dirname(filePath), declNames = [], declRegex = /^(export\s+)?(?:const|let|var)\s+(?:\{([^}]+)\}|(\w+))/gm, funcRegex = /^(export\s+)?(?:async\s+)?function\s+(\w+)/gm;
@@ -207,6 +222,10 @@ return { ${returnedBindings} };
207
222
  })();
208
223
  ${publicAssignments}`.trim();
209
224
  console.log("[stx:bundler] bundled:", hash, "output:", bundled.length, "bytes");
225
+ if (declNames.length > 0 && exposedBindings.length === 0) {
226
+ console.warn(`[stx:bundler] dropped every binding for ${path.basename(filePath)} (${hash}); serving the unbundled source and not caching this result`);
227
+ return code;
228
+ }
210
229
  fs.mkdirSync(cacheDir, { recursive: !0 });
211
230
  await Bun.write(cachePath, bundled);
212
231
  const depsSnapshot = Array.from(inputFiles).map((p) => {
@@ -216,7 +235,10 @@ ${publicAssignments}`.trim();
216
235
  return null;
217
236
  }
218
237
  }).filter((d) => d !== null);
219
- await Bun.write(depsPath, JSON.stringify({ files: depsSnapshot }));
238
+ await Bun.write(depsPath, JSON.stringify({
239
+ metadataVersion: BUNDLE_CACHE_METADATA_VERSION,
240
+ files: depsSnapshot
241
+ }));
220
242
  return bundled;
221
243
  } catch (error) {
222
244
  const msg = error instanceof Error ? error.message : String(error), bunErrors = error?.errors, detail = Array.isArray(bunErrors) && bunErrors.length > 0 ? `
@@ -33,11 +33,31 @@ export declare function extractAndStripCssImports(code: string, options: { fileP
33
33
  * below un-layered rules, so vendor styles default to lowest precedence.
34
34
  */
35
35
  export declare function renderVendorStyleTags(styles: VendorCssImport[]): string;
36
+ /**
37
+ * Turn bare core browser helper usage into a real module import before the
38
+ * client bundler runs.
39
+ *
40
+ * Inline signal scripts execute synchronously while the document is parsed.
41
+ * Loading `window.StacksBrowser` from a later module script is therefore too
42
+ * late. A real import lets Bun inline and tree-shake the selected browser
43
+ * helpers together with the component, exactly like an explicit user import.
44
+ */
45
+ export declare function injectBrowserCoreAutoImports(code: string): BrowserCoreAutoImportResult;
36
46
  /**
37
47
  * Generate destructuring statements for auto-imported symbols
38
48
  * These access globals set up by the STX runtime and @stacksjs/browser
39
49
  */
40
50
  export declare function generateAutoImportDestructuring(stxImports: string[], browserImports: string[]): string;
51
+ /**
52
+ * Generate the browser-runtime bindings needed by a signal setup wrapper.
53
+ *
54
+ * Signal pages and Vue-like signal components use specialized scoped wrappers
55
+ * instead of {@link processClientScript}. They already pull every STX runtime
56
+ * global from `window.stx`, but still need the selected Stacks browser helpers
57
+ * used by their script. Keeping detection here makes classic scripts, signal
58
+ * pages, and signal components share the same browser auto-import contract.
59
+ */
60
+ export declare function generateBrowserAutoImportDestructuring(code: string, exclude?: readonly string[]): string;
41
61
  /**
42
62
  * Build the server → client bridge payload from a template context: the
43
63
  * top-level, JSON-serializable values (excluding internal `__`/`$` keys and
@@ -71,6 +91,10 @@ export declare interface VendorCssImport {
71
91
  resolvedPath: string
72
92
  contents: string
73
93
  }
94
+ export declare interface BrowserCoreAutoImportResult {
95
+ code: string
96
+ imports: string[]
97
+ }
74
98
  // =============================================================================
75
99
  // Types
76
100
  // =============================================================================
@@ -162,6 +162,7 @@ const STX_AUTO_IMPORTS = [
162
162
  "useQuery",
163
163
  "useMutation",
164
164
  "useOptimistic",
165
+ "useId",
165
166
  "useReactiveProp",
166
167
  "useLocalStorage",
167
168
  "useSessionStorage",
@@ -185,6 +186,7 @@ const STX_AUTO_IMPORTS = [
185
186
  "useColorMode",
186
187
  "useDark",
187
188
  "useMediaQuery",
189
+ "useScrollLock",
188
190
  "usePreferredDark",
189
191
  "usePreferredLight",
190
192
  "usePreferredReducedMotion",
@@ -230,8 +232,53 @@ const STX_AUTO_IMPORTS = [
230
232
  "getBrowserConfig",
231
233
  "createBrowserDb",
232
234
  "createBrowserModel",
233
- "isBrowser"
234
- ], JS_BUILTINS = new Set([
235
+ "isBrowser",
236
+ "debounce",
237
+ "throttle",
238
+ "retry",
239
+ "sleep",
240
+ "wait",
241
+ "delay",
242
+ "waitUntil",
243
+ "waitWhile",
244
+ "lazy",
245
+ "clamp",
246
+ "rand",
247
+ "readableSize",
248
+ "useDocumentVisibility",
249
+ "useIntervalFn"
250
+ ];
251
+ export function injectBrowserCoreAutoImports(code) {
252
+ const explicitlyImported = new Set;
253
+ for (const match of code.matchAll(/^\s*import\s+(?:type\s+)?\{([^}]*)\}\s+from\s+['"][^'"]+['"]\s*;?\s*$/gm))
254
+ for (const specifier of match[1].split(",")) {
255
+ const localName = specifier.trim().split(/\s+as\s+/).pop()?.replace(/^type\s+/, "").trim();
256
+ if (localName)
257
+ explicitlyImported.add(localName);
258
+ }
259
+ const locallyDeclared = new Set;
260
+ for (const match of code.matchAll(/\b(?:const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/g))
261
+ locallyDeclared.add(match[1]);
262
+ for (const match of code.matchAll(/\b(?:const|let|var)\s*\{([^}]*)\}/g))
263
+ for (const binding of match[1].split(",")) {
264
+ const localName = binding.trim().split(/[:=]/).pop()?.trim();
265
+ if (localName)
266
+ locallyDeclared.add(localName);
267
+ }
268
+ const imports = BROWSER_CORE_IMPORTS.filter((symbol) => {
269
+ if (explicitlyImported.has(symbol) || locallyDeclared.has(symbol))
270
+ return !1;
271
+ return new RegExp(`\\b${symbol}\\b`).test(code);
272
+ });
273
+ if (imports.length === 0)
274
+ return { code, imports };
275
+ return {
276
+ code: `import { ${imports.join(", ")} } from '@stacksjs/browser'
277
+ ${code}`,
278
+ imports
279
+ };
280
+ }
281
+ const JS_BUILTINS = new Set([
235
282
  "Promise",
236
283
  "Object",
237
284
  "Array",
@@ -350,6 +397,10 @@ ${lines.join(`
350
397
  ${generateAutoImportGuard(stxImports, browserImports)}`;
351
398
  return "";
352
399
  }
400
+ export function generateBrowserAutoImportDestructuring(code, exclude = []) {
401
+ const { browserImports } = transformAutoImports(code), excluded = new Set(exclude);
402
+ return generateAutoImportDestructuring([], browserImports.filter((name) => !excluded.has(name)));
403
+ }
353
404
  function generateAutoImportGuard(stxImports, browserImports) {
354
405
  const entries = [
355
406
  ...stxImports.map((name) => `['${name}', typeof ${name}, 'stx']`),
@@ -753,13 +804,16 @@ function extractTemplateReferences(templateHtml) {
753
804
  return refs;
754
805
  }
755
806
  export async function processClientScript(scriptContent, options = {}) {
756
- let code = scriptContent, autoImportCode = "", wasBundled = !1;
807
+ let code = scriptContent, autoImportCode = "", wasBundled = !1, bundledBrowserImports = [];
757
808
  const cssExtraction = extractAndStripCssImports(code, {
758
809
  filePath: options.filePath,
759
810
  projectRoot: options.projectRoot
760
811
  });
761
812
  code = cssExtraction.code;
762
- const vendorStyleTags = renderVendorStyleTags(cssExtraction.styles), { hasUserImports, bundleClientScript } = await import("./client-script-bundler.js");
813
+ const vendorStyleTags = renderVendorStyleTags(cssExtraction.styles), browserCoreImports = injectBrowserCoreAutoImports(code);
814
+ code = browserCoreImports.code;
815
+ bundledBrowserImports = browserCoreImports.imports;
816
+ const { hasUserImports, bundleClientScript } = await import("./client-script-bundler.js");
763
817
  if (hasUserImports(code)) {
764
818
  console.log("[stx:bundler] user imports detected, bundling...");
765
819
  code = await bundleClientScript(code, options.filePath || "", {
@@ -771,7 +825,7 @@ export async function processClientScript(scriptContent, options = {}) {
771
825
  if (options.autoImports !== !1) {
772
826
  const autoImportResult = transformAutoImports(code);
773
827
  code = autoImportResult.code;
774
- autoImportCode = generateAutoImportDestructuring(autoImportResult.stxImports, autoImportResult.browserImports);
828
+ autoImportCode = generateAutoImportDestructuring(autoImportResult.stxImports, autoImportResult.browserImports.filter((name) => !bundledBrowserImports.includes(name)));
775
829
  }
776
830
  autoImportCode = `${generateServerDataBridge(code, options.serverData)}${autoImportCode}`;
777
831
  code = transformStoreImports(code);
@@ -27,6 +27,7 @@ export declare interface RenderContext {
27
27
  */
28
28
  export declare interface ResolvedProps {
29
29
  static: Record<string, string | boolean>
30
+ staticNames: Record<string, string>
30
31
  serverDynamic: Record<string, unknown>
31
32
  clientReactive: Record<string, string>
32
33
  events: Record<string, string>
@@ -176,6 +176,7 @@ const BOOLEAN_ATTRIBUTE_SENTINEL = "\x00stx-boolean-attribute", htmlTags = new S
176
176
  function parseComponentProps(rawProps, context, options) {
177
177
  const resolved = {
178
178
  static: {},
179
+ staticNames: {},
179
180
  serverDynamic: {},
180
181
  clientReactive: {},
181
182
  events: {}
@@ -199,7 +200,14 @@ function parseComponentProps(rawProps, context, options) {
199
200
  ]), kebabToCamel = (key) => {
200
201
  if (!key.includes("-") || key.startsWith("@") || key.startsWith(":") || key.startsWith("x-") || key.startsWith("__"))
201
202
  return key;
203
+ if (key.startsWith("aria-") || key.startsWith("data-"))
204
+ return key;
202
205
  return key.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
206
+ }, setStatic = (attrName, value) => {
207
+ const propName = kebabToCamel(attrName);
208
+ resolved.static[propName] = value;
209
+ if (propName !== attrName)
210
+ resolved.staticNames[propName] = attrName;
203
211
  };
204
212
  for (const [rawAttrName, attrValue] of Object.entries(rawProps)) {
205
213
  const attrName = rawAttrName.startsWith("x-") && !ALPINE_PASSTHROUGH.has(rawAttrName) && !rawAttrName.startsWith("x-bind:") && !rawAttrName.startsWith("x-on:") ? `:${rawAttrName.slice(2)}` : rawAttrName;
@@ -242,16 +250,16 @@ function parseComponentProps(rawProps, context, options) {
242
250
  const expression = singleExprMatch[1];
243
251
  if (isExpressionSafe(expression)) {
244
252
  const evaluated = createSafeFunction(expression, Object.keys(context))(...Object.values(context));
245
- resolved.static[attrName] = evaluated != null ? String(evaluated) : "";
253
+ setStatic(attrName, evaluated != null ? String(evaluated) : "");
246
254
  } else {
247
255
  if (options.debug)
248
256
  console.error(`Unsafe expression in ${attrName}: ${expression}`);
249
- resolved.static[attrName] = attrValue;
257
+ setStatic(attrName, attrValue);
250
258
  }
251
259
  } catch (error) {
252
260
  if (options.debug)
253
261
  console.error(`Error evaluating expression for ${attrName}:`, error);
254
- resolved.static[attrName] = attrValue;
262
+ setStatic(attrName, attrValue);
255
263
  }
256
264
  else {
257
265
  let result = attrValue;
@@ -271,11 +279,11 @@ function parseComponentProps(rawProps, context, options) {
271
279
  if (options.debug)
272
280
  console.error(`Error evaluating expression in ${attrName}:`, error);
273
281
  }
274
- resolved.static[attrName] = result;
282
+ setStatic(attrName, result);
275
283
  }
276
284
  continue;
277
285
  }
278
- resolved.static[attrName] = attrValue === BOOLEAN_ATTRIBUTE_SENTINEL ? "true" : attrValue;
286
+ setStatic(attrName, attrValue === BOOLEAN_ATTRIBUTE_SENTINEL ? "true" : attrValue);
279
287
  }
280
288
  return resolved;
281
289
  }
@@ -62,6 +62,7 @@ export {
62
62
  breakpoints,
63
63
  type MediaQueryRef,
64
64
  } from './use-media-query';
65
+ export { useScrollLock } from './use-scroll-lock';
65
66
  // Network
66
67
  export {
67
68
  useNetwork,
@@ -31,6 +31,7 @@ export {
31
31
  useIsDesktop,
32
32
  breakpoints
33
33
  } from "./use-media-query.js";
34
+ export { useScrollLock } from "./use-scroll-lock.js";
34
35
  export {
35
36
  useNetwork,
36
37
  useOnline
@@ -0,0 +1,13 @@
1
+ import type { Signal } from '../signals-api';
2
+ /**
3
+ * Reactively lock scrolling on an element, defaulting to the document body.
4
+ *
5
+ * Multiple locks on the same element are reference counted, so releasing one
6
+ * overlay cannot restore scrolling while another overlay is still open.
7
+ */
8
+ export declare function useScrollLock(target?: ScrollLockTarget): Signal<boolean>;
9
+ declare type ScrollLockTarget = | HTMLElement
10
+ | null
11
+ | undefined
12
+ | { current?: HTMLElement | null, value?: HTMLElement | null }
13
+ | (() => HTMLElement | null | undefined);
@@ -0,0 +1,55 @@
1
+ import { onDestroy, state } from "../signals-api.js";
2
+ const scrollLockStates = new WeakMap;
3
+ function resolveTarget(target) {
4
+ let value = target;
5
+ if (typeof value === "function")
6
+ value = value();
7
+ if (value && typeof value === "object" && "current" in value)
8
+ value = value.current;
9
+ if (value && typeof value === "object" && !("nodeType" in value) && "value" in value)
10
+ value = value.value;
11
+ return value || document.body || document.documentElement;
12
+ }
13
+ function acquire(element) {
14
+ const existing = scrollLockStates.get(element);
15
+ if (existing) {
16
+ existing.count += 1;
17
+ return;
18
+ }
19
+ scrollLockStates.set(element, { count: 1, overflow: element.style.overflow });
20
+ element.style.overflow = "hidden";
21
+ }
22
+ function release(element) {
23
+ const existing = scrollLockStates.get(element);
24
+ if (!existing)
25
+ return;
26
+ existing.count -= 1;
27
+ if (existing.count > 0)
28
+ return;
29
+ element.style.overflow = existing.overflow;
30
+ scrollLockStates.delete(element);
31
+ }
32
+ export function useScrollLock(target) {
33
+ const locked = state(!1);
34
+ let lockedElement = null;
35
+ const unsubscribe = locked.subscribe((value) => {
36
+ if (value) {
37
+ if (lockedElement)
38
+ return;
39
+ lockedElement = resolveTarget(target);
40
+ if (lockedElement)
41
+ acquire(lockedElement);
42
+ } else if (lockedElement) {
43
+ release(lockedElement);
44
+ lockedElement = null;
45
+ }
46
+ });
47
+ onDestroy(() => {
48
+ unsubscribe();
49
+ if (lockedElement) {
50
+ release(lockedElement);
51
+ lockedElement = null;
52
+ }
53
+ });
54
+ return locked;
55
+ }
@@ -39,7 +39,7 @@ export declare function useThrottle<T extends (...args: any[]) => any>(fn: T, li
39
39
  * // counter increments every second
40
40
  * ```
41
41
  */
42
- export declare function useInterval(interval?: number, options?: IntervalOptions): IntervalRef;
42
+ export declare function useInterval(interval?: number | ((counter: number) => void), options?: IntervalOptions | number): IntervalRef;
43
43
  /**
44
44
  * One-shot timeout with start/stop control and auto-cleanup.
45
45
  *
@@ -95,16 +95,26 @@ export function useThrottle(fn, limit = 250) {
95
95
  return throttled;
96
96
  }
97
97
  export function useInterval(interval = 1000, options = {}) {
98
+ let callback = null;
99
+ if (typeof interval === "function") {
100
+ callback = interval;
101
+ interval = typeof options === "number" ? options : 1000;
102
+ options = {};
103
+ }
104
+ const delay = interval || 1000, config = typeof options === "number" ? {} : options;
98
105
  let count = 0, id = null, running = !1;
99
- const listeners = new Set, tick = () => {
106
+ const listeners = new Set;
107
+ if (callback)
108
+ listeners.add(callback);
109
+ const tick = () => {
100
110
  count++;
101
111
  listeners.forEach((fn) => fn(count));
102
112
  }, resume = () => {
103
113
  if (running)
104
114
  return;
105
115
  running = !0;
106
- id = setInterval(tick, interval);
107
- if (options.immediate)
116
+ id = setInterval(tick, delay);
117
+ if (config.immediate)
108
118
  tick();
109
119
  }, pause = () => {
110
120
  if (!running)
@@ -74,7 +74,7 @@ export declare function setGlobalProvide<T>(key: string | symbol, value: T): voi
74
74
  * emit('change', 'newValue')
75
75
  * ```
76
76
  */
77
- export declare function defineEmits<T extends Record<string, any[]> = Record<string, any[]>>(_events?: string[] | T): EmitFn<Extract<keyof T, string>>;
77
+ export declare function defineEmits<T extends Record<string, any[]> = Record<string, any[]>>(events?: string[] | T): EmitFn<Extract<keyof T, string>>;
78
78
  /**
79
79
  * Expose public properties from a component instance.
80
80
  * These properties become accessible via template refs from the parent.
@@ -55,16 +55,25 @@ const globalProvides = new Map;
55
55
  export function setGlobalProvide(key, value) {
56
56
  globalProvides.set(key, value);
57
57
  }
58
- export function defineEmits(_events) {
59
- const instance = currentInstance, emit = (event, ...args) => {
58
+ export function defineEmits(events) {
59
+ const instance = currentInstance, element = instance?.el, declaredEvents = Array.isArray(events) ? events : events && typeof events === "object" ? Object.keys(events) : [];
60
+ if (element && declaredEvents.length > 0) {
61
+ const emittedEvents = element.__stx_component_emits ||= Object.create(null);
62
+ for (const event of declaredEvents)
63
+ emittedEvents[event] = !0;
64
+ }
65
+ const emit = (event, ...args) => {
60
66
  if (typeof document > "u")
61
67
  return;
62
- if (instance?.el)
63
- instance.el.dispatchEvent(new CustomEvent(event, {
68
+ if (element) {
69
+ const emittedEvents = element.__stx_component_emits ||= Object.create(null);
70
+ emittedEvents[event] = !0;
71
+ element.dispatchEvent(new CustomEvent(event, {
64
72
  detail: args.length === 1 ? args[0] : args,
65
73
  bubbles: !0,
66
74
  cancelable: !0
67
75
  }));
76
+ }
68
77
  };
69
78
  if (instance)
70
79
  instance.emit = emit;
@@ -186,6 +186,12 @@ else if (data.type === 'event' && data.method) {
186
186
  });
187
187
  }
188
188
 
189
+ // Live component handles, keyed by id, so an event coming back from the host
190
+ // can be routed to the callbacks registered on the right one. Without this
191
+ // the onSelect/onSpaceChange callbacks were stored and never invoked.
192
+ const sidebars = new Map();
193
+ const spacesSidebars = new Map();
194
+
189
195
  function createNativeUIFacade() {
190
196
  class Sidebar {
191
197
  constructor(id) {
@@ -205,10 +211,42 @@ else if (data.type === 'event' && data.method) {
205
211
  return this;
206
212
  }
207
213
  destroy() {
214
+ sidebars.delete(this.id);
208
215
  sendNativeUI('destroyComponent', { id: this.id, type: 'sidebar' });
209
216
  }
210
217
  }
211
218
 
219
+ /**
220
+ * An Arc-style sidebar of swipeable spaces.
221
+ *
222
+ * Separate from Sidebar rather than an option on it: a source list selects
223
+ * one row out of many, while a spaces sidebar selects one whole SCENE, and
224
+ * a host that renders the switcher natively (a segmented control in the
225
+ * toolbar, a Touch Bar strip) needs the space list, not the item list.
226
+ */
227
+ class SpacesSidebar {
228
+ constructor(id) {
229
+ this.id = id;
230
+ this._spaceCallbacks = [];
231
+ }
232
+ setSpaces(spaces) {
233
+ sendNativeUI('setSidebarSpaces', { sidebarId: this.id, spaces });
234
+ return this;
235
+ }
236
+ setActiveSpace(spaceId) {
237
+ sendNativeUI('setActiveSpace', { sidebarId: this.id, spaceId });
238
+ return this;
239
+ }
240
+ onSpaceChange(callback) {
241
+ this._spaceCallbacks.push(callback);
242
+ return this;
243
+ }
244
+ destroy() {
245
+ spacesSidebars.delete(this.id);
246
+ sendNativeUI('destroyComponent', { id: this.id, type: 'spacesSidebar' });
247
+ }
248
+ }
249
+
212
250
  class FileBrowser {
213
251
  constructor(id) {
214
252
  this.id = id;
@@ -245,12 +283,33 @@ else if (data.type === 'event' && data.method) {
245
283
  }
246
284
  }
247
285
 
286
+ // Host -> page. The host names the component in the payload; we look up the
287
+ // handle and fan out to whatever the page registered on it.
288
+ on('sidebar.select', function(params) {
289
+ const sidebar = sidebars.get(params && params.sidebarId);
290
+ if (sidebar) sidebar._selectCallbacks.forEach(fn => fn(params.itemId));
291
+ });
292
+ on('sidebar.spaceChange', function(params) {
293
+ const sidebar = spacesSidebars.get(params && params.sidebarId);
294
+ if (sidebar) sidebar._spaceCallbacks.forEach(fn => fn(params.spaceId));
295
+ });
296
+
248
297
  return {
249
298
  createSidebar(options) {
250
299
  const opts = options || {};
251
300
  const id = opts.id || 'sidebar-' + Date.now() + '-' + Math.random().toString(36).slice(2);
252
301
  sendNativeUI('createSidebar', Object.assign({}, opts, { id }));
253
- return new Sidebar(id);
302
+ const sidebar = new Sidebar(id);
303
+ sidebars.set(id, sidebar);
304
+ return sidebar;
305
+ },
306
+ createSpacesSidebar(options) {
307
+ const opts = options || {};
308
+ const id = opts.id || 'spaces-' + Date.now() + '-' + Math.random().toString(36).slice(2);
309
+ sendNativeUI('createSpacesSidebar', Object.assign({}, opts, { id }));
310
+ const sidebar = new SpacesSidebar(id);
311
+ spacesSidebars.set(id, sidebar);
312
+ return sidebar;
254
313
  },
255
314
  createFileBrowser(options) {
256
315
  const opts = options || {};
@@ -399,6 +458,26 @@ else if (data.type === 'event' && data.method) {
399
458
  platform: () => request('process.platform'),
400
459
  },
401
460
 
461
+ // Gesture stream
462
+ //
463
+ // A webview's wheel events carry no phase information, so a page can only
464
+ // guess where a trackpad swipe begins and ends — typically with an idle
465
+ // timeout, which mistimes the release and cannot see momentum at all. The
466
+ // host holds the real NSEvent, so it forwards the phases instead:
467
+ //
468
+ // { axis: 'horizontal' | 'vertical',
469
+ // phase: 'begin' | 'change' | 'end',
470
+ // deltaX, deltaY, // points since the last event
471
+ // velocityX, velocityY, // points per second, at 'end'
472
+ // momentum: boolean } // the OS is coasting, not the finger
473
+ //
474
+ // Consumers must stay correct when nothing ever arrives: a host without
475
+ // gesture support simply never emits, so keep the wheel path bound and let
476
+ // a begin phase take over from it.
477
+ gestures: {
478
+ onSwipe: (callback) => on('gesture.swipe', callback),
479
+ },
480
+
402
481
  // Native component helpers
403
482
  nativeUI,
404
483
  components: componentAPI