@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.
- package/dist/app-shell.js +28 -1
- package/dist/browser-composables.d.ts +24 -9
- package/dist/build-id.d.ts +49 -0
- package/dist/build-id.js +20 -0
- package/dist/cli.js +897 -659
- package/dist/client-script.js +2 -2
- package/dist/color-mode-boot.d.ts +15 -10
- package/dist/color-mode-boot.js +12 -8
- package/dist/component-client-factories.js +3 -2
- package/dist/component-processing.d.ts +16 -0
- package/dist/component-processing.js +4 -1
- package/dist/component-renderer.js +2 -2
- package/dist/composables/index.d.ts +6 -1
- package/dist/composables/use-color-mode.d.ts +19 -4
- package/dist/composables/use-color-mode.js +37 -24
- package/dist/composables/use-device-orientation.d.ts +8 -1
- package/dist/composables/use-event-source.d.ts +14 -1
- package/dist/composables/use-eye-dropper.d.ts +10 -1
- package/dist/composables/use-idle.d.ts +21 -3
- package/dist/composables/use-storage.d.ts +15 -10
- package/dist/composables/use-storage.js +63 -2
- package/dist/composables/use-text-selection.d.ts +27 -3
- package/dist/composables/use-wake-lock.d.ts +2 -2
- package/dist/craft.js +625 -387
- package/dist/dev-server/serve-app.js +9 -5
- package/dist/dev-server/types.d.ts +8 -1
- package/dist/document-shell.d.ts +55 -0
- package/dist/document-shell.js +103 -3
- package/dist/edge-runtime.d.ts +1 -1
- package/dist/head.js +5 -1
- package/dist/includes.js +4 -2
- package/dist/index.d.ts +17 -0
- package/dist/index.js +9 -0
- package/dist/process.js +7 -3
- package/dist/production-builder.js +3 -1
- package/dist/pwa.js +591 -353
- package/dist/render.d.ts +1 -1
- package/dist/render.js +2 -2
- package/dist/signals.js +280 -44
- package/dist/ssg.js +603 -365
- package/dist/ssr.d.ts +17 -1
- package/dist/stx.d.ts +14 -6
- package/dist/types/config-types.d.ts +1 -0
- package/dist/utils.js +1 -8
- package/dist/variable-extractor.d.ts +8 -0
- package/dist/variable-extractor.js +24 -9
- package/dist/visual-testing.d.ts +26 -3
- package/package.json +5 -5
- package/stx.d.ts +14 -6
package/dist/client-script.js
CHANGED
|
@@ -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
|
-
*
|
|
56
|
+
* preference reading precisely:
|
|
57
57
|
*
|
|
58
|
-
* - `attribute` and `darkClass`
|
|
59
|
-
*
|
|
60
|
-
*
|
|
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
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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';
|
package/dist/color-mode-boot.js
CHANGED
|
@@ -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",
|
|
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(
|
|
8
|
-
throw Error(`colorMode.initialMode must be one of light, dark, auto \u2014 got ${JSON.stringify(
|
|
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
|
-
|
|
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"
|
|
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}
|
|
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 & 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(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/�?39;/g, "'").replace(/'/gi, "'").replace(/&/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?:
|
|
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:
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
46
|
+
function readRaw() {
|
|
37
47
|
if (!isBrowser)
|
|
38
|
-
return;
|
|
48
|
+
return null;
|
|
39
49
|
try {
|
|
40
|
-
localStorage.
|
|
41
|
-
} catch {
|
|
50
|
+
return localStorage.getItem(storageKey);
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
42
54
|
}
|
|
43
|
-
|
|
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
|
|
59
|
+
return;
|
|
46
60
|
try {
|
|
47
|
-
|
|
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(
|
|
64
|
+
function update(pref, persistChoice = !0) {
|
|
65
|
+
preference = normalizeColorMode(pref) ?? "auto";
|
|
66
|
+
resolved = resolve(preference);
|
|
56
67
|
applyToDOM(resolved);
|
|
57
|
-
|
|
68
|
+
if (persistChoice)
|
|
69
|
+
persist(preference);
|
|
58
70
|
listeners.forEach((fn) => fn(resolved, preference));
|
|
59
71
|
}
|
|
60
|
-
|
|
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
|
|
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():
|
|
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):
|
|
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
|
-
}):
|
|
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):
|
|
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():
|
|
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 }):
|
|
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
|
-
*
|
|
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'>):
|
|
35
|
+
export declare function useLocalStorage<T>(key: string, defaultValue: T, options?: Omit<UseStorageOptions<T>, 'storage'>): Signal<T>;
|
|
25
36
|
/**
|
|
26
|
-
*
|
|
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'>):
|
|
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
|
|
171
|
+
return storageSignal("local", "useLocalStorage", key, defaultValue, options);
|
|
111
172
|
}
|
|
112
173
|
export function useSessionStorage(key, defaultValue, options) {
|
|
113
|
-
return
|
|
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):
|
|
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
|
-
}):
|
|
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():
|
|
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
|