@trackunit/react-components 1.25.4 → 1.26.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/index.esm.js CHANGED
@@ -11239,6 +11239,400 @@ const useSessionStorage = (options) => useWebStorage(globalThis.sessionStorage,
11239
11239
  */
11240
11240
  const useSessionStorageReducer = (options) => useWebStorageReducer(globalThis.sessionStorage, options);
11241
11241
 
11242
+ /**
11243
+ * Pure utilities for reading and writing single named entries inside a URL
11244
+ * fragment (the part after `#`).
11245
+ *
11246
+ * # Format contract
11247
+ *
11248
+ * The fragment body (after the leading `#`) is treated as a sequence of
11249
+ * `&`-separated tokens. A token may be:
11250
+ * - **A named entry** — `<key>=<value>` (the first `=` separates key from
11251
+ * value; later `=` chars are part of the value).
11252
+ * - **Foreign content** — anything else (bare scroll anchors like
11253
+ * `section`, hash routes, third-party data, malformed tokens). Foreign
11254
+ * tokens are preserved byte-for-byte, in their original positions.
11255
+ *
11256
+ * # Single-key ownership
11257
+ *
11258
+ * Callers operate on **one** named key per invocation. Every token whose
11259
+ * key does not match the supplied key is foreign — the utility never
11260
+ * categorises or rewrites it. This mirrors how `useSearchParamSync`
11261
+ * treats a single named search param.
11262
+ *
11263
+ * # Value-encoding invariant
11264
+ *
11265
+ * Values passed to {@link writeHashKey} must not contain `&`, `=`, or `#`.
11266
+ * The standard producer for this codebase is `useCustomEncoding`, whose
11267
+ * base64url alphabet (`[A-Za-z0-9_-]`) satisfies the invariant by
11268
+ * construction. Keys are restricted by the existing persistence-key
11269
+ * schema (camelCase, ≤15 chars), so they are likewise safe.
11270
+ *
11271
+ * # Anchor-scroll caveat
11272
+ *
11273
+ * Browsers locate scroll targets by matching the entire fragment string
11274
+ * against element IDs. Appending a named entry to a fragment containing
11275
+ * a bare anchor (e.g. `#section` becoming `#section&userTableTp=...`)
11276
+ * therefore breaks anchor scrolling on that page. This is unavoidable
11277
+ * for any hash-backed shared state. Consumers that rely on scroll
11278
+ * anchors should not also persist state to the hash on the same route.
11279
+ */
11280
+ const TOKEN_SEPARATOR = "&";
11281
+ const KEY_VALUE_SEPARATOR = "=";
11282
+ const stripLeadingHash = (hash) => (hash.startsWith("#") ? hash.slice(1) : hash);
11283
+ const formatHash = (body) => (body.length === 0 ? "" : `#${body}`);
11284
+ /**
11285
+ * Reads the value of a single named entry from a URL fragment.
11286
+ *
11287
+ * @param hash - The fragment string, with or without the leading `#`.
11288
+ * @param key - The entry key to look up.
11289
+ * @returns {string | undefined} The raw value (everything after the first
11290
+ * `=` in the matching token), or `undefined` when the key is absent. An
11291
+ * entry whose token has no `=` is treated as foreign content and ignored.
11292
+ */
11293
+ const readHashKey = (hash, key) => {
11294
+ const body = stripLeadingHash(hash);
11295
+ if (body.length === 0) {
11296
+ return undefined;
11297
+ }
11298
+ for (const token of body.split(TOKEN_SEPARATOR)) {
11299
+ const eqIndex = token.indexOf(KEY_VALUE_SEPARATOR);
11300
+ if (eqIndex > 0 && token.slice(0, eqIndex) === key) {
11301
+ return token.slice(eqIndex + 1);
11302
+ }
11303
+ }
11304
+ return undefined;
11305
+ };
11306
+ /**
11307
+ * Returns a new fragment string with the named entry written, replaced,
11308
+ * or removed. Foreign tokens are preserved in their original order; the
11309
+ * named entry retains its position if it already exists, and is appended
11310
+ * to the end when newly inserted. Empty tokens (e.g. produced by
11311
+ * consecutive `&`) are dropped on the way out.
11312
+ *
11313
+ * @param hash - The current fragment string, with or without the leading `#`.
11314
+ * @param key - The entry key to write.
11315
+ * @param value - The value to set, or `undefined` to remove the entry.
11316
+ * @returns {string} The new fragment string, including a leading `#` when
11317
+ * non-empty, or an empty string when the result has no tokens left.
11318
+ */
11319
+ const writeHashKey = (hash, key, value) => {
11320
+ const body = stripLeadingHash(hash);
11321
+ const tokens = body.length === 0 ? [] : body.split(TOKEN_SEPARATOR);
11322
+ const next = [];
11323
+ let replaced = false;
11324
+ for (const token of tokens) {
11325
+ if (token.length === 0) {
11326
+ continue;
11327
+ }
11328
+ const eqIndex = token.indexOf(KEY_VALUE_SEPARATOR);
11329
+ if (eqIndex > 0 && token.slice(0, eqIndex) === key) {
11330
+ if (value !== undefined && !replaced) {
11331
+ next.push(`${key}${KEY_VALUE_SEPARATOR}${value}`);
11332
+ replaced = true;
11333
+ }
11334
+ // Else: drop the token (either removing the key or dropping a
11335
+ // duplicate occurrence after we already wrote the replacement).
11336
+ continue;
11337
+ }
11338
+ next.push(token);
11339
+ }
11340
+ if (value !== undefined && !replaced) {
11341
+ next.push(`${key}${KEY_VALUE_SEPARATOR}${value}`);
11342
+ }
11343
+ return formatHash(next.join(TOKEN_SEPARATOR));
11344
+ };
11345
+
11346
+ /**
11347
+ * Default maximum length of the resulting URL fragment after adding one
11348
+ * persisted entry. Writes are skipped (localStorage continues to hold the
11349
+ * full state) when adding the entry would push the fragment past this cap.
11350
+ *
11351
+ * The fragment is never sent to the server, so it is not subject to the
11352
+ * AWS WAF managed-rule `SizeRestrictions_QUERYSTRING` cap that applies to
11353
+ * search params (5000 bytes). 64 KB stays well below typical browser URL
11354
+ * limits (Chromium ≳ 100 KB, Firefox ≳ 64 KB on the address bar) while
11355
+ * still leaving generous headroom for any realistic table state.
11356
+ */
11357
+ const MAX_HASH_LENGTH = 64 * 1024;
11358
+ /**
11359
+ * Syncs an encoded string value with a single named entry inside the URL
11360
+ * fragment (`location.hash`) via Tanstack Router.
11361
+ *
11362
+ * Behaviour mirrors `useSearchParamSync`, with two differences:
11363
+ * - Reads from and writes to `location.hash` instead of `location.search`.
11364
+ * - Writes are guarded against {@link MAX_HASH_LENGTH} rather than the
11365
+ * AWS-WAF-driven search-param cap.
11366
+ *
11367
+ * The fragment is treated as a sequence of `&`-separated tokens via the
11368
+ * pure utilities in `./hashFragment`. Foreign tokens (bare anchors, other
11369
+ * named entries) are preserved byte-for-byte on every write.
11370
+ *
11371
+ * @param options - Configuration for the hash entry sync.
11372
+ * @param options.key - The named entry inside the fragment.
11373
+ * @param options.enabled - Set to `false` to disable all URL interaction (default `true`).
11374
+ * @param options.onExternalChange - Called when the entry changes externally
11375
+ * (e.g. browser back/forward, pasted link, foreign router push).
11376
+ * @param options.replace - `true` to always use `replaceState` instead of pushState.
11377
+ */
11378
+ const useHashParamSync = ({ key, enabled = true, onExternalChange, replace: replaceOption, }) => {
11379
+ const navigate = useNavigate();
11380
+ const location = useLocation();
11381
+ const router = useRouter();
11382
+ const lastWrittenRef = useRef(undefined);
11383
+ const pendingWriteRef = useRef(null);
11384
+ const onExternalChangeRef = useRef(onExternalChange);
11385
+ useEffect(() => {
11386
+ onExternalChangeRef.current = onExternalChange;
11387
+ }, [onExternalChange]);
11388
+ const currentHashValue = useMemo(() => {
11389
+ if (!enabled) {
11390
+ return undefined;
11391
+ }
11392
+ return readHashKey(location.hash, key);
11393
+ }, [enabled, location.hash, key]);
11394
+ useEffect(() => {
11395
+ if (!enabled) {
11396
+ return;
11397
+ }
11398
+ if (currentHashValue === undefined) {
11399
+ return;
11400
+ }
11401
+ if (currentHashValue === lastWrittenRef.current) {
11402
+ return;
11403
+ }
11404
+ if (lastWrittenRef.current === undefined) {
11405
+ return;
11406
+ }
11407
+ requestAnimationFrame(() => {
11408
+ onExternalChangeRef.current?.();
11409
+ });
11410
+ }, [currentHashValue, enabled]);
11411
+ // Writes are dispatched via a "pending write" queue rather than calling
11412
+ // `navigate` directly from inside the rAF callback. Tanstack Router's
11413
+ // `router.state.status === "pending"` is common during the first paint
11414
+ // of a route (loaders, suspense), and an earlier implementation that
11415
+ // simply returned in that case lost the write outright — the
11416
+ // URL-restoration path in `usePersistedState` is one-shot (guarded by a
11417
+ // ref), so when a localStorage-→-hash write coincided with a pending
11418
+ // router the canonical hash never got populated and users saw an
11419
+ // intermittent "hash not set on initial visit" bug.
11420
+ //
11421
+ // The queue + retry pattern below makes writes reliable:
11422
+ // - `updateHashParam` stores the latest attempted write in
11423
+ // `pendingWriteRef` and schedules an rAF flush.
11424
+ // - `flushPendingWrite` only navigates when the router is idle; while
11425
+ // pending it leaves the write queued so it can fire later.
11426
+ // - A dedicated effect watches `router.state.status` and re-attempts
11427
+ // the flush whenever the router settles, draining the queue.
11428
+ const flushPendingWrite = useCallback(() => {
11429
+ const pending = pendingWriteRef.current;
11430
+ if (pending === null) {
11431
+ return;
11432
+ }
11433
+ if (router.state.status === "pending") {
11434
+ // Keep the write queued; the status-watcher effect below will retry
11435
+ // when the router transitions back to idle.
11436
+ return;
11437
+ }
11438
+ pendingWriteRef.current = null;
11439
+ const { encodedValue, options } = pending;
11440
+ const nextHash = writeHashKey(location.hash, key, encodedValue);
11441
+ if (nextHash.length > MAX_HASH_LENGTH) {
11442
+ // Drop the write so the fragment never grows past the cap. The
11443
+ // localStorage write performed by `usePersistedState` still holds
11444
+ // the full state, so the layout survives a reload on the same
11445
+ // browser; only the shareable URL is degraded.
11446
+ return;
11447
+ }
11448
+ const shouldReplace = options?.replace ?? replaceOption ?? !Boolean(currentHashValue);
11449
+ // Tanstack Router's `navigate({ hash })` expects the fragment body
11450
+ // *without* the leading `#` — the router adds the delimiter itself
11451
+ // when serialising the URL. Forwarding the `#`-prefixed value from
11452
+ // `writeHashKey` produces a doubled `##` in the address bar
11453
+ // (visually broken, even though browsers still parse the fragment
11454
+ // correctly because URL fragments start at the *first* `#`).
11455
+ const navigateHash = nextHash.startsWith("#") ? nextHash.slice(1) : nextHash;
11456
+ void navigate({
11457
+ to: ".",
11458
+ // Returning the same `prev` reference leaves the search params
11459
+ // unchanged. We must still pass `search` so router types are happy.
11460
+ search: (prev) => prev,
11461
+ hash: navigateHash,
11462
+ replace: shouldReplace,
11463
+ });
11464
+ }, [router.state.status, location.hash, key, currentHashValue, navigate, replaceOption]);
11465
+ useEffect(() => {
11466
+ if (router.state.status === "pending") {
11467
+ return;
11468
+ }
11469
+ if (pendingWriteRef.current === null) {
11470
+ return;
11471
+ }
11472
+ flushPendingWrite();
11473
+ }, [router.state.status, flushPendingWrite]);
11474
+ // Drop any queued write when the consumer unmounts. The queue + retry
11475
+ // pattern above keeps writes pending across a `router.state.status ===
11476
+ // "pending"` phase, which means a write scheduled just before a route
11477
+ // change could otherwise drain onto the *next* route once it settles.
11478
+ // Cancelling here preserves the original "writes are scoped to the
11479
+ // owning route" contract that the pre-queue implementation got for
11480
+ // free by dropping the write outright.
11481
+ useEffect(() => () => {
11482
+ pendingWriteRef.current = null;
11483
+ }, []);
11484
+ const updateHashParam = useCallback((encodedValue, options) => {
11485
+ if (!enabled) {
11486
+ return;
11487
+ }
11488
+ // Dedupe against either the last *committed* write or the value
11489
+ // currently queued for the next rAF. Both represent the caller's
11490
+ // intent so we don't schedule redundant rAF callbacks for identical
11491
+ // requests.
11492
+ const inFlightOrLastWritten = pendingWriteRef.current?.encodedValue ?? lastWrittenRef.current;
11493
+ if (encodedValue !== undefined && encodedValue === inFlightOrLastWritten) {
11494
+ return;
11495
+ }
11496
+ // Prime `lastWrittenRef` here (not inside `flushPendingWrite`) so
11497
+ // the external-change detector treats this value as the caller's
11498
+ // authoritative state even when the flush is a no-op (URL already
11499
+ // matches) or stays queued during a pending router phase. Without
11500
+ // priming, the next "external" change would look identical to the
11501
+ // initial render (`lastWrittenRef === undefined`) and the callback
11502
+ // would be skipped.
11503
+ lastWrittenRef.current = encodedValue;
11504
+ if (currentHashValue === encodedValue) {
11505
+ return;
11506
+ }
11507
+ pendingWriteRef.current = { encodedValue, options };
11508
+ requestAnimationFrame(() => {
11509
+ flushPendingWrite();
11510
+ });
11511
+ }, [enabled, currentHashValue, flushPendingWrite]);
11512
+ return useMemo(() => ({ searchValue: currentHashValue, updateSearchParam: updateHashParam }), [currentHashValue, updateHashParam]);
11513
+ };
11514
+
11515
+ /**
11516
+ * One-shot migration: if the URL still carries a legacy `?<key>=...`
11517
+ * search param from before this hook moved table-style state to the
11518
+ * fragment, decode it, hand it to the caller via `onMigrated`, then strip
11519
+ * the search param via a `replace` navigation so back/forward history
11520
+ * isn't polluted.
11521
+ *
11522
+ * Invariants:
11523
+ * - The strip navigation fires at most once per mount (guarded by
11524
+ * `hasStrippedRef`) so we don't loop on the navigation we ourselves
11525
+ * triggered.
11526
+ * - `onMigrated` is also fired at most once per mount (guarded by
11527
+ * `hasMigratedRef`), but its gating is independent of the strip:
11528
+ * if `hashAlreadyHasValue` flips `true → false` after the strip
11529
+ * (e.g. an external actor clears the canonical hash mid-mount), the
11530
+ * next render still gets a chance to migrate the legacy value into
11531
+ * the hash. Conflating the two flags would have permanently lost
11532
+ * the legacy state in that race.
11533
+ * - Never throws — a decode/validate failure silently drops the value
11534
+ * and still strips the search param. The legacy key is owned by this
11535
+ * codebase's persistence convention (camelCase + `Tp` suffix), so
11536
+ * stripping unparseable values cannot destroy third-party data.
11537
+ * - When `hashAlreadyHasValue` is `true` the hash wins: the search
11538
+ * param is stripped but `onMigrated` is not fired.
11539
+ * - The `location.hash` is preserved verbatim across the strip
11540
+ * navigation so any concurrent hash content (including a freshly
11541
+ * written value) survives the cleanup.
11542
+ */
11543
+ const useMigrateLegacySearchParam = ({ key, enabled, decode, fromUrlValue, validate, hashAlreadyHasValue, onMigrated, }) => {
11544
+ const navigate = useNavigate();
11545
+ const location = useLocation();
11546
+ const search = useSearch({ strict: false, shouldThrow: false });
11547
+ // The three flags are tracked independently because they describe
11548
+ // distinct side effects with distinct lifetimes:
11549
+ //
11550
+ // - `hasStrippedRef` guards the cleanup navigation against looping
11551
+ // on the navigation we ourselves triggered.
11552
+ // - `hasMigratedRef` guards `onMigrated` against being called twice
11553
+ // with the same legacy value.
11554
+ // - `cachedRawValueRef` snapshots the legacy raw string the first
11555
+ // time we observe it. The strip navigation below removes the
11556
+ // value from `search` on the next render, so without a cache we
11557
+ // can never migrate it later. This matters in one specific edge
11558
+ // case: `hashAlreadyHasValue` is `true` on the first render (so
11559
+ // we skip `onMigrated` and just strip), then later flips to
11560
+ // `false` because another actor cleared the canonical hash. With
11561
+ // the cache the next render can still surface the legacy value.
11562
+ //
11563
+ // The previous implementation conflated all of this into a single
11564
+ // `hasRunRef`, which permanently lost the legacy state in that race.
11565
+ const hasStrippedRef = useRef(false);
11566
+ const hasMigratedRef = useRef(false);
11567
+ const cachedRawValueRef = useRef(undefined);
11568
+ const decodeRef = useRef(decode);
11569
+ const fromUrlValueRef = useRef(fromUrlValue);
11570
+ const validateRef = useRef(validate);
11571
+ const onMigratedRef = useRef(onMigrated);
11572
+ useEffect(() => {
11573
+ decodeRef.current = decode;
11574
+ fromUrlValueRef.current = fromUrlValue;
11575
+ validateRef.current = validate;
11576
+ onMigratedRef.current = onMigrated;
11577
+ }, [decode, fromUrlValue, validate, onMigrated]);
11578
+ useEffect(() => {
11579
+ if (!enabled) {
11580
+ return;
11581
+ }
11582
+ if (hasStrippedRef.current && hasMigratedRef.current) {
11583
+ return;
11584
+ }
11585
+ // Snapshot the raw value the first time we see it. After the strip
11586
+ // navigation below, `search?.[key]` is undefined, but a later
11587
+ // render that flips `hashAlreadyHasValue` back to `false` still
11588
+ // needs the original value to migrate.
11589
+ if (cachedRawValueRef.current === undefined) {
11590
+ const rawValue = search?.[key];
11591
+ if (typeof rawValue === "string" || typeof rawValue === "number" || typeof rawValue === "boolean") {
11592
+ cachedRawValueRef.current = String(rawValue);
11593
+ }
11594
+ }
11595
+ const rawString = cachedRawValueRef.current;
11596
+ if (rawString === undefined) {
11597
+ // Nothing legacy to migrate yet. Don't flip either flag — the
11598
+ // param may appear on a later render (e.g. router still hydrating).
11599
+ return;
11600
+ }
11601
+ if (!hasMigratedRef.current && !hashAlreadyHasValue) {
11602
+ let migratedState;
11603
+ try {
11604
+ const decoded = decodeRef.current(rawString);
11605
+ const transformed = fromUrlValueRef.current ? fromUrlValueRef.current(decoded) : decoded;
11606
+ migratedState = validateRef.current(transformed);
11607
+ }
11608
+ catch {
11609
+ migratedState = undefined;
11610
+ }
11611
+ // Invalid legacy data is silently dropped: the search param is
11612
+ // still stripped below, so the URL stays clean. Surfacing a
11613
+ // console warning here would require disabling a lint rule, and
11614
+ // the data loss is bounded — localStorage holds the working state
11615
+ // for the current session, and the user's next interaction
11616
+ // repopulates the hash. We still flip the flag in the invalid
11617
+ // case so we don't burn cycles re-decoding the same bad string
11618
+ // on every render.
11619
+ hasMigratedRef.current = true;
11620
+ if (migratedState !== undefined) {
11621
+ onMigratedRef.current(migratedState);
11622
+ }
11623
+ }
11624
+ if (!hasStrippedRef.current) {
11625
+ hasStrippedRef.current = true;
11626
+ void navigate({
11627
+ to: ".",
11628
+ search: (prev) => ({ ...prev, [key]: undefined }),
11629
+ hash: location.hash,
11630
+ replace: true,
11631
+ });
11632
+ }
11633
+ }, [enabled, key, hashAlreadyHasValue, search, navigate, location.hash]);
11634
+ };
11635
+
11242
11636
  /**
11243
11637
  * Default maximum full URL length after adding one persisted search param
11244
11638
  * before the URL write is skipped (localStorage still persists the full state).
@@ -11267,6 +11661,7 @@ const useSearchParamSync = ({ key, enabled = true, onExternalChange, replace: re
11267
11661
  const router = useRouter();
11268
11662
  const search = useSearch({ strict: false, shouldThrow: false });
11269
11663
  const lastWrittenRef = useRef(undefined);
11664
+ const pendingWriteRef = useRef(null);
11270
11665
  const onExternalChangeRef = useRef(onExternalChange);
11271
11666
  useEffect(() => {
11272
11667
  onExternalChangeRef.current = onExternalChange;
@@ -11310,46 +11705,96 @@ const useSearchParamSync = ({ key, enabled = true, onExternalChange, replace: re
11310
11705
  const urlBase = location.href.length - (location.searchStr.length || 0) + (location.hash.length || 0);
11311
11706
  return urlBase + otherParamsLength + 1 + paramKey.length + 1 + paramValue.length;
11312
11707
  }, [location]);
11708
+ // Writes are dispatched via a "pending write" queue rather than calling
11709
+ // `navigate` directly from inside the rAF callback. See the matching
11710
+ // comment block in `useHashParamSync` for the full rationale — in short,
11711
+ // navigating while `router.state.status === "pending"` was previously
11712
+ // dropped silently, which lost the URL-restoration write performed by
11713
+ // `usePersistedState` on routes that mount during a loader-driven
11714
+ // pending phase.
11715
+ const flushPendingWrite = useCallback(() => {
11716
+ const pending = pendingWriteRef.current;
11717
+ if (pending === null) {
11718
+ return;
11719
+ }
11720
+ if (router.state.status === "pending") {
11721
+ // Keep the write queued; the status-watcher effect below retries on
11722
+ // the next idle transition.
11723
+ return;
11724
+ }
11725
+ pendingWriteRef.current = null;
11726
+ const { encodedValue, options } = pending;
11727
+ const shouldReplace = options?.replace ?? replaceOption ?? !Boolean(currentSearchValue);
11728
+ void navigate({
11729
+ to: ".",
11730
+ search: (prev) => {
11731
+ if (getUrlLengthWithSearchParam(key, encodedValue || "", prev) <= MAX_URL_LENGTH) {
11732
+ return { ...prev, [key]: encodedValue };
11733
+ }
11734
+ else {
11735
+ return { ...prev, [key]: undefined };
11736
+ }
11737
+ },
11738
+ hash: location.hash,
11739
+ replace: shouldReplace,
11740
+ });
11741
+ }, [
11742
+ router.state.status,
11743
+ location.hash,
11744
+ key,
11745
+ currentSearchValue,
11746
+ navigate,
11747
+ replaceOption,
11748
+ getUrlLengthWithSearchParam,
11749
+ ]);
11750
+ useEffect(() => {
11751
+ if (router.state.status === "pending") {
11752
+ return;
11753
+ }
11754
+ if (pendingWriteRef.current === null) {
11755
+ return;
11756
+ }
11757
+ flushPendingWrite();
11758
+ }, [router.state.status, flushPendingWrite]);
11759
+ // Drop any queued write when the consumer unmounts. The queue + retry
11760
+ // pattern above keeps writes pending across a `router.state.status ===
11761
+ // "pending"` phase, which means a write scheduled just before a route
11762
+ // change could otherwise drain onto the *next* route once it settles
11763
+ // (e.g. a debounced filter write leaking into the destination route).
11764
+ // Cancelling here preserves the original "writes are scoped to the
11765
+ // owning route" contract that the pre-queue implementation got for
11766
+ // free by dropping the write outright.
11767
+ useEffect(() => () => {
11768
+ pendingWriteRef.current = null;
11769
+ }, []);
11313
11770
  const updateSearchParam = useCallback((encodedValue, options) => {
11314
11771
  if (!enabled) {
11315
11772
  return;
11316
11773
  }
11317
- if (encodedValue !== undefined && encodedValue === lastWrittenRef.current) {
11774
+ // Dedupe against either the last committed write or the value
11775
+ // currently queued for the next rAF — both represent the caller's
11776
+ // intent so we don't schedule redundant rAF callbacks for identical
11777
+ // requests.
11778
+ const inFlightOrLastWritten = pendingWriteRef.current?.encodedValue ?? lastWrittenRef.current;
11779
+ if (encodedValue !== undefined && encodedValue === inFlightOrLastWritten) {
11318
11780
  return;
11319
11781
  }
11782
+ // Prime `lastWrittenRef` here (not inside `flushPendingWrite`) so
11783
+ // the external-change detector treats this value as the caller's
11784
+ // authoritative state even when the flush is a no-op (URL already
11785
+ // matches) or stays queued during a pending router phase. Without
11786
+ // priming, the next "external" change would look identical to the
11787
+ // initial render (`lastWrittenRef === undefined`) and the callback
11788
+ // would be skipped.
11320
11789
  lastWrittenRef.current = encodedValue;
11321
11790
  if (currentSearchValue === encodedValue) {
11322
11791
  return;
11323
11792
  }
11793
+ pendingWriteRef.current = { encodedValue, options };
11324
11794
  requestAnimationFrame(() => {
11325
- if (router.state.status === "pending") {
11326
- return;
11327
- }
11328
- const shouldReplace = options?.replace ?? replaceOption ?? !Boolean(currentSearchValue);
11329
- void navigate({
11330
- to: ".",
11331
- search: (prev) => {
11332
- if (getUrlLengthWithSearchParam(key, encodedValue || "", prev) <= MAX_URL_LENGTH) {
11333
- return { ...prev, [key]: encodedValue };
11334
- }
11335
- else {
11336
- return { ...prev, [key]: undefined };
11337
- }
11338
- },
11339
- hash: location.hash,
11340
- replace: shouldReplace,
11341
- });
11795
+ flushPendingWrite();
11342
11796
  });
11343
- }, [
11344
- enabled,
11345
- navigate,
11346
- key,
11347
- replaceOption,
11348
- location.hash,
11349
- getUrlLengthWithSearchParam,
11350
- currentSearchValue,
11351
- router.state.status,
11352
- ]);
11797
+ }, [enabled, currentSearchValue, flushPendingWrite]);
11353
11798
  return useMemo(() => ({ searchValue: currentSearchValue, updateSearchParam }), [currentSearchValue, updateSearchParam]);
11354
11799
  };
11355
11800
 
@@ -11368,11 +11813,17 @@ const useStorageKey = (key, userId) => {
11368
11813
  };
11369
11814
 
11370
11815
  /**
11371
- * Generic persistence hook that loads state from URL search params (with
11372
- * localStorage fallback) and writes changes to both.
11816
+ * Generic persistence hook that loads state from a URL slot (search param
11817
+ * or hash fragment, depending on `urlLocation`) with localStorage fallback,
11818
+ * and writes changes to both.
11373
11819
  *
11374
11820
  * On mount the hook resolves the initial state as follows:
11375
- * - If a URL search param exists, decode it and pass through `validate`.
11821
+ * - If the canonical URL slot (search OR hash, per `urlLocation`) holds a
11822
+ * value, decode it and pass through `validate`.
11823
+ * - In `urlLocation: "hash"` mode, if the canonical hash slot is empty
11824
+ * but a **legacy** `?<key>=...` search param is present, decode and
11825
+ * validate that as a one-shot fallback. A migration effect then strips
11826
+ * the legacy search param via a `replace` navigation.
11376
11827
  * - If localStorage is enabled, read and validate its value.
11377
11828
  * - When `mergeWithStorageOnRead` is `false` (default): URL state wins
11378
11829
  * entirely when present, otherwise fall back to localStorage.
@@ -11380,32 +11831,61 @@ const useStorageKey = (key, userId) => {
11380
11831
  * localStorage so fields stripped by `toUrlValue` survive via storage.
11381
11832
  *
11382
11833
  * `persistState` writes the state to localStorage (via `serialize`, default
11383
- * `JSON.stringify`) and syncs to the URL (via `toUrlValue` + encoding).
11384
- * Duplicate writes where the state has not changed (deep equality) are skipped.
11834
+ * `storageSerializer.serialize`) and syncs to the canonical URL slot (via
11835
+ * `toUrlValue` + encoding). Duplicate writes where the state has not
11836
+ * changed (deep equality) are skipped.
11385
11837
  *
11386
11838
  * @param options - Configuration for the persisted state.
11387
- * @param options.key - Unique identifier used for both the URL search param and the localStorage key.
11839
+ * @param options.key - Unique identifier used for both the URL slot and the localStorage key.
11388
11840
  * @param options.validate - Called with the decoded/parsed value; must return `TState` or `undefined`.
11389
11841
  * @param options.serialize - Custom localStorage serialiser (default `storageSerializer.serialize`).
11390
11842
  * @param options.toUrlValue - Transform applied before URL encoding (default: encode state as-is).
11391
11843
  * @param options.fromUrlValue - Transform applied after URL decoding, before validation (default: identity).
11392
11844
  * @param options.enabled - When `false` the URL is neither read nor written (default `true`).
11393
11845
  * @param options.localStorageEnabled - When `false` localStorage is neither read nor written (default `true`).
11394
- * @param options.onExternalChange - Fired when the URL param changes externally (e.g. browser back).
11395
- * @param options.replace - Forwarded to `useSearchParamSync`.
11846
+ * @param options.onExternalChange - Fired when the URL value changes externally (e.g. browser back).
11847
+ * @param options.replace - Forwarded to the underlying URL sync hook.
11396
11848
  * @param options.clientSideUserId - The user ID to use for the localStorage key.
11397
11849
  * @param options.mergeWithStorageOnRead - Merge URL state on top of localStorage
11398
11850
  * state on read (URL wins on shared keys). Use when `toUrlValue` deliberately
11399
11851
  * omits fields so they still survive via localStorage. Default `false`.
11852
+ * @param options.urlLocation - Which URL slot backs the value (`"search"` or `"hash"`). Default `"search"`.
11400
11853
  */
11401
- const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue, enabled = true, localStorageEnabled = true, onExternalChange, replace, clientSideUserId, mergeWithStorageOnRead = false, }) => {
11854
+ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue, enabled = true, localStorageEnabled = true, onExternalChange, replace, clientSideUserId, mergeWithStorageOnRead = false, urlLocation = "search", }) => {
11402
11855
  const { encode, decode } = useCustomEncoding();
11403
- const { searchValue, updateSearchParam } = useSearchParamSync({
11856
+ const searchActive = enabled && urlLocation === "search";
11857
+ const hashActive = enabled && urlLocation === "hash";
11858
+ // Both URL-sync hooks are mounted unconditionally to satisfy the Rules of
11859
+ // Hooks; the inactive one is gated off via `enabled: false` and is a
11860
+ // no-op at runtime.
11861
+ const searchSync = useSearchParamSync({
11862
+ key,
11863
+ enabled: searchActive,
11864
+ onExternalChange,
11865
+ replace,
11866
+ });
11867
+ const hashSync = useHashParamSync({
11404
11868
  key,
11405
- enabled,
11869
+ enabled: hashActive,
11406
11870
  onExternalChange,
11407
11871
  replace,
11408
11872
  });
11873
+ const { searchValue, updateSearchParam } = hashActive ? hashSync : searchSync;
11874
+ // In hash mode, also read the raw search params so a legacy `?<key>=...`
11875
+ // value can hydrate the initial state once, before the migration effect
11876
+ // strips it. In search mode this stays `undefined` so the legacy fallback
11877
+ // is inert.
11878
+ const rawSearch = useSearch({ strict: false, shouldThrow: false });
11879
+ const legacySearchValue = useMemo(() => {
11880
+ if (!hashActive) {
11881
+ return undefined;
11882
+ }
11883
+ const raw = rawSearch?.[key];
11884
+ if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") {
11885
+ return String(raw);
11886
+ }
11887
+ return undefined;
11888
+ }, [hashActive, rawSearch, key]);
11409
11889
  const storageKey = useStorageKey(key, clientSideUserId);
11410
11890
  const toUrlValueRef = useRef(toUrlValue);
11411
11891
  useEffect(() => {
@@ -11427,22 +11907,44 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
11427
11907
  useEffect(() => {
11428
11908
  searchValueRef.current = searchValue;
11429
11909
  }, [searchValue]);
11430
- const readPersistedState = useCallback(({ valueFromUrl, validateState, transformFromUrlValue, }) => {
11431
- let urlState;
11432
- if (enabled && valueFromUrl) {
11910
+ const legacySearchValueRef = useRef(legacySearchValue);
11911
+ useEffect(() => {
11912
+ legacySearchValueRef.current = legacySearchValue;
11913
+ }, [legacySearchValue]);
11914
+ const readPersistedState = useCallback(({ valueFromUrl, legacyValueFromUrl, validateState, transformFromUrlValue, }) => {
11915
+ const tryDecode = (raw) => {
11433
11916
  try {
11434
- const decoded = decode(valueFromUrl);
11917
+ const decoded = decode(raw);
11435
11918
  const transformed = transformFromUrlValue ? transformFromUrlValue(decoded) : decoded;
11436
- urlState = validateState(transformed);
11919
+ return validateState(transformed);
11437
11920
  }
11438
11921
  catch {
11439
- // fall through to localStorage
11922
+ return undefined;
11923
+ }
11924
+ };
11925
+ let urlState;
11926
+ let canonicalUrlValid = false;
11927
+ let usedLegacyUrl = false;
11928
+ if (enabled && valueFromUrl) {
11929
+ urlState = tryDecode(valueFromUrl);
11930
+ if (urlState !== undefined) {
11931
+ canonicalUrlValid = true;
11932
+ }
11933
+ }
11934
+ if (urlState === undefined && enabled && legacyValueFromUrl) {
11935
+ urlState = tryDecode(legacyValueFromUrl);
11936
+ if (urlState !== undefined) {
11937
+ usedLegacyUrl = true;
11440
11938
  }
11441
11939
  }
11442
11940
  // Without the merge option, URL state wins entirely when present —
11443
11941
  // this preserves the original "URL > localStorage" priority.
11444
11942
  if (urlState !== undefined && !mergeWithStorageOnRead) {
11445
- return { initialState: urlState, loadedFromStorage: false };
11943
+ return {
11944
+ initialState: urlState,
11945
+ canonicalUrlHasValidState: canonicalUrlValid,
11946
+ needsUrlRestore: usedLegacyUrl,
11947
+ };
11446
11948
  }
11447
11949
  let storageState;
11448
11950
  if (localStorageEnabled) {
@@ -11460,22 +11962,39 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
11460
11962
  if (urlState !== undefined && storageState !== undefined) {
11461
11963
  // Shallow merge — URL fields take precedence; storage fills in
11462
11964
  // whatever the URL omits (e.g. fields stripped by `toUrlValue`).
11463
- return { initialState: { ...storageState, ...urlState }, loadedFromStorage: false };
11965
+ return {
11966
+ initialState: { ...storageState, ...urlState },
11967
+ canonicalUrlHasValidState: canonicalUrlValid,
11968
+ needsUrlRestore: usedLegacyUrl,
11969
+ };
11464
11970
  }
11465
11971
  if (urlState !== undefined) {
11466
- return { initialState: urlState, loadedFromStorage: false };
11972
+ return {
11973
+ initialState: urlState,
11974
+ canonicalUrlHasValidState: canonicalUrlValid,
11975
+ needsUrlRestore: usedLegacyUrl,
11976
+ };
11467
11977
  }
11468
11978
  if (storageState !== undefined) {
11469
- return { initialState: storageState, loadedFromStorage: true };
11979
+ return {
11980
+ initialState: storageState,
11981
+ canonicalUrlHasValidState: false,
11982
+ needsUrlRestore: true,
11983
+ };
11470
11984
  }
11471
- return { initialState: undefined, loadedFromStorage: false };
11985
+ return {
11986
+ initialState: undefined,
11987
+ canonicalUrlHasValidState: false,
11988
+ needsUrlRestore: false,
11989
+ };
11472
11990
  }, [decode, enabled, localStorageEnabled, storageKey, mergeWithStorageOnRead]);
11473
11991
  const [persistedStateSnapshot, setPersistedStateSnapshot] = useState(() => readPersistedState({
11474
11992
  valueFromUrl: searchValue,
11993
+ legacyValueFromUrl: legacySearchValue,
11475
11994
  validateState: validate,
11476
11995
  transformFromUrlValue: fromUrlValue,
11477
11996
  }));
11478
- const persistedStateReadKey = `${enabled}:${localStorageEnabled}:${mergeWithStorageOnRead}:${storageKey}`;
11997
+ const persistedStateReadKey = `${enabled}:${localStorageEnabled}:${mergeWithStorageOnRead}:${urlLocation}:${storageKey}`;
11479
11998
  const persistedStateReadKeyRef = useRef(persistedStateReadKey);
11480
11999
  useEffect(() => {
11481
12000
  if (persistedStateReadKeyRef.current === persistedStateReadKey) {
@@ -11484,11 +12003,39 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
11484
12003
  persistedStateReadKeyRef.current = persistedStateReadKey;
11485
12004
  setPersistedStateSnapshot(readPersistedState({
11486
12005
  valueFromUrl: searchValueRef.current,
12006
+ legacyValueFromUrl: legacySearchValueRef.current,
11487
12007
  validateState: validateRef.current,
11488
12008
  transformFromUrlValue: fromUrlValueRef.current,
11489
12009
  }));
11490
12010
  }, [persistedStateReadKey, readPersistedState]);
11491
- const { initialState, loadedFromStorage } = persistedStateSnapshot;
12011
+ const { initialState, canonicalUrlHasValidState, needsUrlRestore } = persistedStateSnapshot;
12012
+ // Late-hydration recovery: TanStack Router's `useSearch` can return an
12013
+ // empty object on the very first render of a route that's still in a
12014
+ // loader-driven pending phase. The `useState` initializer above runs
12015
+ // exactly once, so a legacy `?<key>=...` value that only becomes
12016
+ // visible on a later render would otherwise be missed entirely —
12017
+ // `initialState` would resolve to `undefined`, and the migration
12018
+ // effect would strip the legacy param without ever piping it through
12019
+ // `onMigrated`. We re-read here when:
12020
+ // - the snapshot has nothing usable yet (`initialState === undefined`
12021
+ // AND the canonical URL slot has no valid value), and
12022
+ // - a legacy value has just appeared.
12023
+ // Once `initialState` is populated, this effect's guard short-circuits
12024
+ // and the rest of the lifecycle (restore + migration) takes over.
12025
+ useEffect(() => {
12026
+ if (initialState !== undefined || canonicalUrlHasValidState) {
12027
+ return;
12028
+ }
12029
+ if (legacySearchValue === undefined) {
12030
+ return;
12031
+ }
12032
+ setPersistedStateSnapshot(readPersistedState({
12033
+ valueFromUrl: searchValueRef.current,
12034
+ legacyValueFromUrl: legacySearchValue,
12035
+ validateState: validateRef.current,
12036
+ transformFromUrlValue: fromUrlValueRef.current,
12037
+ }));
12038
+ }, [legacySearchValue, initialState, canonicalUrlHasValidState, readPersistedState]);
11492
12039
  const lastPersistedRef = useRef(initialState);
11493
12040
  useEffect(() => {
11494
12041
  lastPersistedRef.current = initialState;
@@ -11496,19 +12043,54 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
11496
12043
  const hasRestoredUrlRef = useRef(false);
11497
12044
  useEffect(() => {
11498
12045
  hasRestoredUrlRef.current = false;
11499
- }, [enabled, localStorageEnabled, storageKey]);
12046
+ }, [enabled, localStorageEnabled, storageKey, urlLocation]);
11500
12047
  useEffect(() => {
11501
12048
  if (hasRestoredUrlRef.current) {
11502
12049
  return;
11503
12050
  }
11504
- if (!enabled || !loadedFromStorage || initialState === undefined || searchValue !== undefined) {
12051
+ // Gate on `canonicalUrlHasValidState`, not on the raw `searchValue`:
12052
+ // an undecodable string sitting in the canonical slot must NOT block
12053
+ // the restore, otherwise a valid legacy/search/storage fallback ends
12054
+ // up silently dropped (the bad string survives the migration's strip
12055
+ // and is the only state left on the next reload).
12056
+ if (!enabled || !needsUrlRestore || initialState === undefined || canonicalUrlHasValidState) {
11505
12057
  return;
11506
12058
  }
11507
12059
  hasRestoredUrlRef.current = true;
11508
12060
  const urlValue = toUrlValueRef.current ? toUrlValueRef.current(initialState) : initialState;
11509
12061
  const encoded = encode(urlValue);
11510
12062
  updateSearchParam(encoded, { replace: true });
11511
- }, [enabled, loadedFromStorage, initialState, searchValue, updateSearchParam, encode]);
12063
+ }, [enabled, needsUrlRestore, initialState, canonicalUrlHasValidState, updateSearchParam, encode]);
12064
+ // Strip the legacy search param so reloads come from the canonical hash
12065
+ // slot and the URL stays tidy. The hook is a no-op outside hash mode.
12066
+ // `hashAlreadyHasValue` is `true` only when the canonical hash slot
12067
+ // already holds *valid* state — a raw but undecodable string must not
12068
+ // suppress `onMigrated`, otherwise a valid legacy value would be lost
12069
+ // on the next reload (see `canonicalUrlHasValidState` for the same
12070
+ // reasoning in the restore effect above).
12071
+ useMigrateLegacySearchParam({
12072
+ key,
12073
+ enabled: hashActive,
12074
+ decode,
12075
+ fromUrlValue,
12076
+ validate,
12077
+ hashAlreadyHasValue: canonicalUrlHasValidState,
12078
+ onMigrated: useCallback((migrated) => {
12079
+ if (dequal(lastPersistedRef.current, migrated)) {
12080
+ return;
12081
+ }
12082
+ lastPersistedRef.current = migrated;
12083
+ if (localStorageEnabled) {
12084
+ const serialized = serializeRef.current
12085
+ ? serializeRef.current(migrated)
12086
+ : storageSerializer.serialize(migrated);
12087
+ localStorage.setItem(storageKey, serialized);
12088
+ }
12089
+ const urlValue = toUrlValueRef.current ? toUrlValueRef.current(migrated) : migrated;
12090
+ const encoded = encode(urlValue);
12091
+ updateSearchParam(encoded, { replace: true });
12092
+ }, [encode, localStorageEnabled, storageKey, updateSearchParam]),
12093
+ });
11512
12094
  const persistState = useCallback((state) => {
11513
12095
  if (dequal(lastPersistedRef.current, state)) {
11514
12096
  return;
@@ -12681,4 +13263,4 @@ const useWindowActivity = ({ onFocus, onBlur, skip = false } = { onBlur: undefin
12681
13263
  */
12682
13264
  setupLibraryTranslations();
12683
13265
 
12684
- export { Alert, Badge, Breadcrumb, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyableText, DEFAULT_SKELETON_PREFERENCE_CARD_PROPS, DetailsList, EmptyState, EmptyValue, ExternalLink, GridAreas, Heading, Highlight, HorizontalOverflowScroller, Icon, IconButton, Indicator, KPI, KPICard, KPICardSkeleton, KPISkeleton, List, ListItem, MAX_URL_LENGTH, MenuDivider, MenuItem, MenuList, MoreMenu, Notice, PackageNameStoryComponent, Page, PageContent, PageHeader, PageHeaderKpiMetrics, PageHeaderSecondaryActions, PageHeaderTitle, Pagination, Polygon, Popover, PopoverContent, PopoverTitle, PopoverTrigger, Portal, PreferenceCard, PreferenceCardSkeleton, Prompt, ROLE_CARD, SHEET_TRANSITION_DURATION, SHEET_TRANSITION_DURATION_MS, SHEET_TRANSITION_EASING, SectionHeader, SegmentedValueBar, Sheet, Sidebar, SkeletonBlock, SkeletonLabel, SkeletonLines, Spacer, Spinner, StarButton, Tab, TabContent, TabList, Tabs, Tag, Text, ToggleGroup, Tooltip, TrendIndicator, TrendIndicators, ValueBar, ZStack, createGrid, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaContainerStyles, cvaContentContainer, cvaContentWrapper, cvaDescriptionCard, cvaIconBackground, cvaIconButton, cvaImgStyles, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInputContainer, cvaInteractableItem, cvaList, cvaListContainer, cvaListItem$1 as cvaListItem, cvaMenu, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, cvaMenuList, cvaMenuListDivider, cvaMenuListItem, cvaMenuListMultiSelect, cvaPageHeader, cvaPageHeaderContainer, cvaPageHeaderHeading, cvaPreferenceCard, cvaTitleCard, cvaToggleGroup, cvaToggleGroupWithSlidingBackground, cvaToggleItem, cvaToggleItemContent, cvaToggleItemText, cvaZStackContainer, cvaZStackItem, defaultPageSize, docs, getDevicePixelRatio, getValueBarColorByValue, iconColorNames, iconPalette, noPagination, preferenceCardGrid, storageSerializer, useBidirectionalScroll, useClickOutside, useContainerBreakpoints, useContinuousTimeout, useCopyToClipboard, useCursorUrlSync, useCustomEncoding, useDebounce, useDevicePixelRatio, useElevatedReducer, useElevatedState, useGridAreas, useHover, useInfiniteScroll, useIsFirstRender, useIsFullscreen, useIsTextTruncated, useKeyboardShortcut, useList, useListItemHeight, useLocalStorage, useLocalStorageReducer, useMeasure, useMergeRefs, useModifierKey, useOverflowBorder, useOverflowItems, usePersistedState, usePopoverContext, usePrevious, usePrompt, useRandomCSSLengths, useRelayPagination, useResize, useScrollBlock, useScrollDetection, useSearchParamSync, useSelfUpdatingRef, useSessionStorage, useSessionStorageReducer, useSheet, useSheetSnap, useStorageKey, useTextSearch, useTimeout, useViewportBreakpoints, useWatch, useWindowActivity };
13266
+ export { Alert, Badge, Breadcrumb, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyableText, DEFAULT_SKELETON_PREFERENCE_CARD_PROPS, DetailsList, EmptyState, EmptyValue, ExternalLink, GridAreas, Heading, Highlight, HorizontalOverflowScroller, Icon, IconButton, Indicator, KPI, KPICard, KPICardSkeleton, KPISkeleton, List, ListItem, MAX_HASH_LENGTH, MAX_URL_LENGTH, MenuDivider, MenuItem, MenuList, MoreMenu, Notice, PackageNameStoryComponent, Page, PageContent, PageHeader, PageHeaderKpiMetrics, PageHeaderSecondaryActions, PageHeaderTitle, Pagination, Polygon, Popover, PopoverContent, PopoverTitle, PopoverTrigger, Portal, PreferenceCard, PreferenceCardSkeleton, Prompt, ROLE_CARD, SHEET_TRANSITION_DURATION, SHEET_TRANSITION_DURATION_MS, SHEET_TRANSITION_EASING, SectionHeader, SegmentedValueBar, Sheet, Sidebar, SkeletonBlock, SkeletonLabel, SkeletonLines, Spacer, Spinner, StarButton, Tab, TabContent, TabList, Tabs, Tag, Text, ToggleGroup, Tooltip, TrendIndicator, TrendIndicators, ValueBar, ZStack, createGrid, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaContainerStyles, cvaContentContainer, cvaContentWrapper, cvaDescriptionCard, cvaIconBackground, cvaIconButton, cvaImgStyles, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInputContainer, cvaInteractableItem, cvaList, cvaListContainer, cvaListItem$1 as cvaListItem, cvaMenu, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, cvaMenuList, cvaMenuListDivider, cvaMenuListItem, cvaMenuListMultiSelect, cvaPageHeader, cvaPageHeaderContainer, cvaPageHeaderHeading, cvaPreferenceCard, cvaTitleCard, cvaToggleGroup, cvaToggleGroupWithSlidingBackground, cvaToggleItem, cvaToggleItemContent, cvaToggleItemText, cvaZStackContainer, cvaZStackItem, defaultPageSize, docs, getDevicePixelRatio, getValueBarColorByValue, iconColorNames, iconPalette, noPagination, preferenceCardGrid, storageSerializer, useBidirectionalScroll, useClickOutside, useContainerBreakpoints, useContinuousTimeout, useCopyToClipboard, useCursorUrlSync, useCustomEncoding, useDebounce, useDevicePixelRatio, useElevatedReducer, useElevatedState, useGridAreas, useHashParamSync, useHover, useInfiniteScroll, useIsFirstRender, useIsFullscreen, useIsTextTruncated, useKeyboardShortcut, useList, useListItemHeight, useLocalStorage, useLocalStorageReducer, useMeasure, useMergeRefs, useModifierKey, useOverflowBorder, useOverflowItems, usePersistedState, usePopoverContext, usePrevious, usePrompt, useRandomCSSLengths, useRelayPagination, useResize, useScrollBlock, useScrollDetection, useSearchParamSync, useSelfUpdatingRef, useSessionStorage, useSessionStorageReducer, useSheet, useSheetSnap, useStorageKey, useTextSearch, useTimeout, useViewportBreakpoints, useWatch, useWindowActivity };