@stacksjs/stx 0.2.151 → 0.2.153

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 (49) hide show
  1. package/dist/app-shell.js +28 -1
  2. package/dist/browser-composables.d.ts +24 -9
  3. package/dist/build-id.d.ts +49 -0
  4. package/dist/build-id.js +20 -0
  5. package/dist/cli.js +897 -659
  6. package/dist/client-script.js +2 -2
  7. package/dist/color-mode-boot.d.ts +15 -10
  8. package/dist/color-mode-boot.js +12 -8
  9. package/dist/component-client-factories.js +3 -2
  10. package/dist/component-processing.d.ts +16 -0
  11. package/dist/component-processing.js +4 -1
  12. package/dist/component-renderer.js +2 -2
  13. package/dist/composables/index.d.ts +6 -1
  14. package/dist/composables/use-color-mode.d.ts +19 -4
  15. package/dist/composables/use-color-mode.js +37 -24
  16. package/dist/composables/use-device-orientation.d.ts +8 -1
  17. package/dist/composables/use-event-source.d.ts +14 -1
  18. package/dist/composables/use-eye-dropper.d.ts +10 -1
  19. package/dist/composables/use-idle.d.ts +21 -3
  20. package/dist/composables/use-storage.d.ts +15 -10
  21. package/dist/composables/use-storage.js +63 -2
  22. package/dist/composables/use-text-selection.d.ts +27 -3
  23. package/dist/composables/use-wake-lock.d.ts +2 -2
  24. package/dist/craft.js +625 -387
  25. package/dist/dev-server/serve-app.js +9 -5
  26. package/dist/dev-server/types.d.ts +8 -1
  27. package/dist/document-shell.d.ts +55 -0
  28. package/dist/document-shell.js +103 -3
  29. package/dist/edge-runtime.d.ts +1 -1
  30. package/dist/head.js +5 -1
  31. package/dist/includes.js +4 -2
  32. package/dist/index.d.ts +17 -0
  33. package/dist/index.js +9 -0
  34. package/dist/process.js +7 -3
  35. package/dist/production-builder.js +3 -1
  36. package/dist/pwa.js +591 -353
  37. package/dist/render.d.ts +1 -1
  38. package/dist/render.js +2 -2
  39. package/dist/signals.js +280 -44
  40. package/dist/ssg.js +603 -365
  41. package/dist/ssr.d.ts +17 -1
  42. package/dist/stx.d.ts +14 -6
  43. package/dist/types/config-types.d.ts +1 -0
  44. package/dist/utils.js +1 -8
  45. package/dist/variable-extractor.d.ts +8 -0
  46. package/dist/variable-extractor.js +24 -9
  47. package/dist/visual-testing.d.ts +26 -3
  48. package/package.json +5 -5
  49. package/stx.d.ts +14 -6
@@ -918,7 +918,7 @@ ${eventCode}
918
918
  function wrapInMount(declarations) {
919
919
  const returnStmt = declarations.length > 0 ? `
920
920
  return { ${declarations.join(", ")} };` : "";
921
- return `${vendorStyleTags}<script data-stx-scoped>
921
+ return `${vendorStyleTags}<script data-stx-scoped data-stx-run="always">
922
922
  window.stx.mount(function() {
923
923
  'use strict';
924
924
  ${autoImportCode}${code}
@@ -937,7 +937,7 @@ ${eventCode}${returnStmt}
937
937
  }
938
938
  }
939
939
  }
940
- return `${vendorStyleTags}<script data-stx-scoped>
940
+ return `${vendorStyleTags}<script data-stx-scoped data-stx-run="always">
941
941
  ;(function() {
942
942
  'use strict';
943
943
  ${autoImportCode}${code}
@@ -53,15 +53,17 @@ export declare function injectColorModeBootScript(html: string, options?: ColorM
53
53
  * hydration. If they disagree the flash comes back — the boot script would set
54
54
  * one thing and hydration would immediately undo it, which is worse than not
55
55
  * having a boot script at all. So this mirrors the runtime's `applyDOM` and
56
- * `readPersisted` precisely, including two limitations that are NOT fixed here:
56
+ * preference reading precisely:
57
57
  *
58
- * - `attribute` and `darkClass` are either/or, never both (stacksjs/stx#1788)
59
- * - the persisted vocabulary is exactly `light | dark | auto` — `'system'`
60
- * is not accepted (also #1788)
58
+ * - `attribute` and `darkClass` both apply when both are configured, and
59
+ * `darkClass: null` opts out of the class (stacksjs/stx#1788)
60
+ * - the accepted vocabulary is `light | dark | auto | system`, with
61
+ * `'system'` an alias for `'auto'` (also #1788)
61
62
  *
62
- * Mirroring the limitation is intentional. When #1788 lands, both sides change
63
- * together; `test/directives/color-mode-boot.test.ts` pins them to the same
64
- * expectations so drift fails the suite rather than shipping a flash.
63
+ * `test/directives/color-mode-boot.test.ts` pins both sides to the same
64
+ * expectations so drift fails the suite rather than shipping a flash. This
65
+ * script never writes to storage — reading is all it needs, and a write here
66
+ * would race the composable's own persistence.
65
67
  *
66
68
  * The script publishes its resolved configuration on `window.__STX_COLOR_MODE__`
67
69
  * so a bare `useColorMode()` call picks up the app's configured storage key and
@@ -82,7 +84,10 @@ export declare const COLOR_MODE_BOOT_GLOBAL: '__STX_COLOR_MODE__';
82
84
  export declare interface ColorModeBootConfig {
83
85
  storageKey?: string
84
86
  initialMode?: ColorModeBootMode
85
- darkClass?: string
86
- attribute?: string
87
+ darkClass?: string | null
88
+ attribute?: string | null
89
+ autoValue?: ColorModeBootAutoValue
87
90
  }
88
- export type ColorModeBootMode = 'light' | 'dark' | 'auto';
91
+ export type ColorModeBootMode = 'light' | 'dark' | 'auto' | 'system';
92
+ /** Spelling written to storage for "follow the system". */
93
+ export type ColorModeBootAutoValue = 'auto' | 'system';
@@ -1,22 +1,26 @@
1
1
  export const COLOR_MODE_BOOT_MARKER = "data-stx-color-mode-boot", COLOR_MODE_BOOT_GLOBAL = "__STX_COLOR_MODE__";
2
- const MODES = new Set(["light", "dark", "auto"]), ATTRIBUTE_PATTERN = /^[a-z][\w:-]*$/i, CLASS_PATTERN = /^[\w-]+$/;
2
+ const MODES = new Set(["light", "dark", "auto", "system"]), AUTO_VALUES = new Set(["auto", "system"]), ATTRIBUTE_PATTERN = /^[a-z][\w:-]*$/i, CLASS_PATTERN = /^[\w-]+$/;
3
3
  function resolveConfig(options) {
4
- const storageKey = options.storageKey ?? "stx-color-mode", initialMode = options.initialMode ?? "auto", darkClass = options.darkClass ?? "dark", attribute = options.attribute ?? null;
4
+ const storageKey = options.storageKey ?? "stx-color-mode", rawInitial = options.initialMode ?? "auto", darkClass = options.darkClass === void 0 ? "dark" : options.darkClass, attribute = options.attribute ?? null, autoValue = options.autoValue ?? null;
5
5
  if (typeof storageKey !== "string" || storageKey.length === 0)
6
6
  throw Error("colorMode.storageKey must be a non-empty string");
7
- if (!MODES.has(initialMode))
8
- throw Error(`colorMode.initialMode must be one of light, dark, auto \u2014 got ${JSON.stringify(initialMode)}`);
9
- if (typeof darkClass !== "string" || !CLASS_PATTERN.test(darkClass))
10
- throw Error(`colorMode.darkClass must be a single CSS class name \u2014 got ${JSON.stringify(darkClass)}`);
7
+ if (!MODES.has(rawInitial))
8
+ throw Error(`colorMode.initialMode must be one of light, dark, auto, system \u2014 got ${JSON.stringify(rawInitial)}`);
9
+ if (darkClass !== null && (typeof darkClass !== "string" || !CLASS_PATTERN.test(darkClass)))
10
+ throw Error(`colorMode.darkClass must be a single CSS class name, or null to opt out \u2014 got ${JSON.stringify(darkClass)}`);
11
11
  if (attribute !== null && (typeof attribute !== "string" || !ATTRIBUTE_PATTERN.test(attribute)))
12
12
  throw Error(`colorMode.attribute must be a valid HTML attribute name \u2014 got ${JSON.stringify(attribute)}`);
13
- return { storageKey, initialMode, darkClass, attribute };
13
+ if (autoValue !== null && !AUTO_VALUES.has(autoValue))
14
+ throw Error(`colorMode.autoValue must be 'auto' or 'system' \u2014 got ${JSON.stringify(autoValue)}`);
15
+ if (darkClass === null && attribute === null)
16
+ throw Error("colorMode needs at least one of darkClass or attribute \u2014 with both null nothing would mark the mode on <html>");
17
+ return { storageKey, initialMode: rawInitial === "system" ? "auto" : rawInitial, darkClass, attribute, autoValue };
14
18
  }
15
19
  function serializeForScript(value) {
16
20
  return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
17
21
  }
18
22
  export function generateColorModeBootScript(options = {}) {
19
- const config = resolveConfig(options), body = `(function(){"use strict";var c=${serializeForScript(config)};var p=c.initialMode;try{var v=localStorage.getItem(c.storageKey);if(v==="light"||v==="dark"||v==="auto")p=v}catch(e){}var m=p;if(p==="auto"){try{m=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}catch(e){m="light"}}var r=document.documentElement;if(c.attribute){r.setAttribute(c.attribute,m)}else if(m==="dark"){r.classList.add(c.darkClass)}else{r.classList.remove(c.darkClass)}try{window.${COLOR_MODE_BOOT_GLOBAL}={storageKey:c.storageKey,initialMode:c.initialMode,darkClass:c.darkClass,attribute:c.attribute,preference:p,mode:m}}catch(e){}}());`;
23
+ const config = resolveConfig(options), body = `(function(){"use strict";var c=${serializeForScript(config)};var p=c.initialMode;var a=c.autoValue;try{var v=localStorage.getItem(c.storageKey);if(v==="light"||v==="dark"){p=v}else if(v==="auto"||v==="system"){p="auto";if(!a)a=v}}catch(e){}var m=p;if(p==="auto"){try{m=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}catch(e){m="light"}}var r=document.documentElement;if(c.attribute)r.setAttribute(c.attribute,m);if(c.darkClass){if(m==="dark")r.classList.add(c.darkClass);else r.classList.remove(c.darkClass)}try{window.${COLOR_MODE_BOOT_GLOBAL}={storageKey:c.storageKey,initialMode:c.initialMode,darkClass:c.darkClass,attribute:c.attribute,autoValue:a||"auto",preference:p,mode:m}}catch(e){}}());`;
20
24
  return `<script ${COLOR_MODE_BOOT_MARKER}>${body}</script>`;
21
25
  }
22
26
  export function injectColorModeBootScript(html, options = {}) {
@@ -32,10 +32,11 @@ export function injectComponentClientFactories(html, context) {
32
32
  const repeatedFactories = [...registry].filter(([, factory]) => factory.instances > 1);
33
33
  if (repeatedFactories.length === 0)
34
34
  return output;
35
- const prelude = `<script data-stx-scoped data-stx-component-factories>
35
+ const prelude = `<script data-stx-scoped data-stx-run="always" data-stx-component-factories>
36
36
  (function() {
37
37
  const factories = window.__stxComponentFactories = window.__stxComponentFactories || {};
38
- ${repeatedFactories.map(([id, factory]) => ` factories[${JSON.stringify(id)}] = factories[${JSON.stringify(id)}] || ${factory.body};`).join(`
38
+ ${repeatedFactories.map(([id, factory]) => ` try { factories[${JSON.stringify(id)}] = factories[${JSON.stringify(id)}] || ${factory.body}; }
39
+ catch (e) { console.error('[stx] component factory ' + ${JSON.stringify(id)} + ' failed to register; instances of it will not hydrate.', e); }`).join(`
39
40
  `)}
40
41
  })();
41
42
  </script>`, firstScopedScript = output.search(/<script\b(?=[^>]*\bdata-stx-scoped\b)/i);
@@ -38,6 +38,22 @@ export declare function findComponentTags(html: string, tagPattern: RegExp, skip
38
38
  * A legacy HTML-entity-escaped JSON format is still accepted as a fallback so
39
39
  * that any externally-produced markup keeps working.
40
40
  */
41
+ /**
42
+ * Decode the entities in a static attribute value.
43
+ *
44
+ * An attribute in a template is HTML source, so `title="Tom &amp; Jerry"` names
45
+ * the string `Tom & Jerry`, and a component that receives it as a prop should
46
+ * see that string rather than the encoding of it.
47
+ *
48
+ * The narrowness is the point. This runs on attribute values, at parse time, and
49
+ * on nothing else: a value that reached a component any other way - from the
50
+ * component's own script, from the parent's context, from a `:bound="expression"`
51
+ * - was never encoded, so decoding it can only destroy escaping somebody meant.
52
+ * That is exactly what a blanket "this looks like HTML, unescape it" pass over
53
+ * the component context used to do, and it turned escaped user text back into
54
+ * live markup.
55
+ */
56
+ export declare function decodeAttributeEntities(value: string): string;
41
57
  export declare function decodeStxProp(attrValue: string, options?: { debug?: boolean }): unknown;
42
58
  /**
43
59
  * Parse multiline attributes from a component tag
@@ -113,6 +113,9 @@ export function findComponentTags(html, tagPattern, skipTags) {
113
113
  }
114
114
  return matches;
115
115
  }
116
+ export function decodeAttributeEntities(value) {
117
+ return value.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#0?39;/g, "'").replace(/&#x27;/gi, "'").replace(/&amp;/g, "&");
118
+ }
116
119
  export function decodeStxProp(attrValue, options) {
117
120
  try {
118
121
  const json = Buffer.from(attrValue, "base64").toString("utf8");
@@ -606,7 +609,7 @@ export async function processCustomElements(template, context, filePath, compone
606
609
  props[attrName] = result;
607
610
  }
608
611
  } else
609
- props[attrName] = attrValue;
612
+ props[attrName] = decodeAttributeEntities(attrValue);
610
613
  }
611
614
  const processedContent = await renderComponentWithSlot(componentPath, props, tag.content, componentsDir, context, filePath, options, processedComponents, dependencies);
612
615
  result = result.substring(0, tag.startIndex) + processedContent + result.substring(tag.endIndex);
@@ -1,6 +1,6 @@
1
1
  import { registry } from "./component-registry.js";
2
2
  import { registerBuiltins } from "./builtins/index.js";
3
- import { decodeStxProp, findComponentTags, parseMultilineAttributes, pascalToKebab, restoreStashedScripts, stashScriptElements } from "./component-processing.js";
3
+ import { decodeAttributeEntities, decodeStxProp, findComponentTags, parseMultilineAttributes, pascalToKebab, restoreStashedScripts, stashScriptElements } from "./component-processing.js";
4
4
  import { maskAtElementPosition, matchHtmlComment } from "./html-masking.js";
5
5
  import { renderComponentWithSlot, userComponentFileExists } from "./utils.js";
6
6
  import { createSafeFunction, isExpressionSafe, safeEvaluateObject } from "./safe-evaluator.js";
@@ -287,7 +287,7 @@ function parseComponentProps(rawProps, context, options) {
287
287
  }
288
288
  continue;
289
289
  }
290
- setStatic(attrName, attrValue === BOOLEAN_ATTRIBUTE_SENTINEL ? "true" : attrValue);
290
+ setStatic(attrName, attrValue === BOOLEAN_ATTRIBUTE_SENTINEL ? "true" : decodeAttributeEntities(attrValue));
291
291
  }
292
292
  return resolved;
293
293
  }
@@ -16,7 +16,12 @@
16
16
  * } from '@stacksjs/stx/composables'
17
17
  * ```
18
18
  */
19
- // Storage
19
+ // Storage. `useLocalStorage` / `useSessionStorage` return a Signal<T> matching
20
+ // the runtime contract and the ambient stx.d.ts; the old StorageRef shape was
21
+ // removed in #1797 as part of the dual-impl unification. Migrate `.value` reads
22
+ // to `s()` and `.value = x` writes to `s.set(x)` (`.set()` is unchanged); for
23
+ // `.remove()` / `.subscribe()` / custom serializers use `useStorage`, which
24
+ // still returns a StorageRef.
20
25
  export {
21
26
  useStorage,
22
27
  useLocalStorage,
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Map any accepted spelling onto the canonical vocabulary, or null if the value
3
+ * is not a color-mode preference at all.
4
+ */
5
+ export declare function normalizeColorMode(value: unknown): ColorMode | null;
1
6
  /**
2
7
  * Full-featured color-mode manager.
3
8
  *
@@ -21,16 +26,17 @@ export declare function useColorMode(options?: ColorModeOptions): ColorModeRef;
21
26
  export declare function useDark(options?: ColorModeOptions): DarkRef;
22
27
  export declare interface ColorModeOptions {
23
28
  storageKey?: string
24
- initialMode?: ColorMode
25
- darkClass?: string
26
- attribute?: string
29
+ initialMode?: ColorModeInput
30
+ darkClass?: string | null
31
+ attribute?: string | null
32
+ autoValue?: AutoValue
27
33
  disableTransitions?: boolean
28
34
  }
29
35
  export declare interface ColorModeRef {
30
36
  readonly mode: 'light' | 'dark'
31
37
  readonly preference: ColorMode
32
38
  readonly isDark: boolean
33
- set: (mode: ColorMode) => void
39
+ set: (mode: ColorModeInput) => void
34
40
  toggle: () => void
35
41
  subscribe: (fn: (mode: 'light' | 'dark', preference: ColorMode) => void) => () => void
36
42
  }
@@ -50,3 +56,12 @@ export declare interface DarkRef {
50
56
  // Types
51
57
  // =============================================================================
52
58
  export type ColorMode = 'light' | 'dark' | 'auto';
59
+ /**
60
+ * What may be *written* into storage for "follow the system".
61
+ *
62
+ * `'system'` is a common spelling for the same concept, and an app that already
63
+ * persists it must not have that overwritten (stacksjs/stx#1788).
64
+ */
65
+ export type AutoValue = 'auto' | 'system';
66
+ /** Accepted on input anywhere a ColorMode is; normalised to ColorMode. */
67
+ export type ColorModeInput = ColorMode | 'system';
@@ -1,12 +1,21 @@
1
+ const AUTO_VALUES = ["auto", "system"];
2
+ export function normalizeColorMode(value) {
3
+ if (value === "light" || value === "dark")
4
+ return value;
5
+ if (typeof value === "string" && AUTO_VALUES.includes(value))
6
+ return "auto";
7
+ return null;
8
+ }
1
9
  export function useColorMode(options = {}) {
2
10
  const boot = typeof window < "u" ? window.__STX_COLOR_MODE__ : null, {
3
11
  storageKey = boot?.storageKey ?? "stx-color-mode",
4
12
  initialMode = boot?.initialMode ?? "auto",
5
13
  darkClass = boot?.darkClass ?? "dark",
6
- attribute = boot?.attribute,
14
+ attribute = boot?.attribute ?? null,
15
+ autoValue: autoValueOption = boot?.autoValue,
7
16
  disableTransitions = !0
8
17
  } = options;
9
- let preference = initialMode, resolved = "light";
18
+ let preference = normalizeColorMode(initialMode) ?? "auto", resolved = "light";
10
19
  const listeners = new Set, cleanups = [], isBrowser = typeof window < "u";
11
20
  function getSystemPreference() {
12
21
  if (!isBrowser)
@@ -24,41 +33,43 @@ export function useColorMode(options = {}) {
24
33
  el.style.setProperty("transition", "none", "important");
25
34
  if (attribute)
26
35
  el.setAttribute(attribute, mode);
27
- else if (mode === "dark")
28
- el.classList.add(darkClass);
29
- else
30
- el.classList.remove(darkClass);
36
+ if (darkClass)
37
+ if (mode === "dark")
38
+ el.classList.add(darkClass);
39
+ else
40
+ el.classList.remove(darkClass);
31
41
  if (disableTransitions) {
32
42
  el.offsetHeight;
33
43
  el.style.removeProperty("transition");
34
44
  }
35
45
  }
36
- function persist(pref) {
46
+ function readRaw() {
37
47
  if (!isBrowser)
38
- return;
48
+ return null;
39
49
  try {
40
- localStorage.setItem(storageKey, pref);
41
- } catch {}
50
+ return localStorage.getItem(storageKey);
51
+ } catch {
52
+ return null;
53
+ }
42
54
  }
43
- function readPersisted() {
55
+ const storedRaw = readRaw();
56
+ let autoValue = autoValueOption ?? (storedRaw !== null && AUTO_VALUES.includes(storedRaw) ? storedRaw : "auto");
57
+ function persist(pref) {
44
58
  if (!isBrowser)
45
- return null;
59
+ return;
46
60
  try {
47
- const v = localStorage.getItem(storageKey);
48
- if (v === "light" || v === "dark" || v === "auto")
49
- return v;
61
+ localStorage.setItem(storageKey, pref === "auto" ? autoValue : pref);
50
62
  } catch {}
51
- return null;
52
63
  }
53
- function update(pref) {
54
- preference = pref;
55
- resolved = resolve(pref);
64
+ function update(pref, persistChoice = !0) {
65
+ preference = normalizeColorMode(pref) ?? "auto";
66
+ resolved = resolve(preference);
56
67
  applyToDOM(resolved);
57
- persist(pref);
68
+ if (persistChoice)
69
+ persist(preference);
58
70
  listeners.forEach((fn) => fn(resolved, preference));
59
71
  }
60
- const persisted = readPersisted();
61
- update(persisted ?? initialMode);
72
+ update(normalizeColorMode(storedRaw) ?? initialMode, !1);
62
73
  if (isBrowser) {
63
74
  const mql = window.matchMedia("(prefers-color-scheme: dark)"), onSystemChange = () => {
64
75
  if (preference === "auto") {
@@ -72,8 +83,10 @@ export function useColorMode(options = {}) {
72
83
  const onStorage = (e) => {
73
84
  if (e.key !== storageKey)
74
85
  return;
75
- const v = e.newValue;
76
- if (v === "light" || v === "dark" || v === "auto") {
86
+ const v = normalizeColorMode(e.newValue);
87
+ if (v) {
88
+ if (v === "auto" && e.newValue && AUTO_VALUES.includes(e.newValue))
89
+ autoValue = e.newValue;
77
90
  preference = v;
78
91
  resolved = resolve(v);
79
92
  applyToDOM(resolved);
@@ -63,7 +63,7 @@ export declare function useDeviceMotion(): DeviceMotionRef;
63
63
  * })
64
64
  * ```
65
65
  */
66
- export declare function useParallax(): void;
66
+ export declare function useParallax(): ParallaxRef;
67
67
  /**
68
68
  * Device Orientation & Motion Composables
69
69
  *
@@ -101,6 +101,13 @@ export declare interface DeviceOrientationRef {
101
101
  isSupported: () => boolean
102
102
  requestPermission: () => Promise<boolean>
103
103
  }
104
+ /** Handle returned by {@link useParallax}. */
105
+ export declare interface ParallaxRef {
106
+ get: () => { x: number, y: number }
107
+ subscribe: (fn: (state: { x: number, y: number }) => void) => () => void
108
+ isSupported: () => boolean
109
+ requestPermission: () => Promise<boolean>
110
+ }
104
111
  export declare interface DeviceMotionRef {
105
112
  get: () => DeviceMotionState
106
113
  subscribe: (fn: (state: DeviceMotionState) => void) => () => void
@@ -40,7 +40,7 @@ export declare function useEventSource<T = unknown>(url: string | URL, options?:
40
40
  * })
41
41
  * ```
42
42
  */
43
- export declare function useSSE<T = unknown>(url: string): void;
43
+ export declare function useSSE<T = unknown>(url: string): SSERef<T>;
44
44
  export declare interface EventSourceState<T = unknown> {
45
45
  status: EventSourceStatus
46
46
  data: T | null
@@ -61,6 +61,19 @@ export declare interface EventSourceOptions {
61
61
  onMessage?: (event: MessageEvent) => void
62
62
  onError?: (event: Event) => void
63
63
  }
64
+ /** Handle returned by {@link useSSE}. */
65
+ export declare interface SSERef<T = unknown> {
66
+ data: {
67
+ get: () => T | null
68
+ subscribe: (fn: (data: T | null) => void) => () => void
69
+ }
70
+ status: {
71
+ get: () => EventSourceStatus
72
+ subscribe: (fn: (status: EventSourceStatus) => void) => () => void
73
+ }
74
+ close: () => void
75
+ open: () => void
76
+ }
64
77
  export declare interface EventSourceRef<T = unknown> {
65
78
  get: () => EventSourceState<T>
66
79
  subscribe: (fn: (state: EventSourceState<T>) => void) => () => void
@@ -69,7 +69,7 @@ export declare function hexToHsl(hex: string): { h: number, s: number, l: number
69
69
  export declare function useColorHistory(options?: {
70
70
  maxHistory?: number
71
71
  onPick?: (color: string) => void
72
- }): void;
72
+ }): ColorHistoryRef;
73
73
  /**
74
74
  * EyeDropper Composables
75
75
  *
@@ -89,6 +89,15 @@ export declare interface EyeDropperOptions {
89
89
  onAbort?: () => void
90
90
  onError?: (error: Error) => void
91
91
  }
92
+ /** Handle returned by {@link useColorHistory}. */
93
+ export declare interface ColorHistoryRef {
94
+ get: () => { current: string | null, history: string[] }
95
+ subscribe: (fn: (state: { current: string | null, history: string[] }) => void) => () => void
96
+ pick: () => Promise<string | null>
97
+ clearHistory: () => void
98
+ removeFromHistory: (color: string) => void
99
+ isSupported: () => boolean
100
+ }
92
101
  export declare interface EyeDropperRef {
93
102
  get: () => EyeDropperState
94
103
  subscribe: (fn: (state: EyeDropperState) => void) => () => void
@@ -32,7 +32,7 @@ export declare function useIdle(options?: IdleOptions): IdleRef;
32
32
  * })
33
33
  * ```
34
34
  */
35
- export declare function useIdleState(timeout?: number): void;
35
+ export declare function useIdleState(timeout?: number): IdleStateRef;
36
36
  /**
37
37
  * Track user's last active timestamp
38
38
  *
@@ -45,7 +45,7 @@ export declare function useIdleState(timeout?: number): void;
45
45
  * })
46
46
  * ```
47
47
  */
48
- export declare function useLastActive(): void;
48
+ export declare function useLastActive(): LastActiveRef;
49
49
  /**
50
50
  * Auto-logout after idle timeout
51
51
  *
@@ -57,7 +57,7 @@ export declare function useLastActive(): void;
57
57
  * })
58
58
  * ```
59
59
  */
60
- export declare function useAutoLogout(timeout: number, onLogout: () => void, options?: { warningTime?: number, onWarning?: (remainingTime: number) => void }): void;
60
+ export declare function useAutoLogout(timeout: number, onLogout: () => void, options?: { warningTime?: number, onWarning?: (remainingTime: number) => void }): AutoLogoutRef;
61
61
  /**
62
62
  * Idle Detection Composables
63
63
  *
@@ -82,3 +82,21 @@ export declare interface IdleRef {
82
82
  reset: () => void
83
83
  isIdle: () => boolean
84
84
  }
85
+ /** Narrowed view of {@link IdleRef} exposing only the idle flag. */
86
+ export declare interface IdleStateRef {
87
+ get: () => boolean
88
+ subscribe: (fn: (isIdle: boolean) => void) => () => void
89
+ reset: () => void
90
+ }
91
+ /** Narrowed view of {@link IdleRef} exposing only the last-active timestamp. */
92
+ export declare interface LastActiveRef {
93
+ get: () => number
94
+ subscribe: (fn: (timestamp: number) => void) => () => void
95
+ reset: () => void
96
+ }
97
+ /** Handle returned by {@link useAutoLogout}. */
98
+ export declare interface AutoLogoutRef {
99
+ get: () => IdleState
100
+ subscribe: (fn: (state: IdleState) => void) => () => void
101
+ reset: () => void
102
+ }
@@ -1,3 +1,4 @@
1
+ import type { Signal } from '../signals-api';
1
2
  /**
2
3
  * Create a reactive storage reference
3
4
  *
@@ -19,13 +20,23 @@
19
20
  */
20
21
  export declare function useStorage<T>(key: string, defaultValue: T, options?: UseStorageOptions<T>): StorageRef<T>;
21
22
  /**
22
- * Shorthand for useStorage with localStorage
23
+ * Reactive localStorage binding. Returns a Signal: `s()` reads, `s.set(v)`
24
+ * writes and persists.
25
+ *
26
+ * The same shape the ambient `stx.d.ts` declares and the same shape a
27
+ * `<script client>` block gets from `window.stx` — one identifier, one
28
+ * contract, whichever way it's imported (stacksjs/stx#1797). It used to return
29
+ * a `StorageRef` here and a signal there, so `.value` worked or silently
30
+ * yielded `undefined` depending on which entry point resolved.
31
+ *
32
+ * For the richer object API — `.value`, `.remove()`, `.subscribe()`, custom
33
+ * serializers — use {@link useStorage}, which is unchanged.
23
34
  */
24
- export declare function useLocalStorage<T>(key: string, defaultValue: T, options?: Omit<UseStorageOptions<T>, 'storage'>): void;
35
+ export declare function useLocalStorage<T>(key: string, defaultValue: T, options?: Omit<UseStorageOptions<T>, 'storage'>): Signal<T>;
25
36
  /**
26
- * Shorthand for useStorage with sessionStorage
37
+ * Reactive sessionStorage binding. Returns a Signal — see {@link useLocalStorage}.
27
38
  */
28
- export declare function useSessionStorage<T>(key: string, defaultValue: T, options?: Omit<UseStorageOptions<T>, 'storage'>): void;
39
+ export declare function useSessionStorage<T>(key: string, defaultValue: T, options?: Omit<UseStorageOptions<T>, 'storage'>): Signal<T>;
29
40
  /**
30
41
  * Clear all items from storage
31
42
  */
@@ -55,10 +66,4 @@ export declare interface StorageRef<T> {
55
66
  remove: () => void
56
67
  subscribe: (callback: (value: T, prev: T | undefined) => void) => () => void
57
68
  }
58
- /**
59
- * useStorage - Reactive localStorage/sessionStorage composable
60
- *
61
- * Similar to Nuxt's useStorage but for STX applications.
62
- * Provides a reactive, type-safe wrapper around Web Storage APIs.
63
- */
64
69
  export type StorageType = 'local' | 'session';
@@ -1,3 +1,4 @@
1
+ import { effect, state } from "../signals-api.js";
1
2
  const defaultSerializer = {
2
3
  read: (value) => {
3
4
  try {
@@ -106,11 +107,71 @@ export function useStorage(key, defaultValue, options = {}) {
106
107
  };
107
108
  return ref;
108
109
  }
110
+ function storageSignal(storageType, label, key, defaultValue, options = {}) {
111
+ const {
112
+ serializer = defaultSerializer,
113
+ mergeDefaults = !1,
114
+ listenToStorageChanges = !0
115
+ } = options, isClient = typeof window < "u", getStorage = () => {
116
+ if (!isClient)
117
+ return null;
118
+ return storageType === "session" ? sessionStorage : localStorage;
119
+ }, parse = (raw) => {
120
+ if (raw === null || raw === void 0)
121
+ return defaultValue;
122
+ try {
123
+ const parsed = serializer.read(raw);
124
+ if (mergeDefaults && typeof defaultValue === "object" && defaultValue !== null)
125
+ return { ...defaultValue, ...parsed };
126
+ return parsed;
127
+ } catch (e) {
128
+ console.warn(`[${label}] Failed to read key "${key}":`, e);
129
+ return defaultValue;
130
+ }
131
+ }, signal = state((() => {
132
+ const store = getStorage();
133
+ if (!store)
134
+ return defaultValue;
135
+ try {
136
+ return parse(store.getItem(key));
137
+ } catch (e) {
138
+ console.warn(`[${label}] Cannot read "${key}":`, e);
139
+ return defaultValue;
140
+ }
141
+ })());
142
+ effect(() => {
143
+ const value = signal(), store = getStorage();
144
+ if (!store)
145
+ return;
146
+ try {
147
+ if (value === null || value === void 0)
148
+ store.removeItem(key);
149
+ else
150
+ store.setItem(key, serializer.write(value));
151
+ } catch (e) {
152
+ console.warn(`[${label}] Cannot persist "${key}":`, e);
153
+ }
154
+ });
155
+ if (isClient && listenToStorageChanges) {
156
+ const onStorage = (event) => {
157
+ if (event.key !== key)
158
+ return;
159
+ if (event.storageArea && event.storageArea !== getStorage())
160
+ return;
161
+ signal.set(parse(event.newValue));
162
+ };
163
+ window.addEventListener("storage", onStorage);
164
+ const maybeOnDestroy = globalThis.onDestroy;
165
+ if (typeof maybeOnDestroy === "function")
166
+ maybeOnDestroy(() => window.removeEventListener("storage", onStorage));
167
+ }
168
+ return signal;
169
+ }
109
170
  export function useLocalStorage(key, defaultValue, options) {
110
- return useStorage(key, defaultValue, { ...options, storage: "local" });
171
+ return storageSignal("local", "useLocalStorage", key, defaultValue, options);
111
172
  }
112
173
  export function useSessionStorage(key, defaultValue, options) {
113
- return useStorage(key, defaultValue, { ...options, storage: "session" });
174
+ return storageSignal("session", "useSessionStorage", key, defaultValue, options);
114
175
  }
115
176
  export function clearStorage(type = "local") {
116
177
  if (typeof window > "u")
@@ -36,7 +36,7 @@ export declare function useTextSelection(options?: TextSelectionOptions): TextSe
36
36
  * })
37
37
  * ```
38
38
  */
39
- export declare function useElementTextSelection(element: Element | null): void;
39
+ export declare function useElementTextSelection(element: Element | null): ElementTextSelectionRef;
40
40
  /**
41
41
  * Create a selection popup that appears when text is selected
42
42
  *
@@ -56,7 +56,7 @@ export declare function useSelectionPopup(options: {
56
56
  onShow?: (text: string, position: { x: number, y: number }) => void
57
57
  onHide?: () => void
58
58
  minLength?: number
59
- }): void;
59
+ }): SelectionPopupRef;
60
60
  /**
61
61
  * Copy selected text to clipboard with custom formatting
62
62
  *
@@ -71,7 +71,7 @@ export declare function useSelectionPopup(options: {
71
71
  * copy.copyWithPrefix('> ')
72
72
  * ```
73
73
  */
74
- export declare function useCopySelection(): void;
74
+ export declare function useCopySelection(): CopySelectionRef;
75
75
  /**
76
76
  * Text Selection Composables
77
77
  *
@@ -87,6 +87,30 @@ export declare interface TextSelectionOptions {
87
87
  onSelect?: (state: TextSelectionState) => void
88
88
  onClear?: () => void
89
89
  }
90
+ /** Handle returned by {@link useElementTextSelection}. */
91
+ export declare interface ElementTextSelectionRef {
92
+ get: () => string
93
+ subscribe: (fn: (text: string) => void) => () => void
94
+ clear: () => void
95
+ selectAll: () => void
96
+ }
97
+ /** Handle returned by {@link useSelectionPopup}. */
98
+ export declare interface SelectionPopupRef {
99
+ get: () => TextSelectionState
100
+ subscribe: (fn: (state: TextSelectionState) => void) => () => void
101
+ isShowing: () => boolean
102
+ }
103
+ /** Handle returned by {@link useCopySelection}. */
104
+ export declare interface CopySelectionRef {
105
+ get: () => TextSelectionState
106
+ subscribe: (fn: (state: TextSelectionState) => void) => () => void
107
+ copy: () => Promise<boolean>
108
+ copyAsMarkdown: () => Promise<boolean>
109
+ copyWithPrefix: (prefix: string) => Promise<boolean>
110
+ copyWithWrapper: (start: string, end: string) => Promise<boolean>
111
+ getText: () => string
112
+ clear: () => void
113
+ }
90
114
  export declare interface TextSelectionRef {
91
115
  get: () => TextSelectionState
92
116
  subscribe: (fn: (state: TextSelectionState) => void) => () => void