hazo_ui 6.1.0 → 6.3.1
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/CHANGE_LOG.md +95 -1
- package/dist/announce-DAnQDTld.d.cts +92 -0
- package/dist/announce-DAnQDTld.d.ts +92 -0
- package/dist/index.cjs +256 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +141 -3
- package/dist/index.d.ts +141 -3
- package/dist/index.js +249 -2
- package/dist/index.js.map +1 -1
- package/dist/index.utils.cjs +72 -0
- package/dist/index.utils.cjs.map +1 -1
- package/dist/index.utils.d.cts +31 -1
- package/dist/index.utils.d.ts +31 -1
- package/dist/index.utils.js +64 -1
- package/dist/index.utils.js.map +1 -1
- package/package.json +7 -8
- package/dist/index.utils-DZVbQXFR.d.cts +0 -20
- package/dist/index.utils-DZVbQXFR.d.ts +0 -20
package/CHANGE_LOG.md
CHANGED
|
@@ -5,7 +5,101 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## v6.1
|
|
8
|
+
## v6.3.1 (2026-07-29)
|
|
9
|
+
|
|
10
|
+
### Fixed — test-app builds again
|
|
11
|
+
|
|
12
|
+
`npm run build:test-app` had been failing, which meant the 15 harness cases
|
|
13
|
+
added in 6.3.0 were never actually executed. Three separate causes, all in the
|
|
14
|
+
test-app rather than the library:
|
|
15
|
+
|
|
16
|
+
- The package had no `build:test-app` script at all (it used non-standard
|
|
17
|
+
`test:build` / `test:dev` names), and the test-app called `next` directly
|
|
18
|
+
instead of going through the `scripts/next.mjs` launcher the workspace
|
|
19
|
+
standard requires. Both adopted; `dev:test-app`/`build:test-app` are now the
|
|
20
|
+
entry points and `PORT` defaults to 3300.
|
|
21
|
+
- `next build` died with "ESM packages (hazo_core) need to be imported" via
|
|
22
|
+
`hazo_connect`'s ESM dist. Fixed with `experimental.esmExternals: 'loose'`.
|
|
23
|
+
Neither `transpilePackages` nor `serverComponentsExternalPackages` helps
|
|
24
|
+
here — the latter matches bare package specifiers, and the workspace symlink
|
|
25
|
+
makes the import trace resolve to a relative path that never matches.
|
|
26
|
+
- Type-check then failed on `CSSProperties is not assignable to
|
|
27
|
+
CSSProperties`: two `@types/react` copies (root 18.3.31, nested 18.3.28),
|
|
28
|
+
with source-aliased `hazo_ui` resolving against one and the test-app's own
|
|
29
|
+
files against the other. Pinned both to the root copy via tsconfig `paths`.
|
|
30
|
+
|
|
31
|
+
### Note on release history
|
|
32
|
+
|
|
33
|
+
`v6.2.0` was committed but never published; `v6.3.0` was published to npm on
|
|
34
|
+
2026-07-21 *before* its source was committed. npm therefore went
|
|
35
|
+
6.1.0 → 6.3.0, and for eight days git and npm disagreed about what 6.3.0 was.
|
|
36
|
+
Both are reconciled as of this entry — noted here because neither history
|
|
37
|
+
tells that story on its own.
|
|
38
|
+
|
|
39
|
+
## v6.3.0 (2026-07-21)
|
|
40
|
+
|
|
41
|
+
### New
|
|
42
|
+
- feat: add `useLongPress()` (touch-only long-press hook) — ported as-is
|
|
43
|
+
from Kinstripe's `src/lib/hooks/useLongPress.ts` (already fully generic;
|
|
44
|
+
no app coupling to remove). Every returned handler
|
|
45
|
+
(`onPointerDown`/`onPointerMove`/`onPointerUp`/`onPointerCancel`/`onClick`)
|
|
46
|
+
is a no-op on fine-pointer (mouse) devices — desktop should keep using
|
|
47
|
+
`onContextMenu` for right-click. Cancels on movement past
|
|
48
|
+
`moveThresholdPx`, early `pointerup`, a second `pointerdown`
|
|
49
|
+
(multi-touch), or `pointercancel`.
|
|
50
|
+
- feat: add `announce()` + `<LiveRegion>` — a screen-reader announcement
|
|
51
|
+
bus (a `window` CustomEvent dispatched by `announce()`, consumed by a
|
|
52
|
+
mounted `<LiveRegion>`), generalised from Kinstripe's `announce.ts` +
|
|
53
|
+
`KsLiveRegion`. Both the hardcoded event name (`"kinstripe:announce"`)
|
|
54
|
+
and the sr-only class name (`"kn-sr-only"`) are now configurable:
|
|
55
|
+
`announce(message, priority, { eventName })` and
|
|
56
|
+
`<LiveRegion eventName className clearAfterMs />`, defaulting to
|
|
57
|
+
`DEFAULT_ANNOUNCE_EVENT` and Tailwind's built-in `"sr-only"` utility so
|
|
58
|
+
it works out of the box. Keeps the two-region (polite/assertive) design,
|
|
59
|
+
the default 6s self-clear, and the duplicate-text re-announce trick.
|
|
60
|
+
`announce()` is also exported from the server-safe `hazo_ui/utils`
|
|
61
|
+
subpath (same dual-export pattern as `cn`).
|
|
62
|
+
- feat: add `useScrollRestore()` — localStorage-backed scroll-position
|
|
63
|
+
save/load, generalised from Kinstripe's `useScrollRestore`. Keeps the
|
|
64
|
+
same `(treeId, viewName)` positional signature; the previously-hardcoded
|
|
65
|
+
`"kinstripe.scrollPos"` key prefix is now an optional third argument
|
|
66
|
+
(`{ prefix }`, default `DEFAULT_SCROLL_RESTORE_PREFIX`). The underlying
|
|
67
|
+
pure helpers (`buildScrollRestoreKey`, `saveScrollPos`, `loadScrollPos`)
|
|
68
|
+
are split into `lib/scroll_restore.ts` (same hook/lib split as
|
|
69
|
+
`useRememberedState`/`remembered_state.ts`) and also exported from
|
|
70
|
+
`hazo_ui/utils`.
|
|
71
|
+
- Demo pages added under `test-app/app/{long-press,live-region,scroll-restore}/`,
|
|
72
|
+
new "Ported utilities (v6.3)" sidebar section, and five new
|
|
73
|
+
`hazo_ui/test-harness` scenarios (`use_long_press`, `announce_dispatch`,
|
|
74
|
+
`live_region_round_trip`, `scroll_restore_helpers`,
|
|
75
|
+
`use_scroll_restore_hook`) registered in
|
|
76
|
+
`test-app/app/harness-self-test/hazo_ui_utility_ports_scenarios.ts` — the
|
|
77
|
+
pointer/DOM-driven ones mount a real React tree via `react-dom/client`
|
|
78
|
+
against the test-app's live browser DOM rather than settling for an
|
|
79
|
+
export-shape check.
|
|
80
|
+
|
|
81
|
+
## v6.2.0 (2026-07-21)
|
|
82
|
+
|
|
83
|
+
### New
|
|
84
|
+
- feat: add `useRememberedState<T>()` hook (FR-020) — a generic
|
|
85
|
+
localStorage-backed "remember last-used state across visits" hook:
|
|
86
|
+
restore-once-on-mount (ref-guarded, immune to `restore` identity churn),
|
|
87
|
+
a debounced write (`debounceMs`, default `600`), and an optional
|
|
88
|
+
`skipWhenUrlParam` escape hatch so an explicit shared-link/saved-record
|
|
89
|
+
query param always wins over remembered state. Generalised from
|
|
90
|
+
stocktools's app-local `useToolMemory` — the caller now supplies the full
|
|
91
|
+
storage `key` and generic `snapshot`/`restore` callbacks instead of a
|
|
92
|
+
hardcoded key prefix and `Seed`-typed shape.
|
|
93
|
+
- feat: add pure helpers `readRememberedState`, `writeRememberedState`,
|
|
94
|
+
`hasUrlParam` backing the hook — SSR-safe, JSON-parse/stringify with
|
|
95
|
+
corrupt-data tolerance, framework-free. Exported from **both** the main
|
|
96
|
+
`hazo_ui` barrel and the server-safe `hazo_ui/utils` subpath (same
|
|
97
|
+
dual-export pattern as `cn`), so Server Components can read/write
|
|
98
|
+
remembered state without pulling in the `"use client"` bundle.
|
|
99
|
+
- Demo page added to `test-app/app/remembered-state/` under the new
|
|
100
|
+
"Remembered state (v6.2)" sidebar section.
|
|
101
|
+
|
|
102
|
+
## v6.1.0 (2026-07-17)
|
|
9
103
|
|
|
10
104
|
### New
|
|
11
105
|
- feat: add `permissionToast` helper (FR-017) — an opinionated wrapper around
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { ClassValue } from 'clsx';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Merges class names using clsx and tailwind-merge
|
|
5
|
+
* @param inputs - Class values to merge
|
|
6
|
+
* @returns Merged class string
|
|
7
|
+
*/
|
|
8
|
+
declare function cn(...inputs: ClassValue[]): string;
|
|
9
|
+
/**
|
|
10
|
+
* Sanitizes a user/admin-supplied href against a scheme allowlist to prevent
|
|
11
|
+
* `javascript:`/`data:`/`vbscript:` URI XSS when the value is rendered into an
|
|
12
|
+
* `<a href>`. Allows http(s), mailto, and same-origin absolute paths (`/…` but
|
|
13
|
+
* not protocol-relative `//…`). Returns `undefined` for anything else, so the
|
|
14
|
+
* anchor renders with no href rather than an active malicious one.
|
|
15
|
+
* @param url - Candidate href (may be undefined)
|
|
16
|
+
* @returns The original url if allowed, otherwise undefined
|
|
17
|
+
*/
|
|
18
|
+
declare function safeHref(url?: string): string | undefined;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Reads and JSON-parses the remembered value stored under `key`. SSR-safe —
|
|
22
|
+
* returns `null` on the server, when nothing is stored, or when the stored
|
|
23
|
+
* value is corrupt/unparsable.
|
|
24
|
+
*
|
|
25
|
+
* @param key - localStorage key to read.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* const seed = readRememberedState<MySeed>("myapp:tool:cgt");
|
|
29
|
+
*/
|
|
30
|
+
declare function readRememberedState<T>(key: string): T | null;
|
|
31
|
+
/**
|
|
32
|
+
* JSON-stringifies and writes `snapshot` under `key`. SSR-safe — no-ops on
|
|
33
|
+
* the server. Quota / private-mode errors are swallowed; losing a memory
|
|
34
|
+
* write is non-fatal.
|
|
35
|
+
*
|
|
36
|
+
* @param key - localStorage key to write.
|
|
37
|
+
* @param snapshot - Serialisable value to remember.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* writeRememberedState("myapp:tool:cgt", { proceeds: 1000 });
|
|
41
|
+
*/
|
|
42
|
+
declare function writeRememberedState<T>(key: string, snapshot: T): void;
|
|
43
|
+
/**
|
|
44
|
+
* True when `paramName` is present in the given search string (e.g.
|
|
45
|
+
* `window.location.search`). Generic form of the "an explicit shared-link /
|
|
46
|
+
* saved-record query param always wins over remembered state" precedence
|
|
47
|
+
* check — callers supply the param name (e.g. `"restore"`); this module
|
|
48
|
+
* never hardcodes one.
|
|
49
|
+
*
|
|
50
|
+
* @param search - A URL search string, e.g. `window.location.search`.
|
|
51
|
+
* @param paramName - The query param to look for.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* hasUrlParam("?restore=abc123", "restore"); // true
|
|
55
|
+
*/
|
|
56
|
+
declare function hasUrlParam(search: string, paramName: string): boolean;
|
|
57
|
+
|
|
58
|
+
type AnnouncePriority = "polite" | "assertive";
|
|
59
|
+
interface LiveAnnouncement {
|
|
60
|
+
message: string;
|
|
61
|
+
priority: AnnouncePriority;
|
|
62
|
+
}
|
|
63
|
+
interface AnnounceOptions {
|
|
64
|
+
/** Custom-event name the paired `LiveRegion` listens on. Must match the `eventName` it was mounted with. Default `DEFAULT_ANNOUNCE_EVENT`. */
|
|
65
|
+
eventName?: string;
|
|
66
|
+
}
|
|
67
|
+
/** Default `window` CustomEvent name used when `announce()` / `LiveRegion` are not given an explicit `eventName`. */
|
|
68
|
+
declare const DEFAULT_ANNOUNCE_EVENT = "hazo_ui:announce";
|
|
69
|
+
/**
|
|
70
|
+
* Dispatch a screen-reader announcement via a `window` CustomEvent. Safe to
|
|
71
|
+
* call from anywhere in the client tree — no-ops during SSR or before a
|
|
72
|
+
* `LiveRegion` has mounted (fire-and-forget; there's no queue/replay).
|
|
73
|
+
*
|
|
74
|
+
* Use `"polite"` (default) for ambient updates ("Sarah joined the canvas",
|
|
75
|
+
* "2 new notifications"). Use `"assertive"` only for things the user must
|
|
76
|
+
* hear immediately (errors that interrupt a task).
|
|
77
|
+
*
|
|
78
|
+
* @param message - Text to announce. Empty / whitespace-only messages are dropped (no dispatch).
|
|
79
|
+
* @param priority - `"polite"` (default) or `"assertive"`.
|
|
80
|
+
* @param options.eventName - Custom-event name to dispatch on. Default `DEFAULT_ANNOUNCE_EVENT`.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* announce("2 new notifications");
|
|
84
|
+
* announce("Save failed — check your connection", "assertive");
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* // namespaced channel — must match the eventName the LiveRegion was mounted with
|
|
88
|
+
* announce("Saved", "polite", { eventName: "myapp:announce" });
|
|
89
|
+
*/
|
|
90
|
+
declare function announce(message: string, priority?: AnnouncePriority, options?: AnnounceOptions): void;
|
|
91
|
+
|
|
92
|
+
export { type AnnounceOptions as A, DEFAULT_ANNOUNCE_EVENT as D, type LiveAnnouncement as L, type AnnouncePriority as a, announce as b, cn as c, hasUrlParam as h, readRememberedState as r, safeHref as s, writeRememberedState as w };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { ClassValue } from 'clsx';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Merges class names using clsx and tailwind-merge
|
|
5
|
+
* @param inputs - Class values to merge
|
|
6
|
+
* @returns Merged class string
|
|
7
|
+
*/
|
|
8
|
+
declare function cn(...inputs: ClassValue[]): string;
|
|
9
|
+
/**
|
|
10
|
+
* Sanitizes a user/admin-supplied href against a scheme allowlist to prevent
|
|
11
|
+
* `javascript:`/`data:`/`vbscript:` URI XSS when the value is rendered into an
|
|
12
|
+
* `<a href>`. Allows http(s), mailto, and same-origin absolute paths (`/…` but
|
|
13
|
+
* not protocol-relative `//…`). Returns `undefined` for anything else, so the
|
|
14
|
+
* anchor renders with no href rather than an active malicious one.
|
|
15
|
+
* @param url - Candidate href (may be undefined)
|
|
16
|
+
* @returns The original url if allowed, otherwise undefined
|
|
17
|
+
*/
|
|
18
|
+
declare function safeHref(url?: string): string | undefined;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Reads and JSON-parses the remembered value stored under `key`. SSR-safe —
|
|
22
|
+
* returns `null` on the server, when nothing is stored, or when the stored
|
|
23
|
+
* value is corrupt/unparsable.
|
|
24
|
+
*
|
|
25
|
+
* @param key - localStorage key to read.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* const seed = readRememberedState<MySeed>("myapp:tool:cgt");
|
|
29
|
+
*/
|
|
30
|
+
declare function readRememberedState<T>(key: string): T | null;
|
|
31
|
+
/**
|
|
32
|
+
* JSON-stringifies and writes `snapshot` under `key`. SSR-safe — no-ops on
|
|
33
|
+
* the server. Quota / private-mode errors are swallowed; losing a memory
|
|
34
|
+
* write is non-fatal.
|
|
35
|
+
*
|
|
36
|
+
* @param key - localStorage key to write.
|
|
37
|
+
* @param snapshot - Serialisable value to remember.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* writeRememberedState("myapp:tool:cgt", { proceeds: 1000 });
|
|
41
|
+
*/
|
|
42
|
+
declare function writeRememberedState<T>(key: string, snapshot: T): void;
|
|
43
|
+
/**
|
|
44
|
+
* True when `paramName` is present in the given search string (e.g.
|
|
45
|
+
* `window.location.search`). Generic form of the "an explicit shared-link /
|
|
46
|
+
* saved-record query param always wins over remembered state" precedence
|
|
47
|
+
* check — callers supply the param name (e.g. `"restore"`); this module
|
|
48
|
+
* never hardcodes one.
|
|
49
|
+
*
|
|
50
|
+
* @param search - A URL search string, e.g. `window.location.search`.
|
|
51
|
+
* @param paramName - The query param to look for.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* hasUrlParam("?restore=abc123", "restore"); // true
|
|
55
|
+
*/
|
|
56
|
+
declare function hasUrlParam(search: string, paramName: string): boolean;
|
|
57
|
+
|
|
58
|
+
type AnnouncePriority = "polite" | "assertive";
|
|
59
|
+
interface LiveAnnouncement {
|
|
60
|
+
message: string;
|
|
61
|
+
priority: AnnouncePriority;
|
|
62
|
+
}
|
|
63
|
+
interface AnnounceOptions {
|
|
64
|
+
/** Custom-event name the paired `LiveRegion` listens on. Must match the `eventName` it was mounted with. Default `DEFAULT_ANNOUNCE_EVENT`. */
|
|
65
|
+
eventName?: string;
|
|
66
|
+
}
|
|
67
|
+
/** Default `window` CustomEvent name used when `announce()` / `LiveRegion` are not given an explicit `eventName`. */
|
|
68
|
+
declare const DEFAULT_ANNOUNCE_EVENT = "hazo_ui:announce";
|
|
69
|
+
/**
|
|
70
|
+
* Dispatch a screen-reader announcement via a `window` CustomEvent. Safe to
|
|
71
|
+
* call from anywhere in the client tree — no-ops during SSR or before a
|
|
72
|
+
* `LiveRegion` has mounted (fire-and-forget; there's no queue/replay).
|
|
73
|
+
*
|
|
74
|
+
* Use `"polite"` (default) for ambient updates ("Sarah joined the canvas",
|
|
75
|
+
* "2 new notifications"). Use `"assertive"` only for things the user must
|
|
76
|
+
* hear immediately (errors that interrupt a task).
|
|
77
|
+
*
|
|
78
|
+
* @param message - Text to announce. Empty / whitespace-only messages are dropped (no dispatch).
|
|
79
|
+
* @param priority - `"polite"` (default) or `"assertive"`.
|
|
80
|
+
* @param options.eventName - Custom-event name to dispatch on. Default `DEFAULT_ANNOUNCE_EVENT`.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* announce("2 new notifications");
|
|
84
|
+
* announce("Save failed — check your connection", "assertive");
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* // namespaced channel — must match the eventName the LiveRegion was mounted with
|
|
88
|
+
* announce("Saved", "polite", { eventName: "myapp:announce" });
|
|
89
|
+
*/
|
|
90
|
+
declare function announce(message: string, priority?: AnnouncePriority, options?: AnnounceOptions): void;
|
|
91
|
+
|
|
92
|
+
export { type AnnounceOptions as A, DEFAULT_ANNOUNCE_EVENT as D, type LiveAnnouncement as L, type AnnouncePriority as a, announce as b, cn as c, hasUrlParam as h, readRememberedState as r, safeHref as s, writeRememberedState as w };
|
package/dist/index.cjs
CHANGED
|
@@ -11944,6 +11944,253 @@ function AppFooter({ variant, footer, identity, className }) {
|
|
|
11944
11944
|
);
|
|
11945
11945
|
}
|
|
11946
11946
|
|
|
11947
|
+
// src/lib/remembered_state.ts
|
|
11948
|
+
function readRememberedState(key) {
|
|
11949
|
+
if (typeof window === "undefined") return null;
|
|
11950
|
+
try {
|
|
11951
|
+
const raw = window.localStorage.getItem(key);
|
|
11952
|
+
return raw ? JSON.parse(raw) : null;
|
|
11953
|
+
} catch {
|
|
11954
|
+
return null;
|
|
11955
|
+
}
|
|
11956
|
+
}
|
|
11957
|
+
function writeRememberedState(key, snapshot) {
|
|
11958
|
+
if (typeof window === "undefined") return;
|
|
11959
|
+
try {
|
|
11960
|
+
window.localStorage.setItem(key, JSON.stringify(snapshot));
|
|
11961
|
+
} catch {
|
|
11962
|
+
}
|
|
11963
|
+
}
|
|
11964
|
+
function hasUrlParam(search, paramName) {
|
|
11965
|
+
try {
|
|
11966
|
+
return new URLSearchParams(search).has(paramName);
|
|
11967
|
+
} catch {
|
|
11968
|
+
return false;
|
|
11969
|
+
}
|
|
11970
|
+
}
|
|
11971
|
+
|
|
11972
|
+
// src/hooks/use_remembered_state.ts
|
|
11973
|
+
function useRememberedState({
|
|
11974
|
+
key,
|
|
11975
|
+
snapshot,
|
|
11976
|
+
restore,
|
|
11977
|
+
enabled = true,
|
|
11978
|
+
debounceMs = 600,
|
|
11979
|
+
skipWhenUrlParam
|
|
11980
|
+
}) {
|
|
11981
|
+
const restoreRef = React27.useRef(restore);
|
|
11982
|
+
React27.useEffect(() => {
|
|
11983
|
+
restoreRef.current = restore;
|
|
11984
|
+
});
|
|
11985
|
+
const didRestore = React27.useRef(false);
|
|
11986
|
+
React27.useEffect(() => {
|
|
11987
|
+
if (didRestore.current) return;
|
|
11988
|
+
didRestore.current = true;
|
|
11989
|
+
if (!enabled) return;
|
|
11990
|
+
if (typeof window === "undefined") return;
|
|
11991
|
+
if (skipWhenUrlParam && hasUrlParam(window.location.search, skipWhenUrlParam)) return;
|
|
11992
|
+
const remembered = readRememberedState(key);
|
|
11993
|
+
if (remembered === null) return;
|
|
11994
|
+
try {
|
|
11995
|
+
restoreRef.current(remembered);
|
|
11996
|
+
} catch {
|
|
11997
|
+
}
|
|
11998
|
+
}, [key, enabled, skipWhenUrlParam]);
|
|
11999
|
+
const snapshotJson = JSON.stringify(snapshot);
|
|
12000
|
+
const writeTimer = React27.useRef(null);
|
|
12001
|
+
React27.useEffect(() => {
|
|
12002
|
+
if (!enabled) return;
|
|
12003
|
+
if (writeTimer.current) clearTimeout(writeTimer.current);
|
|
12004
|
+
writeTimer.current = setTimeout(() => {
|
|
12005
|
+
writeRememberedState(key, snapshot);
|
|
12006
|
+
}, debounceMs);
|
|
12007
|
+
return () => {
|
|
12008
|
+
if (writeTimer.current) clearTimeout(writeTimer.current);
|
|
12009
|
+
};
|
|
12010
|
+
}, [key, snapshotJson, enabled, debounceMs]);
|
|
12011
|
+
}
|
|
12012
|
+
var COARSE_QUERY = "(pointer: coarse)";
|
|
12013
|
+
function subscribe_coarse(notify) {
|
|
12014
|
+
if (typeof window === "undefined") return () => {
|
|
12015
|
+
};
|
|
12016
|
+
const mql = window.matchMedia(COARSE_QUERY);
|
|
12017
|
+
mql.addEventListener("change", notify);
|
|
12018
|
+
return () => mql.removeEventListener("change", notify);
|
|
12019
|
+
}
|
|
12020
|
+
function get_coarse_snapshot() {
|
|
12021
|
+
return window.matchMedia(COARSE_QUERY).matches;
|
|
12022
|
+
}
|
|
12023
|
+
function get_coarse_server_snapshot() {
|
|
12024
|
+
return false;
|
|
12025
|
+
}
|
|
12026
|
+
function useLongPress({
|
|
12027
|
+
onLongPress,
|
|
12028
|
+
delayMs = 500,
|
|
12029
|
+
moveThresholdPx = 8,
|
|
12030
|
+
hapticMs = 10
|
|
12031
|
+
}) {
|
|
12032
|
+
const isCoarse = React27.useSyncExternalStore(
|
|
12033
|
+
subscribe_coarse,
|
|
12034
|
+
get_coarse_snapshot,
|
|
12035
|
+
get_coarse_server_snapshot
|
|
12036
|
+
);
|
|
12037
|
+
const timerRef = React27.useRef(null);
|
|
12038
|
+
const originRef = React27.useRef(null);
|
|
12039
|
+
const firedRef = React27.useRef(false);
|
|
12040
|
+
const eventRef = React27.useRef(null);
|
|
12041
|
+
const cancel = React27.useCallback(() => {
|
|
12042
|
+
if (timerRef.current !== null) {
|
|
12043
|
+
clearTimeout(timerRef.current);
|
|
12044
|
+
timerRef.current = null;
|
|
12045
|
+
}
|
|
12046
|
+
originRef.current = null;
|
|
12047
|
+
eventRef.current = null;
|
|
12048
|
+
}, []);
|
|
12049
|
+
React27.useEffect(() => () => cancel(), [cancel]);
|
|
12050
|
+
const noop = React27.useCallback(() => {
|
|
12051
|
+
}, []);
|
|
12052
|
+
const handlePointerDown = React27.useCallback(
|
|
12053
|
+
(e) => {
|
|
12054
|
+
if (originRef.current && originRef.current.pointerId !== e.pointerId) {
|
|
12055
|
+
cancel();
|
|
12056
|
+
return;
|
|
12057
|
+
}
|
|
12058
|
+
firedRef.current = false;
|
|
12059
|
+
originRef.current = { x: e.clientX, y: e.clientY, pointerId: e.pointerId };
|
|
12060
|
+
eventRef.current = e;
|
|
12061
|
+
timerRef.current = setTimeout(() => {
|
|
12062
|
+
timerRef.current = null;
|
|
12063
|
+
firedRef.current = true;
|
|
12064
|
+
if (hapticMs > 0 && typeof navigator !== "undefined" && "vibrate" in navigator) {
|
|
12065
|
+
try {
|
|
12066
|
+
navigator.vibrate?.(hapticMs);
|
|
12067
|
+
} catch {
|
|
12068
|
+
}
|
|
12069
|
+
}
|
|
12070
|
+
const evt = eventRef.current;
|
|
12071
|
+
if (evt) onLongPress(evt);
|
|
12072
|
+
}, delayMs);
|
|
12073
|
+
},
|
|
12074
|
+
[delayMs, hapticMs, cancel, onLongPress]
|
|
12075
|
+
);
|
|
12076
|
+
const handlePointerMove = React27.useCallback(
|
|
12077
|
+
(e) => {
|
|
12078
|
+
if (!originRef.current) return;
|
|
12079
|
+
const dx = e.clientX - originRef.current.x;
|
|
12080
|
+
const dy = e.clientY - originRef.current.y;
|
|
12081
|
+
if (Math.hypot(dx, dy) > moveThresholdPx) cancel();
|
|
12082
|
+
},
|
|
12083
|
+
[moveThresholdPx, cancel]
|
|
12084
|
+
);
|
|
12085
|
+
const handlePointerUpOrCancel = React27.useCallback(() => {
|
|
12086
|
+
cancel();
|
|
12087
|
+
}, [cancel]);
|
|
12088
|
+
const handleClick = React27.useCallback((e) => {
|
|
12089
|
+
if (firedRef.current) {
|
|
12090
|
+
e.preventDefault();
|
|
12091
|
+
e.stopPropagation();
|
|
12092
|
+
firedRef.current = false;
|
|
12093
|
+
}
|
|
12094
|
+
}, []);
|
|
12095
|
+
if (!isCoarse) {
|
|
12096
|
+
return {
|
|
12097
|
+
onPointerDown: noop,
|
|
12098
|
+
onPointerMove: noop,
|
|
12099
|
+
onPointerUp: noop,
|
|
12100
|
+
onPointerCancel: noop,
|
|
12101
|
+
onClick: noop
|
|
12102
|
+
};
|
|
12103
|
+
}
|
|
12104
|
+
return {
|
|
12105
|
+
onPointerDown: handlePointerDown,
|
|
12106
|
+
onPointerMove: handlePointerMove,
|
|
12107
|
+
onPointerUp: handlePointerUpOrCancel,
|
|
12108
|
+
onPointerCancel: handlePointerUpOrCancel,
|
|
12109
|
+
onClick: handleClick
|
|
12110
|
+
};
|
|
12111
|
+
}
|
|
12112
|
+
|
|
12113
|
+
// src/lib/announce.ts
|
|
12114
|
+
var DEFAULT_ANNOUNCE_EVENT = "hazo_ui:announce";
|
|
12115
|
+
function announce(message, priority = "polite", options) {
|
|
12116
|
+
if (typeof window === "undefined") return;
|
|
12117
|
+
const trimmed = message.trim();
|
|
12118
|
+
if (!trimmed) return;
|
|
12119
|
+
const eventName = options?.eventName ?? DEFAULT_ANNOUNCE_EVENT;
|
|
12120
|
+
window.dispatchEvent(
|
|
12121
|
+
new CustomEvent(eventName, {
|
|
12122
|
+
detail: { message: trimmed, priority }
|
|
12123
|
+
})
|
|
12124
|
+
);
|
|
12125
|
+
}
|
|
12126
|
+
function LiveRegion({
|
|
12127
|
+
eventName = DEFAULT_ANNOUNCE_EVENT,
|
|
12128
|
+
className = "sr-only",
|
|
12129
|
+
clearAfterMs = 6e3
|
|
12130
|
+
}) {
|
|
12131
|
+
const [polite, set_polite] = React27.useState("");
|
|
12132
|
+
const [assertive, set_assertive] = React27.useState("");
|
|
12133
|
+
const polite_timer = React27.useRef(null);
|
|
12134
|
+
const assertive_timer = React27.useRef(null);
|
|
12135
|
+
React27.useEffect(() => {
|
|
12136
|
+
const handler = (e) => {
|
|
12137
|
+
const detail = e.detail;
|
|
12138
|
+
if (!detail?.message) return;
|
|
12139
|
+
if (detail.priority === "assertive") {
|
|
12140
|
+
if (assertive_timer.current) clearTimeout(assertive_timer.current);
|
|
12141
|
+
set_assertive((prev) => prev === detail.message ? `${detail.message} ` : detail.message);
|
|
12142
|
+
assertive_timer.current = setTimeout(() => set_assertive(""), clearAfterMs);
|
|
12143
|
+
} else {
|
|
12144
|
+
if (polite_timer.current) clearTimeout(polite_timer.current);
|
|
12145
|
+
set_polite((prev) => prev === detail.message ? `${detail.message} ` : detail.message);
|
|
12146
|
+
polite_timer.current = setTimeout(() => set_polite(""), clearAfterMs);
|
|
12147
|
+
}
|
|
12148
|
+
};
|
|
12149
|
+
window.addEventListener(eventName, handler);
|
|
12150
|
+
return () => {
|
|
12151
|
+
window.removeEventListener(eventName, handler);
|
|
12152
|
+
if (polite_timer.current) clearTimeout(polite_timer.current);
|
|
12153
|
+
if (assertive_timer.current) clearTimeout(assertive_timer.current);
|
|
12154
|
+
};
|
|
12155
|
+
}, [eventName, clearAfterMs]);
|
|
12156
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
12157
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", className, children: polite }),
|
|
12158
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { role: "alert", "aria-live": "assertive", "aria-atomic": "true", className, children: assertive })
|
|
12159
|
+
] });
|
|
12160
|
+
}
|
|
12161
|
+
|
|
12162
|
+
// src/lib/scroll_restore.ts
|
|
12163
|
+
var DEFAULT_SCROLL_RESTORE_PREFIX = "hazo_ui:scrollPos";
|
|
12164
|
+
function buildScrollRestoreKey(prefix, treeId, viewName) {
|
|
12165
|
+
return `${prefix}.${treeId}.${viewName}`;
|
|
12166
|
+
}
|
|
12167
|
+
function saveScrollPos(key, scrollY) {
|
|
12168
|
+
if (typeof window === "undefined") return;
|
|
12169
|
+
try {
|
|
12170
|
+
window.localStorage.setItem(key, String(Math.round(scrollY)));
|
|
12171
|
+
} catch {
|
|
12172
|
+
}
|
|
12173
|
+
}
|
|
12174
|
+
function loadScrollPos(key) {
|
|
12175
|
+
if (typeof window === "undefined") return null;
|
|
12176
|
+
try {
|
|
12177
|
+
const raw = window.localStorage.getItem(key);
|
|
12178
|
+
if (!raw) return null;
|
|
12179
|
+
const n = Number.parseInt(raw, 10);
|
|
12180
|
+
return Number.isFinite(n) ? n : null;
|
|
12181
|
+
} catch {
|
|
12182
|
+
return null;
|
|
12183
|
+
}
|
|
12184
|
+
}
|
|
12185
|
+
|
|
12186
|
+
// src/hooks/use_scroll_restore.ts
|
|
12187
|
+
function useScrollRestore(treeId, viewName, options) {
|
|
12188
|
+
const key = buildScrollRestoreKey(options?.prefix ?? DEFAULT_SCROLL_RESTORE_PREFIX, treeId, viewName);
|
|
12189
|
+
const save = React27.useCallback((scrollY) => saveScrollPos(key, scrollY), [key]);
|
|
12190
|
+
const load = React27.useCallback(() => loadScrollPos(key), [key]);
|
|
12191
|
+
return { save, load };
|
|
12192
|
+
}
|
|
12193
|
+
|
|
11947
12194
|
Object.defineProperty(exports, "rawToast", {
|
|
11948
12195
|
enumerable: true,
|
|
11949
12196
|
get: function () { return sonner.toast; }
|
|
@@ -12007,6 +12254,7 @@ exports.CollapsibleTrigger = CollapsibleTrigger2;
|
|
|
12007
12254
|
exports.CommandNodeExtension = CommandNodeExtension;
|
|
12008
12255
|
exports.CommandPill = CommandPill;
|
|
12009
12256
|
exports.CommandPopover = CommandPopover;
|
|
12257
|
+
exports.DEFAULT_ANNOUNCE_EVENT = DEFAULT_ANNOUNCE_EVENT;
|
|
12010
12258
|
exports.DateRangeSelector = DateRangeSelector;
|
|
12011
12259
|
exports.Drawer = Drawer;
|
|
12012
12260
|
exports.DrawerClose = DrawerClose;
|
|
@@ -12076,6 +12324,7 @@ exports.Input = Input;
|
|
|
12076
12324
|
exports.InputAffix = InputAffix;
|
|
12077
12325
|
exports.InverseSparkline = InverseSparkline;
|
|
12078
12326
|
exports.Label = Label3;
|
|
12327
|
+
exports.LiveRegion = LiveRegion;
|
|
12079
12328
|
exports.LoadingTimeout = LoadingTimeout;
|
|
12080
12329
|
exports.Logo = Logo;
|
|
12081
12330
|
exports.MarkdownEditor = MarkdownEditor;
|
|
@@ -12137,6 +12386,7 @@ exports.Tooltip = Tooltip;
|
|
|
12137
12386
|
exports.TooltipContent = TooltipContent;
|
|
12138
12387
|
exports.TooltipProvider = TooltipProvider;
|
|
12139
12388
|
exports.TooltipTrigger = TooltipTrigger;
|
|
12389
|
+
exports.announce = announce;
|
|
12140
12390
|
exports.applyKanbanFilter = applyKanbanFilter;
|
|
12141
12391
|
exports.buttonGroupVariants = buttonGroupVariants;
|
|
12142
12392
|
exports.celebrate = celebrate;
|
|
@@ -12149,11 +12399,13 @@ exports.format_num = format_num;
|
|
|
12149
12399
|
exports.generateUUID = generateUUID;
|
|
12150
12400
|
exports.get_hazo_ui_config = get_hazo_ui_config;
|
|
12151
12401
|
exports.get_logger = get_logger;
|
|
12402
|
+
exports.hasUrlParam = hasUrlParam;
|
|
12152
12403
|
exports.isLucideValue = isLucideValue;
|
|
12153
12404
|
exports.median = median;
|
|
12154
12405
|
exports.parse_commands_from_text = parse_commands_from_text;
|
|
12155
12406
|
exports.permissionToast = permissionToast;
|
|
12156
12407
|
exports.pick_x_label_indices = pick_x_label_indices;
|
|
12408
|
+
exports.readRememberedState = readRememberedState;
|
|
12157
12409
|
exports.reset_hazo_ui_config = reset_hazo_ui_config;
|
|
12158
12410
|
exports.resolve_animation_classes = resolve_animation_classes;
|
|
12159
12411
|
exports.safeHref = safeHref;
|
|
@@ -12172,11 +12424,15 @@ exports.useFullscreen = useFullscreen;
|
|
|
12172
12424
|
exports.useIsMobile = useIsMobile;
|
|
12173
12425
|
exports.useLoadingState = useLoadingState;
|
|
12174
12426
|
exports.useLocalStorage = useLocalStorage;
|
|
12427
|
+
exports.useLongPress = useLongPress;
|
|
12175
12428
|
exports.useMediaQuery = useMediaQuery;
|
|
12429
|
+
exports.useRememberedState = useRememberedState;
|
|
12430
|
+
exports.useScrollRestore = useScrollRestore;
|
|
12176
12431
|
exports.useSessionStorage = useSessionStorage;
|
|
12177
12432
|
exports.useViewport = useViewport;
|
|
12178
12433
|
exports.useWakeLock = useWakeLock;
|
|
12179
12434
|
exports.use_fullscreen = use_fullscreen;
|
|
12180
12435
|
exports.use_wake_lock = use_wake_lock;
|
|
12436
|
+
exports.writeRememberedState = writeRememberedState;
|
|
12181
12437
|
//# sourceMappingURL=index.cjs.map
|
|
12182
12438
|
//# sourceMappingURL=index.cjs.map
|