@ai-matrx/kit 0.5.0
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/CHANGELOG.md +132 -0
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/autosave.cjs +92 -0
- package/dist/autosave.cjs.map +1 -0
- package/dist/autosave.d.cts +17 -0
- package/dist/autosave.d.ts +17 -0
- package/dist/autosave.js +72 -0
- package/dist/autosave.js.map +1 -0
- package/dist/clipboard.cjs +152 -0
- package/dist/clipboard.cjs.map +1 -0
- package/dist/clipboard.d.cts +17 -0
- package/dist/clipboard.d.ts +17 -0
- package/dist/clipboard.js +132 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/color-util.cjs +790 -0
- package/dist/color-util.cjs.map +1 -0
- package/dist/color-util.d.cts +235 -0
- package/dist/color-util.d.ts +235 -0
- package/dist/color-util.js +767 -0
- package/dist/color-util.js.map +1 -0
- package/dist/concurrency.cjs +59 -0
- package/dist/concurrency.cjs.map +1 -0
- package/dist/concurrency.d.cts +19 -0
- package/dist/concurrency.d.ts +19 -0
- package/dist/concurrency.js +38 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/confirm.cjs +344 -0
- package/dist/confirm.cjs.map +1 -0
- package/dist/confirm.d.cts +132 -0
- package/dist/confirm.d.ts +132 -0
- package/dist/confirm.js +312 -0
- package/dist/confirm.js.map +1 -0
- package/dist/delimiter-guard.cjs +172 -0
- package/dist/delimiter-guard.cjs.map +1 -0
- package/dist/delimiter-guard.d.cts +119 -0
- package/dist/delimiter-guard.d.ts +119 -0
- package/dist/delimiter-guard.js +151 -0
- package/dist/delimiter-guard.js.map +1 -0
- package/dist/drafts.cjs +223 -0
- package/dist/drafts.cjs.map +1 -0
- package/dist/drafts.d.cts +62 -0
- package/dist/drafts.d.ts +62 -0
- package/dist/drafts.js +201 -0
- package/dist/drafts.js.map +1 -0
- package/dist/idb-store.cjs +178 -0
- package/dist/idb-store.cjs.map +1 -0
- package/dist/idb-store.d.cts +111 -0
- package/dist/idb-store.d.ts +111 -0
- package/dist/idb-store.js +155 -0
- package/dist/idb-store.js.map +1 -0
- package/dist/idle-scheduler.cjs +268 -0
- package/dist/idle-scheduler.cjs.map +1 -0
- package/dist/idle-scheduler.d.cts +185 -0
- package/dist/idle-scheduler.d.ts +185 -0
- package/dist/idle-scheduler.js +246 -0
- package/dist/idle-scheduler.js.map +1 -0
- package/dist/index.cjs +3113 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +897 -0
- package/dist/index.d.ts +897 -0
- package/dist/index.js +3088 -0
- package/dist/index.js.map +1 -0
- package/dist/invalidation.cjs +64 -0
- package/dist/invalidation.cjs.map +1 -0
- package/dist/invalidation.d.cts +52 -0
- package/dist/invalidation.d.ts +52 -0
- package/dist/invalidation.js +43 -0
- package/dist/invalidation.js.map +1 -0
- package/dist/json-format.cjs +319 -0
- package/dist/json-format.cjs.map +1 -0
- package/dist/json-format.d.cts +148 -0
- package/dist/json-format.d.ts +148 -0
- package/dist/json-format.js +286 -0
- package/dist/json-format.js.map +1 -0
- package/dist/latest-request.cjs +36 -0
- package/dist/latest-request.cjs.map +1 -0
- package/dist/latest-request.d.cts +29 -0
- package/dist/latest-request.d.ts +29 -0
- package/dist/latest-request.js +16 -0
- package/dist/latest-request.js.map +1 -0
- package/dist/qr.cjs +109 -0
- package/dist/qr.cjs.map +1 -0
- package/dist/qr.d.cts +47 -0
- package/dist/qr.d.ts +47 -0
- package/dist/qr.js +78 -0
- package/dist/qr.js.map +1 -0
- package/dist/react-tree.cjs +69 -0
- package/dist/react-tree.cjs.map +1 -0
- package/dist/react-tree.d.cts +38 -0
- package/dist/react-tree.d.ts +38 -0
- package/dist/react-tree.js +38 -0
- package/dist/react-tree.js.map +1 -0
- package/dist/search-scoring.cjs +116 -0
- package/dist/search-scoring.cjs.map +1 -0
- package/dist/search-scoring.d.cts +77 -0
- package/dist/search-scoring.d.ts +77 -0
- package/dist/search-scoring.js +95 -0
- package/dist/search-scoring.js.map +1 -0
- package/dist/text-case.cjs +313 -0
- package/dist/text-case.cjs.map +1 -0
- package/dist/text-case.d.cts +53 -0
- package/dist/text-case.d.ts +53 -0
- package/dist/text-case.js +292 -0
- package/dist/text-case.js.map +1 -0
- package/dist/toast.cjs +74 -0
- package/dist/toast.cjs.map +1 -0
- package/dist/toast.d.cts +92 -0
- package/dist/toast.d.ts +92 -0
- package/dist/toast.js +54 -0
- package/dist/toast.js.map +1 -0
- package/dist/url-state.cjs +212 -0
- package/dist/url-state.cjs.map +1 -0
- package/dist/url-state.d.cts +90 -0
- package/dist/url-state.d.ts +90 -0
- package/dist/url-state.js +199 -0
- package/dist/url-state.js.map +1 -0
- package/package.json +286 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/toast.ts"],"sourcesContent":["/**\n * @ai-matrx/kit/toast — the captured sonner wrapper, as a factory.\n *\n * A bare `toast.error(...)` from \"sonner\" is INVISIBLE to any error-capture\n * sink — every modern sonner call site silently bypasses error capture (the\n * exact hole matrx-frontend found in its marketing feature, 2026-07-20).\n * `createMatrxToast` returns a drop-in `toast` whose API is identical to the\n * one you pass in — `error` and `warning` additionally feed the injected\n * capture sink. A user seeing the failure is not evidence that it is minor,\n * so error toasts stay red unless a specific downgrade rule says otherwise.\n * Success/info/etc. pass straight through untouched.\n *\n * Ported from matrx-frontend `lib/toast.ts` with the two seams inverted:\n *\n * - `sonner` is NOT imported: the host passes its own `toast` object into the\n * factory (`createMatrxToast({ toast })`). That removes the peer-resolution\n * problem entirely — this module has zero dependencies — and the input is\n * typed STRUCTURALLY (callable + `.error` + `.warning`) so the returned\n * `toast` keeps the host's full sonner type, and no `.d.ts` in this package\n * references \"sonner\". The `sonner` entry in `peerDependencies` is\n * advisory-only (optional; nothing resolves it at runtime).\n * - `captureError` from the app's diagnostics store becomes the injected\n * `capture?: (info) => void`. Omitted, capture is a no-op and every call\n * forwards identically (the original's payload shape is preserved exactly:\n * `source: \"user-toast\"`, the `[warning] ` prefix, the \"Error toast\"\n * fallback, `raw: { kind, message?, data }`). A throwing `capture` never\n * breaks the toast.\n *\n * Usage (once, in the host app):\n * import { toast as sonnerToast } from \"sonner\";\n * export const { toast, toastErrorAlreadyCaptured } =\n * createMatrxToast({ toast: sonnerToast, capture: captureError });\n */\n\nimport type { ReactNode } from \"react\";\n\n/** What sonner accepts as a toast title (structural: sonner's `titleT`). */\nexport type ToastMessage = ReactNode | (() => ReactNode);\n\n/** Structural stand-in for sonner's `ExternalToast` options bag. */\nexport interface ToastData {\n description?: unknown;\n [key: string]: unknown;\n}\n\n/**\n * The structural surface this factory needs from the injected toast object:\n * callable, with `error` and `warning` methods. sonner's `toast` satisfies\n * this for every version this package targets. Deliberate typing choices so\n * the REAL sonner object is assignable without a cast under\n * `strictFunctionTypes`: the call signature uses `never[]` (accepts any\n * function — consumers call through their own `T`, never this signature),\n * and `error`/`warning` use METHOD syntax for bivariant parameter checks\n * against sonner's `ExternalToast`.\n */\nexport interface SonnerLikeToast {\n (...args: never[]): string | number;\n error(message: ToastMessage, data?: ToastData): string | number;\n warning(message: ToastMessage, data?: ToastData): string | number;\n}\n\n/** The payload handed to the injected capture sink — the original's shape. */\nexport interface CapturedToastInfo {\n source: \"user-toast\";\n message: string;\n userMessage: string;\n raw: {\n kind: \"error\" | \"warning\";\n message: string | undefined;\n data: ToastData | undefined;\n };\n}\n\nexport interface CreateMatrxToastOptions<T extends SonnerLikeToast> {\n /** The host's sonner `toast` object (or any structural equivalent). */\n toast: T;\n /**\n * Error-capture sink fed by `.error` / `.warning`. Omitted: no-op.\n * Must never be load-bearing — a throw here is swallowed.\n */\n capture?: (info: CapturedToastInfo) => void;\n}\n\nexport interface MatrxToast<T extends SonnerLikeToast> {\n /** Drop-in replacement for the injected `toast`, with error/warning capture. */\n toast: T;\n /**\n * Render an error toast when the originating failure was already captured\n * at its canonical boundary. Only for aggregate/derived UI notices; the\n * caller must be able to name the upstream capture seam.\n */\n toastErrorAlreadyCaptured: T[\"error\"];\n}\n\nfunction messageText(message: ToastMessage, data?: ToastData): string {\n const description =\n data && typeof data.description === \"string\" ? data.description : \"\";\n const title = typeof message === \"string\" ? message : \"\";\n return [title, description].filter(Boolean).join(\" — \") || \"Error toast\";\n}\n\n/** Build the captured toast pair around the host's sonner `toast` object. */\nexport function createMatrxToast<T extends SonnerLikeToast>({\n toast: hostToast,\n capture,\n}: CreateMatrxToastOptions<T>): MatrxToast<T> {\n if (typeof hostToast !== \"function\") {\n throw new Error(\n \"createMatrxToast: options.toast must be the sonner `toast` object (or a structural equivalent) — got \" +\n typeof hostToast,\n );\n }\n\n function captureToast(\n kind: \"error\" | \"warning\",\n message: ToastMessage,\n data?: ToastData,\n ): void {\n if (!capture) return;\n try {\n capture({\n source: \"user-toast\",\n message: `${kind === \"warning\" ? \"[warning] \" : \"\"}${messageText(message, data)}`,\n userMessage: messageText(message, data),\n raw: {\n kind,\n message: typeof message === \"string\" ? message : undefined,\n data,\n },\n });\n } catch {\n /* capture must never break the toast */\n }\n }\n\n const error: SonnerLikeToast[\"error\"] = (message, data) => {\n captureToast(\"error\", message, data);\n return hostToast.error(message, data);\n };\n\n const warning: SonnerLikeToast[\"warning\"] = (message, data) => {\n captureToast(\"warning\", message, data);\n return hostToast.warning(message, data);\n };\n\n // The constraint's call signature is `never[]` (see SonnerLikeToast), so\n // forwarding the base call goes through a widened alias of the host object.\n const forward = hostToast as unknown as (\n ...args: Parameters<T>\n ) => ReturnType<T>;\n const toast: T = Object.assign(\n ((...args: Parameters<T>) => forward(...args)) as unknown as T,\n hostToast,\n { error, warning },\n );\n\n const toastErrorAlreadyCaptured = ((\n message: ToastMessage,\n data?: ToastData,\n ) => hostToast.error(message, data)) as unknown as T[\"error\"];\n\n return { toast, toastErrorAlreadyCaptured };\n}\n"],"mappings":";;;AA8FA,SAAS,YAAY,SAAuB,MAA0B;AACpE,QAAM,cACJ,QAAQ,OAAO,KAAK,gBAAgB,WAAW,KAAK,cAAc;AACpE,QAAM,QAAQ,OAAO,YAAY,WAAW,UAAU;AACtD,SAAO,CAAC,OAAO,WAAW,EAAE,OAAO,OAAO,EAAE,KAAK,UAAK,KAAK;AAC7D;AAGO,SAAS,iBAA4C;AAAA,EAC1D,OAAO;AAAA,EACP;AACF,GAA8C;AAC5C,MAAI,OAAO,cAAc,YAAY;AACnC,UAAM,IAAI;AAAA,MACR,+GACE,OAAO;AAAA,IACX;AAAA,EACF;AAEA,WAAS,aACP,MACA,SACA,MACM;AACN,QAAI,CAAC,QAAS;AACd,QAAI;AACF,cAAQ;AAAA,QACN,QAAQ;AAAA,QACR,SAAS,GAAG,SAAS,YAAY,eAAe,EAAE,GAAG,YAAY,SAAS,IAAI,CAAC;AAAA,QAC/E,aAAa,YAAY,SAAS,IAAI;AAAA,QACtC,KAAK;AAAA,UACH;AAAA,UACA,SAAS,OAAO,YAAY,WAAW,UAAU;AAAA,UACjD;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,QAAkC,CAAC,SAAS,SAAS;AACzD,iBAAa,SAAS,SAAS,IAAI;AACnC,WAAO,UAAU,MAAM,SAAS,IAAI;AAAA,EACtC;AAEA,QAAM,UAAsC,CAAC,SAAS,SAAS;AAC7D,iBAAa,WAAW,SAAS,IAAI;AACrC,WAAO,UAAU,QAAQ,SAAS,IAAI;AAAA,EACxC;AAIA,QAAM,UAAU;AAGhB,QAAM,QAAW,OAAO;AAAA,KACrB,IAAI,SAAwB,QAAQ,GAAG,IAAI;AAAA,IAC5C;AAAA,IACA,EAAE,OAAO,QAAQ;AAAA,EACnB;AAEA,QAAM,6BAA6B,CACjC,SACA,SACG,UAAU,MAAM,SAAS,IAAI;AAElC,SAAO,EAAE,OAAO,0BAA0B;AAC5C;","names":[]}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
"use client";
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
|
+
|
|
22
|
+
// src/url-state.ts
|
|
23
|
+
var url_state_exports = {};
|
|
24
|
+
__export(url_state_exports, {
|
|
25
|
+
booleanUrlCodec: () => booleanUrlCodec,
|
|
26
|
+
commitUrlParams: () => commitUrlParams,
|
|
27
|
+
enumUrlCodec: () => enumUrlCodec,
|
|
28
|
+
historyModeForParamChange: () => historyModeForParamChange,
|
|
29
|
+
jsonUrlCodec: () => jsonUrlCodec,
|
|
30
|
+
positiveIntegerUrlCodec: () => positiveIntegerUrlCodec,
|
|
31
|
+
setUrlStateRouter: () => setUrlStateRouter,
|
|
32
|
+
stringUrlCodec: () => stringUrlCodec,
|
|
33
|
+
useMirroredUrlState: () => useMirroredUrlState,
|
|
34
|
+
useUrlSearchParams: () => useUrlSearchParams,
|
|
35
|
+
useUrlState: () => useUrlState
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(url_state_exports);
|
|
38
|
+
var import_react = require("react");
|
|
39
|
+
var URL_STATE_EVENT = "matrx:url-state";
|
|
40
|
+
var ROUTER_SLOT = /* @__PURE__ */ Symbol.for("ai-matrx.kit.url-state-router");
|
|
41
|
+
function getRouter() {
|
|
42
|
+
return globalThis[ROUTER_SLOT] ?? null;
|
|
43
|
+
}
|
|
44
|
+
function setUrlStateRouter(router) {
|
|
45
|
+
globalThis[ROUTER_SLOT] = router;
|
|
46
|
+
}
|
|
47
|
+
function currentLocation() {
|
|
48
|
+
const custom = getRouter()?.getLocation;
|
|
49
|
+
if (custom) return custom();
|
|
50
|
+
return {
|
|
51
|
+
pathname: window.location.pathname,
|
|
52
|
+
search: window.location.search,
|
|
53
|
+
hash: window.location.hash
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function subscribeToUrl(listener) {
|
|
57
|
+
window.addEventListener("popstate", listener);
|
|
58
|
+
window.addEventListener(URL_STATE_EVENT, listener);
|
|
59
|
+
return () => {
|
|
60
|
+
window.removeEventListener("popstate", listener);
|
|
61
|
+
window.removeEventListener(URL_STATE_EVENT, listener);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function getUrlSnapshot() {
|
|
65
|
+
return currentLocation().search;
|
|
66
|
+
}
|
|
67
|
+
function getServerUrlSnapshot() {
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
function useUrlSearchParams() {
|
|
71
|
+
const search = (0, import_react.useSyncExternalStore)(
|
|
72
|
+
subscribeToUrl,
|
|
73
|
+
getUrlSnapshot,
|
|
74
|
+
getServerUrlSnapshot
|
|
75
|
+
);
|
|
76
|
+
return (0, import_react.useMemo)(() => new URLSearchParams(search), [search]);
|
|
77
|
+
}
|
|
78
|
+
function commitUrlParams(patch, history) {
|
|
79
|
+
const location = currentLocation();
|
|
80
|
+
const params = new URLSearchParams(location.search);
|
|
81
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
82
|
+
if (value === null || value === "") params.delete(key);
|
|
83
|
+
else params.set(key, value);
|
|
84
|
+
}
|
|
85
|
+
const query = params.toString();
|
|
86
|
+
const next = `${location.pathname}${query ? `?${query}` : ""}${location.hash}`;
|
|
87
|
+
const current = `${location.pathname}${location.search}${location.hash}`;
|
|
88
|
+
if (next === current) return;
|
|
89
|
+
const router = getRouter();
|
|
90
|
+
if (router) {
|
|
91
|
+
if (history === "replace") router.replace(next);
|
|
92
|
+
else router.push(next);
|
|
93
|
+
} else if (history === "replace") {
|
|
94
|
+
window.history.replaceState(window.history.state, "", next);
|
|
95
|
+
} else {
|
|
96
|
+
window.history.pushState(window.history.state, "", next);
|
|
97
|
+
}
|
|
98
|
+
window.dispatchEvent(new Event(URL_STATE_EVENT));
|
|
99
|
+
}
|
|
100
|
+
function historyModeForParamChange(current, next, textKeys) {
|
|
101
|
+
const keys = /* @__PURE__ */ new Set([...current.keys(), ...next.keys()]);
|
|
102
|
+
const changed = [...keys].filter(
|
|
103
|
+
(key) => current.get(key) !== next.get(key)
|
|
104
|
+
);
|
|
105
|
+
if (changed.length === 0) return "replace";
|
|
106
|
+
return changed.every((key) => textKeys.includes(key)) ? "replace" : "push";
|
|
107
|
+
}
|
|
108
|
+
function useUrlState(key, codec) {
|
|
109
|
+
const searchParams = useUrlSearchParams();
|
|
110
|
+
const value = codec.parse(searchParams.get(key));
|
|
111
|
+
const setValue = (next, options) => {
|
|
112
|
+
commitUrlParams(
|
|
113
|
+
{ [key]: codec.serialize(next) },
|
|
114
|
+
options?.history ?? "push"
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
return [value, setValue];
|
|
118
|
+
}
|
|
119
|
+
function stringUrlCodec(defaultValue = "") {
|
|
120
|
+
return {
|
|
121
|
+
defaultValue,
|
|
122
|
+
parse: (raw) => raw ?? defaultValue,
|
|
123
|
+
serialize: (value) => value === defaultValue ? null : value
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function enumUrlCodec(values, defaultValue) {
|
|
127
|
+
const allowed = new Set(values);
|
|
128
|
+
return {
|
|
129
|
+
defaultValue,
|
|
130
|
+
parse: (raw) => raw && allowed.has(raw) ? raw : defaultValue,
|
|
131
|
+
serialize: (value) => value === defaultValue ? null : value
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function booleanUrlCodec(defaultValue = false) {
|
|
135
|
+
return {
|
|
136
|
+
defaultValue,
|
|
137
|
+
parse: (raw) => raw === null ? defaultValue : raw === "1",
|
|
138
|
+
serialize: (value) => value === defaultValue ? null : value ? "1" : "0"
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function positiveIntegerUrlCodec(defaultValue) {
|
|
142
|
+
return {
|
|
143
|
+
defaultValue,
|
|
144
|
+
parse: (raw) => {
|
|
145
|
+
const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
|
|
146
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultValue;
|
|
147
|
+
},
|
|
148
|
+
serialize: (value) => value === defaultValue || !Number.isFinite(value) || value <= 0 ? null : String(Math.trunc(value))
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function jsonUrlCodec(defaultValue, isValid) {
|
|
152
|
+
return {
|
|
153
|
+
defaultValue,
|
|
154
|
+
parse: (raw) => {
|
|
155
|
+
if (!raw) return defaultValue;
|
|
156
|
+
try {
|
|
157
|
+
const parsed = JSON.parse(raw);
|
|
158
|
+
return isValid(parsed) ? parsed : defaultValue;
|
|
159
|
+
} catch {
|
|
160
|
+
return defaultValue;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
serialize: (value) => JSON.stringify(value) === JSON.stringify(defaultValue) ? null : JSON.stringify(value)
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function useMirroredUrlState(options) {
|
|
167
|
+
const { parse, toParams, isSame, textKeys = [], resetKey } = options;
|
|
168
|
+
const params = useUrlSearchParams();
|
|
169
|
+
const optionsRef = (0, import_react.useRef)(options);
|
|
170
|
+
optionsRef.current = options;
|
|
171
|
+
const [value, setValue] = (0, import_react.useState)(
|
|
172
|
+
() => parse(
|
|
173
|
+
typeof window === "undefined" ? new URLSearchParams() : new URLSearchParams(currentLocation().search)
|
|
174
|
+
)
|
|
175
|
+
);
|
|
176
|
+
(0, import_react.useEffect)(() => {
|
|
177
|
+
if (typeof window === "undefined") return;
|
|
178
|
+
const current = new URLSearchParams(currentLocation().search);
|
|
179
|
+
const patch = optionsRef.current.toParams(value);
|
|
180
|
+
const next = new URLSearchParams(current);
|
|
181
|
+
for (const [key, v] of Object.entries(patch)) {
|
|
182
|
+
if (v === null || v === "") next.delete(key);
|
|
183
|
+
else next.set(key, v);
|
|
184
|
+
}
|
|
185
|
+
if (next.toString() === current.toString()) return;
|
|
186
|
+
commitUrlParams(patch, historyModeForParamChange(current, next, textKeys));
|
|
187
|
+
}, [value]);
|
|
188
|
+
(0, import_react.useEffect)(() => {
|
|
189
|
+
const fromUrl = optionsRef.current.parse(
|
|
190
|
+
new URLSearchParams(currentLocation().search)
|
|
191
|
+
);
|
|
192
|
+
setValue((prev) => optionsRef.current.isSame(prev, fromUrl) ? prev : fromUrl);
|
|
193
|
+
}, [params]);
|
|
194
|
+
const previousResetKey = (0, import_react.useRef)(resetKey);
|
|
195
|
+
(0, import_react.useEffect)(() => {
|
|
196
|
+
const previous = previousResetKey.current;
|
|
197
|
+
previousResetKey.current = resetKey;
|
|
198
|
+
if (previous === resetKey) return;
|
|
199
|
+
if (previous === void 0 || previous === "") return;
|
|
200
|
+
if (resetKey === void 0 || resetKey === "") return;
|
|
201
|
+
const cleared = optionsRef.current.parse(new URLSearchParams());
|
|
202
|
+
setValue(cleared);
|
|
203
|
+
commitUrlParams(optionsRef.current.toParams(cleared), "replace");
|
|
204
|
+
}, [resetKey]);
|
|
205
|
+
const update = (0, import_react.useCallback)((updater) => {
|
|
206
|
+
setValue(
|
|
207
|
+
(prev) => typeof updater === "function" ? updater(prev) : updater
|
|
208
|
+
);
|
|
209
|
+
}, []);
|
|
210
|
+
return [value, update];
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=url-state.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/url-state.ts"],"sourcesContent":["\"use client\";\n\n/**\n * @ai-matrx/kit/url-state — THE canonical URL-state core. Every surface that\n * puts view state in the address bar goes through here.\n *\n * The URL is the source of truth: refresh, copied links, and browser\n * back/forward all reproduce the same value. Discrete controls push a history\n * entry by default; high-frequency text inputs opt into `replace` so a single\n * search does not create one entry per keystroke.\n *\n * WHY A RAW `history.pushState` IS A BUG, not just a style choice. It fires no\n * event and no popstate, so every OTHER url-backed control on the page keeps\n * rendering stale values until something unrelated re-renders it.\n * `commitUrlParams` dispatches `matrx:url-state`, which is what\n * `useUrlSearchParams` subscribes to. (Measured in the Matrx frontend,\n * 2026-08-25: 33 hand-rolled writes across 21 files, every one of them\n * silent.)\n *\n * PICK THE RIGHT ONE:\n * one control owns one parameter → `useUrlState` + a codec\n * a cluster of values moving together → `useMirroredUrlState`\n *\n * Codecs below cover string / enum / boolean / positive-integer / JSON, and all\n * of them OMIT the default rather than writing it, so a pristine surface has a\n * clean URL and a link carries only what the user actually chose.\n *\n * Ported verbatim from matrx-frontend `lib/url-state/useUrlState.ts`. The\n * original deliberately imports NOTHING from `next/navigation` — it writes\n * through the History API and notifies subscribers itself, which is exactly\n * how it runs in the Next.js App Router host. This port adds ONE optional\n * seam on top of that verbatim behavior: `setUrlStateRouter` lets a host whose\n * router must observe/perform the writes (react-router, a memory router in\n * tests, a native shell) inject `{ push, replace, getLocation? }`. With no\n * router injected — the default, and the Next.js wiring — behavior is\n * byte-identical to the original. The injected router lives on `globalThis`\n * under `Symbol.for(\"ai-matrx.kit.url-state-router\")` because the package\n * builds `splitting: false` dual ESM/CJS: module-level state would silently\n * split the registration across bundle graphs (the confirm-opener hazard).\n */\n\nimport {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n} from \"react\";\n\nconst URL_STATE_EVENT = \"matrx:url-state\";\n\n/**\n * The optional injected router. `push`/`replace` receive the full relative\n * URL (`pathname?query#hash`) exactly as the History API default would write\n * it. `getLocation` overrides where the current URL is read from (a memory\n * router); absent, `window.location` is read.\n */\nexport interface UrlStateRouter {\n push: (url: string) => void;\n replace: (url: string) => void;\n getLocation?: (() => { pathname: string; search: string; hash: string }) | undefined;\n}\n\nconst ROUTER_SLOT = Symbol.for(\"ai-matrx.kit.url-state-router\");\n\nfunction getRouter(): UrlStateRouter | null {\n return (\n (globalThis as Record<symbol, UrlStateRouter | null | undefined>)[\n ROUTER_SLOT\n ] ?? null\n );\n}\n\n/**\n * Inject (or with `null` remove) the host router. Optional — with none set,\n * writes go through `window.history` push/replaceState, which is the verbatim\n * Matrx-frontend behavior and correct for Next.js App Router hosts.\n */\nexport function setUrlStateRouter(router: UrlStateRouter | null): void {\n (globalThis as Record<symbol, UrlStateRouter | null | undefined>)[\n ROUTER_SLOT\n ] = router;\n}\n\nfunction currentLocation(): { pathname: string; search: string; hash: string } {\n const custom = getRouter()?.getLocation;\n if (custom) return custom();\n return {\n pathname: window.location.pathname,\n search: window.location.search,\n hash: window.location.hash,\n };\n}\n\nfunction subscribeToUrl(listener: () => void) {\n window.addEventListener(\"popstate\", listener);\n window.addEventListener(URL_STATE_EVENT, listener);\n return () => {\n window.removeEventListener(\"popstate\", listener);\n window.removeEventListener(URL_STATE_EVENT, listener);\n };\n}\n\nfunction getUrlSnapshot() {\n return currentLocation().search;\n}\n\nfunction getServerUrlSnapshot() {\n return \"\";\n}\n\n/** Reactive query-string snapshot for URL-backed primitives. */\nexport function useUrlSearchParams(): URLSearchParams {\n const search = useSyncExternalStore(\n subscribeToUrl,\n getUrlSnapshot,\n getServerUrlSnapshot,\n );\n return useMemo(() => new URLSearchParams(search), [search]);\n}\n\nexport type UrlHistoryMode = \"push\" | \"replace\";\n\nexport interface UrlStateCodec<T> {\n defaultValue: T;\n parse: (raw: string | null) => T;\n serialize: (value: T) => string | null;\n}\n\nexport interface SetUrlStateOptions {\n history?: UrlHistoryMode | undefined;\n}\n\nexport function commitUrlParams(\n patch: Readonly<Record<string, string | null>>,\n history: UrlHistoryMode,\n) {\n const location = currentLocation();\n const params = new URLSearchParams(location.search);\n for (const [key, value] of Object.entries(patch)) {\n if (value === null || value === \"\") params.delete(key);\n else params.set(key, value);\n }\n\n const query = params.toString();\n const next = `${location.pathname}${query ? `?${query}` : \"\"}${location.hash}`;\n const current = `${location.pathname}${location.search}${location.hash}`;\n if (next === current) return;\n\n const router = getRouter();\n if (router) {\n if (history === \"replace\") router.replace(next);\n else router.push(next);\n } else if (history === \"replace\") {\n window.history.replaceState(window.history.state, \"\", next);\n } else {\n window.history.pushState(window.history.state, \"\", next);\n }\n window.dispatchEvent(new Event(URL_STATE_EVENT));\n}\n\n/**\n * Classify a URL transition for surfaces that MIRROR state (a store, local\n * state) into the URL from an effect, where there is no single call site to\n * label.\n *\n * THE RULE: a discrete user decision (tab, filter, sort, page, selection)\n * PUSHES, so Back undoes exactly that one step; only high-frequency text\n * (`textKeys` — search boxes, a slider being dragged) REPLACES, so one search\n * is one entry instead of one per keystroke.\n */\nexport function historyModeForParamChange(\n current: URLSearchParams,\n next: URLSearchParams,\n textKeys: readonly string[],\n): UrlHistoryMode {\n const keys = new Set([...current.keys(), ...next.keys()]);\n const changed = [...keys].filter(\n (key) => current.get(key) !== next.get(key),\n );\n if (changed.length === 0) return \"replace\";\n return changed.every((key) => textKeys.includes(key)) ? \"replace\" : \"push\";\n}\n\nexport function useUrlState<T>(\n key: string,\n codec: UrlStateCodec<T>,\n): readonly [T, (value: T, options?: SetUrlStateOptions) => void] {\n const searchParams = useUrlSearchParams();\n const value = codec.parse(searchParams.get(key));\n\n const setValue = (next: T, options?: SetUrlStateOptions) => {\n commitUrlParams(\n { [key]: codec.serialize(next) },\n options?.history ?? \"push\",\n );\n };\n\n return [value, setValue] as const;\n}\n\nexport function stringUrlCodec(defaultValue = \"\"): UrlStateCodec<string> {\n return {\n defaultValue,\n parse: (raw) => raw ?? defaultValue,\n serialize: (value) => (value === defaultValue ? null : value),\n };\n}\n\nexport function enumUrlCodec<const T extends string>(\n values: readonly T[],\n defaultValue: T,\n): UrlStateCodec<T> {\n const allowed = new Set<string>(values);\n return {\n defaultValue,\n parse: (raw) => (raw && allowed.has(raw) ? (raw as T) : defaultValue),\n serialize: (value) => (value === defaultValue ? null : value),\n };\n}\n\nexport function booleanUrlCodec(defaultValue = false): UrlStateCodec<boolean> {\n return {\n defaultValue,\n parse: (raw) => (raw === null ? defaultValue : raw === \"1\"),\n serialize: (value) => (value === defaultValue ? null : value ? \"1\" : \"0\"),\n };\n}\n\nexport function positiveIntegerUrlCodec(\n defaultValue: number,\n): UrlStateCodec<number> {\n return {\n defaultValue,\n parse: (raw) => {\n const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;\n return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultValue;\n },\n serialize: (value) =>\n value === defaultValue || !Number.isFinite(value) || value <= 0\n ? null\n : String(Math.trunc(value)),\n };\n}\n\nexport function jsonUrlCodec<T>(\n defaultValue: T,\n isValid: (value: unknown) => value is T,\n): UrlStateCodec<T> {\n return {\n defaultValue,\n parse: (raw) => {\n if (!raw) return defaultValue;\n try {\n const parsed: unknown = JSON.parse(raw);\n return isValid(parsed) ? parsed : defaultValue;\n } catch {\n return defaultValue;\n }\n },\n serialize: (value) =>\n JSON.stringify(value) === JSON.stringify(defaultValue)\n ? null\n : JSON.stringify(value),\n };\n}\n\n/**\n * Mirror a whole view-state OBJECT into the URL, in both directions.\n *\n * THE PATTERN 18 FILES WERE HAND-ROLLING. `useUrlState` is right when one\n * control owns one parameter. It is the wrong shape when a surface holds a\n * cluster of related values (search + sort + filters + page) that must move\n * together, be seeded from the URL on first render, and follow Back/Forward.\n * Every surface that needed that wrote its own `history.pushState` — and NONE\n * of them dispatched the sync event, so any other URL-backed control on the\n * same page silently kept showing stale values after they wrote.\n *\n * 🚨 THE LOOP IS BROKEN BY VALUE, NEVER BY BOOKKEEPING. The obvious guard —\n * remember the last URL you wrote and ignore anything matching it — looks\n * equivalent and is not: pressing Forward to a view already visited produces a\n * URL you did indeed write before, so the guard swallows it and the address bar\n * moves while the surface does not. Compare the DECODED value to the current\n * one instead; your own write compares equal and stops.\n *\n * Seeding happens in the `useState` initialiser, not an effect, so the first\n * render already shows the requested view rather than flashing the default and\n * fetching the wrong page before correcting itself.\n */\nexport interface MirroredUrlStateOptions<T> {\n /** Decode the whole value from the query string. Must never throw. */\n parse: (params: URLSearchParams) => T;\n /** Encode it; `null` for a key means \"omit\", which keeps defaults out of the URL. */\n toParams: (value: T) => Record<string, string | null>;\n /** Value equality — what stops the two directions fighting. */\n isSame: (a: T, b: T) => boolean;\n /** Keys that REPLACE instead of pushing (search boxes, dragged sliders). */\n textKeys?: readonly string[] | undefined;\n /**\n * Changing this clears the mirrored state and its parameters — for when the\n * surface switches to a different subject and the old view would be a lie.\n */\n resetKey?: string | undefined;\n}\n\nexport function useMirroredUrlState<T>(\n options: MirroredUrlStateOptions<T>,\n): readonly [T, (updater: T | ((prev: T) => T)) => void] {\n const { parse, toParams, isSame, textKeys = [], resetKey } = options;\n const params = useUrlSearchParams();\n\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const [value, setValue] = useState<T>(() =>\n parse(\n typeof window === \"undefined\"\n ? new URLSearchParams()\n : new URLSearchParams(currentLocation().search),\n ),\n );\n\n // value → URL\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n const current = new URLSearchParams(currentLocation().search);\n const patch = optionsRef.current.toParams(value);\n\n const next = new URLSearchParams(current);\n for (const [key, v] of Object.entries(patch)) {\n if (v === null || v === \"\") next.delete(key);\n else next.set(key, v);\n }\n if (next.toString() === current.toString()) return;\n\n commitUrlParams(patch, historyModeForParamChange(current, next, textKeys));\n // `textKeys` is a literal in every caller; `value` is the real trigger.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [value]);\n\n // URL → value (Back/Forward, a pasted link, another control writing)\n useEffect(() => {\n // Read the URL at EFFECT time, not the render snapshot. The value→URL\n // effect runs first. When it commits a local filter/sort decision it\n // synchronously notifies the external store, but `params` in this render\n // still describes the URL from before that write. Re-applying that stale\n // snapshot here ping-pongs state and URL until React raises #185.\n const fromUrl = optionsRef.current.parse(\n new URLSearchParams(currentLocation().search),\n );\n setValue((prev) => (optionsRef.current.isSame(prev, fromUrl) ? prev : fromUrl));\n }, [params]);\n\n // Clearing on a subject change is deliberate; clearing on a subject ARRIVING\n // is destructive. A surface whose id is undefined for its first render (async\n // params, a late store hydration, a suspended boundary) would otherwise wipe\n // the very view the user just loaded — and the symptom is indistinguishable\n // from \"the URL never saved my filters\", because the write lands and is\n // erased a tick later.\n //\n // So: only a transition between two REAL, DIFFERENT keys resets.\n const previousResetKey = useRef(resetKey);\n useEffect(() => {\n const previous = previousResetKey.current;\n previousResetKey.current = resetKey;\n if (previous === resetKey) return;\n if (previous === undefined || previous === \"\") return; // arriving, not changing\n if (resetKey === undefined || resetKey === \"\") return; // leaving, not changing\n\n const cleared = optionsRef.current.parse(new URLSearchParams());\n setValue(cleared);\n commitUrlParams(optionsRef.current.toParams(cleared), \"replace\");\n }, [resetKey]);\n\n const update = useCallback((updater: T | ((prev: T) => T)) => {\n setValue((prev) =>\n typeof updater === \"function\"\n ? (updater as (p: T) => T)(prev)\n : updater,\n );\n }, []);\n\n return [value, update] as const;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyCA,mBAOO;AAEP,IAAM,kBAAkB;AAcxB,IAAM,cAAc,uBAAO,IAAI,+BAA+B;AAE9D,SAAS,YAAmC;AAC1C,SACG,WACC,WACF,KAAK;AAET;AAOO,SAAS,kBAAkB,QAAqC;AACrE,EAAC,WACC,WACF,IAAI;AACN;AAEA,SAAS,kBAAsE;AAC7E,QAAM,SAAS,UAAU,GAAG;AAC5B,MAAI,OAAQ,QAAO,OAAO;AAC1B,SAAO;AAAA,IACL,UAAU,OAAO,SAAS;AAAA,IAC1B,QAAQ,OAAO,SAAS;AAAA,IACxB,MAAM,OAAO,SAAS;AAAA,EACxB;AACF;AAEA,SAAS,eAAe,UAAsB;AAC5C,SAAO,iBAAiB,YAAY,QAAQ;AAC5C,SAAO,iBAAiB,iBAAiB,QAAQ;AACjD,SAAO,MAAM;AACX,WAAO,oBAAoB,YAAY,QAAQ;AAC/C,WAAO,oBAAoB,iBAAiB,QAAQ;AAAA,EACtD;AACF;AAEA,SAAS,iBAAiB;AACxB,SAAO,gBAAgB,EAAE;AAC3B;AAEA,SAAS,uBAAuB;AAC9B,SAAO;AACT;AAGO,SAAS,qBAAsC;AACpD,QAAM,aAAS;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,aAAO,sBAAQ,MAAM,IAAI,gBAAgB,MAAM,GAAG,CAAC,MAAM,CAAC;AAC5D;AAcO,SAAS,gBACd,OACA,SACA;AACA,QAAM,WAAW,gBAAgB;AACjC,QAAM,SAAS,IAAI,gBAAgB,SAAS,MAAM;AAClD,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,UAAU,QAAQ,UAAU,GAAI,QAAO,OAAO,GAAG;AAAA,QAChD,QAAO,IAAI,KAAK,KAAK;AAAA,EAC5B;AAEA,QAAM,QAAQ,OAAO,SAAS;AAC9B,QAAM,OAAO,GAAG,SAAS,QAAQ,GAAG,QAAQ,IAAI,KAAK,KAAK,EAAE,GAAG,SAAS,IAAI;AAC5E,QAAM,UAAU,GAAG,SAAS,QAAQ,GAAG,SAAS,MAAM,GAAG,SAAS,IAAI;AACtE,MAAI,SAAS,QAAS;AAEtB,QAAM,SAAS,UAAU;AACzB,MAAI,QAAQ;AACV,QAAI,YAAY,UAAW,QAAO,QAAQ,IAAI;AAAA,QACzC,QAAO,KAAK,IAAI;AAAA,EACvB,WAAW,YAAY,WAAW;AAChC,WAAO,QAAQ,aAAa,OAAO,QAAQ,OAAO,IAAI,IAAI;AAAA,EAC5D,OAAO;AACL,WAAO,QAAQ,UAAU,OAAO,QAAQ,OAAO,IAAI,IAAI;AAAA,EACzD;AACA,SAAO,cAAc,IAAI,MAAM,eAAe,CAAC;AACjD;AAYO,SAAS,0BACd,SACA,MACA,UACgB;AAChB,QAAM,OAAO,oBAAI,IAAI,CAAC,GAAG,QAAQ,KAAK,GAAG,GAAG,KAAK,KAAK,CAAC,CAAC;AACxD,QAAM,UAAU,CAAC,GAAG,IAAI,EAAE;AAAA,IACxB,CAAC,QAAQ,QAAQ,IAAI,GAAG,MAAM,KAAK,IAAI,GAAG;AAAA,EAC5C;AACA,MAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,SAAO,QAAQ,MAAM,CAAC,QAAQ,SAAS,SAAS,GAAG,CAAC,IAAI,YAAY;AACtE;AAEO,SAAS,YACd,KACA,OACgE;AAChE,QAAM,eAAe,mBAAmB;AACxC,QAAM,QAAQ,MAAM,MAAM,aAAa,IAAI,GAAG,CAAC;AAE/C,QAAM,WAAW,CAAC,MAAS,YAAiC;AAC1D;AAAA,MACE,EAAE,CAAC,GAAG,GAAG,MAAM,UAAU,IAAI,EAAE;AAAA,MAC/B,SAAS,WAAW;AAAA,IACtB;AAAA,EACF;AAEA,SAAO,CAAC,OAAO,QAAQ;AACzB;AAEO,SAAS,eAAe,eAAe,IAA2B;AACvE,SAAO;AAAA,IACL;AAAA,IACA,OAAO,CAAC,QAAQ,OAAO;AAAA,IACvB,WAAW,CAAC,UAAW,UAAU,eAAe,OAAO;AAAA,EACzD;AACF;AAEO,SAAS,aACd,QACA,cACkB;AAClB,QAAM,UAAU,IAAI,IAAY,MAAM;AACtC,SAAO;AAAA,IACL;AAAA,IACA,OAAO,CAAC,QAAS,OAAO,QAAQ,IAAI,GAAG,IAAK,MAAY;AAAA,IACxD,WAAW,CAAC,UAAW,UAAU,eAAe,OAAO;AAAA,EACzD;AACF;AAEO,SAAS,gBAAgB,eAAe,OAA+B;AAC5E,SAAO;AAAA,IACL;AAAA,IACA,OAAO,CAAC,QAAS,QAAQ,OAAO,eAAe,QAAQ;AAAA,IACvD,WAAW,CAAC,UAAW,UAAU,eAAe,OAAO,QAAQ,MAAM;AAAA,EACvE;AACF;AAEO,SAAS,wBACd,cACuB;AACvB,SAAO;AAAA,IACL;AAAA,IACA,OAAO,CAAC,QAAQ;AACd,YAAM,SAAS,MAAM,OAAO,SAAS,KAAK,EAAE,IAAI,OAAO;AACvD,aAAO,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,SAAS;AAAA,IAC1D;AAAA,IACA,WAAW,CAAC,UACV,UAAU,gBAAgB,CAAC,OAAO,SAAS,KAAK,KAAK,SAAS,IAC1D,OACA,OAAO,KAAK,MAAM,KAAK,CAAC;AAAA,EAChC;AACF;AAEO,SAAS,aACd,cACA,SACkB;AAClB,SAAO;AAAA,IACL;AAAA,IACA,OAAO,CAAC,QAAQ;AACd,UAAI,CAAC,IAAK,QAAO;AACjB,UAAI;AACF,cAAM,SAAkB,KAAK,MAAM,GAAG;AACtC,eAAO,QAAQ,MAAM,IAAI,SAAS;AAAA,MACpC,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA,WAAW,CAAC,UACV,KAAK,UAAU,KAAK,MAAM,KAAK,UAAU,YAAY,IACjD,OACA,KAAK,UAAU,KAAK;AAAA,EAC5B;AACF;AAwCO,SAAS,oBACd,SACuD;AACvD,QAAM,EAAE,OAAO,UAAU,QAAQ,WAAW,CAAC,GAAG,SAAS,IAAI;AAC7D,QAAM,SAAS,mBAAmB;AAElC,QAAM,iBAAa,qBAAO,OAAO;AACjC,aAAW,UAAU;AAErB,QAAM,CAAC,OAAO,QAAQ,QAAI;AAAA,IAAY,MACpC;AAAA,MACE,OAAO,WAAW,cACd,IAAI,gBAAgB,IACpB,IAAI,gBAAgB,gBAAgB,EAAE,MAAM;AAAA,IAClD;AAAA,EACF;AAGA,8BAAU,MAAM;AACd,QAAI,OAAO,WAAW,YAAa;AACnC,UAAM,UAAU,IAAI,gBAAgB,gBAAgB,EAAE,MAAM;AAC5D,UAAM,QAAQ,WAAW,QAAQ,SAAS,KAAK;AAE/C,UAAM,OAAO,IAAI,gBAAgB,OAAO;AACxC,eAAW,CAAC,KAAK,CAAC,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC5C,UAAI,MAAM,QAAQ,MAAM,GAAI,MAAK,OAAO,GAAG;AAAA,UACtC,MAAK,IAAI,KAAK,CAAC;AAAA,IACtB;AACA,QAAI,KAAK,SAAS,MAAM,QAAQ,SAAS,EAAG;AAE5C,oBAAgB,OAAO,0BAA0B,SAAS,MAAM,QAAQ,CAAC;AAAA,EAG3E,GAAG,CAAC,KAAK,CAAC;AAGV,8BAAU,MAAM;AAMd,UAAM,UAAU,WAAW,QAAQ;AAAA,MACjC,IAAI,gBAAgB,gBAAgB,EAAE,MAAM;AAAA,IAC9C;AACA,aAAS,CAAC,SAAU,WAAW,QAAQ,OAAO,MAAM,OAAO,IAAI,OAAO,OAAQ;AAAA,EAChF,GAAG,CAAC,MAAM,CAAC;AAUX,QAAM,uBAAmB,qBAAO,QAAQ;AACxC,8BAAU,MAAM;AACd,UAAM,WAAW,iBAAiB;AAClC,qBAAiB,UAAU;AAC3B,QAAI,aAAa,SAAU;AAC3B,QAAI,aAAa,UAAa,aAAa,GAAI;AAC/C,QAAI,aAAa,UAAa,aAAa,GAAI;AAE/C,UAAM,UAAU,WAAW,QAAQ,MAAM,IAAI,gBAAgB,CAAC;AAC9D,aAAS,OAAO;AAChB,oBAAgB,WAAW,QAAQ,SAAS,OAAO,GAAG,SAAS;AAAA,EACjE,GAAG,CAAC,QAAQ,CAAC;AAEb,QAAM,aAAS,0BAAY,CAAC,YAAkC;AAC5D;AAAA,MAAS,CAAC,SACR,OAAO,YAAY,aACd,QAAwB,IAAI,IAC7B;AAAA,IACN;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SAAO,CAAC,OAAO,MAAM;AACvB;","names":[]}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The optional injected router. `push`/`replace` receive the full relative
|
|
3
|
+
* URL (`pathname?query#hash`) exactly as the History API default would write
|
|
4
|
+
* it. `getLocation` overrides where the current URL is read from (a memory
|
|
5
|
+
* router); absent, `window.location` is read.
|
|
6
|
+
*/
|
|
7
|
+
interface UrlStateRouter {
|
|
8
|
+
push: (url: string) => void;
|
|
9
|
+
replace: (url: string) => void;
|
|
10
|
+
getLocation?: (() => {
|
|
11
|
+
pathname: string;
|
|
12
|
+
search: string;
|
|
13
|
+
hash: string;
|
|
14
|
+
}) | undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Inject (or with `null` remove) the host router. Optional — with none set,
|
|
18
|
+
* writes go through `window.history` push/replaceState, which is the verbatim
|
|
19
|
+
* Matrx-frontend behavior and correct for Next.js App Router hosts.
|
|
20
|
+
*/
|
|
21
|
+
declare function setUrlStateRouter(router: UrlStateRouter | null): void;
|
|
22
|
+
/** Reactive query-string snapshot for URL-backed primitives. */
|
|
23
|
+
declare function useUrlSearchParams(): URLSearchParams;
|
|
24
|
+
type UrlHistoryMode = "push" | "replace";
|
|
25
|
+
interface UrlStateCodec<T> {
|
|
26
|
+
defaultValue: T;
|
|
27
|
+
parse: (raw: string | null) => T;
|
|
28
|
+
serialize: (value: T) => string | null;
|
|
29
|
+
}
|
|
30
|
+
interface SetUrlStateOptions {
|
|
31
|
+
history?: UrlHistoryMode | undefined;
|
|
32
|
+
}
|
|
33
|
+
declare function commitUrlParams(patch: Readonly<Record<string, string | null>>, history: UrlHistoryMode): void;
|
|
34
|
+
/**
|
|
35
|
+
* Classify a URL transition for surfaces that MIRROR state (a store, local
|
|
36
|
+
* state) into the URL from an effect, where there is no single call site to
|
|
37
|
+
* label.
|
|
38
|
+
*
|
|
39
|
+
* THE RULE: a discrete user decision (tab, filter, sort, page, selection)
|
|
40
|
+
* PUSHES, so Back undoes exactly that one step; only high-frequency text
|
|
41
|
+
* (`textKeys` — search boxes, a slider being dragged) REPLACES, so one search
|
|
42
|
+
* is one entry instead of one per keystroke.
|
|
43
|
+
*/
|
|
44
|
+
declare function historyModeForParamChange(current: URLSearchParams, next: URLSearchParams, textKeys: readonly string[]): UrlHistoryMode;
|
|
45
|
+
declare function useUrlState<T>(key: string, codec: UrlStateCodec<T>): readonly [T, (value: T, options?: SetUrlStateOptions) => void];
|
|
46
|
+
declare function stringUrlCodec(defaultValue?: string): UrlStateCodec<string>;
|
|
47
|
+
declare function enumUrlCodec<const T extends string>(values: readonly T[], defaultValue: T): UrlStateCodec<T>;
|
|
48
|
+
declare function booleanUrlCodec(defaultValue?: boolean): UrlStateCodec<boolean>;
|
|
49
|
+
declare function positiveIntegerUrlCodec(defaultValue: number): UrlStateCodec<number>;
|
|
50
|
+
declare function jsonUrlCodec<T>(defaultValue: T, isValid: (value: unknown) => value is T): UrlStateCodec<T>;
|
|
51
|
+
/**
|
|
52
|
+
* Mirror a whole view-state OBJECT into the URL, in both directions.
|
|
53
|
+
*
|
|
54
|
+
* THE PATTERN 18 FILES WERE HAND-ROLLING. `useUrlState` is right when one
|
|
55
|
+
* control owns one parameter. It is the wrong shape when a surface holds a
|
|
56
|
+
* cluster of related values (search + sort + filters + page) that must move
|
|
57
|
+
* together, be seeded from the URL on first render, and follow Back/Forward.
|
|
58
|
+
* Every surface that needed that wrote its own `history.pushState` — and NONE
|
|
59
|
+
* of them dispatched the sync event, so any other URL-backed control on the
|
|
60
|
+
* same page silently kept showing stale values after they wrote.
|
|
61
|
+
*
|
|
62
|
+
* 🚨 THE LOOP IS BROKEN BY VALUE, NEVER BY BOOKKEEPING. The obvious guard —
|
|
63
|
+
* remember the last URL you wrote and ignore anything matching it — looks
|
|
64
|
+
* equivalent and is not: pressing Forward to a view already visited produces a
|
|
65
|
+
* URL you did indeed write before, so the guard swallows it and the address bar
|
|
66
|
+
* moves while the surface does not. Compare the DECODED value to the current
|
|
67
|
+
* one instead; your own write compares equal and stops.
|
|
68
|
+
*
|
|
69
|
+
* Seeding happens in the `useState` initialiser, not an effect, so the first
|
|
70
|
+
* render already shows the requested view rather than flashing the default and
|
|
71
|
+
* fetching the wrong page before correcting itself.
|
|
72
|
+
*/
|
|
73
|
+
interface MirroredUrlStateOptions<T> {
|
|
74
|
+
/** Decode the whole value from the query string. Must never throw. */
|
|
75
|
+
parse: (params: URLSearchParams) => T;
|
|
76
|
+
/** Encode it; `null` for a key means "omit", which keeps defaults out of the URL. */
|
|
77
|
+
toParams: (value: T) => Record<string, string | null>;
|
|
78
|
+
/** Value equality — what stops the two directions fighting. */
|
|
79
|
+
isSame: (a: T, b: T) => boolean;
|
|
80
|
+
/** Keys that REPLACE instead of pushing (search boxes, dragged sliders). */
|
|
81
|
+
textKeys?: readonly string[] | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Changing this clears the mirrored state and its parameters — for when the
|
|
84
|
+
* surface switches to a different subject and the old view would be a lie.
|
|
85
|
+
*/
|
|
86
|
+
resetKey?: string | undefined;
|
|
87
|
+
}
|
|
88
|
+
declare function useMirroredUrlState<T>(options: MirroredUrlStateOptions<T>): readonly [T, (updater: T | ((prev: T) => T)) => void];
|
|
89
|
+
|
|
90
|
+
export { type MirroredUrlStateOptions, type SetUrlStateOptions, type UrlHistoryMode, type UrlStateCodec, type UrlStateRouter, booleanUrlCodec, commitUrlParams, enumUrlCodec, historyModeForParamChange, jsonUrlCodec, positiveIntegerUrlCodec, setUrlStateRouter, stringUrlCodec, useMirroredUrlState, useUrlSearchParams, useUrlState };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The optional injected router. `push`/`replace` receive the full relative
|
|
3
|
+
* URL (`pathname?query#hash`) exactly as the History API default would write
|
|
4
|
+
* it. `getLocation` overrides where the current URL is read from (a memory
|
|
5
|
+
* router); absent, `window.location` is read.
|
|
6
|
+
*/
|
|
7
|
+
interface UrlStateRouter {
|
|
8
|
+
push: (url: string) => void;
|
|
9
|
+
replace: (url: string) => void;
|
|
10
|
+
getLocation?: (() => {
|
|
11
|
+
pathname: string;
|
|
12
|
+
search: string;
|
|
13
|
+
hash: string;
|
|
14
|
+
}) | undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Inject (or with `null` remove) the host router. Optional — with none set,
|
|
18
|
+
* writes go through `window.history` push/replaceState, which is the verbatim
|
|
19
|
+
* Matrx-frontend behavior and correct for Next.js App Router hosts.
|
|
20
|
+
*/
|
|
21
|
+
declare function setUrlStateRouter(router: UrlStateRouter | null): void;
|
|
22
|
+
/** Reactive query-string snapshot for URL-backed primitives. */
|
|
23
|
+
declare function useUrlSearchParams(): URLSearchParams;
|
|
24
|
+
type UrlHistoryMode = "push" | "replace";
|
|
25
|
+
interface UrlStateCodec<T> {
|
|
26
|
+
defaultValue: T;
|
|
27
|
+
parse: (raw: string | null) => T;
|
|
28
|
+
serialize: (value: T) => string | null;
|
|
29
|
+
}
|
|
30
|
+
interface SetUrlStateOptions {
|
|
31
|
+
history?: UrlHistoryMode | undefined;
|
|
32
|
+
}
|
|
33
|
+
declare function commitUrlParams(patch: Readonly<Record<string, string | null>>, history: UrlHistoryMode): void;
|
|
34
|
+
/**
|
|
35
|
+
* Classify a URL transition for surfaces that MIRROR state (a store, local
|
|
36
|
+
* state) into the URL from an effect, where there is no single call site to
|
|
37
|
+
* label.
|
|
38
|
+
*
|
|
39
|
+
* THE RULE: a discrete user decision (tab, filter, sort, page, selection)
|
|
40
|
+
* PUSHES, so Back undoes exactly that one step; only high-frequency text
|
|
41
|
+
* (`textKeys` — search boxes, a slider being dragged) REPLACES, so one search
|
|
42
|
+
* is one entry instead of one per keystroke.
|
|
43
|
+
*/
|
|
44
|
+
declare function historyModeForParamChange(current: URLSearchParams, next: URLSearchParams, textKeys: readonly string[]): UrlHistoryMode;
|
|
45
|
+
declare function useUrlState<T>(key: string, codec: UrlStateCodec<T>): readonly [T, (value: T, options?: SetUrlStateOptions) => void];
|
|
46
|
+
declare function stringUrlCodec(defaultValue?: string): UrlStateCodec<string>;
|
|
47
|
+
declare function enumUrlCodec<const T extends string>(values: readonly T[], defaultValue: T): UrlStateCodec<T>;
|
|
48
|
+
declare function booleanUrlCodec(defaultValue?: boolean): UrlStateCodec<boolean>;
|
|
49
|
+
declare function positiveIntegerUrlCodec(defaultValue: number): UrlStateCodec<number>;
|
|
50
|
+
declare function jsonUrlCodec<T>(defaultValue: T, isValid: (value: unknown) => value is T): UrlStateCodec<T>;
|
|
51
|
+
/**
|
|
52
|
+
* Mirror a whole view-state OBJECT into the URL, in both directions.
|
|
53
|
+
*
|
|
54
|
+
* THE PATTERN 18 FILES WERE HAND-ROLLING. `useUrlState` is right when one
|
|
55
|
+
* control owns one parameter. It is the wrong shape when a surface holds a
|
|
56
|
+
* cluster of related values (search + sort + filters + page) that must move
|
|
57
|
+
* together, be seeded from the URL on first render, and follow Back/Forward.
|
|
58
|
+
* Every surface that needed that wrote its own `history.pushState` — and NONE
|
|
59
|
+
* of them dispatched the sync event, so any other URL-backed control on the
|
|
60
|
+
* same page silently kept showing stale values after they wrote.
|
|
61
|
+
*
|
|
62
|
+
* 🚨 THE LOOP IS BROKEN BY VALUE, NEVER BY BOOKKEEPING. The obvious guard —
|
|
63
|
+
* remember the last URL you wrote and ignore anything matching it — looks
|
|
64
|
+
* equivalent and is not: pressing Forward to a view already visited produces a
|
|
65
|
+
* URL you did indeed write before, so the guard swallows it and the address bar
|
|
66
|
+
* moves while the surface does not. Compare the DECODED value to the current
|
|
67
|
+
* one instead; your own write compares equal and stops.
|
|
68
|
+
*
|
|
69
|
+
* Seeding happens in the `useState` initialiser, not an effect, so the first
|
|
70
|
+
* render already shows the requested view rather than flashing the default and
|
|
71
|
+
* fetching the wrong page before correcting itself.
|
|
72
|
+
*/
|
|
73
|
+
interface MirroredUrlStateOptions<T> {
|
|
74
|
+
/** Decode the whole value from the query string. Must never throw. */
|
|
75
|
+
parse: (params: URLSearchParams) => T;
|
|
76
|
+
/** Encode it; `null` for a key means "omit", which keeps defaults out of the URL. */
|
|
77
|
+
toParams: (value: T) => Record<string, string | null>;
|
|
78
|
+
/** Value equality — what stops the two directions fighting. */
|
|
79
|
+
isSame: (a: T, b: T) => boolean;
|
|
80
|
+
/** Keys that REPLACE instead of pushing (search boxes, dragged sliders). */
|
|
81
|
+
textKeys?: readonly string[] | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Changing this clears the mirrored state and its parameters — for when the
|
|
84
|
+
* surface switches to a different subject and the old view would be a lie.
|
|
85
|
+
*/
|
|
86
|
+
resetKey?: string | undefined;
|
|
87
|
+
}
|
|
88
|
+
declare function useMirroredUrlState<T>(options: MirroredUrlStateOptions<T>): readonly [T, (updater: T | ((prev: T) => T)) => void];
|
|
89
|
+
|
|
90
|
+
export { type MirroredUrlStateOptions, type SetUrlStateOptions, type UrlHistoryMode, type UrlStateCodec, type UrlStateRouter, booleanUrlCodec, commitUrlParams, enumUrlCodec, historyModeForParamChange, jsonUrlCodec, positiveIntegerUrlCodec, setUrlStateRouter, stringUrlCodec, useMirroredUrlState, useUrlSearchParams, useUrlState };
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
// src/url-state.ts
|
|
5
|
+
import {
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
useSyncExternalStore
|
|
12
|
+
} from "react";
|
|
13
|
+
var URL_STATE_EVENT = "matrx:url-state";
|
|
14
|
+
var ROUTER_SLOT = /* @__PURE__ */ Symbol.for("ai-matrx.kit.url-state-router");
|
|
15
|
+
function getRouter() {
|
|
16
|
+
return globalThis[ROUTER_SLOT] ?? null;
|
|
17
|
+
}
|
|
18
|
+
function setUrlStateRouter(router) {
|
|
19
|
+
globalThis[ROUTER_SLOT] = router;
|
|
20
|
+
}
|
|
21
|
+
function currentLocation() {
|
|
22
|
+
const custom = getRouter()?.getLocation;
|
|
23
|
+
if (custom) return custom();
|
|
24
|
+
return {
|
|
25
|
+
pathname: window.location.pathname,
|
|
26
|
+
search: window.location.search,
|
|
27
|
+
hash: window.location.hash
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function subscribeToUrl(listener) {
|
|
31
|
+
window.addEventListener("popstate", listener);
|
|
32
|
+
window.addEventListener(URL_STATE_EVENT, listener);
|
|
33
|
+
return () => {
|
|
34
|
+
window.removeEventListener("popstate", listener);
|
|
35
|
+
window.removeEventListener(URL_STATE_EVENT, listener);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function getUrlSnapshot() {
|
|
39
|
+
return currentLocation().search;
|
|
40
|
+
}
|
|
41
|
+
function getServerUrlSnapshot() {
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
function useUrlSearchParams() {
|
|
45
|
+
const search = useSyncExternalStore(
|
|
46
|
+
subscribeToUrl,
|
|
47
|
+
getUrlSnapshot,
|
|
48
|
+
getServerUrlSnapshot
|
|
49
|
+
);
|
|
50
|
+
return useMemo(() => new URLSearchParams(search), [search]);
|
|
51
|
+
}
|
|
52
|
+
function commitUrlParams(patch, history) {
|
|
53
|
+
const location = currentLocation();
|
|
54
|
+
const params = new URLSearchParams(location.search);
|
|
55
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
56
|
+
if (value === null || value === "") params.delete(key);
|
|
57
|
+
else params.set(key, value);
|
|
58
|
+
}
|
|
59
|
+
const query = params.toString();
|
|
60
|
+
const next = `${location.pathname}${query ? `?${query}` : ""}${location.hash}`;
|
|
61
|
+
const current = `${location.pathname}${location.search}${location.hash}`;
|
|
62
|
+
if (next === current) return;
|
|
63
|
+
const router = getRouter();
|
|
64
|
+
if (router) {
|
|
65
|
+
if (history === "replace") router.replace(next);
|
|
66
|
+
else router.push(next);
|
|
67
|
+
} else if (history === "replace") {
|
|
68
|
+
window.history.replaceState(window.history.state, "", next);
|
|
69
|
+
} else {
|
|
70
|
+
window.history.pushState(window.history.state, "", next);
|
|
71
|
+
}
|
|
72
|
+
window.dispatchEvent(new Event(URL_STATE_EVENT));
|
|
73
|
+
}
|
|
74
|
+
function historyModeForParamChange(current, next, textKeys) {
|
|
75
|
+
const keys = /* @__PURE__ */ new Set([...current.keys(), ...next.keys()]);
|
|
76
|
+
const changed = [...keys].filter(
|
|
77
|
+
(key) => current.get(key) !== next.get(key)
|
|
78
|
+
);
|
|
79
|
+
if (changed.length === 0) return "replace";
|
|
80
|
+
return changed.every((key) => textKeys.includes(key)) ? "replace" : "push";
|
|
81
|
+
}
|
|
82
|
+
function useUrlState(key, codec) {
|
|
83
|
+
const searchParams = useUrlSearchParams();
|
|
84
|
+
const value = codec.parse(searchParams.get(key));
|
|
85
|
+
const setValue = (next, options) => {
|
|
86
|
+
commitUrlParams(
|
|
87
|
+
{ [key]: codec.serialize(next) },
|
|
88
|
+
options?.history ?? "push"
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
return [value, setValue];
|
|
92
|
+
}
|
|
93
|
+
function stringUrlCodec(defaultValue = "") {
|
|
94
|
+
return {
|
|
95
|
+
defaultValue,
|
|
96
|
+
parse: (raw) => raw ?? defaultValue,
|
|
97
|
+
serialize: (value) => value === defaultValue ? null : value
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function enumUrlCodec(values, defaultValue) {
|
|
101
|
+
const allowed = new Set(values);
|
|
102
|
+
return {
|
|
103
|
+
defaultValue,
|
|
104
|
+
parse: (raw) => raw && allowed.has(raw) ? raw : defaultValue,
|
|
105
|
+
serialize: (value) => value === defaultValue ? null : value
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function booleanUrlCodec(defaultValue = false) {
|
|
109
|
+
return {
|
|
110
|
+
defaultValue,
|
|
111
|
+
parse: (raw) => raw === null ? defaultValue : raw === "1",
|
|
112
|
+
serialize: (value) => value === defaultValue ? null : value ? "1" : "0"
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function positiveIntegerUrlCodec(defaultValue) {
|
|
116
|
+
return {
|
|
117
|
+
defaultValue,
|
|
118
|
+
parse: (raw) => {
|
|
119
|
+
const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
|
|
120
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultValue;
|
|
121
|
+
},
|
|
122
|
+
serialize: (value) => value === defaultValue || !Number.isFinite(value) || value <= 0 ? null : String(Math.trunc(value))
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function jsonUrlCodec(defaultValue, isValid) {
|
|
126
|
+
return {
|
|
127
|
+
defaultValue,
|
|
128
|
+
parse: (raw) => {
|
|
129
|
+
if (!raw) return defaultValue;
|
|
130
|
+
try {
|
|
131
|
+
const parsed = JSON.parse(raw);
|
|
132
|
+
return isValid(parsed) ? parsed : defaultValue;
|
|
133
|
+
} catch {
|
|
134
|
+
return defaultValue;
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
serialize: (value) => JSON.stringify(value) === JSON.stringify(defaultValue) ? null : JSON.stringify(value)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function useMirroredUrlState(options) {
|
|
141
|
+
const { parse, toParams, isSame, textKeys = [], resetKey } = options;
|
|
142
|
+
const params = useUrlSearchParams();
|
|
143
|
+
const optionsRef = useRef(options);
|
|
144
|
+
optionsRef.current = options;
|
|
145
|
+
const [value, setValue] = useState(
|
|
146
|
+
() => parse(
|
|
147
|
+
typeof window === "undefined" ? new URLSearchParams() : new URLSearchParams(currentLocation().search)
|
|
148
|
+
)
|
|
149
|
+
);
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (typeof window === "undefined") return;
|
|
152
|
+
const current = new URLSearchParams(currentLocation().search);
|
|
153
|
+
const patch = optionsRef.current.toParams(value);
|
|
154
|
+
const next = new URLSearchParams(current);
|
|
155
|
+
for (const [key, v] of Object.entries(patch)) {
|
|
156
|
+
if (v === null || v === "") next.delete(key);
|
|
157
|
+
else next.set(key, v);
|
|
158
|
+
}
|
|
159
|
+
if (next.toString() === current.toString()) return;
|
|
160
|
+
commitUrlParams(patch, historyModeForParamChange(current, next, textKeys));
|
|
161
|
+
}, [value]);
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
const fromUrl = optionsRef.current.parse(
|
|
164
|
+
new URLSearchParams(currentLocation().search)
|
|
165
|
+
);
|
|
166
|
+
setValue((prev) => optionsRef.current.isSame(prev, fromUrl) ? prev : fromUrl);
|
|
167
|
+
}, [params]);
|
|
168
|
+
const previousResetKey = useRef(resetKey);
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
const previous = previousResetKey.current;
|
|
171
|
+
previousResetKey.current = resetKey;
|
|
172
|
+
if (previous === resetKey) return;
|
|
173
|
+
if (previous === void 0 || previous === "") return;
|
|
174
|
+
if (resetKey === void 0 || resetKey === "") return;
|
|
175
|
+
const cleared = optionsRef.current.parse(new URLSearchParams());
|
|
176
|
+
setValue(cleared);
|
|
177
|
+
commitUrlParams(optionsRef.current.toParams(cleared), "replace");
|
|
178
|
+
}, [resetKey]);
|
|
179
|
+
const update = useCallback((updater) => {
|
|
180
|
+
setValue(
|
|
181
|
+
(prev) => typeof updater === "function" ? updater(prev) : updater
|
|
182
|
+
);
|
|
183
|
+
}, []);
|
|
184
|
+
return [value, update];
|
|
185
|
+
}
|
|
186
|
+
export {
|
|
187
|
+
booleanUrlCodec,
|
|
188
|
+
commitUrlParams,
|
|
189
|
+
enumUrlCodec,
|
|
190
|
+
historyModeForParamChange,
|
|
191
|
+
jsonUrlCodec,
|
|
192
|
+
positiveIntegerUrlCodec,
|
|
193
|
+
setUrlStateRouter,
|
|
194
|
+
stringUrlCodec,
|
|
195
|
+
useMirroredUrlState,
|
|
196
|
+
useUrlSearchParams,
|
|
197
|
+
useUrlState
|
|
198
|
+
};
|
|
199
|
+
//# sourceMappingURL=url-state.js.map
|