@flamingo-stack/openframe-frontend-core 0.0.591 → 0.0.592-snapshot.20260905013229
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/dist/{chunk-VGBTCUPG.js → chunk-2645GIM2.js} +2 -2
- package/dist/{chunk-BQE4HTDR.js → chunk-2JNCZDJR.js} +2 -2
- package/dist/{chunk-USDG6IWK.cjs → chunk-2KISKWOY.cjs} +7 -7
- package/dist/{chunk-USDG6IWK.cjs.map → chunk-2KISKWOY.cjs.map} +1 -1
- package/dist/{chunk-OBJEFMOI.cjs → chunk-2PMCHL37.cjs} +66 -66
- package/dist/{chunk-OBJEFMOI.cjs.map → chunk-2PMCHL37.cjs.map} +1 -1
- package/dist/{chunk-NXYOUBOL.cjs → chunk-2VOBGNEY.cjs} +21 -21
- package/dist/{chunk-NXYOUBOL.cjs.map → chunk-2VOBGNEY.cjs.map} +1 -1
- package/dist/{chunk-LUWFLC3T.js → chunk-3ZHDEZQB.js} +3 -3
- package/dist/{chunk-SW3CZ2KS.cjs → chunk-4DRZZWC7.cjs} +152 -86
- package/dist/chunk-4DRZZWC7.cjs.map +1 -0
- package/dist/{chunk-XK7SLEZB.cjs → chunk-5SGTTSQN.cjs} +3 -3
- package/dist/{chunk-XK7SLEZB.cjs.map → chunk-5SGTTSQN.cjs.map} +1 -1
- package/dist/{chunk-YMAHVHCX.cjs → chunk-BZEWPW5A.cjs} +3 -3
- package/dist/{chunk-YMAHVHCX.cjs.map → chunk-BZEWPW5A.cjs.map} +1 -1
- package/dist/{chunk-FHKP4H6N.cjs → chunk-CMAO4PPL.cjs} +5 -5
- package/dist/{chunk-FHKP4H6N.cjs.map → chunk-CMAO4PPL.cjs.map} +1 -1
- package/dist/{chunk-ZZF6Z7KB.cjs → chunk-FKXDDJEQ.cjs} +11 -11
- package/dist/{chunk-ZZF6Z7KB.cjs.map → chunk-FKXDDJEQ.cjs.map} +1 -1
- package/dist/{chunk-E455M3WJ.js → chunk-GEP3O6NU.js} +4 -4
- package/dist/{chunk-OKWGSJAI.js → chunk-GGQZBSFN.js} +1 -1
- package/dist/chunk-GGQZBSFN.js.map +1 -0
- package/dist/{chunk-RGCBUNMJ.js → chunk-GXPOEAXL.js} +2 -2
- package/dist/{chunk-RNXY35DZ.cjs → chunk-H64ZHSYR.cjs} +1 -1
- package/dist/chunk-H64ZHSYR.cjs.map +1 -0
- package/dist/{chunk-AMCARR6Y.cjs → chunk-HMRWH2HC.cjs} +15 -15
- package/dist/{chunk-AMCARR6Y.cjs.map → chunk-HMRWH2HC.cjs.map} +1 -1
- package/dist/{chunk-ZFHIMK4W.js → chunk-J3D2WHWC.js} +5 -5
- package/dist/{chunk-T6RY2MC6.js → chunk-KPO6TWEV.js} +8 -8
- package/dist/{chunk-CWO3YFVZ.js → chunk-LRQCKSZC.js} +2 -2
- package/dist/{chunk-2TJSDSXW.js → chunk-MOHSA65M.js} +2 -2
- package/dist/{chunk-4UP2SGSO.js → chunk-MPNCYET3.js} +3 -3
- package/dist/{chunk-U2XRBHBX.js → chunk-MV3XJXPG.js} +9 -9
- package/dist/{chunk-JHVCH3CK.cjs → chunk-NEFNARFF.cjs} +6 -6
- package/dist/{chunk-JHVCH3CK.cjs.map → chunk-NEFNARFF.cjs.map} +1 -1
- package/dist/{chunk-HV6MO4HG.cjs → chunk-NRHU4V3E.cjs} +37 -37
- package/dist/{chunk-HV6MO4HG.cjs.map → chunk-NRHU4V3E.cjs.map} +1 -1
- package/dist/{chunk-B7ZCBGUN.cjs → chunk-O4VZOA2Q.cjs} +3 -3
- package/dist/{chunk-B7ZCBGUN.cjs.map → chunk-O4VZOA2Q.cjs.map} +1 -1
- package/dist/{chunk-YEKPHSWC.cjs → chunk-OAAQKMMU.cjs} +22 -22
- package/dist/{chunk-YEKPHSWC.cjs.map → chunk-OAAQKMMU.cjs.map} +1 -1
- package/dist/{chunk-DKVNW2QD.cjs → chunk-QHTDFF52.cjs} +3 -3
- package/dist/{chunk-DKVNW2QD.cjs.map → chunk-QHTDFF52.cjs.map} +1 -1
- package/dist/{chunk-LL6J6N76.js → chunk-R2J6XMP6.js} +3 -3
- package/dist/{chunk-WKOJL6A2.cjs → chunk-S6G6KJ57.cjs} +10 -10
- package/dist/{chunk-WKOJL6A2.cjs.map → chunk-S6G6KJ57.cjs.map} +1 -1
- package/dist/{chunk-VGY7D6LG.cjs → chunk-STYNZ4QM.cjs} +3 -3
- package/dist/{chunk-VGY7D6LG.cjs.map → chunk-STYNZ4QM.cjs.map} +1 -1
- package/dist/{chunk-BOED5ISE.js → chunk-UG27ITVP.js} +6 -6
- package/dist/{chunk-AZ4A4RBL.cjs → chunk-VDAMJEMR.cjs} +60 -60
- package/dist/{chunk-AZ4A4RBL.cjs.map → chunk-VDAMJEMR.cjs.map} +1 -1
- package/dist/{chunk-ZYA6274A.js → chunk-VUGQ3Z6E.js} +110 -44
- package/dist/chunk-VUGQ3Z6E.js.map +1 -0
- package/dist/{chunk-5SJSG5RV.cjs → chunk-WY2GQ62H.cjs} +8 -8
- package/dist/{chunk-5SJSG5RV.cjs.map → chunk-WY2GQ62H.cjs.map} +1 -1
- package/dist/{chunk-CXILZQFT.cjs → chunk-WZHARPOT.cjs} +19 -19
- package/dist/{chunk-CXILZQFT.cjs.map → chunk-WZHARPOT.cjs.map} +1 -1
- package/dist/{chunk-WMZ5AUTB.js → chunk-WZJOILPU.js} +2 -2
- package/dist/{chunk-QVAKJB64.js → chunk-X7XO73M5.js} +2 -2
- package/dist/{chunk-ALTSOKHP.js → chunk-XH5QBMXD.js} +2 -2
- package/dist/{chunk-WIMGAPVH.js → chunk-YAZY24ZK.js} +4 -4
- package/dist/{chunk-OS72RV57.cjs → chunk-YSJ4TA6V.cjs} +3 -3
- package/dist/{chunk-OS72RV57.cjs.map → chunk-YSJ4TA6V.cjs.map} +1 -1
- package/dist/{chunk-U3V4TPT5.js → chunk-ZR72HRUJ.js} +3 -3
- package/dist/{chunk-BATTVHZT.js → chunk-ZZHHKE2Q.js} +3 -3
- package/dist/components/case-studies/index.cjs +10 -10
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/docs/index.cjs +8 -8
- package/dist/components/docs/index.js +7 -7
- package/dist/components/embeds/index.cjs +5 -5
- package/dist/components/embeds/index.js +4 -4
- package/dist/components/faq/index.cjs +6 -6
- package/dist/components/faq/index.js +5 -5
- package/dist/components/features/index.cjs +4 -4
- package/dist/components/features/index.js +3 -3
- package/dist/components/help-center-pages/index.cjs +27 -27
- package/dist/components/help-center-pages/index.js +18 -18
- package/dist/components/index.cjs +40 -40
- package/dist/components/index.js +18 -18
- package/dist/components/meeting-scheduler/index.cjs +31 -31
- package/dist/components/meeting-scheduler/index.js +5 -5
- package/dist/components/navigation/index.cjs +7 -7
- package/dist/components/navigation/index.js +6 -6
- package/dist/components/onboarding-guides/index.cjs +8 -8
- package/dist/components/onboarding-guides/index.js +7 -7
- package/dist/components/related-content/index.cjs +4 -4
- package/dist/components/related-content/index.js +3 -3
- package/dist/components/tickets/index.cjs +9 -9
- package/dist/components/tickets/index.js +8 -8
- package/dist/components/ui/data-table/data-table-body.d.ts +18 -1
- package/dist/components/ui/data-table/data-table-body.d.ts.map +1 -1
- package/dist/components/ui/data-table/data-table-row.d.ts +13 -1
- package/dist/components/ui/data-table/data-table-row.d.ts.map +1 -1
- package/dist/components/ui/data-table/data-table-skeleton.d.ts +9 -1
- package/dist/components/ui/data-table/data-table-skeleton.d.ts.map +1 -1
- package/dist/components/ui/data-table/index.d.ts +1 -1
- package/dist/components/ui/entity-image.d.ts +12 -1
- package/dist/components/ui/entity-image.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +4 -4
- package/dist/components/ui/index.js +3 -3
- package/dist/hooks/index.cjs +3 -3
- package/dist/hooks/index.js +2 -2
- package/dist/index.cjs +8 -8
- package/dist/index.js +7 -7
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/search-params.cjs.map +1 -1
- package/dist/utils/search-params.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/data-table-row-height.test.tsx +170 -0
- package/src/__tests__/mlg-platform-leaves.test.ts +1 -1
- package/src/components/ui/data-table/data-table-body.tsx +46 -7
- package/src/components/ui/data-table/data-table-row.tsx +38 -8
- package/src/components/ui/data-table/data-table-skeleton.tsx +27 -3
- package/src/components/ui/entity-image.tsx +19 -3
- package/src/hooks/state/__tests__/use-api-params.test.ts +1 -1
- package/src/utils/search-params.ts +4 -4
- package/dist/chunk-OKWGSJAI.js.map +0 -1
- package/dist/chunk-RNXY35DZ.cjs.map +0 -1
- package/dist/chunk-SW3CZ2KS.cjs.map +0 -1
- package/dist/chunk-ZYA6274A.js.map +0 -1
- /package/dist/{chunk-VGBTCUPG.js.map → chunk-2645GIM2.js.map} +0 -0
- /package/dist/{chunk-BQE4HTDR.js.map → chunk-2JNCZDJR.js.map} +0 -0
- /package/dist/{chunk-LUWFLC3T.js.map → chunk-3ZHDEZQB.js.map} +0 -0
- /package/dist/{chunk-E455M3WJ.js.map → chunk-GEP3O6NU.js.map} +0 -0
- /package/dist/{chunk-RGCBUNMJ.js.map → chunk-GXPOEAXL.js.map} +0 -0
- /package/dist/{chunk-ZFHIMK4W.js.map → chunk-J3D2WHWC.js.map} +0 -0
- /package/dist/{chunk-T6RY2MC6.js.map → chunk-KPO6TWEV.js.map} +0 -0
- /package/dist/{chunk-CWO3YFVZ.js.map → chunk-LRQCKSZC.js.map} +0 -0
- /package/dist/{chunk-2TJSDSXW.js.map → chunk-MOHSA65M.js.map} +0 -0
- /package/dist/{chunk-4UP2SGSO.js.map → chunk-MPNCYET3.js.map} +0 -0
- /package/dist/{chunk-U2XRBHBX.js.map → chunk-MV3XJXPG.js.map} +0 -0
- /package/dist/{chunk-LL6J6N76.js.map → chunk-R2J6XMP6.js.map} +0 -0
- /package/dist/{chunk-BOED5ISE.js.map → chunk-UG27ITVP.js.map} +0 -0
- /package/dist/{chunk-WMZ5AUTB.js.map → chunk-WZJOILPU.js.map} +0 -0
- /package/dist/{chunk-QVAKJB64.js.map → chunk-X7XO73M5.js.map} +0 -0
- /package/dist/{chunk-ALTSOKHP.js.map → chunk-XH5QBMXD.js.map} +0 -0
- /package/dist/{chunk-WIMGAPVH.js.map → chunk-YAZY24ZK.js.map} +0 -0
- /package/dist/{chunk-U3V4TPT5.js.map → chunk-ZR72HRUJ.js.map} +0 -0
- /package/dist/{chunk-BATTVHZT.js.map → chunk-ZZHHKE2Q.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/search-params.ts"],"names":[],"mappings":"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","file":"search-params.cjs","sourcesContent":["/**\n * URL parameter schema engine — the ONE grammar for every URL/API integer,\n * the ONE omission rule, the ONE serializer.\n *\n * Hoisted out of `hooks/state/` so SERVER code (route handlers, DALs) and the\n * CLIENT hook parse the same URL with the same function: a param contract that\n * disagrees across the wire is the bug class this removes. JSX-free leaf with\n * its own `exports` subpath; `hooks/state/*` re-exports every name it used to own.\n */\n\n/** JavaScript types a URL parameter can carry. `int` is a whole number with a floor. */\nexport type JSType = 'string' | 'number' | 'int' | 'boolean' | 'array' | 'object';\n\n// ── The integer grammar ─────────────────────────────────────────────────────\n\nexport interface PositiveIntOptions {\n /** Smallest accepted value (default 1). Anything below falls back. */\n min?: number;\n /** Largest accepted value. Applied to the PARSED value AND to the fallback. */\n max?: number;\n}\n\n/**\n * THE integer grammar for URL params, env vars, and page sizes.\n *\n * `null`/`undefined`/`''` → fallback. Otherwise `Math.trunc(Number(raw))`, which\n * accepts `'12'`, `12`, `'12.9'` (→ 12) and `'0x10'` (→ 16), and rejects `'12abc'`\n * and `'abc'` (`NaN`) — the historical `parseInt` accepted `'12abc'` as 12 and\n * produced `NaN` for `'abc'`. A value below `min` falls back. The RESULT (parsed\n * value or fallback alike) is clamped to `max`, so \"never exceeds max\" has one owner.\n */\nexport function positiveInt<TFallback = number>(\n raw: string | number | null | undefined,\n fallback: TFallback,\n { min = 1, max }: PositiveIntOptions = {},\n): number | TFallback {\n // The fallback is returned AS GIVEN — capping only ever applies to a value\n // parsed out of the input, so a caller with no default (`null`) gets its\n // sentinel back untouched instead of `Math.min(null, max)`.\n const capped = (v: number) => (max == null ? v : Math.min(v, max));\n if (raw === null || raw === undefined || raw === '') {\n return typeof fallback === 'number' ? (capped(fallback) as number | TFallback) : fallback;\n }\n const n = Math.trunc(Number(raw));\n if (!Number.isFinite(n) || n < min) {\n return typeof fallback === 'number' ? (capped(fallback) as number | TFallback) : fallback;\n }\n return capped(n);\n}\n\n/** Ceilings every paged surface shares. */\nexport const PAGE_PARAM_LIMITS = { maxPage: 10_000, maxPageSize: 100 } as const;\n\n/** Total pages for a row count. Floors at 1, so an empty result is still \"page 1 of 1\". */\nexport function pageCount(total: number, pageSize: number): number {\n if (!Number.isFinite(total) || !Number.isFinite(pageSize) || pageSize <= 0) return 1;\n return Math.max(1, Math.ceil(total / pageSize));\n}\n\n/** Clamp a requested page into `1..totalPages`. */\nexport function clampPageToTotal(page: number, totalPages: number): number {\n return Math.min(page, Math.max(1, totalPages));\n}\n\n// ── Schema ──────────────────────────────────────────────────────────────────\n\ntype OutputTypeMap = {\n string: string;\n number: number;\n int: number;\n boolean: boolean;\n array: string[];\n object: Record<string, unknown>;\n};\n\nexport type OutputTypeForJSType<T extends JSType> = OutputTypeMap[T];\n\nexport interface BaseParamConfig<T extends JSType = JSType> {\n type: T;\n default?: OutputTypeMap[T];\n required?: boolean;\n}\n\n/** An integer parameter with an optional floor and ceiling. */\nexport interface IntParamConfig extends BaseParamConfig<'int'> {\n min?: number;\n max?: number;\n}\n\nexport type ParamConfig<T extends JSType = JSType> = T extends 'int' ? IntParamConfig : BaseParamConfig<T>;\n\nexport type ParamSchema = Record<string, ParamConfig>;\n\n/** Identity helper that preserves literal types through a schema object. */\nexport function defineParamSchema<T extends ParamSchema>(schema: T): T {\n return schema;\n}\n\n/**\n * Build a `Record<K, V>` from a key tuple. Keeps `keyof` EXACT when spreading a\n * shared config across derived keys (an inline `Object.fromEntries` widens to\n * `Record<string, V>` and loses the key union).\n */\nexport function fromKeys<K extends string, V>(keys: readonly K[], value: V): Record<K, V> {\n return Object.fromEntries(keys.map(k => [k, value])) as Record<K, V>;\n}\n\n// ── Reading ─────────────────────────────────────────────────────────────────\n\n/** Anything a URL can be read from: `URLSearchParams`, Next's `searchParams`, or a plain record. */\nexport type ParamInput = URLSearchParams | Record<string, string | string[] | undefined> | null | undefined;\n\n/** THE first-value rule for a duplicated key (`?a=1&a=2` reads as `1`). */\nexport function firstParamValue(input: ParamInput, key: string): string | undefined {\n if (!input) return undefined;\n if (input instanceof URLSearchParams) return input.get(key) ?? undefined;\n const value = input[key];\n return Array.isArray(value) ? value[0] : value;\n}\n\n/** Every value for a key (arrays read repeated params, then a `,`-joined single value). */\nfunction allParamValues(input: ParamInput, key: string): string[] {\n if (!input) return [];\n const raw = input instanceof URLSearchParams ? input.getAll(key) : input[key];\n const list = Array.isArray(raw) ? raw : raw === undefined ? [] : [raw];\n if (list.length === 1 && list[0]?.includes(',')) return list[0].split(',').filter(Boolean);\n return list.filter(v => v !== undefined && v !== '');\n}\n\nexport interface ParseSchemaOptions {\n /**\n * What an ABSENT scalar with no declared `default` resolves to.\n * `'undefined'` (default) keeps today's behaviour; `'null'` is the explicit\n * \"this filter is unset\" spelling that survives JSON and `URLSearchParams`.\n */\n absent?: 'undefined' | 'null';\n}\n\nexport type AbsentValue<Opts extends ParseSchemaOptions | undefined> = Opts extends { absent: 'null' }\n ? null\n : undefined;\n\n/**\n * The parsed shape of a schema: keys with a `default` and every array key are\n * non-nullable; an undefaulted scalar carries the caller's absent value.\n */\nexport type InferParsedParams<TSchema extends ParamSchema, Absent = undefined> = {\n [K in keyof TSchema]: TSchema[K] extends { type: 'array' }\n ? string[]\n : TSchema[K] extends { default: infer D }\n ? D\n : TSchema[K]['type'] extends infer T\n ? T extends JSType\n ? OutputTypeForJSType<T> | Absent\n : never\n : never;\n};\n\nfunction coerceScalar(raw: string | undefined, config: ParamConfig): unknown {\n if (raw === undefined || raw === '') return undefined;\n switch (config.type) {\n case 'int': {\n const int = config as IntParamConfig;\n // ONE integer grammar, default or not. Without a declared default the\n // fallback is `undefined` (an unparseable value is absent), which\n // `positiveInt` returns untouched — the branch used to open-code the\n // truncation, the min floor and the max clamp a second time.\n return positiveInt<undefined>(raw, int.default as undefined, { min: int.min, max: int.max });\n }\n case 'number': {\n const n = Number(raw);\n return Number.isFinite(n) ? n : undefined;\n }\n case 'boolean':\n return raw === 'true' || raw === '1';\n case 'object':\n try {\n return JSON.parse(raw) as Record<string, unknown>;\n } catch {\n return undefined;\n }\n default:\n return raw;\n }\n}\n\n/**\n * Parse a URL into a schema's shape. THE reader — the server route and the\n * client hook both call this, so a URL means exactly one thing.\n *\n * PRECEDENCE: an absent or empty SCALAR takes the declared `default` when there\n * is one, else the `absent` value. ARRAY keys never take `absent`: they are `[]`.\n */\nexport function parseSchemaParams<TSchema extends ParamSchema, Opts extends ParseSchemaOptions = Record<string, never>>(\n schema: TSchema,\n input: ParamInput,\n options?: Opts,\n): InferParsedParams<TSchema, AbsentValue<Opts>> {\n const absent = options?.absent === 'null' ? null : undefined;\n const out: Record<string, unknown> = {};\n for (const [key, config] of Object.entries(schema)) {\n if (config.type === 'array') {\n const values = allParamValues(input, key);\n out[key] = values.length > 0 ? values : ((config.default as string[] | undefined) ?? []);\n continue;\n }\n const coerced = coerceScalar(firstParamValue(input, key), config);\n out[key] = coerced !== undefined ? coerced : (config.default ?? absent);\n }\n return out as InferParsedParams<TSchema, AbsentValue<Opts>>;\n}\n\n// ── Writing ─────────────────────────────────────────────────────────────────\n\n/**\n * A schema entry OR a legacy flattened param (which spells its default\n * `defaultValue`).\n *\n * A UNION, not a bag of two optional fields: with only `default?` and\n * `defaultValue?` this is a WEAK TYPE, and TypeScript rejects any argument\n * sharing neither — including `{ type: 'string' }`, the single most common\n * schema entry there is. Naming the real config shapes fixes that without an\n * index signature, which `FlattenedParam` (an interface) could never satisfy.\n */\ntype OmissionConfig = ParamConfig | { default?: unknown; defaultValue?: unknown };\n\n/**\n * THE omission rule: `null`, `undefined`, `''`, `[]`, and a value equal to the\n * declared default are left out, so a URL only ever carries what differs.\n */\nexport function shouldIncludeInUrl(value: unknown, config: OmissionConfig | undefined): boolean {\n if (value === null || value === undefined) return false;\n if (Array.isArray(value) && value.length === 0) return false;\n if (value === '') return false;\n // `in` narrowing rather than a cast: the union's members genuinely differ —\n // a schema entry spells its default `default`, a flattened param spells it\n // `defaultValue`, and neither is obliged to carry the other's field.\n const fromDefault = config && 'default' in config ? config.default : undefined;\n const fromLegacy = config && 'defaultValue' in config ? config.defaultValue : undefined;\n const declared = fromDefault !== undefined ? fromDefault : fromLegacy;\n if (declared !== undefined && value === declared) return false;\n return true;\n}\n\nexport interface CreateSearchParamsOptions {\n /** Join arrays with this separator instead of repeating the key (`;` for the hub's list APIs). */\n arrayJoin?: string;\n}\n\n/** Build `URLSearchParams` from a value record (arrays repeat the key by default). */\nexport function createSearchParams(\n params: Record<string, unknown>,\n { arrayJoin }: CreateSearchParamsOptions = {},\n): URLSearchParams {\n const search = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n // The emptiness half of the omission rule has ONE owner. (No config here:\n // callers that must also drop schema DEFAULTS filter through\n // `shouldIncludeInUrl` with the config first — see `serializeSchemaParams`.)\n if (!shouldIncludeInUrl(value, undefined)) continue;\n if (Array.isArray(value)) {\n // Empty ELEMENTS are dropped, and an array that filters to nothing is\n // absent (a cleared multi-select must not leave `?tags=` behind).\n const items = value.filter(v => v !== null && v !== undefined && v !== '');\n if (items.length === 0) continue;\n if (arrayJoin) search.set(key, items.join(arrayJoin));\n else for (const item of items) search.append(key, String(item));\n continue;\n }\n if (typeof value === 'object') {\n search.set(key, JSON.stringify(value));\n continue;\n }\n search.set(key, String(value));\n }\n return search;\n}\n\n/** Serialize params to a query string, omitting everything `shouldIncludeInUrl` rejects. */\nexport function serializeSchemaParams<TSchema extends ParamSchema>(\n schema: TSchema,\n params: Record<string, unknown>,\n options?: CreateSearchParamsOptions,\n): string {\n const kept: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(params)) {\n if (shouldIncludeInUrl(value, schema[key])) kept[key] = value;\n }\n return createSearchParams(kept, options).toString();\n}\n\n/** Append a query string to a path, or return the path unchanged when it is empty. */\nexport function withQuery(path: string, queryString: string | null | undefined): string {\n return queryString ? `${path}?${queryString}` : path;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/search-params.ts"],"names":[],"mappings":"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","file":"search-params.cjs","sourcesContent":["/**\n * URL parameter schema engine — the ONE grammar for every URL/API integer,\n * the ONE omission rule, the ONE serializer.\n *\n * Hoisted out of `hooks/state/` so SERVER code (route handlers, DALs) and the\n * CLIENT hook parse the same URL with the same function: a param contract that\n * disagrees across the wire is the bug class this removes. JSX-free leaf with\n * its own `exports` subpath; `hooks/state/*` re-exports every name it used to own.\n */\n\n/** JavaScript types a URL parameter can carry. `int` is a whole number with a floor. */\nexport type JSType = 'string' | 'number' | 'int' | 'boolean' | 'array' | 'object';\n\n// ── The integer grammar ─────────────────────────────────────────────────────\n\nexport interface PositiveIntOptions {\n /** Smallest accepted value (default 1). Anything below falls back. */\n min?: number;\n /** Largest accepted value. Applied to the PARSED value AND to the fallback. */\n max?: number;\n}\n\n/**\n * THE integer grammar for URL params, env vars, and page sizes.\n *\n * `null`/`undefined`/`''` → fallback. Otherwise `Math.trunc(Number(raw))`, which\n * accepts `'12'`, `12`, `'12.9'` (→ 12) and `'0x10'` (→ 16), and rejects `'12abc'`\n * and `'abc'` (`NaN`) — the historical `parseInt` accepted `'12abc'` as 12 and\n * produced `NaN` for `'abc'`. A value below `min` falls back. The RESULT (parsed\n * value or fallback alike) is clamped to `max`, so \"never exceeds max\" has one owner.\n */\nexport function positiveInt<TFallback = number>(\n raw: string | number | null | undefined,\n fallback: TFallback,\n { min = 1, max }: PositiveIntOptions = {},\n): number | TFallback {\n // The fallback is returned AS GIVEN — capping only ever applies to a value\n // parsed out of the input, so a caller with no default (`null`) gets its\n // sentinel back untouched instead of `Math.min(null, max)`.\n const capped = (v: number) => (max == null ? v : Math.min(v, max));\n if (raw === null || raw === undefined || raw === '') {\n return typeof fallback === 'number' ? capped(fallback) : fallback;\n }\n const n = Math.trunc(Number(raw));\n if (!Number.isFinite(n) || n < min) {\n return typeof fallback === 'number' ? capped(fallback) : fallback;\n }\n return capped(n);\n}\n\n/** Ceilings every paged surface shares. */\nexport const PAGE_PARAM_LIMITS = { maxPage: 10_000, maxPageSize: 100 } as const;\n\n/** Total pages for a row count. Floors at 1, so an empty result is still \"page 1 of 1\". */\nexport function pageCount(total: number, pageSize: number): number {\n if (!Number.isFinite(total) || !Number.isFinite(pageSize) || pageSize <= 0) return 1;\n return Math.max(1, Math.ceil(total / pageSize));\n}\n\n/** Clamp a requested page into `1..totalPages`. */\nexport function clampPageToTotal(page: number, totalPages: number): number {\n return Math.min(page, Math.max(1, totalPages));\n}\n\n// ── Schema ──────────────────────────────────────────────────────────────────\n\ntype OutputTypeMap = {\n string: string;\n number: number;\n int: number;\n boolean: boolean;\n array: string[];\n object: Record<string, unknown>;\n};\n\nexport type OutputTypeForJSType<T extends JSType> = OutputTypeMap[T];\n\nexport interface BaseParamConfig<T extends JSType = JSType> {\n type: T;\n default?: OutputTypeMap[T];\n required?: boolean;\n}\n\n/** An integer parameter with an optional floor and ceiling. */\nexport interface IntParamConfig extends BaseParamConfig<'int'> {\n min?: number;\n max?: number;\n}\n\nexport type ParamConfig<T extends JSType = JSType> = T extends 'int' ? IntParamConfig : BaseParamConfig<T>;\n\nexport type ParamSchema = Record<string, ParamConfig>;\n\n/** Identity helper that preserves literal types through a schema object. */\nexport function defineParamSchema<T extends ParamSchema>(schema: T): T {\n return schema;\n}\n\n/**\n * Build a `Record<K, V>` from a key tuple. Keeps `keyof` EXACT when spreading a\n * shared config across derived keys (an inline `Object.fromEntries` widens to\n * `Record<string, V>` and loses the key union).\n */\nexport function fromKeys<K extends string, V>(keys: readonly K[], value: V): Record<K, V> {\n return Object.fromEntries(keys.map(k => [k, value])) as Record<K, V>;\n}\n\n// ── Reading ─────────────────────────────────────────────────────────────────\n\n/** Anything a URL can be read from: `URLSearchParams`, Next's `searchParams`, or a plain record. */\nexport type ParamInput = URLSearchParams | Record<string, string | string[] | undefined> | null | undefined;\n\n/** THE first-value rule for a duplicated key (`?a=1&a=2` reads as `1`). */\nexport function firstParamValue(input: ParamInput, key: string): string | undefined {\n if (!input) return undefined;\n if (input instanceof URLSearchParams) return input.get(key) ?? undefined;\n const value = input[key];\n return Array.isArray(value) ? value[0] : value;\n}\n\n/** Every value for a key (arrays read repeated params, then a `,`-joined single value). */\nfunction allParamValues(input: ParamInput, key: string): string[] {\n if (!input) return [];\n const raw = input instanceof URLSearchParams ? input.getAll(key) : input[key];\n const list = Array.isArray(raw) ? raw : raw === undefined ? [] : [raw];\n if (list.length === 1 && list[0]?.includes(',')) return list[0].split(',').filter(Boolean);\n return list.filter(v => v !== undefined && v !== '');\n}\n\nexport interface ParseSchemaOptions {\n /**\n * What an ABSENT scalar with no declared `default` resolves to.\n * `'undefined'` (default) keeps today's behaviour; `'null'` is the explicit\n * \"this filter is unset\" spelling that survives JSON and `URLSearchParams`.\n */\n absent?: 'undefined' | 'null';\n}\n\nexport type AbsentValue<Opts extends ParseSchemaOptions | undefined> = Opts extends { absent: 'null' }\n ? null\n : undefined;\n\n/**\n * The parsed shape of a schema: keys with a `default` and every array key are\n * non-nullable; an undefaulted scalar carries the caller's absent value.\n */\nexport type InferParsedParams<TSchema extends ParamSchema, Absent = undefined> = {\n [K in keyof TSchema]: TSchema[K] extends { type: 'array' }\n ? string[]\n : TSchema[K] extends { default: infer D }\n ? D\n : TSchema[K]['type'] extends infer T\n ? T extends JSType\n ? OutputTypeForJSType<T> | Absent\n : never\n : never;\n};\n\nfunction coerceScalar(raw: string | undefined, config: ParamConfig): unknown {\n if (raw === undefined || raw === '') return undefined;\n switch (config.type) {\n case 'int': {\n const int = config;\n // ONE integer grammar, default or not. Without a declared default the\n // fallback is `undefined` (an unparseable value is absent), which\n // `positiveInt` returns untouched — the branch used to open-code the\n // truncation, the min floor and the max clamp a second time.\n return positiveInt<undefined>(raw, int.default as undefined, { min: int.min, max: int.max });\n }\n case 'number': {\n const n = Number(raw);\n return Number.isFinite(n) ? n : undefined;\n }\n case 'boolean':\n return raw === 'true' || raw === '1';\n case 'object':\n try {\n return JSON.parse(raw) as Record<string, unknown>;\n } catch {\n return undefined;\n }\n default:\n return raw;\n }\n}\n\n/**\n * Parse a URL into a schema's shape. THE reader — the server route and the\n * client hook both call this, so a URL means exactly one thing.\n *\n * PRECEDENCE: an absent or empty SCALAR takes the declared `default` when there\n * is one, else the `absent` value. ARRAY keys never take `absent`: they are `[]`.\n */\nexport function parseSchemaParams<TSchema extends ParamSchema, Opts extends ParseSchemaOptions = Record<string, never>>(\n schema: TSchema,\n input: ParamInput,\n options?: Opts,\n): InferParsedParams<TSchema, AbsentValue<Opts>> {\n const absent = options?.absent === 'null' ? null : undefined;\n const out: Record<string, unknown> = {};\n for (const [key, config] of Object.entries(schema)) {\n if (config.type === 'array') {\n const values = allParamValues(input, key);\n out[key] = values.length > 0 ? values : (config.default ?? []);\n continue;\n }\n const coerced = coerceScalar(firstParamValue(input, key), config);\n out[key] = coerced !== undefined ? coerced : (config.default ?? absent);\n }\n return out as InferParsedParams<TSchema, AbsentValue<Opts>>;\n}\n\n// ── Writing ─────────────────────────────────────────────────────────────────\n\n/**\n * A schema entry OR a legacy flattened param (which spells its default\n * `defaultValue`).\n *\n * A UNION, not a bag of two optional fields: with only `default?` and\n * `defaultValue?` this is a WEAK TYPE, and TypeScript rejects any argument\n * sharing neither — including `{ type: 'string' }`, the single most common\n * schema entry there is. Naming the real config shapes fixes that without an\n * index signature, which `FlattenedParam` (an interface) could never satisfy.\n */\ntype OmissionConfig = ParamConfig | { default?: unknown; defaultValue?: unknown };\n\n/**\n * THE omission rule: `null`, `undefined`, `''`, `[]`, and a value equal to the\n * declared default are left out, so a URL only ever carries what differs.\n */\nexport function shouldIncludeInUrl(value: unknown, config: OmissionConfig | undefined): boolean {\n if (value === null || value === undefined) return false;\n if (Array.isArray(value) && value.length === 0) return false;\n if (value === '') return false;\n // `in` narrowing rather than a cast: the union's members genuinely differ —\n // a schema entry spells its default `default`, a flattened param spells it\n // `defaultValue`, and neither is obliged to carry the other's field.\n const fromDefault = config && 'default' in config ? config.default : undefined;\n const fromLegacy = config && 'defaultValue' in config ? config.defaultValue : undefined;\n const declared = fromDefault !== undefined ? fromDefault : fromLegacy;\n if (declared !== undefined && value === declared) return false;\n return true;\n}\n\nexport interface CreateSearchParamsOptions {\n /** Join arrays with this separator instead of repeating the key (`;` for the hub's list APIs). */\n arrayJoin?: string;\n}\n\n/** Build `URLSearchParams` from a value record (arrays repeat the key by default). */\nexport function createSearchParams(\n params: Record<string, unknown>,\n { arrayJoin }: CreateSearchParamsOptions = {},\n): URLSearchParams {\n const search = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n // The emptiness half of the omission rule has ONE owner. (No config here:\n // callers that must also drop schema DEFAULTS filter through\n // `shouldIncludeInUrl` with the config first — see `serializeSchemaParams`.)\n if (!shouldIncludeInUrl(value, undefined)) continue;\n if (Array.isArray(value)) {\n // Empty ELEMENTS are dropped, and an array that filters to nothing is\n // absent (a cleared multi-select must not leave `?tags=` behind).\n const items = value.filter(v => v !== null && v !== undefined && v !== '');\n if (items.length === 0) continue;\n if (arrayJoin) search.set(key, items.join(arrayJoin));\n else for (const item of items) search.append(key, String(item));\n continue;\n }\n if (typeof value === 'object') {\n search.set(key, JSON.stringify(value));\n continue;\n }\n search.set(key, String(value));\n }\n return search;\n}\n\n/** Serialize params to a query string, omitting everything `shouldIncludeInUrl` rejects. */\nexport function serializeSchemaParams<TSchema extends ParamSchema>(\n schema: TSchema,\n params: Record<string, unknown>,\n options?: CreateSearchParamsOptions,\n): string {\n const kept: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(params)) {\n if (shouldIncludeInUrl(value, schema[key])) kept[key] = value;\n }\n return createSearchParams(kept, options).toString();\n}\n\n/** Append a query string to a path, or return the path unchanged when it is empty. */\nexport function withQuery(path: string, queryString: string | null | undefined): string {\n return queryString ? `${path}?${queryString}` : path;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/search-params.ts"],"names":[],"mappings":"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","file":"search-params.js","sourcesContent":["/**\n * URL parameter schema engine — the ONE grammar for every URL/API integer,\n * the ONE omission rule, the ONE serializer.\n *\n * Hoisted out of `hooks/state/` so SERVER code (route handlers, DALs) and the\n * CLIENT hook parse the same URL with the same function: a param contract that\n * disagrees across the wire is the bug class this removes. JSX-free leaf with\n * its own `exports` subpath; `hooks/state/*` re-exports every name it used to own.\n */\n\n/** JavaScript types a URL parameter can carry. `int` is a whole number with a floor. */\nexport type JSType = 'string' | 'number' | 'int' | 'boolean' | 'array' | 'object';\n\n// ── The integer grammar ─────────────────────────────────────────────────────\n\nexport interface PositiveIntOptions {\n /** Smallest accepted value (default 1). Anything below falls back. */\n min?: number;\n /** Largest accepted value. Applied to the PARSED value AND to the fallback. */\n max?: number;\n}\n\n/**\n * THE integer grammar for URL params, env vars, and page sizes.\n *\n * `null`/`undefined`/`''` → fallback. Otherwise `Math.trunc(Number(raw))`, which\n * accepts `'12'`, `12`, `'12.9'` (→ 12) and `'0x10'` (→ 16), and rejects `'12abc'`\n * and `'abc'` (`NaN`) — the historical `parseInt` accepted `'12abc'` as 12 and\n * produced `NaN` for `'abc'`. A value below `min` falls back. The RESULT (parsed\n * value or fallback alike) is clamped to `max`, so \"never exceeds max\" has one owner.\n */\nexport function positiveInt<TFallback = number>(\n raw: string | number | null | undefined,\n fallback: TFallback,\n { min = 1, max }: PositiveIntOptions = {},\n): number | TFallback {\n // The fallback is returned AS GIVEN — capping only ever applies to a value\n // parsed out of the input, so a caller with no default (`null`) gets its\n // sentinel back untouched instead of `Math.min(null, max)`.\n const capped = (v: number) => (max == null ? v : Math.min(v, max));\n if (raw === null || raw === undefined || raw === '') {\n return typeof fallback === 'number' ? (capped(fallback) as number | TFallback) : fallback;\n }\n const n = Math.trunc(Number(raw));\n if (!Number.isFinite(n) || n < min) {\n return typeof fallback === 'number' ? (capped(fallback) as number | TFallback) : fallback;\n }\n return capped(n);\n}\n\n/** Ceilings every paged surface shares. */\nexport const PAGE_PARAM_LIMITS = { maxPage: 10_000, maxPageSize: 100 } as const;\n\n/** Total pages for a row count. Floors at 1, so an empty result is still \"page 1 of 1\". */\nexport function pageCount(total: number, pageSize: number): number {\n if (!Number.isFinite(total) || !Number.isFinite(pageSize) || pageSize <= 0) return 1;\n return Math.max(1, Math.ceil(total / pageSize));\n}\n\n/** Clamp a requested page into `1..totalPages`. */\nexport function clampPageToTotal(page: number, totalPages: number): number {\n return Math.min(page, Math.max(1, totalPages));\n}\n\n// ── Schema ──────────────────────────────────────────────────────────────────\n\ntype OutputTypeMap = {\n string: string;\n number: number;\n int: number;\n boolean: boolean;\n array: string[];\n object: Record<string, unknown>;\n};\n\nexport type OutputTypeForJSType<T extends JSType> = OutputTypeMap[T];\n\nexport interface BaseParamConfig<T extends JSType = JSType> {\n type: T;\n default?: OutputTypeMap[T];\n required?: boolean;\n}\n\n/** An integer parameter with an optional floor and ceiling. */\nexport interface IntParamConfig extends BaseParamConfig<'int'> {\n min?: number;\n max?: number;\n}\n\nexport type ParamConfig<T extends JSType = JSType> = T extends 'int' ? IntParamConfig : BaseParamConfig<T>;\n\nexport type ParamSchema = Record<string, ParamConfig>;\n\n/** Identity helper that preserves literal types through a schema object. */\nexport function defineParamSchema<T extends ParamSchema>(schema: T): T {\n return schema;\n}\n\n/**\n * Build a `Record<K, V>` from a key tuple. Keeps `keyof` EXACT when spreading a\n * shared config across derived keys (an inline `Object.fromEntries` widens to\n * `Record<string, V>` and loses the key union).\n */\nexport function fromKeys<K extends string, V>(keys: readonly K[], value: V): Record<K, V> {\n return Object.fromEntries(keys.map(k => [k, value])) as Record<K, V>;\n}\n\n// ── Reading ─────────────────────────────────────────────────────────────────\n\n/** Anything a URL can be read from: `URLSearchParams`, Next's `searchParams`, or a plain record. */\nexport type ParamInput = URLSearchParams | Record<string, string | string[] | undefined> | null | undefined;\n\n/** THE first-value rule for a duplicated key (`?a=1&a=2` reads as `1`). */\nexport function firstParamValue(input: ParamInput, key: string): string | undefined {\n if (!input) return undefined;\n if (input instanceof URLSearchParams) return input.get(key) ?? undefined;\n const value = input[key];\n return Array.isArray(value) ? value[0] : value;\n}\n\n/** Every value for a key (arrays read repeated params, then a `,`-joined single value). */\nfunction allParamValues(input: ParamInput, key: string): string[] {\n if (!input) return [];\n const raw = input instanceof URLSearchParams ? input.getAll(key) : input[key];\n const list = Array.isArray(raw) ? raw : raw === undefined ? [] : [raw];\n if (list.length === 1 && list[0]?.includes(',')) return list[0].split(',').filter(Boolean);\n return list.filter(v => v !== undefined && v !== '');\n}\n\nexport interface ParseSchemaOptions {\n /**\n * What an ABSENT scalar with no declared `default` resolves to.\n * `'undefined'` (default) keeps today's behaviour; `'null'` is the explicit\n * \"this filter is unset\" spelling that survives JSON and `URLSearchParams`.\n */\n absent?: 'undefined' | 'null';\n}\n\nexport type AbsentValue<Opts extends ParseSchemaOptions | undefined> = Opts extends { absent: 'null' }\n ? null\n : undefined;\n\n/**\n * The parsed shape of a schema: keys with a `default` and every array key are\n * non-nullable; an undefaulted scalar carries the caller's absent value.\n */\nexport type InferParsedParams<TSchema extends ParamSchema, Absent = undefined> = {\n [K in keyof TSchema]: TSchema[K] extends { type: 'array' }\n ? string[]\n : TSchema[K] extends { default: infer D }\n ? D\n : TSchema[K]['type'] extends infer T\n ? T extends JSType\n ? OutputTypeForJSType<T> | Absent\n : never\n : never;\n};\n\nfunction coerceScalar(raw: string | undefined, config: ParamConfig): unknown {\n if (raw === undefined || raw === '') return undefined;\n switch (config.type) {\n case 'int': {\n const int = config as IntParamConfig;\n // ONE integer grammar, default or not. Without a declared default the\n // fallback is `undefined` (an unparseable value is absent), which\n // `positiveInt` returns untouched — the branch used to open-code the\n // truncation, the min floor and the max clamp a second time.\n return positiveInt<undefined>(raw, int.default as undefined, { min: int.min, max: int.max });\n }\n case 'number': {\n const n = Number(raw);\n return Number.isFinite(n) ? n : undefined;\n }\n case 'boolean':\n return raw === 'true' || raw === '1';\n case 'object':\n try {\n return JSON.parse(raw) as Record<string, unknown>;\n } catch {\n return undefined;\n }\n default:\n return raw;\n }\n}\n\n/**\n * Parse a URL into a schema's shape. THE reader — the server route and the\n * client hook both call this, so a URL means exactly one thing.\n *\n * PRECEDENCE: an absent or empty SCALAR takes the declared `default` when there\n * is one, else the `absent` value. ARRAY keys never take `absent`: they are `[]`.\n */\nexport function parseSchemaParams<TSchema extends ParamSchema, Opts extends ParseSchemaOptions = Record<string, never>>(\n schema: TSchema,\n input: ParamInput,\n options?: Opts,\n): InferParsedParams<TSchema, AbsentValue<Opts>> {\n const absent = options?.absent === 'null' ? null : undefined;\n const out: Record<string, unknown> = {};\n for (const [key, config] of Object.entries(schema)) {\n if (config.type === 'array') {\n const values = allParamValues(input, key);\n out[key] = values.length > 0 ? values : ((config.default as string[] | undefined) ?? []);\n continue;\n }\n const coerced = coerceScalar(firstParamValue(input, key), config);\n out[key] = coerced !== undefined ? coerced : (config.default ?? absent);\n }\n return out as InferParsedParams<TSchema, AbsentValue<Opts>>;\n}\n\n// ── Writing ─────────────────────────────────────────────────────────────────\n\n/**\n * A schema entry OR a legacy flattened param (which spells its default\n * `defaultValue`).\n *\n * A UNION, not a bag of two optional fields: with only `default?` and\n * `defaultValue?` this is a WEAK TYPE, and TypeScript rejects any argument\n * sharing neither — including `{ type: 'string' }`, the single most common\n * schema entry there is. Naming the real config shapes fixes that without an\n * index signature, which `FlattenedParam` (an interface) could never satisfy.\n */\ntype OmissionConfig = ParamConfig | { default?: unknown; defaultValue?: unknown };\n\n/**\n * THE omission rule: `null`, `undefined`, `''`, `[]`, and a value equal to the\n * declared default are left out, so a URL only ever carries what differs.\n */\nexport function shouldIncludeInUrl(value: unknown, config: OmissionConfig | undefined): boolean {\n if (value === null || value === undefined) return false;\n if (Array.isArray(value) && value.length === 0) return false;\n if (value === '') return false;\n // `in` narrowing rather than a cast: the union's members genuinely differ —\n // a schema entry spells its default `default`, a flattened param spells it\n // `defaultValue`, and neither is obliged to carry the other's field.\n const fromDefault = config && 'default' in config ? config.default : undefined;\n const fromLegacy = config && 'defaultValue' in config ? config.defaultValue : undefined;\n const declared = fromDefault !== undefined ? fromDefault : fromLegacy;\n if (declared !== undefined && value === declared) return false;\n return true;\n}\n\nexport interface CreateSearchParamsOptions {\n /** Join arrays with this separator instead of repeating the key (`;` for the hub's list APIs). */\n arrayJoin?: string;\n}\n\n/** Build `URLSearchParams` from a value record (arrays repeat the key by default). */\nexport function createSearchParams(\n params: Record<string, unknown>,\n { arrayJoin }: CreateSearchParamsOptions = {},\n): URLSearchParams {\n const search = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n // The emptiness half of the omission rule has ONE owner. (No config here:\n // callers that must also drop schema DEFAULTS filter through\n // `shouldIncludeInUrl` with the config first — see `serializeSchemaParams`.)\n if (!shouldIncludeInUrl(value, undefined)) continue;\n if (Array.isArray(value)) {\n // Empty ELEMENTS are dropped, and an array that filters to nothing is\n // absent (a cleared multi-select must not leave `?tags=` behind).\n const items = value.filter(v => v !== null && v !== undefined && v !== '');\n if (items.length === 0) continue;\n if (arrayJoin) search.set(key, items.join(arrayJoin));\n else for (const item of items) search.append(key, String(item));\n continue;\n }\n if (typeof value === 'object') {\n search.set(key, JSON.stringify(value));\n continue;\n }\n search.set(key, String(value));\n }\n return search;\n}\n\n/** Serialize params to a query string, omitting everything `shouldIncludeInUrl` rejects. */\nexport function serializeSchemaParams<TSchema extends ParamSchema>(\n schema: TSchema,\n params: Record<string, unknown>,\n options?: CreateSearchParamsOptions,\n): string {\n const kept: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(params)) {\n if (shouldIncludeInUrl(value, schema[key])) kept[key] = value;\n }\n return createSearchParams(kept, options).toString();\n}\n\n/** Append a query string to a path, or return the path unchanged when it is empty. */\nexport function withQuery(path: string, queryString: string | null | undefined): string {\n return queryString ? `${path}?${queryString}` : path;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/search-params.ts"],"names":[],"mappings":"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","file":"search-params.js","sourcesContent":["/**\n * URL parameter schema engine — the ONE grammar for every URL/API integer,\n * the ONE omission rule, the ONE serializer.\n *\n * Hoisted out of `hooks/state/` so SERVER code (route handlers, DALs) and the\n * CLIENT hook parse the same URL with the same function: a param contract that\n * disagrees across the wire is the bug class this removes. JSX-free leaf with\n * its own `exports` subpath; `hooks/state/*` re-exports every name it used to own.\n */\n\n/** JavaScript types a URL parameter can carry. `int` is a whole number with a floor. */\nexport type JSType = 'string' | 'number' | 'int' | 'boolean' | 'array' | 'object';\n\n// ── The integer grammar ─────────────────────────────────────────────────────\n\nexport interface PositiveIntOptions {\n /** Smallest accepted value (default 1). Anything below falls back. */\n min?: number;\n /** Largest accepted value. Applied to the PARSED value AND to the fallback. */\n max?: number;\n}\n\n/**\n * THE integer grammar for URL params, env vars, and page sizes.\n *\n * `null`/`undefined`/`''` → fallback. Otherwise `Math.trunc(Number(raw))`, which\n * accepts `'12'`, `12`, `'12.9'` (→ 12) and `'0x10'` (→ 16), and rejects `'12abc'`\n * and `'abc'` (`NaN`) — the historical `parseInt` accepted `'12abc'` as 12 and\n * produced `NaN` for `'abc'`. A value below `min` falls back. The RESULT (parsed\n * value or fallback alike) is clamped to `max`, so \"never exceeds max\" has one owner.\n */\nexport function positiveInt<TFallback = number>(\n raw: string | number | null | undefined,\n fallback: TFallback,\n { min = 1, max }: PositiveIntOptions = {},\n): number | TFallback {\n // The fallback is returned AS GIVEN — capping only ever applies to a value\n // parsed out of the input, so a caller with no default (`null`) gets its\n // sentinel back untouched instead of `Math.min(null, max)`.\n const capped = (v: number) => (max == null ? v : Math.min(v, max));\n if (raw === null || raw === undefined || raw === '') {\n return typeof fallback === 'number' ? capped(fallback) : fallback;\n }\n const n = Math.trunc(Number(raw));\n if (!Number.isFinite(n) || n < min) {\n return typeof fallback === 'number' ? capped(fallback) : fallback;\n }\n return capped(n);\n}\n\n/** Ceilings every paged surface shares. */\nexport const PAGE_PARAM_LIMITS = { maxPage: 10_000, maxPageSize: 100 } as const;\n\n/** Total pages for a row count. Floors at 1, so an empty result is still \"page 1 of 1\". */\nexport function pageCount(total: number, pageSize: number): number {\n if (!Number.isFinite(total) || !Number.isFinite(pageSize) || pageSize <= 0) return 1;\n return Math.max(1, Math.ceil(total / pageSize));\n}\n\n/** Clamp a requested page into `1..totalPages`. */\nexport function clampPageToTotal(page: number, totalPages: number): number {\n return Math.min(page, Math.max(1, totalPages));\n}\n\n// ── Schema ──────────────────────────────────────────────────────────────────\n\ntype OutputTypeMap = {\n string: string;\n number: number;\n int: number;\n boolean: boolean;\n array: string[];\n object: Record<string, unknown>;\n};\n\nexport type OutputTypeForJSType<T extends JSType> = OutputTypeMap[T];\n\nexport interface BaseParamConfig<T extends JSType = JSType> {\n type: T;\n default?: OutputTypeMap[T];\n required?: boolean;\n}\n\n/** An integer parameter with an optional floor and ceiling. */\nexport interface IntParamConfig extends BaseParamConfig<'int'> {\n min?: number;\n max?: number;\n}\n\nexport type ParamConfig<T extends JSType = JSType> = T extends 'int' ? IntParamConfig : BaseParamConfig<T>;\n\nexport type ParamSchema = Record<string, ParamConfig>;\n\n/** Identity helper that preserves literal types through a schema object. */\nexport function defineParamSchema<T extends ParamSchema>(schema: T): T {\n return schema;\n}\n\n/**\n * Build a `Record<K, V>` from a key tuple. Keeps `keyof` EXACT when spreading a\n * shared config across derived keys (an inline `Object.fromEntries` widens to\n * `Record<string, V>` and loses the key union).\n */\nexport function fromKeys<K extends string, V>(keys: readonly K[], value: V): Record<K, V> {\n return Object.fromEntries(keys.map(k => [k, value])) as Record<K, V>;\n}\n\n// ── Reading ─────────────────────────────────────────────────────────────────\n\n/** Anything a URL can be read from: `URLSearchParams`, Next's `searchParams`, or a plain record. */\nexport type ParamInput = URLSearchParams | Record<string, string | string[] | undefined> | null | undefined;\n\n/** THE first-value rule for a duplicated key (`?a=1&a=2` reads as `1`). */\nexport function firstParamValue(input: ParamInput, key: string): string | undefined {\n if (!input) return undefined;\n if (input instanceof URLSearchParams) return input.get(key) ?? undefined;\n const value = input[key];\n return Array.isArray(value) ? value[0] : value;\n}\n\n/** Every value for a key (arrays read repeated params, then a `,`-joined single value). */\nfunction allParamValues(input: ParamInput, key: string): string[] {\n if (!input) return [];\n const raw = input instanceof URLSearchParams ? input.getAll(key) : input[key];\n const list = Array.isArray(raw) ? raw : raw === undefined ? [] : [raw];\n if (list.length === 1 && list[0]?.includes(',')) return list[0].split(',').filter(Boolean);\n return list.filter(v => v !== undefined && v !== '');\n}\n\nexport interface ParseSchemaOptions {\n /**\n * What an ABSENT scalar with no declared `default` resolves to.\n * `'undefined'` (default) keeps today's behaviour; `'null'` is the explicit\n * \"this filter is unset\" spelling that survives JSON and `URLSearchParams`.\n */\n absent?: 'undefined' | 'null';\n}\n\nexport type AbsentValue<Opts extends ParseSchemaOptions | undefined> = Opts extends { absent: 'null' }\n ? null\n : undefined;\n\n/**\n * The parsed shape of a schema: keys with a `default` and every array key are\n * non-nullable; an undefaulted scalar carries the caller's absent value.\n */\nexport type InferParsedParams<TSchema extends ParamSchema, Absent = undefined> = {\n [K in keyof TSchema]: TSchema[K] extends { type: 'array' }\n ? string[]\n : TSchema[K] extends { default: infer D }\n ? D\n : TSchema[K]['type'] extends infer T\n ? T extends JSType\n ? OutputTypeForJSType<T> | Absent\n : never\n : never;\n};\n\nfunction coerceScalar(raw: string | undefined, config: ParamConfig): unknown {\n if (raw === undefined || raw === '') return undefined;\n switch (config.type) {\n case 'int': {\n const int = config;\n // ONE integer grammar, default or not. Without a declared default the\n // fallback is `undefined` (an unparseable value is absent), which\n // `positiveInt` returns untouched — the branch used to open-code the\n // truncation, the min floor and the max clamp a second time.\n return positiveInt<undefined>(raw, int.default as undefined, { min: int.min, max: int.max });\n }\n case 'number': {\n const n = Number(raw);\n return Number.isFinite(n) ? n : undefined;\n }\n case 'boolean':\n return raw === 'true' || raw === '1';\n case 'object':\n try {\n return JSON.parse(raw) as Record<string, unknown>;\n } catch {\n return undefined;\n }\n default:\n return raw;\n }\n}\n\n/**\n * Parse a URL into a schema's shape. THE reader — the server route and the\n * client hook both call this, so a URL means exactly one thing.\n *\n * PRECEDENCE: an absent or empty SCALAR takes the declared `default` when there\n * is one, else the `absent` value. ARRAY keys never take `absent`: they are `[]`.\n */\nexport function parseSchemaParams<TSchema extends ParamSchema, Opts extends ParseSchemaOptions = Record<string, never>>(\n schema: TSchema,\n input: ParamInput,\n options?: Opts,\n): InferParsedParams<TSchema, AbsentValue<Opts>> {\n const absent = options?.absent === 'null' ? null : undefined;\n const out: Record<string, unknown> = {};\n for (const [key, config] of Object.entries(schema)) {\n if (config.type === 'array') {\n const values = allParamValues(input, key);\n out[key] = values.length > 0 ? values : (config.default ?? []);\n continue;\n }\n const coerced = coerceScalar(firstParamValue(input, key), config);\n out[key] = coerced !== undefined ? coerced : (config.default ?? absent);\n }\n return out as InferParsedParams<TSchema, AbsentValue<Opts>>;\n}\n\n// ── Writing ─────────────────────────────────────────────────────────────────\n\n/**\n * A schema entry OR a legacy flattened param (which spells its default\n * `defaultValue`).\n *\n * A UNION, not a bag of two optional fields: with only `default?` and\n * `defaultValue?` this is a WEAK TYPE, and TypeScript rejects any argument\n * sharing neither — including `{ type: 'string' }`, the single most common\n * schema entry there is. Naming the real config shapes fixes that without an\n * index signature, which `FlattenedParam` (an interface) could never satisfy.\n */\ntype OmissionConfig = ParamConfig | { default?: unknown; defaultValue?: unknown };\n\n/**\n * THE omission rule: `null`, `undefined`, `''`, `[]`, and a value equal to the\n * declared default are left out, so a URL only ever carries what differs.\n */\nexport function shouldIncludeInUrl(value: unknown, config: OmissionConfig | undefined): boolean {\n if (value === null || value === undefined) return false;\n if (Array.isArray(value) && value.length === 0) return false;\n if (value === '') return false;\n // `in` narrowing rather than a cast: the union's members genuinely differ —\n // a schema entry spells its default `default`, a flattened param spells it\n // `defaultValue`, and neither is obliged to carry the other's field.\n const fromDefault = config && 'default' in config ? config.default : undefined;\n const fromLegacy = config && 'defaultValue' in config ? config.defaultValue : undefined;\n const declared = fromDefault !== undefined ? fromDefault : fromLegacy;\n if (declared !== undefined && value === declared) return false;\n return true;\n}\n\nexport interface CreateSearchParamsOptions {\n /** Join arrays with this separator instead of repeating the key (`;` for the hub's list APIs). */\n arrayJoin?: string;\n}\n\n/** Build `URLSearchParams` from a value record (arrays repeat the key by default). */\nexport function createSearchParams(\n params: Record<string, unknown>,\n { arrayJoin }: CreateSearchParamsOptions = {},\n): URLSearchParams {\n const search = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n // The emptiness half of the omission rule has ONE owner. (No config here:\n // callers that must also drop schema DEFAULTS filter through\n // `shouldIncludeInUrl` with the config first — see `serializeSchemaParams`.)\n if (!shouldIncludeInUrl(value, undefined)) continue;\n if (Array.isArray(value)) {\n // Empty ELEMENTS are dropped, and an array that filters to nothing is\n // absent (a cleared multi-select must not leave `?tags=` behind).\n const items = value.filter(v => v !== null && v !== undefined && v !== '');\n if (items.length === 0) continue;\n if (arrayJoin) search.set(key, items.join(arrayJoin));\n else for (const item of items) search.append(key, String(item));\n continue;\n }\n if (typeof value === 'object') {\n search.set(key, JSON.stringify(value));\n continue;\n }\n search.set(key, String(value));\n }\n return search;\n}\n\n/** Serialize params to a query string, omitting everything `shouldIncludeInUrl` rejects. */\nexport function serializeSchemaParams<TSchema extends ParamSchema>(\n schema: TSchema,\n params: Record<string, unknown>,\n options?: CreateSearchParamsOptions,\n): string {\n const kept: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(params)) {\n if (shouldIncludeInUrl(value, schema[key])) kept[key] = value;\n }\n return createSearchParams(kept, options).toString();\n}\n\n/** Append a query string to a path, or return the path unchanged when it is empty. */\nexport function withQuery(path: string, queryString: string | null | undefined): string {\n return queryString ? `${path}?${queryString}` : path;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { createColumnHelper } from '@tanstack/react-table';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { DataTable } from '../components/ui/data-table';
|
|
5
|
+
import { useDataTable } from '../components/ui/data-table/use-data-table';
|
|
6
|
+
import { EntityImage } from '../components/ui/entity-image';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `minRows` promises a stable table height. It kept that promise only for a
|
|
10
|
+
* table whose rows are the design height — a table stating its own height got
|
|
11
|
+
* pad rows and a skeleton at the design height instead, so a short page came up
|
|
12
|
+
* short by the difference, per missing row.
|
|
13
|
+
*
|
|
14
|
+
* These lock the one thing that makes the promise true: EVERY row slot a table
|
|
15
|
+
* can draw — a real row, a pad row, a skeleton row — reserves the SAME height.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
interface Row {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const columnHelper = createColumnHelper<Row>();
|
|
24
|
+
const columns = [columnHelper.accessor('name', { id: 'name', header: 'Name' })];
|
|
25
|
+
|
|
26
|
+
const ROW_H = 'h-[112px]';
|
|
27
|
+
|
|
28
|
+
function Table({
|
|
29
|
+
data,
|
|
30
|
+
loading = false,
|
|
31
|
+
rowHeightClassName,
|
|
32
|
+
}: {
|
|
33
|
+
data: Row[];
|
|
34
|
+
loading?: boolean;
|
|
35
|
+
rowHeightClassName?: string;
|
|
36
|
+
}) {
|
|
37
|
+
const table = useDataTable({ data, columns });
|
|
38
|
+
return (
|
|
39
|
+
<DataTable table={table}>
|
|
40
|
+
<DataTable.Body<Row>
|
|
41
|
+
loading={loading}
|
|
42
|
+
minRows={4}
|
|
43
|
+
skeletonRows={4}
|
|
44
|
+
autoHeight
|
|
45
|
+
rowHeightClassName={rowHeightClassName}
|
|
46
|
+
/>
|
|
47
|
+
</DataTable>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The same table, but every row carries a sub-row beneath its cells. */
|
|
52
|
+
function TableWithSubRows({ data, rowHeightClassName }: { data: Row[]; rowHeightClassName?: string }) {
|
|
53
|
+
const table = useDataTable({ data, columns });
|
|
54
|
+
return (
|
|
55
|
+
<DataTable table={table}>
|
|
56
|
+
<DataTable.Body<Row>
|
|
57
|
+
minRows={4}
|
|
58
|
+
skeletonRows={4}
|
|
59
|
+
rowHeightClassName={rowHeightClassName}
|
|
60
|
+
renderSubRow={() => <div style={{ height: 96 }}>sub</div>}
|
|
61
|
+
/>
|
|
62
|
+
</DataTable>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const rows = (n: number): Row[] => Array.from({ length: n }, (_, i) => ({ id: String(i), name: `Row ${i}` }));
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Every explicit height utility in the rendered markup.
|
|
70
|
+
*
|
|
71
|
+
* Read off the markup rather than through a selector, because the height a row
|
|
72
|
+
* slot reserves is exactly what ships in the class attribute — and matching an
|
|
73
|
+
* arbitrary-value class through `querySelectorAll` is its own trap.
|
|
74
|
+
*/
|
|
75
|
+
function heightClasses(container: HTMLElement): string[] {
|
|
76
|
+
return [...container.innerHTML.matchAll(/\b(?:md:)?(?:min-)?h-\[[^\]\s"]+\]/g)].map(m => m[0]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The class list of every row-card slot (real or pad) in the rendered markup. */
|
|
80
|
+
function slotClasses(container: HTMLElement): string[] {
|
|
81
|
+
return [...container.innerHTML.matchAll(/class="([^"]*\brounded-md\b[^"]*)"/g)].map(m => m[1]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe('DataTable row height', () => {
|
|
85
|
+
it('reserves the SAME height for real rows and the pad rows beside them', () => {
|
|
86
|
+
// Two real rows in a table promising four: two pad rows make up the rest.
|
|
87
|
+
const { container } = render(<Table data={rows(2)} rowHeightClassName={ROW_H} />);
|
|
88
|
+
const heights = heightClasses(container);
|
|
89
|
+
|
|
90
|
+
expect(heights.length).toBeGreaterThan(0);
|
|
91
|
+
// Not "most of them" — every single slot, or the table's height moves with
|
|
92
|
+
// how many results the page happens to hold.
|
|
93
|
+
expect(new Set(heights)).toEqual(new Set([ROW_H]));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('reserves that height in the SKELETON too, so loading and loaded agree', () => {
|
|
97
|
+
const { container } = render(<Table data={[]} loading rowHeightClassName={ROW_H} />);
|
|
98
|
+
expect(new Set(heightClasses(container))).toEqual(new Set([ROW_H]));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('reserves it for an EMPTY board, which is when a collapse is most visible', () => {
|
|
102
|
+
const { container } = render(<Table data={[]} rowHeightClassName={ROW_H} />);
|
|
103
|
+
expect(new Set(heightClasses(container))).toEqual(new Set([ROW_H]));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('REPLACES the height rather than merging, so no md: default survives', () => {
|
|
107
|
+
const { container } = render(<Table data={rows(1)} rowHeightClassName={ROW_H} />);
|
|
108
|
+
const html = container.innerHTML;
|
|
109
|
+
// The exact leftover that made a caller-supplied height hold on a phone and
|
|
110
|
+
// lose on a desktop.
|
|
111
|
+
expect(html).not.toContain('md:h-[78px]');
|
|
112
|
+
expect(html).not.toContain('md:min-h-[78px]');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('gives a pad row the same BORDER BOX as the row it stands in for', () => {
|
|
116
|
+
// A real row card draws a 1px border on each side, so its outer block is
|
|
117
|
+
// the inner height plus 2. A pad row without one is 2px shorter — nothing
|
|
118
|
+
// per row, 54px across a 27-row remainder.
|
|
119
|
+
const { container } = render(<Table data={rows(2)} rowHeightClassName={ROW_H} />);
|
|
120
|
+
const slots = slotClasses(container);
|
|
121
|
+
|
|
122
|
+
expect(slots.length).toBeGreaterThanOrEqual(4);
|
|
123
|
+
expect(slots.every(cls => cls.split(/\s+/).includes('border'))).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('sizes the ROW SLOT, so a sub-row does not make real rows outgrow pad rows', () => {
|
|
127
|
+
// The trap: a row with a sub-row is taller than its cells. Sizing the cells
|
|
128
|
+
// left every pad row short by exactly the sub-row — 96px each on a phone
|
|
129
|
+
// board — which is the same jump `minRows` exists to prevent, reintroduced
|
|
130
|
+
// one level down.
|
|
131
|
+
const { container } = render(<TableWithSubRows data={rows(2)} rowHeightClassName={ROW_H} />);
|
|
132
|
+
const slots = slotClasses(container);
|
|
133
|
+
|
|
134
|
+
expect(slots.length).toBeGreaterThanOrEqual(4);
|
|
135
|
+
// Every card — the two real rows and the two pad rows — carries the height.
|
|
136
|
+
expect(slots.every(cls => cls.split(/\s+/).includes(ROW_H))).toBe(true);
|
|
137
|
+
// And the cells inside a real row FILL it rather than restating it, or the
|
|
138
|
+
// card would be the cells plus the sub-row.
|
|
139
|
+
expect(container.innerHTML).toContain('flex-1');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('keeps the design height when a table does not state one', () => {
|
|
143
|
+
const { container } = render(<Table data={rows(1)} />);
|
|
144
|
+
expect(container.innerHTML).toContain('md:h-[78px]');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('EntityImage size', () => {
|
|
149
|
+
it('SUBSTITUTES the default size, leaving no responsive half behind', () => {
|
|
150
|
+
const { container } = render(<EntityImage src="https://example.test/a.png" alt="A" sizeClassName="size-6" />);
|
|
151
|
+
const html = container.innerHTML;
|
|
152
|
+
expect(html).toContain('size-6');
|
|
153
|
+
// `h-4 w-6` through className used to leave this behind, which is the whole
|
|
154
|
+
// bug: 16px on a phone, 60px on a desktop.
|
|
155
|
+
expect(html).not.toContain('md:size-[60px]');
|
|
156
|
+
expect(html).not.toContain('size-[52px]');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('applies the same substitution to the initials fallback', () => {
|
|
160
|
+
const { container } = render(<EntityImage src={null} alt="New York" sizeClassName="size-6" />);
|
|
161
|
+
const html = container.innerHTML;
|
|
162
|
+
expect(html).toContain('size-6');
|
|
163
|
+
expect(html).not.toContain('md:size-[60px]');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('keeps the design size when a caller does not state one', () => {
|
|
167
|
+
const { container } = render(<EntityImage src={null} alt="New York" />);
|
|
168
|
+
expect(container.innerHTML).toContain('md:size-[60px]');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -131,7 +131,7 @@ describe('common leaf', () => {
|
|
|
131
131
|
expect(clamp(5, 1, 3)).toBe(3);
|
|
132
132
|
expect(clamp(-5, 1, 3)).toBe(1);
|
|
133
133
|
expect(pick({ a: 1, b: 2, c: 3 }, ['a', 'c'])).toEqual({ a: 1, c: 3 });
|
|
134
|
-
expect(pick({ a: undefined }
|
|
134
|
+
expect(pick({ a: undefined }, ['a'])).toEqual({ a: undefined });
|
|
135
135
|
});
|
|
136
136
|
it('states the no-client-cache rule once', () => {
|
|
137
137
|
expect(NO_CLIENT_CACHE).toEqual({ staleTime: 0 });
|
|
@@ -30,6 +30,23 @@ export interface DataTableBodyProps<T = unknown> {
|
|
|
30
30
|
* the row instead of clipping. Default keeps the fixed height.
|
|
31
31
|
*/
|
|
32
32
|
autoHeight?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* REPLACES the design row height for THIS table's rows, its pad rows and its
|
|
35
|
+
* skeleton alike — one number, so they cannot disagree.
|
|
36
|
+
*
|
|
37
|
+
* Why a prop and not a className: `minRows` promises a stable table height,
|
|
38
|
+
* but its pad rows and the skeleton were hard-coded to the design height
|
|
39
|
+
* while a row passing `autoHeight` renders as tall as its content. A table
|
|
40
|
+
* with 112px rows padded a short page with 78px placeholders and came up
|
|
41
|
+
* 34px per missing row too short — on a 15-row page with 5 results, 340px of
|
|
42
|
+
* jump against every other page. Appending a height through `rowClassName`
|
|
43
|
+
* cannot fix it either: tailwind-merge drops the plain `h-[66px]` but keeps
|
|
44
|
+
* the `md:h-[78px]` beside it, so the override holds on a phone and loses on
|
|
45
|
+
* a desktop. Pass it here and every row slot in the table agrees.
|
|
46
|
+
*
|
|
47
|
+
* Responsive values belong in the string itself, e.g. `'h-[200px] md:h-[112px]'`.
|
|
48
|
+
*/
|
|
49
|
+
rowHeightClassName?: string;
|
|
33
50
|
/**
|
|
34
51
|
* Click anywhere on a row (except elements with `data-no-row-click`). Prefer
|
|
35
52
|
* `useCallback` to avoid breaking `React.memo` on rows.
|
|
@@ -68,6 +85,7 @@ export function DataTableBody<T = unknown>({
|
|
|
68
85
|
rowClassName,
|
|
69
86
|
compact,
|
|
70
87
|
autoHeight,
|
|
88
|
+
rowHeightClassName,
|
|
71
89
|
onRowClick,
|
|
72
90
|
rowHref,
|
|
73
91
|
minRows,
|
|
@@ -79,7 +97,7 @@ export function DataTableBody<T = unknown>({
|
|
|
79
97
|
if (loading && rows.length === 0) {
|
|
80
98
|
return (
|
|
81
99
|
<div className={cn('flex w-full flex-col gap-[var(--spacing-system-xsf)]', className)}>
|
|
82
|
-
<DataTableSkeleton rows={skeletonRows} />
|
|
100
|
+
<DataTableSkeleton rows={skeletonRows} rowHeightClassName={rowHeightClassName} />
|
|
83
101
|
</div>
|
|
84
102
|
);
|
|
85
103
|
}
|
|
@@ -100,7 +118,7 @@ export function DataTableBody<T = unknown>({
|
|
|
100
118
|
if (minRows) {
|
|
101
119
|
return (
|
|
102
120
|
<div className={cn('relative flex w-full flex-col gap-[var(--spacing-system-xsf)]', className)}>
|
|
103
|
-
<PlaceholderRows count={minRows} />
|
|
121
|
+
<PlaceholderRows count={minRows} rowHeightClassName={rowHeightClassName} />
|
|
104
122
|
<div className="absolute inset-0 flex items-center justify-center">{empty}</div>
|
|
105
123
|
</div>
|
|
106
124
|
);
|
|
@@ -125,12 +143,13 @@ export function DataTableBody<T = unknown>({
|
|
|
125
143
|
href={href}
|
|
126
144
|
compact={compact}
|
|
127
145
|
autoHeight={autoHeight}
|
|
146
|
+
rowHeightClassName={rowHeightClassName}
|
|
128
147
|
className={cls}
|
|
129
148
|
subRow={renderSubRow?.(item)}
|
|
130
149
|
/>
|
|
131
150
|
);
|
|
132
151
|
})}
|
|
133
|
-
{padCount > 0 && <PlaceholderRows count={padCount} />}
|
|
152
|
+
{padCount > 0 && <PlaceholderRows count={padCount} rowHeightClassName={rowHeightClassName} />}
|
|
134
153
|
</div>
|
|
135
154
|
);
|
|
136
155
|
}
|
|
@@ -142,17 +161,37 @@ export function DataTableBody<T = unknown>({
|
|
|
142
161
|
* sites use it, so a padded page and an empty one are the same height by
|
|
143
162
|
* construction rather than by two matching guesses.
|
|
144
163
|
*/
|
|
145
|
-
function PlaceholderRows({ count }: { count: number }) {
|
|
164
|
+
function PlaceholderRows({ count, rowHeightClassName }: { count: number; rowHeightClassName?: string }) {
|
|
146
165
|
return (
|
|
147
166
|
<>
|
|
148
167
|
{Array.from({ length: count }).map((_, i) => (
|
|
149
168
|
<div
|
|
150
169
|
key={`placeholder-${i}`}
|
|
151
|
-
|
|
170
|
+
// `border-transparent`, not no border: a real row card draws a 1px
|
|
171
|
+
// border on each side, so its outer block is the inner height PLUS 2.
|
|
172
|
+
// A pad row without one is 2px shorter than the row it stands in for
|
|
173
|
+
// — invisible per row, 54px over a 27-row remainder, which is the
|
|
174
|
+
// same shift `minRows` exists to prevent.
|
|
175
|
+
className={cn(
|
|
176
|
+
'pointer-events-none relative overflow-hidden rounded-md border border-transparent',
|
|
177
|
+
// The stated slot height belongs on the CARD, exactly as a real
|
|
178
|
+
// row carries it — a real row with a sub-row is taller than its
|
|
179
|
+
// cells, so reserving the cells' height here left every pad row
|
|
180
|
+
// short by the sub-row.
|
|
181
|
+
rowHeightClassName,
|
|
182
|
+
)}
|
|
152
183
|
aria-hidden="true"
|
|
153
184
|
>
|
|
154
|
-
<div
|
|
155
|
-
|
|
185
|
+
<div
|
|
186
|
+
className={cn('hidden py-0 md:flex', ROW_SHELL_CLASSES, rowHeightClassName ? 'h-full' : ROW_HEIGHT_DESKTOP)}
|
|
187
|
+
/>
|
|
188
|
+
<div
|
|
189
|
+
className={cn(
|
|
190
|
+
'flex justify-start py-0 md:hidden',
|
|
191
|
+
ROW_SHELL_CLASSES,
|
|
192
|
+
rowHeightClassName ? 'h-full' : ROW_HEIGHT_MOBILE,
|
|
193
|
+
)}
|
|
194
|
+
/>
|
|
156
195
|
</div>
|
|
157
196
|
))}
|
|
158
197
|
</>
|
|
@@ -20,6 +20,18 @@ export interface DataTableRowProps<T> {
|
|
|
20
20
|
* clipped. Default keeps the fixed height.
|
|
21
21
|
*/
|
|
22
22
|
autoHeight?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* REPLACES the height this row would otherwise take (`compact`, `autoHeight`
|
|
25
|
+
* or the design default). The one place a table states how tall a row is, so
|
|
26
|
+
* the pad rows and the skeleton can reserve the SAME number — see
|
|
27
|
+
* `DataTableBodyProps.rowHeightClassName`.
|
|
28
|
+
*
|
|
29
|
+
* It sizes the row SLOT — the whole card — not the cells inside it. A row
|
|
30
|
+
* with a `subRow` is taller than its cells, so sizing the cells would leave
|
|
31
|
+
* the pad rows (which have no sub-row) short by exactly the sub-row: on a
|
|
32
|
+
* phone board that was 96px per missing row.
|
|
33
|
+
*/
|
|
34
|
+
rowHeightClassName?: string;
|
|
23
35
|
className?: string;
|
|
24
36
|
/** Expandable content rendered below the cells, inside the same card. */
|
|
25
37
|
subRow?: ReactNode;
|
|
@@ -62,7 +74,16 @@ export interface DataTableRowProps<T> {
|
|
|
62
74
|
* for `onClick` and `className` (if function) via `useCallback` / `useMemo` in
|
|
63
75
|
* the consumer. `href` and string `className` are compared by value.
|
|
64
76
|
*/
|
|
65
|
-
function DataTableRowImpl<T>({
|
|
77
|
+
function DataTableRowImpl<T>({
|
|
78
|
+
row,
|
|
79
|
+
onClick,
|
|
80
|
+
href,
|
|
81
|
+
compact,
|
|
82
|
+
autoHeight,
|
|
83
|
+
rowHeightClassName,
|
|
84
|
+
className,
|
|
85
|
+
subRow,
|
|
86
|
+
}: DataTableRowProps<T>) {
|
|
66
87
|
const hasSubRow = subRow != null && subRow !== false;
|
|
67
88
|
// A sub-row carries its own interactive controls, so it must not live inside the
|
|
68
89
|
// row-level <Link>; when present, the link wraps only the cells.
|
|
@@ -90,7 +111,11 @@ function DataTableRowImpl<T>({ row, onClick, href, compact, autoHeight, classNam
|
|
|
90
111
|
);
|
|
91
112
|
|
|
92
113
|
const containerClassName = cn(
|
|
93
|
-
'
|
|
114
|
+
'overflow-hidden rounded-md border border-ods-border bg-ods-card text-inherit no-underline',
|
|
115
|
+
// A stated slot height goes HERE, on the card, with the cells flexing to
|
|
116
|
+
// fill whatever the sub-row leaves. `border-box` sizing means the number a
|
|
117
|
+
// caller writes is the number the row occupies, borders included.
|
|
118
|
+
rowHeightClassName ? `flex flex-col ${rowHeightClassName}` : 'block',
|
|
94
119
|
// With a sub-row the link wraps only the cells, so keep the clickable affordance off the whole card.
|
|
95
120
|
(onClick || isWholeCardLink) && 'cursor-pointer transition-colors hover:bg-ods-bg-active',
|
|
96
121
|
className,
|
|
@@ -101,11 +126,13 @@ function DataTableRowImpl<T>({ row, onClick, href, compact, autoHeight, classNam
|
|
|
101
126
|
className={cn(
|
|
102
127
|
'flex',
|
|
103
128
|
ROW_SHELL_CLASSES,
|
|
104
|
-
|
|
105
|
-
? '
|
|
106
|
-
:
|
|
107
|
-
? '
|
|
108
|
-
:
|
|
129
|
+
rowHeightClassName
|
|
130
|
+
? 'min-h-0 flex-1 py-0'
|
|
131
|
+
: compact
|
|
132
|
+
? 'py-[var(--spacing-system-xsf)]'
|
|
133
|
+
: autoHeight
|
|
134
|
+
? 'min-h-[66px] py-[var(--spacing-system-sf)] md:min-h-[78px]'
|
|
135
|
+
: `py-0 ${ROW_HEIGHT_DESKTOP}`,
|
|
109
136
|
hasSubRow && 'border-b border-ods-border',
|
|
110
137
|
)}
|
|
111
138
|
>
|
|
@@ -153,7 +180,10 @@ function DataTableRowImpl<T>({ row, onClick, href, compact, autoHeight, classNam
|
|
|
153
180
|
<Link
|
|
154
181
|
href={href}
|
|
155
182
|
prefetch={false}
|
|
156
|
-
className=
|
|
183
|
+
className={cn(
|
|
184
|
+
'cursor-pointer text-inherit no-underline transition-colors hover:bg-ods-bg-active',
|
|
185
|
+
rowHeightClassName ? 'flex min-h-0 flex-1 flex-col' : 'block',
|
|
186
|
+
)}
|
|
157
187
|
onClick={handleClick}
|
|
158
188
|
>
|
|
159
189
|
{cells}
|
|
@@ -29,9 +29,17 @@ export interface DataTableSkeletonProps {
|
|
|
29
29
|
rows?: number;
|
|
30
30
|
className?: string;
|
|
31
31
|
rowClassName?: string;
|
|
32
|
+
/**
|
|
33
|
+
* REPLACES the design row height, for a table whose rows are not that tall.
|
|
34
|
+
* Sizes the CARD, as a real row does.
|
|
35
|
+
* Appending a height through `rowClassName` cannot do this: tailwind-merge
|
|
36
|
+
* drops the plain `h-[66px]` but keeps `md:h-[78px]`, so the override held
|
|
37
|
+
* on a phone and lost on a desktop. See `DataTableBodyProps.rowHeightClassName`.
|
|
38
|
+
*/
|
|
39
|
+
rowHeightClassName?: string;
|
|
32
40
|
}
|
|
33
41
|
|
|
34
|
-
export function DataTableSkeleton({ rows = 10, className, rowClassName }: DataTableSkeletonProps) {
|
|
42
|
+
export function DataTableSkeleton({ rows = 10, className, rowClassName, rowHeightClassName }: DataTableSkeletonProps) {
|
|
35
43
|
const table = useDataTableContext();
|
|
36
44
|
const columns = table.getVisibleFlatColumns();
|
|
37
45
|
const firstColumnId = columns[0]?.id;
|
|
@@ -43,10 +51,19 @@ export function DataTableSkeleton({ rows = 10, className, rowClassName }: DataTa
|
|
|
43
51
|
key={index}
|
|
44
52
|
className={cn(
|
|
45
53
|
'relative animate-pulse overflow-hidden rounded-md border border-ods-border bg-ods-card',
|
|
54
|
+
// The slot height sits on the card, matching a real row.
|
|
55
|
+
rowHeightClassName,
|
|
46
56
|
className,
|
|
47
57
|
)}
|
|
48
58
|
>
|
|
49
|
-
<div
|
|
59
|
+
<div
|
|
60
|
+
className={cn(
|
|
61
|
+
'hidden py-0 md:flex',
|
|
62
|
+
ROW_SHELL_CLASSES,
|
|
63
|
+
rowHeightClassName ? 'h-full' : ROW_HEIGHT_DESKTOP,
|
|
64
|
+
rowClassName,
|
|
65
|
+
)}
|
|
66
|
+
>
|
|
50
67
|
{columns.map(column => {
|
|
51
68
|
const meta = column.columnDef.meta;
|
|
52
69
|
return (
|
|
@@ -71,7 +88,14 @@ export function DataTableSkeleton({ rows = 10, className, rowClassName }: DataTa
|
|
|
71
88
|
);
|
|
72
89
|
})}
|
|
73
90
|
</div>
|
|
74
|
-
<div
|
|
91
|
+
<div
|
|
92
|
+
className={cn(
|
|
93
|
+
'flex justify-start py-0 md:hidden',
|
|
94
|
+
ROW_SHELL_CLASSES,
|
|
95
|
+
rowHeightClassName ? 'h-full' : ROW_HEIGHT_MOBILE,
|
|
96
|
+
rowClassName,
|
|
97
|
+
)}
|
|
98
|
+
>
|
|
75
99
|
<div className="flex min-w-0 flex-1 flex-col justify-center py-[var(--spacing-system-sf)]">
|
|
76
100
|
<div className="mb-[var(--spacing-system-xsf)] h-4 w-3/4 rounded-sm bg-ods-bg-surface" />
|
|
77
101
|
<div className="h-3 w-1/2 rounded-sm bg-ods-bg-surface opacity-60" />
|