@stacksjs/stx 0.2.152 → 0.2.154

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 (88) hide show
  1. package/dist/animation.js +6 -6
  2. package/dist/app-shell.js +2 -1
  3. package/dist/browser-composables.d.ts +8 -0
  4. package/dist/build-id.d.ts +49 -0
  5. package/dist/build-id.js +20 -0
  6. package/dist/build-message.d.ts +10 -0
  7. package/dist/build-message.js +26 -0
  8. package/dist/builtins/attrs.js +6 -1
  9. package/dist/cli.js +1743 -1255
  10. package/dist/client-script-bundler.js +2 -4
  11. package/dist/client-script.d.ts +29 -0
  12. package/dist/client-script.js +142 -97
  13. package/dist/color-mode-boot.d.ts +46 -10
  14. package/dist/color-mode-boot.js +53 -13
  15. package/dist/component-client-factories.js +3 -2
  16. package/dist/component-renderer.js +10 -21
  17. package/dist/composables/index.d.ts +8 -1
  18. package/dist/composables/use-clipboard.d.ts +1 -5
  19. package/dist/composables/use-clipboard.js +57 -46
  20. package/dist/composables/use-color-mode.d.ts +19 -4
  21. package/dist/composables/use-color-mode.js +49 -28
  22. package/dist/composables/use-router.d.ts +28 -11
  23. package/dist/composables/use-router.js +63 -9
  24. package/dist/composables/use-storage.d.ts +14 -15
  25. package/dist/composables/use-storage.js +63 -2
  26. package/dist/config.js +22 -7
  27. package/dist/craft.js +1319 -841
  28. package/dist/dev-server/crosswind.js +8 -6
  29. package/dist/dev-server/serve-app.js +13 -7
  30. package/dist/dev-server/serve-markdown.js +4 -1
  31. package/dist/dev-server/serve-multi.js +32 -20
  32. package/dist/dev-server/types.d.ts +8 -1
  33. package/dist/document-shell.d.ts +120 -0
  34. package/dist/document-shell.js +161 -14
  35. package/dist/dynamic-components.js +123 -4
  36. package/dist/events.d.ts +26 -49
  37. package/dist/events.js +23 -9
  38. package/dist/expressions.d.ts +1 -1
  39. package/dist/expressions.js +3 -3
  40. package/dist/framework-composables.d.ts +41 -0
  41. package/dist/framework-composables.js +115 -0
  42. package/dist/head.js +5 -1
  43. package/dist/hot-reload.js +8 -2
  44. package/dist/includes.js +4 -4
  45. package/dist/index.d.ts +17 -0
  46. package/dist/index.js +9 -0
  47. package/dist/isolated-build.d.ts +12 -0
  48. package/dist/isolated-build.js +18 -0
  49. package/dist/plugin-system.d.ts +1 -1
  50. package/dist/process.js +46 -12
  51. package/dist/production-builder.js +3 -1
  52. package/dist/production-server.js +4 -1
  53. package/dist/prop-sentinels.d.ts +31 -0
  54. package/dist/prop-sentinels.js +7 -0
  55. package/dist/pwa.js +1258 -780
  56. package/dist/reactive.js +1 -1
  57. package/dist/reactivity.d.ts +0 -15
  58. package/dist/reactivity.js +15 -6
  59. package/dist/runtime-globals.d.ts +40 -2
  60. package/dist/runtime-globals.js +90 -0
  61. package/dist/runtime-injection.d.ts +51 -0
  62. package/dist/runtime-injection.js +13 -0
  63. package/dist/script-emit.d.ts +20 -0
  64. package/dist/script-emit.js +11 -0
  65. package/dist/script-validation.js +12 -7
  66. package/dist/signal-processing.js +25 -40
  67. package/dist/signals-api.d.ts +30 -0
  68. package/dist/signals-api.js +18 -0
  69. package/dist/signals.js +532 -85
  70. package/dist/site-builder/build.js +21 -7
  71. package/dist/site-builder/seo.js +19 -10
  72. package/dist/site-builder/theme.js +5 -4
  73. package/dist/site-builder/types.d.ts +1 -0
  74. package/dist/ssg.js +1255 -777
  75. package/dist/state-management.js +4 -1
  76. package/dist/store-loader.d.ts +0 -11
  77. package/dist/store-loader.js +27 -3
  78. package/dist/strict-lint.d.ts +9 -6
  79. package/dist/strict-lint.js +2 -21
  80. package/dist/stx.d.ts +125 -22
  81. package/dist/types/config-types.d.ts +1 -0
  82. package/dist/unresolved-identifiers.d.ts +50 -0
  83. package/dist/unresolved-identifiers.js +146 -0
  84. package/dist/utils.js +3 -3
  85. package/dist/variable-extractor.js +17 -1
  86. package/dist/x-element.js +1 -1
  87. package/package.json +5 -5
  88. package/stx.d.ts +125 -22
@@ -19,11 +19,6 @@ export declare function useClipboard(options?: { timeout?: number }): ClipboardR
19
19
  * Copy text to clipboard (one-shot helper)
20
20
  */
21
21
  export declare function copyToClipboard(text: string): Promise<boolean>;
22
- /**
23
- * useClipboard - Reactive clipboard composable
24
- *
25
- * Provides easy access to the Clipboard API with fallbacks.
26
- */
27
22
  export declare interface ClipboardRef {
28
23
  text: string
29
24
  isSupported: boolean
@@ -31,4 +26,5 @@ export declare interface ClipboardRef {
31
26
  copy: (text: string) => Promise<boolean>
32
27
  read: () => Promise<string>
33
28
  subscribe: (callback: (text: string) => void) => () => void
29
+ stop: () => void
34
30
  }
@@ -1,3 +1,4 @@
1
+ import { onDestroy } from "../signals-api.js";
1
2
  export function useClipboard(options = {}) {
2
3
  const { timeout = 1500 } = options, subscribers = new Set;
3
4
  let currentText = "", copied = !1, copiedTimeout = null;
@@ -8,42 +9,17 @@ export function useClipboard(options = {}) {
8
9
  } catch (e) {
9
10
  console.error("[useClipboard] Subscriber error:", e);
10
11
  }
11
- };
12
- return {
13
- get text() {
14
- return currentText;
15
- },
16
- get isSupported() {
17
- return isSupported;
18
- },
19
- get copied() {
20
- return copied;
21
- },
22
- copy: async (text) => {
23
- if (!isSupported)
24
- try {
25
- const textarea = document.createElement("textarea");
26
- textarea.value = text;
27
- textarea.style.position = "fixed";
28
- textarea.style.opacity = "0";
29
- document.body.appendChild(textarea);
30
- textarea.select();
31
- document.execCommand("copy");
32
- document.body.removeChild(textarea);
33
- currentText = text;
34
- copied = !0;
35
- notify(text);
36
- if (copiedTimeout)
37
- clearTimeout(copiedTimeout);
38
- copiedTimeout = setTimeout(() => {
39
- copied = !1;
40
- }, timeout);
41
- return !0;
42
- } catch {
43
- return !1;
44
- }
12
+ }, copy = async (text) => {
13
+ if (!isSupported)
45
14
  try {
46
- await navigator.clipboard.writeText(text);
15
+ const textarea = document.createElement("textarea");
16
+ textarea.value = text;
17
+ textarea.style.position = "fixed";
18
+ textarea.style.opacity = "0";
19
+ document.body.appendChild(textarea);
20
+ textarea.select();
21
+ document.execCommand("copy");
22
+ document.body.removeChild(textarea);
47
23
  currentText = text;
48
24
  copied = !0;
49
25
  notify(text);
@@ -51,27 +27,62 @@ export function useClipboard(options = {}) {
51
27
  clearTimeout(copiedTimeout);
52
28
  copiedTimeout = setTimeout(() => {
53
29
  copied = !1;
30
+ notify(currentText);
54
31
  }, timeout);
55
32
  return !0;
56
33
  } catch {
57
34
  return !1;
58
35
  }
36
+ try {
37
+ await navigator.clipboard.writeText(text);
38
+ currentText = text;
39
+ copied = !0;
40
+ notify(text);
41
+ if (copiedTimeout)
42
+ clearTimeout(copiedTimeout);
43
+ copiedTimeout = setTimeout(() => {
44
+ copied = !1;
45
+ notify(currentText);
46
+ }, timeout);
47
+ return !0;
48
+ } catch {
49
+ return !1;
50
+ }
51
+ }, read = async () => {
52
+ if (!isSupported)
53
+ return "";
54
+ try {
55
+ const text = await navigator.clipboard.readText();
56
+ currentText = text;
57
+ return text;
58
+ } catch {
59
+ return "";
60
+ }
61
+ }, stop = () => {
62
+ if (copiedTimeout) {
63
+ clearTimeout(copiedTimeout);
64
+ copiedTimeout = null;
65
+ }
66
+ subscribers.clear();
67
+ };
68
+ onDestroy(stop);
69
+ return {
70
+ get text() {
71
+ return currentText;
59
72
  },
60
- read: async () => {
61
- if (!isSupported)
62
- return "";
63
- try {
64
- const text = await navigator.clipboard.readText();
65
- currentText = text;
66
- return text;
67
- } catch {
68
- return "";
69
- }
73
+ get isSupported() {
74
+ return isSupported;
70
75
  },
76
+ get copied() {
77
+ return copied;
78
+ },
79
+ copy,
80
+ read,
71
81
  subscribe: (callback) => {
72
82
  subscribers.add(callback);
73
83
  return () => subscribers.delete(callback);
74
- }
84
+ },
85
+ stop
75
86
  };
76
87
  }
77
88
  export async function copyToClipboard(text) {
@@ -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,29 @@
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
- const boot = typeof window < "u" ? window.__STX_COLOR_MODE__ : null, {
3
- storageKey = boot?.storageKey ?? "stx-color-mode",
4
- initialMode = boot?.initialMode ?? "auto",
5
- darkClass = boot?.darkClass ?? "dark",
6
- attribute = boot?.attribute,
10
+ const boot = typeof window < "u" ? window.__STX_COLOR_MODE__ : null;
11
+ function pick(fromOptions, fromBoot, fallback) {
12
+ if (fromOptions !== void 0)
13
+ return fromOptions;
14
+ if (boot && typeof boot === "object" && fromBoot !== void 0)
15
+ return fromBoot;
16
+ return fallback;
17
+ }
18
+ const {
19
+ storageKey = pick(void 0, boot?.storageKey, "stx-color-mode"),
20
+ initialMode = pick(void 0, boot?.initialMode, "auto"),
21
+ darkClass = pick(void 0, boot?.darkClass, "dark"),
22
+ attribute = pick(void 0, boot?.attribute, null),
23
+ autoValue: autoValueOption = boot?.autoValue,
7
24
  disableTransitions = !0
8
25
  } = options;
9
- let preference = initialMode, resolved = "light";
26
+ let preference = normalizeColorMode(initialMode) ?? "auto", resolved = "light";
10
27
  const listeners = new Set, cleanups = [], isBrowser = typeof window < "u";
11
28
  function getSystemPreference() {
12
29
  if (!isBrowser)
@@ -24,41 +41,43 @@ export function useColorMode(options = {}) {
24
41
  el.style.setProperty("transition", "none", "important");
25
42
  if (attribute)
26
43
  el.setAttribute(attribute, mode);
27
- else if (mode === "dark")
28
- el.classList.add(darkClass);
29
- else
30
- el.classList.remove(darkClass);
44
+ if (darkClass)
45
+ if (mode === "dark")
46
+ el.classList.add(darkClass);
47
+ else
48
+ el.classList.remove(darkClass);
31
49
  if (disableTransitions) {
32
50
  el.offsetHeight;
33
51
  el.style.removeProperty("transition");
34
52
  }
35
53
  }
36
- function persist(pref) {
54
+ function readRaw() {
37
55
  if (!isBrowser)
38
- return;
56
+ return null;
39
57
  try {
40
- localStorage.setItem(storageKey, pref);
41
- } catch {}
58
+ return localStorage.getItem(storageKey);
59
+ } catch {
60
+ return null;
61
+ }
42
62
  }
43
- function readPersisted() {
63
+ const storedRaw = readRaw();
64
+ let autoValue = autoValueOption ?? (storedRaw !== null && AUTO_VALUES.includes(storedRaw) ? storedRaw : "auto");
65
+ function persist(pref) {
44
66
  if (!isBrowser)
45
- return null;
67
+ return;
46
68
  try {
47
- const v = localStorage.getItem(storageKey);
48
- if (v === "light" || v === "dark" || v === "auto")
49
- return v;
69
+ localStorage.setItem(storageKey, pref === "auto" ? autoValue : pref);
50
70
  } catch {}
51
- return null;
52
71
  }
53
- function update(pref) {
54
- preference = pref;
55
- resolved = resolve(pref);
72
+ function update(pref, persistChoice = !0) {
73
+ preference = normalizeColorMode(pref) ?? "auto";
74
+ resolved = resolve(preference);
56
75
  applyToDOM(resolved);
57
- persist(pref);
76
+ if (persistChoice)
77
+ persist(preference);
58
78
  listeners.forEach((fn) => fn(resolved, preference));
59
79
  }
60
- const persisted = readPersisted();
61
- update(persisted ?? initialMode);
80
+ update(normalizeColorMode(storedRaw) ?? initialMode, !1);
62
81
  if (isBrowser) {
63
82
  const mql = window.matchMedia("(prefers-color-scheme: dark)"), onSystemChange = () => {
64
83
  if (preference === "auto") {
@@ -72,8 +91,10 @@ export function useColorMode(options = {}) {
72
91
  const onStorage = (e) => {
73
92
  if (e.key !== storageKey)
74
93
  return;
75
- const v = e.newValue;
76
- if (v === "light" || v === "dark" || v === "auto") {
94
+ const v = normalizeColorMode(e.newValue);
95
+ if (v) {
96
+ if (v === "auto" && e.newValue && AUTO_VALUES.includes(e.newValue))
97
+ autoValue = e.newValue;
77
98
  preference = v;
78
99
  resolved = resolve(v);
79
100
  applyToDOM(resolved);
@@ -1,13 +1,20 @@
1
1
  /**
2
- * Navigate to a URL. Uses stxRouter if available, otherwise falls back to location.href.
2
+ * Navigate to a URL. Uses stxRouter if available, otherwise falls back to a
3
+ * full document load.
4
+ *
5
+ * Kept in step with the client runtime's `navigate` (#1807), which used to take
6
+ * a bare `forceReload` boolean while the shipped declaration promised
7
+ * `{ replace }` — so `navigate(url, { replace: true })` type-checked and then
8
+ * did a full load that PUSHED a history entry.
3
9
  *
4
10
  * @example
5
11
  * ```ts
6
12
  * navigate('/about')
7
- * navigate('https://example.com')
13
+ * navigate('/dashboard', { replace: true })
14
+ * navigate('/legacy', { reload: true })
8
15
  * ```
9
16
  */
10
- export declare function navigate(url: string): void;
17
+ export declare function navigate(url: string, options?: NavigateOptions | boolean): void;
11
18
  /**
12
19
  * Go back in browser history.
13
20
  */
@@ -42,12 +49,6 @@ export declare function setRouteParams(params: Record<string, string>): void;
42
49
  * ```
43
50
  */
44
51
  export declare function useSearchParams(): SearchParamsRef;
45
- /**
46
- * useRouter - Navigation composable for STX applications
47
- *
48
- * Provides clean APIs for navigation without raw `window.location` or `window.history`.
49
- * These are server-safe stubs; the actual runtime implementations live in signals.ts.
50
- */
51
52
  export declare interface RouteInfo {
52
53
  path: string
53
54
  fullPath: string
@@ -55,9 +56,25 @@ export declare interface RouteInfo {
55
56
  query: Record<string, string>
56
57
  params: Record<string, string>
57
58
  }
59
+ /**
60
+ * How a search-param mutation is written to session history.
61
+ *
62
+ * Spelled `{ replace }` to match `navigate()`, which already takes it — before
63
+ * this the two halves of the routing API disagreed on whether replacing was
64
+ * expressible at all.
65
+ */
66
+ export declare interface SearchParamsCommitOptions {
67
+ replace?: boolean
68
+ }
58
69
  export declare interface SearchParamsRef {
59
70
  data: { (): Record<string, string>, set: (v: Record<string, string>) => void }
60
71
  get: (key: string) => string | undefined
61
- set: (key: string, value: string) => void
62
- setAll: (obj: Record<string, string>) => void
72
+ has: (key: string) => boolean
73
+ set: (key: string, value: string, options?: SearchParamsCommitOptions) => void
74
+ delete: (key: string, options?: SearchParamsCommitOptions) => void
75
+ setAll: (obj: Record<string, string>, options?: SearchParamsCommitOptions) => void
76
+ }
77
+ export declare interface NavigateOptions {
78
+ replace?: boolean
79
+ reload?: boolean
63
80
  }
@@ -1,9 +1,16 @@
1
- export function navigate(url) {
2
- if (typeof window < "u")
3
- if (window.stxRouter?.navigate)
4
- window.stxRouter.navigate(url);
5
- else
6
- window.location.href = url;
1
+ import { state } from "../signals-api.js";
2
+ export function navigate(url, options) {
3
+ if (typeof window > "u")
4
+ return;
5
+ const opts = options && typeof options === "object" ? options : { reload: !!options }, router = window.stxRouter;
6
+ if (!opts.reload && router?.navigate) {
7
+ router.navigate(url, { replace: !!opts.replace });
8
+ return;
9
+ }
10
+ if (opts.replace)
11
+ window.location.replace(url);
12
+ else
13
+ window.location.href = url;
7
14
  }
8
15
  export function goBack() {
9
16
  if (typeof window < "u")
@@ -45,7 +52,54 @@ export function setRouteParams(params) {
45
52
  window.stx.setRouteParams(params);
46
53
  }
47
54
  export function useSearchParams() {
48
- return { data: Object.assign(() => ({}), { set: () => {} }), get: () => {
49
- return;
50
- }, set: () => {}, setAll: () => {} };
55
+ if (typeof window < "u" && window.stx?.useSearchParams)
56
+ return window.stx.useSearchParams();
57
+ if (typeof window > "u")
58
+ return {
59
+ data: Object.assign(() => ({}), { set: () => {} }),
60
+ get: () => {
61
+ return;
62
+ },
63
+ has: () => !1,
64
+ set: () => {},
65
+ delete: () => {},
66
+ setAll: () => {}
67
+ };
68
+ const read = () => {
69
+ const out = {};
70
+ new URLSearchParams(window.location.search).forEach((v, k) => {
71
+ out[k] = v;
72
+ });
73
+ return out;
74
+ }, data = state(read()), sync = () => data.set(read());
75
+ window.addEventListener("popstate", sync);
76
+ window.addEventListener("stx:navigate", sync);
77
+ const commit = (url, options) => {
78
+ if (!!(options && typeof options === "object" && options.replace))
79
+ window.history.replaceState({}, "", url);
80
+ else
81
+ window.history.pushState({}, "", url);
82
+ sync();
83
+ }, has = (key) => Object.prototype.hasOwnProperty.call(data(), key);
84
+ return {
85
+ data,
86
+ get: (key) => has(key) ? data()[key] : void 0,
87
+ has,
88
+ set: (key, value, options) => {
89
+ const url = new URL(window.location.href);
90
+ url.searchParams.set(key, value);
91
+ commit(url, options);
92
+ },
93
+ delete: (key, options) => {
94
+ const url = new URL(window.location.href);
95
+ url.searchParams.delete(key);
96
+ commit(url, options);
97
+ },
98
+ setAll: (obj, options) => {
99
+ const url = new URL(window.location.href);
100
+ for (const [k, v] of Object.entries(obj))
101
+ url.searchParams.set(k, v);
102
+ commit(url, options);
103
+ }
104
+ };
51
105
  }
@@ -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")
package/dist/config.js CHANGED
@@ -103,7 +103,7 @@ export const defaultConfig = {
103
103
  },
104
104
  router: {
105
105
  enabled: !0,
106
- container: "#app-content",
106
+ container: "main",
107
107
  linkSelector: "a[href]",
108
108
  viewTransitions: !0,
109
109
  scrollToTop: !0,
@@ -360,18 +360,31 @@ export const defaultConfig = {
360
360
  verbose: !1
361
361
  }
362
362
  };
363
- function resolveStxRoot(configRoot, configPagesDir) {
363
+ function resolveStxRoot(configRoot, configPagesDir, cwd) {
364
364
  const defaultPagesDir = configPagesDir || "pages";
365
365
  if (configRoot)
366
366
  return { root: configRoot, pagesDir: defaultPagesDir };
367
- const resourcesViews = path.join(process.cwd(), "resources", "views"), resourcesLayouts = path.join(process.cwd(), "resources", "layouts");
367
+ const base = cwd || process.cwd(), resourcesViews = path.join(base, "resources", "views"), resourcesLayouts = path.join(base, "resources", "layouts");
368
368
  if (fs.existsSync(resourcesViews) && fs.existsSync(resourcesLayouts))
369
369
  return { root: "resources", pagesDir: "views" };
370
- const resourcesViewsPages = path.join(process.cwd(), "resources", "views", "pages");
370
+ const resourcesViewsPages = path.join(base, "resources", "views", "pages");
371
371
  if (fs.existsSync(resourcesViewsPages))
372
372
  return { root: "resources/views", pagesDir: defaultPagesDir };
373
373
  return { root: ".", pagesDir: defaultPagesDir };
374
374
  }
375
+ function warnMissingDirs(loaded, cwd) {
376
+ const keys = ["componentsDir", "layoutsDir", "partialsDir"];
377
+ for (const key of keys) {
378
+ const value = loaded[key];
379
+ if (typeof value !== "string" || !value)
380
+ continue;
381
+ const abs = path.isAbsolute(value) ? value : path.join(cwd, value);
382
+ if (fs.existsSync(abs))
383
+ continue;
384
+ const prefixed = !!loaded.root && loaded.root !== ".";
385
+ console.warn(`[stx] ${String(key)} resolves to "${abs}", which does not exist. Configured as "${value}"` + (prefixed ? ` and prefixed with root "${loaded.root}" \u2014 a value that already includes the root gets it twice.` : "."));
386
+ }
387
+ }
375
388
  function applyStateDir(loaded) {
376
389
  setStateDir(loaded.stateDir);
377
390
  loaded.stateDir = stateDirName();
@@ -405,7 +418,7 @@ export async function loadStxConfig(cwd) {
405
418
  verbose: !1
406
419
  })).config;
407
420
  applyStateDir(loaded);
408
- const resolved = resolveStxRoot(loaded.root, loaded.pagesDir);
421
+ const resolved = resolveStxRoot(loaded.root, loaded.pagesDir, effectiveCwd);
409
422
  loaded.root = resolved.root;
410
423
  loaded.pagesDir = resolved.pagesDir;
411
424
  if (loaded.root && loaded.root !== ".") {
@@ -417,6 +430,7 @@ export async function loadStxConfig(cwd) {
417
430
  if (loaded.layoutsDir && !path.isAbsolute(loaded.layoutsDir))
418
431
  loaded.layoutsDir = path.join(rootPrefix, loaded.layoutsDir);
419
432
  }
433
+ warnMissingDirs(loaded, effectiveCwd);
420
434
  if (loaded.plugins && loaded.plugins.length > 0) {
421
435
  const { pluginManager } = await import("./plugin-system.js"), pluginComponentDirs = [], pluginPageDirs = [];
422
436
  for (const pluginEntry of loaded.plugins) {
@@ -434,8 +448,9 @@ export async function loadStxConfig(cwd) {
434
448
  const plugin = pluginModule.default || pluginModule;
435
449
  pluginManager.register(plugin, loaded);
436
450
  if (plugin.components) {
437
- const dir = path.resolve(pluginDir, plugin.components);
438
- pluginComponentDirs.push(dir);
451
+ const dirs = Array.isArray(plugin.components) ? plugin.components : [plugin.components];
452
+ for (const entry of dirs)
453
+ pluginComponentDirs.push(path.resolve(pluginDir, entry));
439
454
  }
440
455
  if (plugin.pages) {
441
456
  const dir = path.resolve(pluginDir, plugin.pages), prefix = pluginOptions.path || "/";