@stacksjs/stx 0.2.152 → 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.
@@ -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);
@@ -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);
@@ -1,3 +1,4 @@
1
+ import type { Signal } from '../signals-api';
1
2
  /**
2
3
  * Create a reactive storage reference
3
4
  *
@@ -19,19 +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.
23
25
  *
24
- * The return type is annotated rather than inferred on purpose: the shipped
25
- * declarations are emitted by bun-plugin-dtsx, which resolves an unannotated
26
- * export to `void`. Consumers importing from `@stacksjs/stx/composables` then
27
- * got `Property 'value' does not exist on type 'void'` for a function that
28
- * plainly returns a StorageRef. See stacksjs/stx#1796.
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.
29
34
  */
30
- export declare function useLocalStorage<T>(key: string, defaultValue: T, options?: Omit<UseStorageOptions<T>, 'storage'>): StorageRef<T>;
35
+ export declare function useLocalStorage<T>(key: string, defaultValue: T, options?: Omit<UseStorageOptions<T>, 'storage'>): Signal<T>;
31
36
  /**
32
- * Shorthand for useStorage with sessionStorage
37
+ * Reactive sessionStorage binding. Returns a Signal — see {@link useLocalStorage}.
33
38
  */
34
- export declare function useSessionStorage<T>(key: string, defaultValue: T, options?: Omit<UseStorageOptions<T>, 'storage'>): StorageRef<T>;
39
+ export declare function useSessionStorage<T>(key: string, defaultValue: T, options?: Omit<UseStorageOptions<T>, 'storage'>): Signal<T>;
35
40
  /**
36
41
  * Clear all items from storage
37
42
  */
@@ -61,10 +66,4 @@ export declare interface StorageRef<T> {
61
66
  remove: () => void
62
67
  subscribe: (callback: (value: T, prev: T | undefined) => void) => () => void
63
68
  }
64
- /**
65
- * useStorage - Reactive localStorage/sessionStorage composable
66
- *
67
- * Similar to Nuxt's useStorage but for STX applications.
68
- * Provides a reactive, type-safe wrapper around Web Storage APIs.
69
- */
70
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")