hazo_ui 6.0.0 → 6.3.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/CHANGE_LOG.md +71 -0
- package/README.md +24 -1
- package/dist/announce-DAnQDTld.d.cts +92 -0
- package/dist/announce-DAnQDTld.d.ts +92 -0
- package/dist/index.cjs +274 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +160 -3
- package/dist/index.d.ts +160 -3
- package/dist/index.js +266 -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 +3 -3
- package/dist/index.utils-DZVbQXFR.d.cts +0 -20
- package/dist/index.utils-DZVbQXFR.d.ts +0 -20
package/CHANGE_LOG.md
CHANGED
|
@@ -5,6 +5,77 @@ 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.3.0 (2026-07-21)
|
|
9
|
+
|
|
10
|
+
### New
|
|
11
|
+
- feat: add `useLongPress()` (touch-only long-press hook) — ported as-is
|
|
12
|
+
from Kinstripe's `src/lib/hooks/useLongPress.ts` (already fully generic;
|
|
13
|
+
no app coupling to remove). Every returned handler
|
|
14
|
+
(`onPointerDown`/`onPointerMove`/`onPointerUp`/`onPointerCancel`/`onClick`)
|
|
15
|
+
is a no-op on fine-pointer (mouse) devices — desktop should keep using
|
|
16
|
+
`onContextMenu` for right-click. Cancels on movement past
|
|
17
|
+
`moveThresholdPx`, early `pointerup`, a second `pointerdown`
|
|
18
|
+
(multi-touch), or `pointercancel`.
|
|
19
|
+
- feat: add `announce()` + `<LiveRegion>` — a screen-reader announcement
|
|
20
|
+
bus (a `window` CustomEvent dispatched by `announce()`, consumed by a
|
|
21
|
+
mounted `<LiveRegion>`), generalised from Kinstripe's `announce.ts` +
|
|
22
|
+
`KsLiveRegion`. Both the hardcoded event name (`"kinstripe:announce"`)
|
|
23
|
+
and the sr-only class name (`"kn-sr-only"`) are now configurable:
|
|
24
|
+
`announce(message, priority, { eventName })` and
|
|
25
|
+
`<LiveRegion eventName className clearAfterMs />`, defaulting to
|
|
26
|
+
`DEFAULT_ANNOUNCE_EVENT` and Tailwind's built-in `"sr-only"` utility so
|
|
27
|
+
it works out of the box. Keeps the two-region (polite/assertive) design,
|
|
28
|
+
the default 6s self-clear, and the duplicate-text re-announce trick.
|
|
29
|
+
`announce()` is also exported from the server-safe `hazo_ui/utils`
|
|
30
|
+
subpath (same dual-export pattern as `cn`).
|
|
31
|
+
- feat: add `useScrollRestore()` — localStorage-backed scroll-position
|
|
32
|
+
save/load, generalised from Kinstripe's `useScrollRestore`. Keeps the
|
|
33
|
+
same `(treeId, viewName)` positional signature; the previously-hardcoded
|
|
34
|
+
`"kinstripe.scrollPos"` key prefix is now an optional third argument
|
|
35
|
+
(`{ prefix }`, default `DEFAULT_SCROLL_RESTORE_PREFIX`). The underlying
|
|
36
|
+
pure helpers (`buildScrollRestoreKey`, `saveScrollPos`, `loadScrollPos`)
|
|
37
|
+
are split into `lib/scroll_restore.ts` (same hook/lib split as
|
|
38
|
+
`useRememberedState`/`remembered_state.ts`) and also exported from
|
|
39
|
+
`hazo_ui/utils`.
|
|
40
|
+
- Demo pages added under `test-app/app/{long-press,live-region,scroll-restore}/`,
|
|
41
|
+
new "Ported utilities (v6.3)" sidebar section, and five new
|
|
42
|
+
`hazo_ui/test-harness` scenarios (`use_long_press`, `announce_dispatch`,
|
|
43
|
+
`live_region_round_trip`, `scroll_restore_helpers`,
|
|
44
|
+
`use_scroll_restore_hook`) registered in
|
|
45
|
+
`test-app/app/harness-self-test/hazo_ui_utility_ports_scenarios.ts` — the
|
|
46
|
+
pointer/DOM-driven ones mount a real React tree via `react-dom/client`
|
|
47
|
+
against the test-app's live browser DOM rather than settling for an
|
|
48
|
+
export-shape check.
|
|
49
|
+
|
|
50
|
+
## v6.2.0 (2026-07-21)
|
|
51
|
+
|
|
52
|
+
### New
|
|
53
|
+
- feat: add `useRememberedState<T>()` hook (FR-020) — a generic
|
|
54
|
+
localStorage-backed "remember last-used state across visits" hook:
|
|
55
|
+
restore-once-on-mount (ref-guarded, immune to `restore` identity churn),
|
|
56
|
+
a debounced write (`debounceMs`, default `600`), and an optional
|
|
57
|
+
`skipWhenUrlParam` escape hatch so an explicit shared-link/saved-record
|
|
58
|
+
query param always wins over remembered state. Generalised from
|
|
59
|
+
stocktools's app-local `useToolMemory` — the caller now supplies the full
|
|
60
|
+
storage `key` and generic `snapshot`/`restore` callbacks instead of a
|
|
61
|
+
hardcoded key prefix and `Seed`-typed shape.
|
|
62
|
+
- feat: add pure helpers `readRememberedState`, `writeRememberedState`,
|
|
63
|
+
`hasUrlParam` backing the hook — SSR-safe, JSON-parse/stringify with
|
|
64
|
+
corrupt-data tolerance, framework-free. Exported from **both** the main
|
|
65
|
+
`hazo_ui` barrel and the server-safe `hazo_ui/utils` subpath (same
|
|
66
|
+
dual-export pattern as `cn`), so Server Components can read/write
|
|
67
|
+
remembered state without pulling in the `"use client"` bundle.
|
|
68
|
+
- Demo page added to `test-app/app/remembered-state/` under the new
|
|
69
|
+
"Remembered state (v6.2)" sidebar section.
|
|
70
|
+
|
|
71
|
+
## v6.1.0 (2026-07-17)
|
|
72
|
+
|
|
73
|
+
### New
|
|
74
|
+
- feat: add `permissionToast` helper (FR-017) — an opinionated wrapper around
|
|
75
|
+
`errorToast` that standardizes permission-failure toasts (fixed "Permission
|
|
76
|
+
required" title, composed missing-permission description, optional action
|
|
77
|
+
button). Exported alongside `PermissionToastOptions` from `hazo_ui`.
|
|
78
|
+
|
|
8
79
|
## v6.0.0 (2026-07-11)
|
|
9
80
|
|
|
10
81
|
Major — the P1 hazo_theme rehome. hazo_ui stops owning theme tokens, preset, and provider;
|
package/README.md
CHANGED
|
@@ -226,7 +226,7 @@ Lightweight, opinionated components for the four ubiquitous async states: **load
|
|
|
226
226
|
- **[ErrorPage](#errorpage)** - Full-page error fallback with title, description, error code tag, correlation id, and CTAs.
|
|
227
227
|
- **[LoadingTimeout](#loadingtimeout)** - Wraps a loading region and escalates messaging at 5s / 15s / 30s thresholds before showing a retry banner.
|
|
228
228
|
- **[ProgressiveImage](#progressiveimage)** - Three-stage image render: grey placeholder → blurred LQIP → sharp final image.
|
|
229
|
-
- **[HazoUiToaster + toast helpers](#toasts-hazouitoaster--successtoast--errortoast)** - `sonner`-backed toaster with `successToast()` and `
|
|
229
|
+
- **[HazoUiToaster + toast helpers](#toasts-hazouitoaster--successtoast--errortoast)** - `sonner`-backed toaster with `successToast()`, `errorToast()`, and `permissionToast()` imperative helpers.
|
|
230
230
|
- **[useLoadingState](#useloadingstate)** hook - `{ isLoading, setLoading, withLoading }` with an async wrapper.
|
|
231
231
|
- **[useErrorDisplay](#useerrordisplay)** hook - Passive `{ error, setError, clearError }` that coerces `Error` instances to strings.
|
|
232
232
|
|
|
@@ -3443,6 +3443,29 @@ try { await sync(); } catch (e) {
|
|
|
3443
3443
|
| `duration` | `number` (ms) | `3000` success / `5000` error |
|
|
3444
3444
|
| `action` | `{ label: string; onClick: () => void }` | — |
|
|
3445
3445
|
|
|
3446
|
+
**`permissionToast(opts: PermissionToastOptions)`** — an opinionated wrapper around
|
|
3447
|
+
`errorToast` for permission-failure UX. Always titles the toast `"Permission required"`
|
|
3448
|
+
and composes a description from the missing permission key(s) (and optional human
|
|
3449
|
+
descriptions), so every "you can't do that" moment in the app reads consistently:
|
|
3450
|
+
|
|
3451
|
+
```tsx
|
|
3452
|
+
import { permissionToast } from "hazo_ui";
|
|
3453
|
+
|
|
3454
|
+
permissionToast({
|
|
3455
|
+
missingPermissions: ["env.migrate"],
|
|
3456
|
+
descriptions: { "env.migrate": "Migrate environments" },
|
|
3457
|
+
action: { label: "Request access", onClick: () => requestAccess() },
|
|
3458
|
+
});
|
|
3459
|
+
// → "Permission required" / "Missing: `env.migrate` — Migrate environments"
|
|
3460
|
+
```
|
|
3461
|
+
|
|
3462
|
+
| Prop | Type | Default |
|
|
3463
|
+
|---|---|---|
|
|
3464
|
+
| `missingPermissions` | `string[]` | **required** |
|
|
3465
|
+
| `descriptions` | `Record<string, string>` | — |
|
|
3466
|
+
| `action` | `{ label: string; onClick: () => void }` | — |
|
|
3467
|
+
| `duration` | `number` (ms) | `5000` |
|
|
3468
|
+
|
|
3446
3469
|
Also exports `rawToast` (re-export of sonner's `toast`) for advanced use cases.
|
|
3447
3470
|
|
|
3448
3471
|
### useWakeLock / use_wake_lock
|
|
@@ -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
|
@@ -8819,6 +8819,23 @@ function errorToast({ title, description, duration = 5e3, action }) {
|
|
|
8819
8819
|
action: action ? { label: action.label, onClick: action.onClick } : void 0
|
|
8820
8820
|
});
|
|
8821
8821
|
}
|
|
8822
|
+
function permissionToast({
|
|
8823
|
+
missingPermissions,
|
|
8824
|
+
descriptions,
|
|
8825
|
+
action,
|
|
8826
|
+
duration = 5e3
|
|
8827
|
+
}) {
|
|
8828
|
+
const description = missingPermissions.length === 0 ? "You do not have permission to perform this action." : `Missing: ${missingPermissions.map((key) => {
|
|
8829
|
+
const label = descriptions?.[key];
|
|
8830
|
+
return label ? `\`${key}\` \u2014 ${label}` : `\`${key}\``;
|
|
8831
|
+
}).join("; ")}`;
|
|
8832
|
+
return errorToast({
|
|
8833
|
+
title: "Permission required",
|
|
8834
|
+
description,
|
|
8835
|
+
action,
|
|
8836
|
+
duration
|
|
8837
|
+
});
|
|
8838
|
+
}
|
|
8822
8839
|
function useLoadingState(initial = false) {
|
|
8823
8840
|
const [isLoading, setLoading] = React27.useState(initial);
|
|
8824
8841
|
const withLoading = React27.useCallback(async (fn) => {
|
|
@@ -11927,6 +11944,253 @@ function AppFooter({ variant, footer, identity, className }) {
|
|
|
11927
11944
|
);
|
|
11928
11945
|
}
|
|
11929
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
|
+
|
|
11930
12194
|
Object.defineProperty(exports, "rawToast", {
|
|
11931
12195
|
enumerable: true,
|
|
11932
12196
|
get: function () { return sonner.toast; }
|
|
@@ -11990,6 +12254,7 @@ exports.CollapsibleTrigger = CollapsibleTrigger2;
|
|
|
11990
12254
|
exports.CommandNodeExtension = CommandNodeExtension;
|
|
11991
12255
|
exports.CommandPill = CommandPill;
|
|
11992
12256
|
exports.CommandPopover = CommandPopover;
|
|
12257
|
+
exports.DEFAULT_ANNOUNCE_EVENT = DEFAULT_ANNOUNCE_EVENT;
|
|
11993
12258
|
exports.DateRangeSelector = DateRangeSelector;
|
|
11994
12259
|
exports.Drawer = Drawer;
|
|
11995
12260
|
exports.DrawerClose = DrawerClose;
|
|
@@ -12059,6 +12324,7 @@ exports.Input = Input;
|
|
|
12059
12324
|
exports.InputAffix = InputAffix;
|
|
12060
12325
|
exports.InverseSparkline = InverseSparkline;
|
|
12061
12326
|
exports.Label = Label3;
|
|
12327
|
+
exports.LiveRegion = LiveRegion;
|
|
12062
12328
|
exports.LoadingTimeout = LoadingTimeout;
|
|
12063
12329
|
exports.Logo = Logo;
|
|
12064
12330
|
exports.MarkdownEditor = MarkdownEditor;
|
|
@@ -12120,6 +12386,7 @@ exports.Tooltip = Tooltip;
|
|
|
12120
12386
|
exports.TooltipContent = TooltipContent;
|
|
12121
12387
|
exports.TooltipProvider = TooltipProvider;
|
|
12122
12388
|
exports.TooltipTrigger = TooltipTrigger;
|
|
12389
|
+
exports.announce = announce;
|
|
12123
12390
|
exports.applyKanbanFilter = applyKanbanFilter;
|
|
12124
12391
|
exports.buttonGroupVariants = buttonGroupVariants;
|
|
12125
12392
|
exports.celebrate = celebrate;
|
|
@@ -12132,10 +12399,13 @@ exports.format_num = format_num;
|
|
|
12132
12399
|
exports.generateUUID = generateUUID;
|
|
12133
12400
|
exports.get_hazo_ui_config = get_hazo_ui_config;
|
|
12134
12401
|
exports.get_logger = get_logger;
|
|
12402
|
+
exports.hasUrlParam = hasUrlParam;
|
|
12135
12403
|
exports.isLucideValue = isLucideValue;
|
|
12136
12404
|
exports.median = median;
|
|
12137
12405
|
exports.parse_commands_from_text = parse_commands_from_text;
|
|
12406
|
+
exports.permissionToast = permissionToast;
|
|
12138
12407
|
exports.pick_x_label_indices = pick_x_label_indices;
|
|
12408
|
+
exports.readRememberedState = readRememberedState;
|
|
12139
12409
|
exports.reset_hazo_ui_config = reset_hazo_ui_config;
|
|
12140
12410
|
exports.resolve_animation_classes = resolve_animation_classes;
|
|
12141
12411
|
exports.safeHref = safeHref;
|
|
@@ -12154,11 +12424,15 @@ exports.useFullscreen = useFullscreen;
|
|
|
12154
12424
|
exports.useIsMobile = useIsMobile;
|
|
12155
12425
|
exports.useLoadingState = useLoadingState;
|
|
12156
12426
|
exports.useLocalStorage = useLocalStorage;
|
|
12427
|
+
exports.useLongPress = useLongPress;
|
|
12157
12428
|
exports.useMediaQuery = useMediaQuery;
|
|
12429
|
+
exports.useRememberedState = useRememberedState;
|
|
12430
|
+
exports.useScrollRestore = useScrollRestore;
|
|
12158
12431
|
exports.useSessionStorage = useSessionStorage;
|
|
12159
12432
|
exports.useViewport = useViewport;
|
|
12160
12433
|
exports.useWakeLock = useWakeLock;
|
|
12161
12434
|
exports.use_fullscreen = use_fullscreen;
|
|
12162
12435
|
exports.use_wake_lock = use_wake_lock;
|
|
12436
|
+
exports.writeRememberedState = writeRememberedState;
|
|
12163
12437
|
//# sourceMappingURL=index.cjs.map
|
|
12164
12438
|
//# sourceMappingURL=index.cjs.map
|