@maestro-js/components 1.0.0-alpha.18 → 1.0.0-alpha.2

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.
Files changed (56) hide show
  1. package/commands/add.ts +1 -16
  2. package/dist/components.json +1 -1
  3. package/dist/index.js +1 -14
  4. package/package.json +5 -5
  5. package/registry.json +790 -598
  6. package/scripts/build.ts +4 -11
  7. package/src/components/alert-dialog.tsx +3 -3
  8. package/src/components/avatar.tsx +1 -1
  9. package/src/components/button-link.tsx +0 -3
  10. package/src/components/button.tsx +0 -4
  11. package/src/components/checkbox.tsx +7 -7
  12. package/src/components/chip.tsx +7 -7
  13. package/src/components/container.tsx +3 -3
  14. package/src/components/dialog.tsx +7 -21
  15. package/src/components/disclosure.tsx +1 -1
  16. package/src/components/drawer.tsx +5 -19
  17. package/src/components/form.tsx +5 -27
  18. package/src/components/helpers/form-field.tsx +8 -7
  19. package/src/components/helpers/get-button-classes.ts +23 -23
  20. package/src/components/helpers/headless-button.tsx +1 -1
  21. package/src/components/inline-alert.tsx +3 -3
  22. package/src/components/labeled-value.tsx +2 -2
  23. package/src/components/menu.tsx +26 -60
  24. package/src/components/month-day-input.tsx +1 -2
  25. package/src/components/multiselect.tsx +70 -74
  26. package/src/components/radio.tsx +8 -8
  27. package/src/components/select.tsx +8 -9
  28. package/src/components/stepper.tsx +6 -6
  29. package/src/components/switch.tsx +7 -7
  30. package/src/components/tabs.tsx +1 -1
  31. package/src/components/tag-field.tsx +3 -3
  32. package/src/components/text-area.tsx +1 -1
  33. package/src/components/text-field.tsx +1 -4
  34. package/src/components/toast.tsx +6 -6
  35. package/src/components/toggle-button-group.tsx +2 -2
  36. package/src/utils/icons.d.ts +1 -2
  37. package/src/utils/use-tab-indicator.ts +9 -9
  38. package/src/components/date-range.tsx +0 -138
  39. package/src/components/optional-link.tsx +0 -15
  40. package/src/components/table/headless-templated-row-table.ts +0 -550
  41. package/src/components/table/index.tsx +0 -41
  42. package/src/components/table/server-table.ts +0 -143
  43. package/src/components/table/table-actions.tsx +0 -76
  44. package/src/components/table/table-container.tsx +0 -173
  45. package/src/components/table/table-container.type-test.ts +0 -155
  46. package/src/components/table/table-filters/date-range-filter.tsx +0 -227
  47. package/src/components/table/table-filters/select-filter.tsx +0 -211
  48. package/src/components/table/table-filters/sort.tsx +0 -85
  49. package/src/components/table/table-primitives.tsx +0 -226
  50. package/src/components/table/table-toolbar.tsx +0 -300
  51. package/src/components/table/table-types.ts +0 -61
  52. package/src/components/table/use-client-table.ts +0 -157
  53. package/src/components/table/use-client-table.type-test.ts +0 -217
  54. package/src/components/table/use-server-table.ts +0 -192
  55. package/src/components/table/use-server-table.type-test.ts +0 -174
  56. package/src/utils/use-query-state.ts +0 -626
@@ -1,626 +0,0 @@
1
- import { useLocation, useNavigation, useSearchParams as useSearchParamsRR } from 'react-router'
2
- import { dateFns, dateTimeFns, durationFns, type Iso } from 'iso-fns'
3
- import React from 'react'
4
- import { useStableAccessor } from './use-stable-accessor'
5
-
6
- // ────────────────────────────────────────────────────────────────
7
- // Types
8
- // ────────────────────────────────────────────────────────────────
9
-
10
- interface QueryStateNavigateOptions {
11
- replace?: boolean
12
- preventScrollReset?: boolean
13
- /** When true, updates the URL via `history.pushState`/`replaceState` without triggering a React Router navigation. */
14
- shallow?: boolean
15
- }
16
-
17
- interface CodecDef<T> {
18
- name: string
19
- defaultValue?: T
20
- ['~type']: T
21
- /** When true, the param is removed from the URL when the value matches `defaultValue`. */
22
- clearOnDefault?: boolean
23
- }
24
-
25
- interface Codec<Def extends CodecDef<any>> {
26
- _def: Def
27
- encode(value: Def['~type']): string | null
28
- decode(value: string | null): Def['~type']
29
- }
30
-
31
- interface CodecConfigurator<Def extends CodecDef<any>> extends Codec<Def> {
32
- default(defaultValue: Def['~type']): CodecConfigurator<Def>
33
- clearOnDefault(clear: boolean): CodecConfigurator<Def>
34
- }
35
-
36
- function makeConfigurator<Def extends CodecDef<any>>(codec: Codec<Def>): CodecConfigurator<Def> {
37
- return {
38
- ...codec,
39
- default(defaultValue) {
40
- return makeConfigurator({ ...codec, _def: { ...codec._def, defaultValue } })
41
- },
42
- clearOnDefault(clear) {
43
- return makeConfigurator({ ...codec, _def: { ...codec._def, clearOnDefault: clear } })
44
- }
45
- }
46
- }
47
-
48
- // ────────────────────────────────────────────────────────────────
49
- // Codec defs
50
- // ────────────────────────────────────────────────────────────────
51
-
52
- interface NoopDef extends CodecDef<string | null> {
53
- name: 'noop'
54
- ['~type']: string | null
55
- }
56
-
57
- interface StringDef extends CodecDef<string | null> {
58
- name: 'string'
59
- ['~type']: string | null
60
- }
61
-
62
- interface ValidatedDef<T extends string> extends CodecDef<T | null> {
63
- name: string
64
- ['~type']: T | null
65
- isValid: (v: string) => v is T
66
- }
67
-
68
- interface RelativeDateTimeRangeDef extends CodecDef<Iso.Duration | { start: Iso.DateTime; end: Iso.DateTime } | null> {
69
- name: 'relativeDateTimeRange'
70
- ['~type']: Iso.Duration | { start: Iso.DateTime; end: Iso.DateTime } | null
71
- }
72
-
73
- interface IntDef extends CodecDef<number | null> {
74
- name: 'int'
75
- ['~type']: number | null
76
- }
77
-
78
- interface FloatDef extends CodecDef<number | null> {
79
- name: 'float'
80
- ['~type']: number | null
81
- }
82
-
83
- interface HexDef extends CodecDef<number | null> {
84
- name: 'hex'
85
- ['~type']: number | null
86
- }
87
-
88
- interface IndexDef extends CodecDef<number | null> {
89
- name: 'index'
90
- ['~type']: number | null
91
- }
92
-
93
- interface BooleanDef extends CodecDef<boolean | null> {
94
- name: 'boolean'
95
- ['~type']: boolean | null
96
- }
97
-
98
- interface EnumDef<T extends string> extends CodecDef<T | null> {
99
- name: 'enum'
100
- ['~type']: T | null
101
- values: readonly T[]
102
- }
103
-
104
- interface EnumArrayDef<T extends string> extends CodecDef<T[]> {
105
- name: 'enumArray'
106
- ['~type']: T[]
107
- values: readonly T[]
108
- }
109
-
110
- // ────────────────────────────────────────────────────────────────
111
- // Codec factories
112
- // ────────────────────────────────────────────────────────────────
113
-
114
- /** Pass-through — no transformation, value is `string | null`. */
115
- function noop(def: Omit<NoopDef, '~type'>): Codec<NoopDef> {
116
- return {
117
- _def: { ...def, ['~type']: null as any },
118
- encode: (value) => value,
119
- decode: (value) => value
120
- }
121
- }
122
-
123
- /** String codec — pass-through with `string | null` type. */
124
- function string(def: Omit<StringDef, '~type'>): Codec<StringDef> {
125
- return {
126
- _def: { ...def, ['~type']: null as any },
127
- encode: (value) => value,
128
- decode: (value) => value
129
- }
130
- }
131
-
132
- /** Validates a string against a guard function. Used for date, dateTime, duration. */
133
- function validated<T extends string>(def: Omit<ValidatedDef<T>, '~type'>): Codec<ValidatedDef<T>> {
134
- return {
135
- _def: { ...def, ['~type']: null as any },
136
- encode: (value) => (value != null && def.isValid(value) ? value : null),
137
- decode: (value) => (value !== null && def.isValid(value) ? value : null)
138
- }
139
- }
140
-
141
- /**
142
- * Either an ISO duration (relative range) or a `start~end` pair of ISO
143
- * date-times (absolute range). Useful for "last 7 days" vs a custom window.
144
- */
145
- function relativeDateTimeRange(def: Omit<RelativeDateTimeRangeDef, '~type'>): Codec<RelativeDateTimeRangeDef> {
146
- return {
147
- _def: { ...def, ['~type']: null as any },
148
- encode: (value) => {
149
- if (value == null) return null
150
- if (durationFns.isValid(value)) return value
151
- if (typeof value === 'object' && 'start' in value && 'end' in value) {
152
- return dateTimeFns.isValid(value.start) && dateTimeFns.isValid(value.end) ? `${value.start}~${value.end}` : null
153
- }
154
- return null
155
- },
156
- decode: (value) => {
157
- if (value === null) return null
158
- if (durationFns.isValid(value)) return value
159
- const [start, end] = value.split('~')
160
- return dateTimeFns.isValid(start) && dateTimeFns.isValid(end) ? { start, end } : null
161
- }
162
- }
163
- }
164
-
165
- /** Integer (base-10). Non-integer or missing values decode to `null`. */
166
- function int(def: Omit<IntDef, '~type'>): Codec<IntDef> {
167
- return {
168
- _def: { ...def, ['~type']: null as any },
169
- encode: (value) => (value != null && Number.isInteger(value) ? value.toString() : null),
170
- decode: (value) => {
171
- if (value === null) return null
172
- const num = Number(value)
173
- return Number.isInteger(num) ? num : null
174
- }
175
- }
176
- }
177
-
178
- /** Finite floating-point number. `NaN` / `Infinity` decode to `null`. */
179
- function float(def: Omit<FloatDef, '~type'>): Codec<FloatDef> {
180
- return {
181
- _def: { ...def, ['~type']: null as any },
182
- encode: (value) => (value != null && Number.isFinite(value) ? value.toString() : null),
183
- decode: (value) => {
184
- if (value === null) return null
185
- const num = Number(value)
186
- return Number.isFinite(num) ? num : null
187
- }
188
- }
189
- }
190
-
191
- /** Hexadecimal integer (with or without `0x` prefix). Stored without prefix. */
192
- function hex(def: Omit<HexDef, '~type'>): Codec<HexDef> {
193
- return {
194
- _def: { ...def, ['~type']: null as any },
195
- encode: (value) => (typeof value === 'number' && Number.isFinite(value) ? value.toString(16) : null),
196
- decode: (value) => {
197
- if (value === null) return null
198
- const body = value.trim().toLowerCase().replace(/^0x/, '')
199
- if (body.length === 0 || /[^0-9a-f]/i.test(body)) return null
200
- const num = parseInt(body, 16)
201
- return Number.isFinite(num) ? num : null
202
- }
203
- }
204
- }
205
-
206
- /**
207
- * Zero-based index stored as a **1-based** number in the URL.
208
- * `?page=1` decodes to `0`, `0` encodes to `"1"`.
209
- */
210
- function index(def: Omit<IndexDef, '~type'>): Codec<IndexDef> {
211
- return {
212
- _def: { ...def, ['~type']: null as any },
213
- encode: (value) => (typeof value === 'number' && Number.isInteger(value) && value >= 0 ? (value + 1).toString() : null),
214
- decode: (value) => {
215
- if (value === null) return null
216
- const num = Number(value)
217
- return Number.isInteger(num) && num >= 1 ? num - 1 : null
218
- }
219
- }
220
- }
221
-
222
- /** Boolean accepting common truthy/falsy strings (`1`, `true`, `yes`, `on`, etc). Encodes as `"1"` / `"0"`. */
223
- function boolean(def: Omit<BooleanDef, '~type'>): Codec<BooleanDef> {
224
- return {
225
- _def: { ...def, ['~type']: null as any },
226
- encode: (value) => (value == null ? null : value ? '1' : '0'),
227
- decode: (value) => {
228
- if (value === null) return null
229
- const v = value.trim().toLowerCase()
230
- if (v === '1' || v === 'true' || v === 't' || v === 'yes' || v === 'y' || v === 'on') return true
231
- if (v === '0' || v === 'false' || v === 'f' || v === 'no' || v === 'n' || v === 'off') return false
232
- return null
233
- }
234
- }
235
- }
236
-
237
- /** Constrained to one of the provided `values`. Anything else decodes to `null`. */
238
- function enumCodec<T extends string>(def: Omit<EnumDef<T>, '~type'>): Codec<EnumDef<T>> {
239
- return {
240
- _def: { ...def, ['~type']: null as any },
241
- encode: (value) => value ?? null,
242
- decode: (value) => {
243
- if (value === null) return null
244
- return (def.values as readonly string[]).includes(value) ? (value as T) : null
245
- }
246
- }
247
- }
248
-
249
- /**
250
- * Comma-separated list constrained to the provided `values`.
251
- * Decodes to `T[]` (empty array when missing). Encodes in the canonical
252
- * order of `values` so the same selection always produces the same URL.
253
- */
254
- function enumArray<T extends string>(def: Omit<EnumArrayDef<T>, '~type'>): Codec<EnumArrayDef<T>> {
255
- return {
256
- _def: { ...def, ['~type']: null as any },
257
- encode: (value) => {
258
- if (!value.length) return null
259
- return value
260
- .sort((v1, v2) => def.values.indexOf(v1) - def.values.indexOf(v2))
261
- .map((v) => encodeURIComponent(v))
262
- .join(',')
263
- },
264
- decode: (value) => {
265
- if (value === null) return []
266
- return value
267
- .split(',')
268
- .map((v) => decodeURIComponent(v))
269
- .filter((v) => def.values.includes(v as T)) as T[]
270
- }
271
- }
272
- }
273
-
274
- // ────────────────────────────────────────────────────────────────
275
- // Serialize / Deserialize
276
- // ────────────────────────────────────────────────────────────────
277
-
278
- /** Extract the serializable def from a codec. */
279
- function serializeCodec<Def extends CodecDef<any>>(codec: Codec<Def>): Def {
280
- return codec._def
281
- }
282
-
283
- type CodecFactory = (def: any) => Codec<any>
284
-
285
- const defaultFactories: Record<string, CodecFactory> = {
286
- noop,
287
- string,
288
- date: validated,
289
- dateTime: validated,
290
- duration: validated,
291
- relativeDateTimeRange,
292
- int,
293
- float,
294
- hex,
295
- index,
296
- boolean,
297
- enum: enumCodec,
298
- enumArray
299
- }
300
-
301
- /** Reconstruct a codec from a serialized def. */
302
- function deserializeCodec<Def extends CodecDef<any>>(
303
- def: Def,
304
- factories: Record<string, CodecFactory> = defaultFactories
305
- ): Codec<Def> {
306
- const factory = factories[def.name]
307
- if (!factory) throw new Error(`Unknown codec: ${def.name}`)
308
- return factory(def) as Codec<Def>
309
- }
310
-
311
- type SerializedCodec<C extends Codec<CodecDef<any>>> = C['_def']
312
-
313
- type SerializedLoader<L extends QueryStateLoader> = { [K in keyof L]: SerializedCodec<L[K]> }
314
-
315
- /** Serialize a loader into a plain object of defs, keyed by param name. */
316
- function serializeLoader<L extends QueryStateLoader>(loader: L): SerializedLoader<L> {
317
- return Object.fromEntries(
318
- Object.entries(loader).map(([name, codec]) => [name, serializeCodec(codec)])
319
- ) as SerializedLoader<L>
320
- }
321
-
322
- /** Reconstruct a loader from a serialized object of defs. */
323
- function deserializeLoader<L extends QueryStateLoader>(
324
- defs: SerializedLoader<L>,
325
- factories: Record<string, CodecFactory> = defaultFactories
326
- ): L {
327
- return Object.fromEntries(
328
- Object.entries(defs).map(([name, def]) => [name, deserializeCodec(def as CodecDef<any>, factories)])
329
- ) as L
330
- }
331
-
332
- // ────────────────────────────────────────────────────────────────
333
- // Built-in codecs
334
- // ────────────────────────────────────────────────────────────────
335
-
336
- /**
337
- * Library of built-in codecs for common URL search-param types.
338
- *
339
- * Each codec converts between a URL string and a typed value, returning `null`
340
- * for missing or invalid params. Chain `.default(v)` to substitute a
341
- * fallback, or `.clearOnDefault(true)` to remove the param when it matches the default.
342
- */
343
- const codecs = {
344
- /** Pass-through — no transformation, value is `string | null`. */
345
- noop: () => makeConfigurator(noop({ name: 'noop' })),
346
-
347
- /** String codec — pass-through defaulting to `''` (never null). */
348
- string: () => makeConfigurator(string({ name: 'string' })).default(''),
349
-
350
- /** ISO date (`YYYY-MM-DD`). Invalid strings decode to `null`. */
351
- date: () => makeConfigurator(validated({ name: 'date', isValid: dateFns.isValid })),
352
-
353
- /** ISO date-time (`YYYY-MM-DDTHH:mm:ss`). Invalid strings decode to `null`. */
354
- dateTime: () => makeConfigurator(validated({ name: 'dateTime', isValid: dateTimeFns.isValid })),
355
-
356
- /** ISO duration (`P1DT2H`, etc). Invalid strings decode to `null`. */
357
- duration: () => makeConfigurator(validated({ name: 'duration', isValid: durationFns.isValid })),
358
-
359
- /**
360
- * Either an ISO duration (relative range) or a `start~end` pair of ISO
361
- * date-times (absolute range). Useful for "last 7 days" vs a custom window.
362
- */
363
- relativeDateTimeRange: () => makeConfigurator(relativeDateTimeRange({ name: 'relativeDateTimeRange' })),
364
-
365
- /** Integer (base-10). Non-integer or missing values decode to `null`. */
366
- int: () => makeConfigurator(int({ name: 'int' })),
367
-
368
- /** Finite floating-point number. `NaN` / `Infinity` decode to `null`. */
369
- float: () => makeConfigurator(float({ name: 'float' })),
370
-
371
- /** Hexadecimal integer (with or without `0x` prefix). Stored without prefix. */
372
- hex: () => makeConfigurator(hex({ name: 'hex' })),
373
-
374
- /**
375
- * Zero-based index stored as a **1-based** number in the URL.
376
- * `?page=1` decodes to `0`, `0` encodes to `"1"`.
377
- */
378
- index: () => makeConfigurator(index({ name: 'index' })),
379
-
380
- /** Boolean accepting common truthy/falsy strings (`1`, `true`, `yes`, `on`, etc). Encodes as `"1"` / `"0"`. */
381
- boolean: () => makeConfigurator(boolean({ name: 'boolean' })),
382
-
383
- /** Constrained to one of the provided `values`. Anything else decodes to `null`. */
384
- enum: <T extends string>(values: readonly T[]) => makeConfigurator(enumCodec({ name: 'enum', values })),
385
-
386
- /**
387
- * Comma-separated list constrained to the provided `values`.
388
- * Decodes to `T[]` (empty array when missing). Encodes in the canonical
389
- * order of `values` so the same selection always produces the same URL.
390
- */
391
- enumArray: <T extends string>(values: readonly T[]) => makeConfigurator(enumArray({ name: 'enumArray', values }))
392
- }
393
-
394
- // ────────────────────────────────────────────────────────────────
395
- // Loader types
396
- // ────────────────────────────────────────────────────────────────
397
-
398
- /** A map of param names to codecs, defining the shape of search-param state. */
399
- type QueryStateLoader<T extends { [key: string]: any } = { [key: string]: any }> = {
400
- [key in keyof T]: Codec<CodecDef<T[key]>>
401
- }
402
-
403
- type ExtractTypeFromLoader<L extends QueryStateLoader> = {
404
- [key in keyof L]: L[key] extends Codec<infer S> ? S['~type'] : never
405
- }
406
-
407
- // ────────────────────────────────────────────────────────────────
408
- // Internal helpers
409
- // ────────────────────────────────────────────────────────────────
410
-
411
- function getQueryParam<T>(naturalValue: string | null, codec: Codec<CodecDef<T>>): T {
412
- try {
413
- const decoded = codec.decode(naturalValue)
414
- return (decoded ?? codec?._def?.defaultValue ?? null) as T
415
- } catch {
416
- return (codec?._def?.defaultValue ?? null) as T
417
- }
418
- }
419
-
420
- function setQueryParam<V>({
421
- searchParams,
422
- name,
423
- value,
424
- codec
425
- }: {
426
- searchParams: URLSearchParams
427
- name: string
428
- value: V
429
- codec: Codec<CodecDef<V>>
430
- }): void {
431
- const encoded = codec.encode(value)
432
- if (
433
- encoded === null ||
434
- (codec._def?.defaultValue != null && codec._def?.clearOnDefault && encoded === codec.encode(codec._def?.defaultValue))
435
- ) {
436
- searchParams.delete(name)
437
- } else {
438
- searchParams.set(name, encoded)
439
- }
440
- }
441
-
442
- function queryParamsToObject<L extends QueryStateLoader>(params: URLSearchParams, loader: L) {
443
- return Object.fromEntries(
444
- Object.entries(loader).map(([name, codec]) => [name, getQueryParam(params.get(name), codec)])
445
- ) as ExtractTypeFromLoader<L>
446
- }
447
-
448
- // ────────────────────────────────────────────────────────────────
449
- // Public API
450
- // ────────────────────────────────────────────────────────────────
451
-
452
- /**
453
- * Decode search params from various sources (URL string, `Request`, `URLSearchParams`, or
454
- * an object with a `.search` property) using a loader. Useful in loaders/actions on the server side.
455
- */
456
- function getSearchParamsState<L extends QueryStateLoader>(
457
- searchParams: URLSearchParams | string | Request | { search: string },
458
- loader: L
459
- ) {
460
- if (typeof searchParams === 'string') {
461
- return queryParamsToObject(new URL(searchParams).searchParams, loader)
462
- } else if ('url' in searchParams) {
463
- return queryParamsToObject(new URL(searchParams.url).searchParams, loader)
464
- } else if ('search' in searchParams) {
465
- return queryParamsToObject(new URLSearchParams(searchParams.search), loader)
466
- } else {
467
- return queryParamsToObject(searchParams, loader)
468
- }
469
- }
470
-
471
- /**
472
- * React hook that binds a single search param to component state via a codec.
473
- * Returns `[value, setValue]` like `React.useState`. The setter accepts a value
474
- * or updater function and optional {@link QueryStateNavigateOptions}.
475
- *
476
- * Reads from the pending navigation location when one is in flight so the UI
477
- * reflects optimistic state during transitions.
478
- */
479
- function useState<S>(name: string, codec: Codec<CodecDef<S>>): [S, (val: S | ((prevState: S) => S), navigateOpts?: QueryStateNavigateOptions) => void] {
480
- const [searchParamsRaw, setSearchParams] = useSearchParamsRR()
481
- const navigation = useNavigation()
482
-
483
- const naturalValue = React.useMemo(
484
- () => (navigation.location?.search ? new URLSearchParams(navigation.location.search) : searchParamsRaw).get(name),
485
- [name, searchParamsRaw, navigation.location]
486
- )
487
- const value = React.useMemo(
488
- () => getQueryParam<S>(naturalValue, codec),
489
- // eslint-disable-next-line react-hooks/exhaustive-deps
490
- [naturalValue]
491
- )
492
-
493
- const setValue = React.useCallback(
494
- (val: S | ((prevState: S) => S), navigateOpts?: QueryStateNavigateOptions) => {
495
- const { replace = true, preventScrollReset = false, shallow = false } = { ...codec, ...navigateOpts }
496
-
497
- function updateParams(params: URLSearchParams) {
498
- const newParams = new URLSearchParams(params)
499
- const oldValue = getQueryParam(newParams.get(name), codec)
500
- const newValue = typeof val === 'function' ? (val as (prevState: S) => S)(oldValue) : val
501
- setQueryParam({ searchParams: newParams, name, value: newValue, codec })
502
- return newParams
503
- }
504
- if (shallow) {
505
- const params = new URLSearchParams(window.location.search)
506
- const newUrl = [
507
- [window.location.pathname, updateParams(params).toString()].filter(Boolean).join('?'),
508
- window.location.hash
509
- ].join('#')
510
- if (replace) {
511
- window.history.replaceState(null, '', newUrl)
512
- } else {
513
- window.history.pushState(null, '', newUrl)
514
- }
515
- } else {
516
- setSearchParams(updateParams, { replace, preventScrollReset })
517
- }
518
- },
519
- // eslint-disable-next-line react-hooks/exhaustive-deps
520
- [name, setSearchParams]
521
- )
522
-
523
- return [value, setValue] as const
524
- }
525
-
526
- /**
527
- * React hook that decodes *all* search params defined by a loader into a typed object.
528
- * Read-only — for read/write access to individual params, use {@link useState}.
529
- *
530
- * Reads from the pending navigation location when one is in flight.
531
- */
532
- function useSearchParams<L extends QueryStateLoader>(
533
- loader: L
534
- ): [
535
- state: ExtractTypeFromLoader<L>,
536
- setState: (
537
- val: ExtractTypeFromLoader<L> | ((prevState: ExtractTypeFromLoader<L>) => ExtractTypeFromLoader<L>),
538
- navigateOpts?: QueryStateNavigateOptions
539
- ) => void
540
- ] {
541
- const location = useLocation()
542
- const { location: pendingLocation } = useNavigation()
543
-
544
- const search = pendingLocation?.search ?? location.search
545
-
546
- const getLoader = useStableAccessor(loader)
547
- const [_, setSearchParamsRaw] = useSearchParamsRR()
548
-
549
- const setSearchParam = React.useCallback(
550
- (
551
- val: ExtractTypeFromLoader<L> | ((prevState: ExtractTypeFromLoader<L>) => ExtractTypeFromLoader<L>),
552
- navigateOpts?: QueryStateNavigateOptions
553
- ) => {
554
- const loader = getLoader()
555
- const { replace = true, preventScrollReset = false, shallow = false } = { ...navigateOpts }
556
-
557
- function updateParams(params: URLSearchParams) {
558
- const newParams = new URLSearchParams(params)
559
- const oldValue = queryParamsToObject(new URLSearchParams(search), loader)
560
- const newValue = typeof val === 'function' ? val(oldValue) : val
561
- Object.entries(loader).forEach(([name, codec]) =>
562
- setQueryParam({ searchParams: newParams, name, value: newValue[name], codec })
563
- )
564
- return newParams
565
- }
566
- if (shallow) {
567
- const params = new URLSearchParams(window.location.search)
568
- const newUrl = [
569
- [window.location.pathname, updateParams(params).toString()].filter(Boolean).join('?'),
570
- window.location.hash
571
- ].join('#')
572
- if (replace) {
573
- window.history.replaceState(null, '', newUrl)
574
- } else {
575
- window.history.pushState(null, '', newUrl)
576
- }
577
- } else {
578
- setSearchParamsRaw(updateParams, { replace, preventScrollReset })
579
- }
580
- },
581
- [getLoader, setSearchParamsRaw]
582
- )
583
-
584
- const searchParams = React.useMemo(() => queryParamsToObject(new URLSearchParams(search), loader), [search, loader])
585
-
586
- return [searchParams, setSearchParam]
587
- }
588
-
589
- /**
590
- * Identity function that provides type inference for a loader definition.
591
- *
592
- * @example
593
- * ```ts
594
- * const loader = SearchParams.createLoader({
595
- * page: SearchParams.codecs.int.withDefaultValue(0),
596
- * sort: SearchParams.codecs.enum(['name', 'date'] as const)
597
- * })
598
- * ```
599
- */
600
- function createLoader<L extends QueryStateLoader>(loader: L): L {
601
- return loader
602
- }
603
-
604
- export const SearchParams = {
605
- codecs,
606
- serializeCodec,
607
- deserializeCodec,
608
- serializeLoader,
609
- deserializeLoader,
610
- createLoader,
611
- getSearchParamsState,
612
- useState,
613
- useSearchParams
614
- }
615
-
616
- export declare namespace SearchParams {
617
- export {
618
- Codec,
619
- CodecDef,
620
- QueryStateLoader,
621
- ExtractTypeFromLoader,
622
- QueryStateNavigateOptions,
623
- SerializedCodec,
624
- SerializedLoader
625
- }
626
- }