@flamingo-stack/openframe-frontend-core 0.0.591 → 0.0.592

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 (142) hide show
  1. package/dist/{chunk-BQE4HTDR.js → chunk-2JNCZDJR.js} +2 -2
  2. package/dist/{chunk-YEKPHSWC.cjs → chunk-3JOBM57M.cjs} +22 -22
  3. package/dist/{chunk-YEKPHSWC.cjs.map → chunk-3JOBM57M.cjs.map} +1 -1
  4. package/dist/{chunk-WKOJL6A2.cjs → chunk-5LJZWTOE.cjs} +10 -10
  5. package/dist/{chunk-WKOJL6A2.cjs.map → chunk-5LJZWTOE.cjs.map} +1 -1
  6. package/dist/{chunk-NXYOUBOL.cjs → chunk-5MHDMDPX.cjs} +21 -21
  7. package/dist/{chunk-NXYOUBOL.cjs.map → chunk-5MHDMDPX.cjs.map} +1 -1
  8. package/dist/{chunk-U2XRBHBX.js → chunk-6GMMR667.js} +9 -9
  9. package/dist/{chunk-WIMGAPVH.js → chunk-ASFEUMFW.js} +4 -4
  10. package/dist/{chunk-4UP2SGSO.js → chunk-AUA6OSU7.js} +3 -3
  11. package/dist/{chunk-YMAHVHCX.cjs → chunk-BZEWPW5A.cjs} +3 -3
  12. package/dist/{chunk-YMAHVHCX.cjs.map → chunk-BZEWPW5A.cjs.map} +1 -1
  13. package/dist/{chunk-SW3CZ2KS.cjs → chunk-COY57SZP.cjs} +129 -84
  14. package/dist/chunk-COY57SZP.cjs.map +1 -0
  15. package/dist/{chunk-VGBTCUPG.js → chunk-CRW4WRMH.js} +2 -2
  16. package/dist/{chunk-E455M3WJ.js → chunk-F543W3F2.js} +4 -4
  17. package/dist/{chunk-ZZF6Z7KB.cjs → chunk-FKXDDJEQ.cjs} +11 -11
  18. package/dist/{chunk-ZZF6Z7KB.cjs.map → chunk-FKXDDJEQ.cjs.map} +1 -1
  19. package/dist/{chunk-RGCBUNMJ.js → chunk-FSCQOWXI.js} +2 -2
  20. package/dist/{chunk-USDG6IWK.cjs → chunk-FVADHGDP.cjs} +7 -7
  21. package/dist/{chunk-USDG6IWK.cjs.map → chunk-FVADHGDP.cjs.map} +1 -1
  22. package/dist/{chunk-OKWGSJAI.js → chunk-GGQZBSFN.js} +1 -1
  23. package/dist/chunk-GGQZBSFN.js.map +1 -0
  24. package/dist/{chunk-RNXY35DZ.cjs → chunk-H64ZHSYR.cjs} +1 -1
  25. package/dist/chunk-H64ZHSYR.cjs.map +1 -0
  26. package/dist/{chunk-OS72RV57.cjs → chunk-HGEA6PW7.cjs} +3 -3
  27. package/dist/{chunk-OS72RV57.cjs.map → chunk-HGEA6PW7.cjs.map} +1 -1
  28. package/dist/{chunk-ZFHIMK4W.js → chunk-JIINHJIT.js} +5 -5
  29. package/dist/{chunk-CWO3YFVZ.js → chunk-LRQCKSZC.js} +2 -2
  30. package/dist/{chunk-LL6J6N76.js → chunk-LXD2QJFG.js} +3 -3
  31. package/dist/{chunk-2TJSDSXW.js → chunk-MOHSA65M.js} +2 -2
  32. package/dist/{chunk-JHVCH3CK.cjs → chunk-NEFNARFF.cjs} +6 -6
  33. package/dist/{chunk-JHVCH3CK.cjs.map → chunk-NEFNARFF.cjs.map} +1 -1
  34. package/dist/{chunk-XK7SLEZB.cjs → chunk-NICIJRZ4.cjs} +3 -3
  35. package/dist/{chunk-XK7SLEZB.cjs.map → chunk-NICIJRZ4.cjs.map} +1 -1
  36. package/dist/{chunk-LUWFLC3T.js → chunk-NYH3NVUF.js} +3 -3
  37. package/dist/{chunk-BOED5ISE.js → chunk-NZQYZKIN.js} +6 -6
  38. package/dist/{chunk-AZ4A4RBL.cjs → chunk-O4VGEJ7I.cjs} +60 -60
  39. package/dist/{chunk-AZ4A4RBL.cjs.map → chunk-O4VGEJ7I.cjs.map} +1 -1
  40. package/dist/{chunk-B7ZCBGUN.cjs → chunk-O4VZOA2Q.cjs} +3 -3
  41. package/dist/{chunk-B7ZCBGUN.cjs.map → chunk-O4VZOA2Q.cjs.map} +1 -1
  42. package/dist/{chunk-QVAKJB64.js → chunk-OO6AMBHV.js} +2 -2
  43. package/dist/{chunk-U3V4TPT5.js → chunk-R25AUM2R.js} +3 -3
  44. package/dist/{chunk-VGY7D6LG.cjs → chunk-STYNZ4QM.cjs} +3 -3
  45. package/dist/{chunk-VGY7D6LG.cjs.map → chunk-STYNZ4QM.cjs.map} +1 -1
  46. package/dist/{chunk-FHKP4H6N.cjs → chunk-TQMZFXWZ.cjs} +5 -5
  47. package/dist/{chunk-FHKP4H6N.cjs.map → chunk-TQMZFXWZ.cjs.map} +1 -1
  48. package/dist/{chunk-AMCARR6Y.cjs → chunk-V7RUX5CX.cjs} +15 -15
  49. package/dist/{chunk-AMCARR6Y.cjs.map → chunk-V7RUX5CX.cjs.map} +1 -1
  50. package/dist/{chunk-CXILZQFT.cjs → chunk-W2RUBABW.cjs} +19 -19
  51. package/dist/{chunk-CXILZQFT.cjs.map → chunk-W2RUBABW.cjs.map} +1 -1
  52. package/dist/{chunk-DKVNW2QD.cjs → chunk-WOGLPNK3.cjs} +3 -3
  53. package/dist/{chunk-DKVNW2QD.cjs.map → chunk-WOGLPNK3.cjs.map} +1 -1
  54. package/dist/{chunk-HV6MO4HG.cjs → chunk-WP3AVAMP.cjs} +37 -37
  55. package/dist/{chunk-HV6MO4HG.cjs.map → chunk-WP3AVAMP.cjs.map} +1 -1
  56. package/dist/{chunk-ZYA6274A.js → chunk-WVHQQ3NY.js} +87 -42
  57. package/dist/chunk-WVHQQ3NY.js.map +1 -0
  58. package/dist/{chunk-WMZ5AUTB.js → chunk-WZJOILPU.js} +2 -2
  59. package/dist/{chunk-ALTSOKHP.js → chunk-XHDBU5EI.js} +2 -2
  60. package/dist/{chunk-T6RY2MC6.js → chunk-XNGE5EE5.js} +8 -8
  61. package/dist/{chunk-5SJSG5RV.cjs → chunk-YBENO4XG.cjs} +8 -8
  62. package/dist/{chunk-5SJSG5RV.cjs.map → chunk-YBENO4XG.cjs.map} +1 -1
  63. package/dist/{chunk-OBJEFMOI.cjs → chunk-ZT2Z2BYH.cjs} +66 -66
  64. package/dist/{chunk-OBJEFMOI.cjs.map → chunk-ZT2Z2BYH.cjs.map} +1 -1
  65. package/dist/{chunk-BATTVHZT.js → chunk-ZZHHKE2Q.js} +3 -3
  66. package/dist/components/case-studies/index.cjs +10 -10
  67. package/dist/components/case-studies/index.js +4 -4
  68. package/dist/components/contact/index.cjs +5 -5
  69. package/dist/components/contact/index.js +4 -4
  70. package/dist/components/docs/index.cjs +8 -8
  71. package/dist/components/docs/index.js +7 -7
  72. package/dist/components/embeds/index.cjs +5 -5
  73. package/dist/components/embeds/index.js +4 -4
  74. package/dist/components/faq/index.cjs +6 -6
  75. package/dist/components/faq/index.js +5 -5
  76. package/dist/components/features/index.cjs +4 -4
  77. package/dist/components/features/index.js +3 -3
  78. package/dist/components/help-center-pages/index.cjs +27 -27
  79. package/dist/components/help-center-pages/index.js +18 -18
  80. package/dist/components/index.cjs +40 -40
  81. package/dist/components/index.js +18 -18
  82. package/dist/components/meeting-scheduler/index.cjs +31 -31
  83. package/dist/components/meeting-scheduler/index.js +5 -5
  84. package/dist/components/navigation/index.cjs +7 -7
  85. package/dist/components/navigation/index.js +6 -6
  86. package/dist/components/onboarding-guides/index.cjs +8 -8
  87. package/dist/components/onboarding-guides/index.js +7 -7
  88. package/dist/components/related-content/index.cjs +4 -4
  89. package/dist/components/related-content/index.js +3 -3
  90. package/dist/components/tickets/index.cjs +9 -9
  91. package/dist/components/tickets/index.js +8 -8
  92. package/dist/components/ui/data-table/data-table-body.d.ts +18 -1
  93. package/dist/components/ui/data-table/data-table-body.d.ts.map +1 -1
  94. package/dist/components/ui/data-table/data-table-row.d.ts +8 -1
  95. package/dist/components/ui/data-table/data-table-row.d.ts.map +1 -1
  96. package/dist/components/ui/data-table/data-table-skeleton.d.ts +8 -1
  97. package/dist/components/ui/data-table/data-table-skeleton.d.ts.map +1 -1
  98. package/dist/components/ui/data-table/index.d.ts +1 -1
  99. package/dist/components/ui/entity-image.d.ts +12 -1
  100. package/dist/components/ui/entity-image.d.ts.map +1 -1
  101. package/dist/components/ui/index.cjs +4 -4
  102. package/dist/components/ui/index.js +3 -3
  103. package/dist/hooks/index.cjs +3 -3
  104. package/dist/hooks/index.js +2 -2
  105. package/dist/index.cjs +8 -8
  106. package/dist/index.js +7 -7
  107. package/dist/utils/index.cjs.map +1 -1
  108. package/dist/utils/index.js.map +1 -1
  109. package/dist/utils/search-params.cjs.map +1 -1
  110. package/dist/utils/search-params.js.map +1 -1
  111. package/package.json +1 -1
  112. package/src/__tests__/data-table-row-height.test.tsx +139 -0
  113. package/src/__tests__/mlg-platform-leaves.test.ts +1 -1
  114. package/src/components/ui/data-table/data-table-body.tsx +37 -7
  115. package/src/components/ui/data-table/data-table-row.tsx +24 -6
  116. package/src/components/ui/data-table/data-table-skeleton.tsx +24 -3
  117. package/src/components/ui/entity-image.tsx +19 -3
  118. package/src/hooks/state/__tests__/use-api-params.test.ts +1 -1
  119. package/src/utils/search-params.ts +4 -4
  120. package/dist/chunk-OKWGSJAI.js.map +0 -1
  121. package/dist/chunk-RNXY35DZ.cjs.map +0 -1
  122. package/dist/chunk-SW3CZ2KS.cjs.map +0 -1
  123. package/dist/chunk-ZYA6274A.js.map +0 -1
  124. /package/dist/{chunk-BQE4HTDR.js.map → chunk-2JNCZDJR.js.map} +0 -0
  125. /package/dist/{chunk-U2XRBHBX.js.map → chunk-6GMMR667.js.map} +0 -0
  126. /package/dist/{chunk-WIMGAPVH.js.map → chunk-ASFEUMFW.js.map} +0 -0
  127. /package/dist/{chunk-4UP2SGSO.js.map → chunk-AUA6OSU7.js.map} +0 -0
  128. /package/dist/{chunk-VGBTCUPG.js.map → chunk-CRW4WRMH.js.map} +0 -0
  129. /package/dist/{chunk-E455M3WJ.js.map → chunk-F543W3F2.js.map} +0 -0
  130. /package/dist/{chunk-RGCBUNMJ.js.map → chunk-FSCQOWXI.js.map} +0 -0
  131. /package/dist/{chunk-ZFHIMK4W.js.map → chunk-JIINHJIT.js.map} +0 -0
  132. /package/dist/{chunk-CWO3YFVZ.js.map → chunk-LRQCKSZC.js.map} +0 -0
  133. /package/dist/{chunk-LL6J6N76.js.map → chunk-LXD2QJFG.js.map} +0 -0
  134. /package/dist/{chunk-2TJSDSXW.js.map → chunk-MOHSA65M.js.map} +0 -0
  135. /package/dist/{chunk-LUWFLC3T.js.map → chunk-NYH3NVUF.js.map} +0 -0
  136. /package/dist/{chunk-BOED5ISE.js.map → chunk-NZQYZKIN.js.map} +0 -0
  137. /package/dist/{chunk-QVAKJB64.js.map → chunk-OO6AMBHV.js.map} +0 -0
  138. /package/dist/{chunk-U3V4TPT5.js.map → chunk-R25AUM2R.js.map} +0 -0
  139. /package/dist/{chunk-WMZ5AUTB.js.map → chunk-WZJOILPU.js.map} +0 -0
  140. /package/dist/{chunk-ALTSOKHP.js.map → chunk-XHDBU5EI.js.map} +0 -0
  141. /package/dist/{chunk-T6RY2MC6.js.map → chunk-XNGE5EE5.js.map} +0 -0
  142. /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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.591",
3
+ "version": "0.0.592",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,139 @@
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
+ const rows = (n: number): Row[] => Array.from({ length: n }, (_, i) => ({ id: String(i), name: `Row ${i}` }));
52
+
53
+ /**
54
+ * Every explicit height utility in the rendered markup.
55
+ *
56
+ * Read off the markup rather than through a selector, because the height a row
57
+ * slot reserves is exactly what ships in the class attribute — and matching an
58
+ * arbitrary-value class through `querySelectorAll` is its own trap.
59
+ */
60
+ function heightClasses(container: HTMLElement): string[] {
61
+ return [...container.innerHTML.matchAll(/\b(?:md:)?(?:min-)?h-\[[^\]\s"]+\]/g)].map(m => m[0]);
62
+ }
63
+
64
+ /** The class list of every row-card slot (real or pad) in the rendered markup. */
65
+ function slotClasses(container: HTMLElement): string[] {
66
+ return [...container.innerHTML.matchAll(/class="([^"]*\brounded-md\b[^"]*)"/g)].map(m => m[1]);
67
+ }
68
+
69
+ describe('DataTable row height', () => {
70
+ it('reserves the SAME height for real rows and the pad rows beside them', () => {
71
+ // Two real rows in a table promising four: two pad rows make up the rest.
72
+ const { container } = render(<Table data={rows(2)} rowHeightClassName={ROW_H} />);
73
+ const heights = heightClasses(container);
74
+
75
+ expect(heights.length).toBeGreaterThan(0);
76
+ // Not "most of them" — every single slot, or the table's height moves with
77
+ // how many results the page happens to hold.
78
+ expect(new Set(heights)).toEqual(new Set([ROW_H]));
79
+ });
80
+
81
+ it('reserves that height in the SKELETON too, so loading and loaded agree', () => {
82
+ const { container } = render(<Table data={[]} loading rowHeightClassName={ROW_H} />);
83
+ expect(new Set(heightClasses(container))).toEqual(new Set([ROW_H]));
84
+ });
85
+
86
+ it('reserves it for an EMPTY board, which is when a collapse is most visible', () => {
87
+ const { container } = render(<Table data={[]} rowHeightClassName={ROW_H} />);
88
+ expect(new Set(heightClasses(container))).toEqual(new Set([ROW_H]));
89
+ });
90
+
91
+ it('REPLACES the height rather than merging, so no md: default survives', () => {
92
+ const { container } = render(<Table data={rows(1)} rowHeightClassName={ROW_H} />);
93
+ const html = container.innerHTML;
94
+ // The exact leftover that made a caller-supplied height hold on a phone and
95
+ // lose on a desktop.
96
+ expect(html).not.toContain('md:h-[78px]');
97
+ expect(html).not.toContain('md:min-h-[78px]');
98
+ });
99
+
100
+ it('gives a pad row the same BORDER BOX as the row it stands in for', () => {
101
+ // A real row card draws a 1px border on each side, so its outer block is
102
+ // the inner height plus 2. A pad row without one is 2px shorter — nothing
103
+ // per row, 54px across a 27-row remainder.
104
+ const { container } = render(<Table data={rows(2)} rowHeightClassName={ROW_H} />);
105
+ const slots = slotClasses(container);
106
+
107
+ expect(slots.length).toBeGreaterThanOrEqual(4);
108
+ expect(slots.every(cls => cls.split(/\s+/).includes('border'))).toBe(true);
109
+ });
110
+
111
+ it('keeps the design height when a table does not state one', () => {
112
+ const { container } = render(<Table data={rows(1)} />);
113
+ expect(container.innerHTML).toContain('md:h-[78px]');
114
+ });
115
+ });
116
+
117
+ describe('EntityImage size', () => {
118
+ it('SUBSTITUTES the default size, leaving no responsive half behind', () => {
119
+ const { container } = render(<EntityImage src="https://example.test/a.png" alt="A" sizeClassName="size-6" />);
120
+ const html = container.innerHTML;
121
+ expect(html).toContain('size-6');
122
+ // `h-4 w-6` through className used to leave this behind, which is the whole
123
+ // bug: 16px on a phone, 60px on a desktop.
124
+ expect(html).not.toContain('md:size-[60px]');
125
+ expect(html).not.toContain('size-[52px]');
126
+ });
127
+
128
+ it('applies the same substitution to the initials fallback', () => {
129
+ const { container } = render(<EntityImage src={null} alt="New York" sizeClassName="size-6" />);
130
+ const html = container.innerHTML;
131
+ expect(html).toContain('size-6');
132
+ expect(html).not.toContain('md:size-[60px]');
133
+ });
134
+
135
+ it('keeps the design size when a caller does not state one', () => {
136
+ const { container } = render(<EntityImage src={null} alt="New York" />);
137
+ expect(container.innerHTML).toContain('md:size-[60px]');
138
+ });
139
+ });
@@ -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 } as { a: number | undefined }, ['a'])).toEqual({ 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,28 @@ 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
- className="pointer-events-none relative overflow-hidden rounded-md"
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="pointer-events-none relative overflow-hidden rounded-md border border-transparent"
152
176
  aria-hidden="true"
153
177
  >
154
- <div className={cn('hidden py-0 md:flex', ROW_SHELL_CLASSES, ROW_HEIGHT_DESKTOP)} />
155
- <div className={cn('flex justify-start py-0 md:hidden', ROW_SHELL_CLASSES, ROW_HEIGHT_MOBILE)} />
178
+ <div className={cn('hidden py-0 md:flex', ROW_SHELL_CLASSES, rowHeightClassName ?? ROW_HEIGHT_DESKTOP)} />
179
+ <div
180
+ className={cn(
181
+ 'flex justify-start py-0 md:hidden',
182
+ ROW_SHELL_CLASSES,
183
+ rowHeightClassName ?? ROW_HEIGHT_MOBILE,
184
+ )}
185
+ />
156
186
  </div>
157
187
  ))}
158
188
  </>
@@ -20,6 +20,13 @@ 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
+ rowHeightClassName?: string;
23
30
  className?: string;
24
31
  /** Expandable content rendered below the cells, inside the same card. */
25
32
  subRow?: ReactNode;
@@ -62,7 +69,16 @@ export interface DataTableRowProps<T> {
62
69
  * for `onClick` and `className` (if function) via `useCallback` / `useMemo` in
63
70
  * the consumer. `href` and string `className` are compared by value.
64
71
  */
65
- function DataTableRowImpl<T>({ row, onClick, href, compact, autoHeight, className, subRow }: DataTableRowProps<T>) {
72
+ function DataTableRowImpl<T>({
73
+ row,
74
+ onClick,
75
+ href,
76
+ compact,
77
+ autoHeight,
78
+ rowHeightClassName,
79
+ className,
80
+ subRow,
81
+ }: DataTableRowProps<T>) {
66
82
  const hasSubRow = subRow != null && subRow !== false;
67
83
  // A sub-row carries its own interactive controls, so it must not live inside the
68
84
  // row-level <Link>; when present, the link wraps only the cells.
@@ -101,11 +117,13 @@ function DataTableRowImpl<T>({ row, onClick, href, compact, autoHeight, classNam
101
117
  className={cn(
102
118
  'flex',
103
119
  ROW_SHELL_CLASSES,
104
- compact
105
- ? 'py-[var(--spacing-system-xsf)]'
106
- : autoHeight
107
- ? 'min-h-[66px] py-[var(--spacing-system-sf)] md:min-h-[78px]'
108
- : `py-0 ${ROW_HEIGHT_DESKTOP}`,
120
+ rowHeightClassName
121
+ ? `py-0 ${rowHeightClassName}`
122
+ : compact
123
+ ? 'py-[var(--spacing-system-xsf)]'
124
+ : autoHeight
125
+ ? 'min-h-[66px] py-[var(--spacing-system-sf)] md:min-h-[78px]'
126
+ : `py-0 ${ROW_HEIGHT_DESKTOP}`,
109
127
  hasSubRow && 'border-b border-ods-border',
110
128
  )}
111
129
  >
@@ -29,9 +29,16 @@ 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
+ * Appending a height through `rowClassName` cannot do this: tailwind-merge
35
+ * drops the plain `h-[66px]` but keeps `md:h-[78px]`, so the override held
36
+ * on a phone and lost on a desktop. See `DataTableBodyProps.rowHeightClassName`.
37
+ */
38
+ rowHeightClassName?: string;
32
39
  }
33
40
 
34
- export function DataTableSkeleton({ rows = 10, className, rowClassName }: DataTableSkeletonProps) {
41
+ export function DataTableSkeleton({ rows = 10, className, rowClassName, rowHeightClassName }: DataTableSkeletonProps) {
35
42
  const table = useDataTableContext();
36
43
  const columns = table.getVisibleFlatColumns();
37
44
  const firstColumnId = columns[0]?.id;
@@ -46,7 +53,14 @@ export function DataTableSkeleton({ rows = 10, className, rowClassName }: DataTa
46
53
  className,
47
54
  )}
48
55
  >
49
- <div className={cn('hidden py-0 md:flex', ROW_SHELL_CLASSES, ROW_HEIGHT_DESKTOP, rowClassName)}>
56
+ <div
57
+ className={cn(
58
+ 'hidden py-0 md:flex',
59
+ ROW_SHELL_CLASSES,
60
+ rowHeightClassName ?? ROW_HEIGHT_DESKTOP,
61
+ rowClassName,
62
+ )}
63
+ >
50
64
  {columns.map(column => {
51
65
  const meta = column.columnDef.meta;
52
66
  return (
@@ -71,7 +85,14 @@ export function DataTableSkeleton({ rows = 10, className, rowClassName }: DataTa
71
85
  );
72
86
  })}
73
87
  </div>
74
- <div className={cn('flex justify-start py-0 md:hidden', ROW_SHELL_CLASSES, ROW_HEIGHT_MOBILE, rowClassName)}>
88
+ <div
89
+ className={cn(
90
+ 'flex justify-start py-0 md:hidden',
91
+ ROW_SHELL_CLASSES,
92
+ rowHeightClassName ?? ROW_HEIGHT_MOBILE,
93
+ rowClassName,
94
+ )}
95
+ >
75
96
  <div className="flex min-w-0 flex-1 flex-col justify-center py-[var(--spacing-system-sf)]">
76
97
  <div className="mb-[var(--spacing-system-xsf)] h-4 w-3/4 rounded-sm bg-ods-bg-surface" />
77
98
  <div className="h-3 w-1/2 rounded-sm bg-ods-bg-surface opacity-60" />
@@ -6,15 +6,29 @@ import { useAuthedImageSrc } from '../../hooks/use-authed-image-src';
6
6
  import { cn } from '../../utils/cn';
7
7
  import { personInitials } from '../../utils/format';
8
8
 
9
+ /** The design size, when a caller does not state one. */
10
+ const ENTITY_IMAGE_SIZE = 'size-[52px] md:size-[60px]';
11
+
9
12
  export interface EntityImageProps {
10
13
  src?: string | null;
11
14
  alt?: string;
12
15
  /** Overrides the initials source. Defaults to `alt`. */
13
16
  fallbackText?: string;
17
+ /**
18
+ * REPLACES the default size, for both the image and its initials fallback.
19
+ *
20
+ * Sizing through `className` does NOT work here and cannot be made to:
21
+ * the default is responsive (`size-[52px] md:size-[60px]`), and
22
+ * tailwind-merge lets `size-*` override `w`/`h` but never the reverse — so a
23
+ * caller passing `h-4 w-6` dropped the base `size-[52px]` and kept
24
+ * `md:size-[60px]`, rendering 16px on a phone and 60px on a desktop. State
25
+ * the size here instead; it is substituted, not merged.
26
+ */
27
+ sizeClassName?: string;
14
28
  className?: string;
15
29
  }
16
30
 
17
- export function EntityImage({ src, alt, fallbackText, className }: EntityImageProps) {
31
+ export function EntityImage({ src, alt, fallbackText, sizeClassName, className }: EntityImageProps) {
18
32
  const resolvedSrc = useAuthedImageSrc(src);
19
33
  // Which src failed, rather than a bare "failed" flag reset from an effect on
20
34
  // every src change. The reset is then implicit — a new src simply is not the
@@ -31,7 +45,8 @@ export function EntityImage({ src, alt, fallbackText, className }: EntityImagePr
31
45
  <div
32
46
  aria-label={alt}
33
47
  className={cn(
34
- 'flex size-[52px] shrink-0 select-none items-center justify-center rounded-md border border-ods-border bg-ods-bg text-ods-text-secondary text-h4 md:size-[60px]',
48
+ 'flex shrink-0 select-none items-center justify-center rounded-md border border-ods-border bg-ods-bg text-ods-text-secondary text-h4',
49
+ sizeClassName ?? ENTITY_IMAGE_SIZE,
35
50
  className,
36
51
  )}
37
52
  >
@@ -46,7 +61,8 @@ export function EntityImage({ src, alt, fallbackText, className }: EntityImagePr
46
61
  alt={alt ?? ''}
47
62
  onError={() => setFailedSrc(resolvedSrc)}
48
63
  className={cn(
49
- 'size-[52px] shrink-0 rounded-md border border-ods-border object-contain md:size-[60px]',
64
+ 'shrink-0 rounded-md border border-ods-border object-contain',
65
+ sizeClassName ?? ENTITY_IMAGE_SIZE,
50
66
  className,
51
67
  )}
52
68
  // Intrinsic hint only — the responsive `size-*` classes above own the
@@ -802,7 +802,7 @@ describe('useApiParams', () => {
802
802
  result.current.setParam('page', 1);
803
803
  });
804
804
 
805
- const written = mockReplace.mock.calls[mockReplace.mock.calls.length - 1][0] as string;
805
+ const written = mockReplace.mock.calls[mockReplace.mock.calls.length - 1][0];
806
806
  expect(written).not.toContain('page=1');
807
807
  });
808
808
 
@@ -39,11 +39,11 @@ export function positiveInt<TFallback = number>(
39
39
  // sentinel back untouched instead of `Math.min(null, max)`.
40
40
  const capped = (v: number) => (max == null ? v : Math.min(v, max));
41
41
  if (raw === null || raw === undefined || raw === '') {
42
- return typeof fallback === 'number' ? (capped(fallback) as number | TFallback) : fallback;
42
+ return typeof fallback === 'number' ? capped(fallback) : fallback;
43
43
  }
44
44
  const n = Math.trunc(Number(raw));
45
45
  if (!Number.isFinite(n) || n < min) {
46
- return typeof fallback === 'number' ? (capped(fallback) as number | TFallback) : fallback;
46
+ return typeof fallback === 'number' ? capped(fallback) : fallback;
47
47
  }
48
48
  return capped(n);
49
49
  }
@@ -160,7 +160,7 @@ function coerceScalar(raw: string | undefined, config: ParamConfig): unknown {
160
160
  if (raw === undefined || raw === '') return undefined;
161
161
  switch (config.type) {
162
162
  case 'int': {
163
- const int = config as IntParamConfig;
163
+ const int = config;
164
164
  // ONE integer grammar, default or not. Without a declared default the
165
165
  // fallback is `undefined` (an unparseable value is absent), which
166
166
  // `positiveInt` returns untouched — the branch used to open-code the
@@ -201,7 +201,7 @@ export function parseSchemaParams<TSchema extends ParamSchema, Opts extends Pars
201
201
  for (const [key, config] of Object.entries(schema)) {
202
202
  if (config.type === 'array') {
203
203
  const values = allParamValues(input, key);
204
- out[key] = values.length > 0 ? values : ((config.default as string[] | undefined) ?? []);
204
+ out[key] = values.length > 0 ? values : (config.default ?? []);
205
205
  continue;
206
206
  }
207
207
  const coerced = coerceScalar(firstParamValue(input, key), config);