@eifi1/ui-kit 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/README.md +37 -23
  2. package/dist/components/alert-banner.d.ts +57 -5
  3. package/dist/components/alert-banner.js +63 -17
  4. package/dist/components/alert-banner.js.map +1 -1
  5. package/dist/components/amount-input.d.ts +6 -0
  6. package/dist/components/breadcrumbs.d.ts +60 -0
  7. package/dist/components/breadcrumbs.js +88 -0
  8. package/dist/components/breadcrumbs.js.map +1 -0
  9. package/dist/components/bulk-action-bar.d.ts +112 -0
  10. package/dist/components/bulk-action-bar.js +196 -0
  11. package/dist/components/bulk-action-bar.js.map +1 -0
  12. package/dist/components/button-group.d.ts +58 -3
  13. package/dist/components/button-group.js +56 -5
  14. package/dist/components/button-group.js.map +1 -1
  15. package/dist/components/calculator.d.ts +6 -0
  16. package/dist/components/calendar-heatmap.d.ts +124 -0
  17. package/dist/components/calendar-heatmap.js +295 -0
  18. package/dist/components/calendar-heatmap.js.map +1 -0
  19. package/dist/components/chip.d.ts +31 -5
  20. package/dist/components/chip.js +85 -5
  21. package/dist/components/chip.js.map +1 -1
  22. package/dist/components/choice-card.d.ts +34 -2
  23. package/dist/components/choice-card.js +49 -0
  24. package/dist/components/choice-card.js.map +1 -1
  25. package/dist/components/copy-button.d.ts +14 -4
  26. package/dist/components/copy-button.js +17 -4
  27. package/dist/components/copy-button.js.map +1 -1
  28. package/dist/components/data-table.js +2 -0
  29. package/dist/components/data-table.js.map +1 -1
  30. package/dist/components/date-picker.d.ts +22 -2
  31. package/dist/components/date-picker.js +11 -3
  32. package/dist/components/date-picker.js.map +1 -1
  33. package/dist/components/description-list.d.ts +43 -6
  34. package/dist/components/description-list.js +91 -11
  35. package/dist/components/description-list.js.map +1 -1
  36. package/dist/components/dialog-frame.d.ts +7 -0
  37. package/dist/components/dialog-frame.js.map +1 -1
  38. package/dist/components/disclosure.d.ts +57 -2
  39. package/dist/components/disclosure.js +24 -12
  40. package/dist/components/disclosure.js.map +1 -1
  41. package/dist/components/field.d.ts +43 -0
  42. package/dist/components/field.js +52 -0
  43. package/dist/components/field.js.map +1 -0
  44. package/dist/components/file-button.d.ts +1 -0
  45. package/dist/components/file-dropzone.d.ts +4 -4
  46. package/dist/components/file-dropzone.js +2 -1
  47. package/dist/components/file-dropzone.js.map +1 -1
  48. package/dist/components/floating-panel.d.ts +147 -4
  49. package/dist/components/floating-panel.js +188 -28
  50. package/dist/components/floating-panel.js.map +1 -1
  51. package/dist/components/full-bleed-dialog.d.ts +23 -4
  52. package/dist/components/full-bleed-dialog.js +11 -2
  53. package/dist/components/full-bleed-dialog.js.map +1 -1
  54. package/dist/components/list.d.ts +187 -0
  55. package/dist/components/list.js +221 -0
  56. package/dist/components/list.js.map +1 -0
  57. package/dist/components/menu-item.d.ts +109 -0
  58. package/dist/components/menu-item.js +88 -0
  59. package/dist/components/menu-item.js.map +1 -0
  60. package/dist/components/mini-calendar.d.ts +70 -5
  61. package/dist/components/mini-calendar.js +166 -62
  62. package/dist/components/mini-calendar.js.map +1 -1
  63. package/dist/components/modal.d.ts +23 -1
  64. package/dist/components/modal.js +36 -10
  65. package/dist/components/modal.js.map +1 -1
  66. package/dist/components/nav-pills.d.ts +66 -0
  67. package/dist/components/nav-pills.js +65 -0
  68. package/dist/components/nav-pills.js.map +1 -0
  69. package/dist/components/number-field.d.ts +6 -0
  70. package/dist/components/number-input.d.ts +6 -0
  71. package/dist/components/numpad-sheet.d.ts +6 -0
  72. package/dist/components/page-contents.js +1 -2
  73. package/dist/components/page-contents.js.map +1 -1
  74. package/dist/components/page-header.d.ts +37 -0
  75. package/dist/components/page-header.js +36 -0
  76. package/dist/components/page-header.js.map +1 -0
  77. package/dist/components/progress-bar.d.ts +31 -2
  78. package/dist/components/progress-bar.js +73 -7
  79. package/dist/components/progress-bar.js.map +1 -1
  80. package/dist/components/series-chart-labels.d.ts +3 -0
  81. package/dist/components/series-chart-labels.js +2 -1
  82. package/dist/components/series-chart-labels.js.map +1 -1
  83. package/dist/components/series-chart.d.ts +32 -0
  84. package/dist/components/series-chart.js +141 -4
  85. package/dist/components/series-chart.js.map +1 -1
  86. package/dist/components/settings-fields.d.ts +1 -0
  87. package/dist/components/status-dot.d.ts +53 -0
  88. package/dist/components/status-dot.js +57 -0
  89. package/dist/components/status-dot.js.map +1 -0
  90. package/dist/components/table.d.ts +78 -13
  91. package/dist/components/table.js +84 -11
  92. package/dist/components/table.js.map +1 -1
  93. package/dist/components/text.d.ts +70 -0
  94. package/dist/components/text.js +24 -0
  95. package/dist/components/text.js.map +1 -0
  96. package/dist/components/time-input.d.ts +1 -0
  97. package/dist/components/toast.d.ts +167 -0
  98. package/dist/components/toast.js +226 -0
  99. package/dist/components/toast.js.map +1 -0
  100. package/dist/components/toggle-group.d.ts +44 -1
  101. package/dist/components/toggle-group.js +73 -4
  102. package/dist/components/toggle-group.js.map +1 -1
  103. package/dist/components/tooltip.d.ts +46 -15
  104. package/dist/components/tooltip.js +87 -47
  105. package/dist/components/tooltip.js.map +1 -1
  106. package/dist/components/ui.d.ts +158 -7
  107. package/dist/components/ui.js +191 -10
  108. package/dist/components/ui.js.map +1 -1
  109. package/dist/components/user-avatar.d.ts +25 -3
  110. package/dist/components/user-avatar.js +32 -3
  111. package/dist/components/user-avatar.js.map +1 -1
  112. package/dist/hooks/use-close-transition.d.ts +7 -1
  113. package/dist/hooks/use-close-transition.js +5 -2
  114. package/dist/hooks/use-close-transition.js.map +1 -1
  115. package/dist/hooks/use-copy-to-clipboard.js +1 -1
  116. package/dist/hooks/use-copy-to-clipboard.js.map +1 -1
  117. package/dist/hooks/use-file-drop.d.ts +1 -0
  118. package/dist/i18n/defaults.d.ts +6 -0
  119. package/dist/i18n/defaults.js +11 -1
  120. package/dist/i18n/defaults.js.map +1 -1
  121. package/dist/i18n/kit-labels.d.ts +11 -0
  122. package/dist/i18n/kit-labels.js.map +1 -1
  123. package/dist/i18n/locales/de-CH-informal.d.ts +6 -0
  124. package/dist/i18n/locales/de-CH.d.ts +6 -0
  125. package/dist/i18n/locales/de-informal.d.ts +6 -0
  126. package/dist/i18n/locales/de.d.ts +6 -0
  127. package/dist/i18n/locales/de.js +30 -2
  128. package/dist/i18n/locales/de.js.map +1 -1
  129. package/dist/i18n/locales/es.d.ts +6 -0
  130. package/dist/i18n/locales/es.js +30 -2
  131. package/dist/i18n/locales/es.js.map +1 -1
  132. package/dist/i18n/locales/fr.d.ts +6 -0
  133. package/dist/i18n/locales/fr.js +30 -2
  134. package/dist/i18n/locales/fr.js.map +1 -1
  135. package/dist/i18n/locales/hu.d.ts +6 -0
  136. package/dist/i18n/locales/hu.js +30 -2
  137. package/dist/i18n/locales/hu.js.map +1 -1
  138. package/dist/i18n/locales/it.d.ts +6 -0
  139. package/dist/i18n/locales/it.js +30 -2
  140. package/dist/i18n/locales/it.js.map +1 -1
  141. package/dist/i18n/locales/zh.d.ts +6 -0
  142. package/dist/i18n/locales/zh.js +30 -2
  143. package/dist/i18n/locales/zh.js.map +1 -1
  144. package/dist/index.d.ts +27 -15
  145. package/dist/index.js +15 -0
  146. package/dist/index.js.map +1 -1
  147. package/dist/lib/clipping.d.ts +25 -0
  148. package/dist/lib/clipping.js +17 -0
  149. package/dist/lib/clipping.js.map +1 -0
  150. package/dist/lib/dates.d.ts +17 -1
  151. package/dist/lib/dates.js +19 -0
  152. package/dist/lib/dates.js.map +1 -1
  153. package/dist/rhf/form.d.ts +1 -0
  154. package/dist/rhf/use-rhf-wizard-step.d.ts +22 -0
  155. package/dist/rhf/use-rhf-wizard-step.js +38 -0
  156. package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
  157. package/dist/rhf.d.ts +2 -0
  158. package/dist/rhf.js +1 -0
  159. package/dist/rhf.js.map +1 -1
  160. package/dist/search/command-palette.d.ts +19 -2
  161. package/dist/search/command-palette.js +16 -5
  162. package/dist/search/command-palette.js.map +1 -1
  163. package/dist/search/global-search.d.ts +26 -1
  164. package/dist/search/global-search.js +29 -13
  165. package/dist/search/global-search.js.map +1 -1
  166. package/dist/search.d.ts +1 -1
  167. package/dist/shell/topbar-action-menu.d.ts +73 -3
  168. package/dist/shell/topbar-action-menu.js +100 -27
  169. package/dist/shell/topbar-action-menu.js.map +1 -1
  170. package/dist/shell.d.ts +2 -1
  171. package/dist/wizard/stepper-nav.d.ts +1 -0
  172. package/dist/wizard/stepper-nav.js +1 -1
  173. package/dist/wizard/stepper-nav.js.map +1 -1
  174. package/dist/wizard/types.d.ts +2 -2
  175. package/dist/wizard/types.js.map +1 -1
  176. package/dist/wizard/use-wizard.js +1 -1
  177. package/dist/wizard/use-wizard.js.map +1 -1
  178. package/dist/wizard/wizard-context.d.ts +7 -1
  179. package/dist/wizard/wizard-context.js +4 -0
  180. package/dist/wizard/wizard-context.js.map +1 -1
  181. package/dist/wizard/wizard-summary.js +3 -3
  182. package/dist/wizard/wizard-summary.js.map +1 -1
  183. package/dist/wizard.d.ts +2 -1
  184. package/package.json +16 -12
  185. package/src/components/alert-banner.tsx +149 -21
  186. package/src/components/breadcrumbs.tsx +168 -0
  187. package/src/components/bulk-action-bar.tsx +362 -0
  188. package/src/components/button-group.tsx +124 -4
  189. package/src/components/calendar-heatmap.tsx +504 -0
  190. package/src/components/chip.tsx +128 -7
  191. package/src/components/choice-card.tsx +97 -1
  192. package/src/components/copy-button.tsx +29 -7
  193. package/src/components/data-table.tsx +7 -0
  194. package/src/components/date-picker.tsx +49 -1
  195. package/src/components/description-list.tsx +168 -15
  196. package/src/components/dialog-frame.tsx +7 -0
  197. package/src/components/disclosure.tsx +93 -21
  198. package/src/components/field.tsx +137 -0
  199. package/src/components/file-dropzone.tsx +9 -10
  200. package/src/components/floating-panel.tsx +366 -15
  201. package/src/components/full-bleed-dialog.tsx +42 -5
  202. package/src/components/list.tsx +443 -0
  203. package/src/components/menu-item.tsx +235 -0
  204. package/src/components/mini-calendar.tsx +245 -54
  205. package/src/components/modal.tsx +80 -17
  206. package/src/components/nav-pills.tsx +145 -0
  207. package/src/components/page-contents.tsx +4 -4
  208. package/src/components/page-header.tsx +68 -0
  209. package/src/components/progress-bar.tsx +129 -11
  210. package/src/components/series-chart-labels.ts +4 -0
  211. package/src/components/series-chart.tsx +274 -3
  212. package/src/components/status-dot.tsx +107 -0
  213. package/src/components/table.tsx +176 -17
  214. package/src/components/text.tsx +97 -0
  215. package/src/components/toast.tsx +441 -0
  216. package/src/components/toggle-group.tsx +128 -4
  217. package/src/components/tooltip.tsx +200 -100
  218. package/src/components/ui.tsx +381 -14
  219. package/src/components/user-avatar.tsx +57 -3
  220. package/src/hooks/use-close-transition.ts +14 -5
  221. package/src/hooks/use-copy-to-clipboard.ts +1 -1
  222. package/src/i18n/defaults.ts +10 -0
  223. package/src/i18n/kit-labels.tsx +10 -0
  224. package/src/i18n/locales/de.ts +29 -0
  225. package/src/i18n/locales/es.ts +29 -0
  226. package/src/i18n/locales/fr.ts +29 -0
  227. package/src/i18n/locales/hu.ts +29 -0
  228. package/src/i18n/locales/it.ts +29 -0
  229. package/src/i18n/locales/zh.ts +29 -0
  230. package/src/index.ts +31 -0
  231. package/src/lib/clipping.ts +34 -0
  232. package/src/lib/dates.ts +37 -0
  233. package/src/rhf/use-rhf-wizard-step.ts +113 -0
  234. package/src/rhf.ts +2 -0
  235. package/src/search/command-palette.tsx +42 -5
  236. package/src/search/global-search.tsx +59 -18
  237. package/src/shell/topbar-action-menu.tsx +239 -49
  238. package/src/wizard/stepper-nav.tsx +2 -1
  239. package/src/wizard/types.ts +2 -2
  240. package/src/wizard/use-wizard.ts +3 -3
  241. package/src/wizard/wizard-context.tsx +9 -0
  242. package/src/wizard/wizard-summary.tsx +14 -10
  243. package/tokens.css +97 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/dates.ts"],"sourcesContent":["/**\n * ISO date helpers, collected from copies that were scattered across the pages.\n *\n * **Every \"what day is it\" helper here reads the LOCAL calendar** — one family, not\n * two. It used to be two: these all built a local Date and then read it back through\n * `toISOString()`, which is UTC, so east of Greenwich they all returned YESTERDAY\n * between local midnight and the UTC rollover. The old module note called that\n * deliberate and warned against changing it. It was not defensible, and dev#471 is\n * what it cost:\n *\n * > *\"Just entered a tx now from scheduled. But it still shows as upcoming. Is there\n * > a problem with timezones? It is currently 1 o clock middle european summer time.\"*\n *\n * There was: at 01:00 CEST `todayIso()` answered with the 14th, the row the scheduler\n * had just booked was dated the 15th, and `isUpcoming` compares the two strings — so a\n * transaction entered a second ago read as one still to come. The same slip moved\n * `startOfMonthIso()` onto the last day of the PREVIOUS month for an hour every first\n * of the month, which is a report range nobody would have questioned.\n *\n * A calendar day only means anything to the human reading it, and every caller here —\n * the register's today, the date-range presets, the budget cursor, a new\n * transaction's default date — is asking that question. There is no caller for whom\n * \"the UTC day\" is the right answer; the server compares plain dates with no zone at\n * all, so sending it the user's local day is also what it wants.\n */\n\n/** Zero-pad a number to two digits (\"3\" -> \"03\"). */\nexport function pad(n: number): string {\n return String(n).padStart(2, \"0\");\n}\n\n/** Today as \"YYYY-MM-DD\" on the LOCAL calendar (see the module note). */\nexport function todayIso(): string {\n return toLocalIso(new Date());\n}\n\n/** A Date as a local-time \"YYYY-MM-DD\" string. */\nexport function toLocalIso(d: Date): string {\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;\n}\n\n/** A Date as a local-time \"YYYY-MM\" month key. */\nexport function monthKey(d: Date): string {\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}`;\n}\n\n// ---------- Date-range presets + calendar helpers ----------\n// Local-calendar like todayIso (see the module note); used by the data-table date\n// filter and its MiniCalendar. Each one mutates a local Date and must therefore READ\n// it back locally — going out through toISOString() is what shifted every one of\n// these by a day near midnight (dev#471).\n\n/** Today shifted by `days`, as \"YYYY-MM-DD\". */\nexport function shiftIso(days: number): string {\n const d = new Date();\n d.setDate(d.getDate() + days);\n return toLocalIso(d);\n}\n\n/** First day (Monday) of the current week, as \"YYYY-MM-DD\". */\nexport function startOfWeekIso(): string {\n const d = new Date();\n const day = d.getDay();\n const diff = (day + 6) % 7; // monday = 0\n d.setDate(d.getDate() - diff);\n return toLocalIso(d);\n}\n\n/** First day of the month `offsetMonths` from now, as \"YYYY-MM-DD\". */\nexport function startOfMonthIso(offsetMonths = 0): string {\n const d = new Date();\n d.setDate(1);\n d.setMonth(d.getMonth() + offsetMonths);\n return toLocalIso(d);\n}\n\n/** Last day of the month `offsetMonths` from now, as \"YYYY-MM-DD\". */\nexport function endOfMonthIso(offsetMonths = 0): string {\n const d = new Date();\n d.setDate(1);\n d.setMonth(d.getMonth() + offsetMonths + 1);\n d.setDate(0);\n return toLocalIso(d);\n}\n\n/** First day of the year `offsetYears` from now, as \"YYYY-MM-DD\". */\nexport function startOfYearIso(offsetYears = 0): string {\n const d = new Date();\n d.setMonth(0);\n d.setDate(1);\n d.setFullYear(d.getFullYear() + offsetYears);\n return toLocalIso(d);\n}\n\n/** Last day of the year `offsetYears` from now, as \"YYYY-MM-DD\". */\nexport function endOfYearIso(offsetYears = 0): string {\n const d = new Date();\n d.setMonth(11);\n d.setDate(31);\n d.setFullYear(d.getFullYear() + offsetYears);\n return toLocalIso(d);\n}\n\n/** Parse a \"YYYY-MM-DD\" string to a local-midnight Date, or null if malformed —\n * where \"malformed\" includes a date that does not exist.\n *\n * The falsy check alone was not enough. `new Date(y, m - 1, d)` ROLLS OVER out-of-range\n * components rather than refusing them, so \"2026-13-45\" came back as 2027-02-14 and\n * \"2026-02-30\" as 2026-03-02: a valid-looking Date for a day that never happened. No\n * caller could tell — {@link formatIsoDate} blanks only on `null`, so the invented\n * date formatted and displayed exactly like a real one.\n *\n * The round-trip is the check: build the Date, then read the components back off it.\n * A value that rolled over cannot answer with the numbers it was given. That is the\n * same shape {@link sameYmd} already expresses, applied to a Date and its source. */\nexport function parseIsoDate(s: string): Date | null {\n if (!s) return null;\n const [y, m, d] = s.split(\"-\").map(Number);\n if (!y || !m || !d) return null;\n if (!Number.isInteger(y) || !Number.isInteger(m) || !Number.isInteger(d)) return null;\n const dt = new Date(y, m - 1, d);\n if (Number.isNaN(dt.getTime())) return null;\n if (dt.getFullYear() !== y || dt.getMonth() !== m - 1 || dt.getDate() !== d) return null;\n return dt;\n}\n\n/** Format a \"YYYY-MM-DD\" string for display in `locale` (empty string when\n * unparseable). Defaults to the locale's short numeric date; pass `options` to\n * override (e.g. `{ dateStyle: \"medium\" }`). */\nexport function formatIsoDate(\n iso: string,\n locale: string,\n options?: Intl.DateTimeFormatOptions,\n): string {\n const d = parseIsoDate(iso);\n return d ? d.toLocaleDateString(locale, options) : \"\";\n}\n\n/** A \"YYYY-MM-DD\" string shifted by `days` (negative shifts back), or the input\n * unchanged when it isn't a parseable date.\n *\n * Local-time throughout — {@link parseIsoDate} builds a local-midnight Date and\n * {@link toLocalIso} reads local components back, so this never slides a day the way\n * a `toISOString()` round-trip does west of UTC. */\nexport function addDaysIso(iso: string, days: number): string {\n const d = parseIsoDate(iso);\n if (!d) return iso;\n d.setDate(d.getDate() + days);\n return toLocalIso(d);\n}\n\n/** Whether two Dates fall on the same local calendar day. */\nexport function sameYmd(a: Date, b: Date): boolean {\n return (\n a.getFullYear() === b.getFullYear() &&\n a.getMonth() === b.getMonth() &&\n a.getDate() === b.getDate()\n );\n}\n\nexport interface DateRangePreset {\n key: string;\n from: string;\n to: string;\n}\n\n/**\n * The named ranges offered by the data-table date filter (local-calendar, like\n * {@link todayIso} — see the module note). `key` maps to the consumer's\n * `table.preset_*` labels.\n */\nexport function dateRangePresets(): DateRangePreset[] {\n const mondayOffset = (new Date().getDay() + 6) % 7; // monday = 0\n return [\n { key: \"today\", from: todayIso(), to: todayIso() },\n { key: \"yesterday\", from: shiftIso(-1), to: shiftIso(-1) },\n { key: \"this_week\", from: startOfWeekIso(), to: todayIso() },\n { key: \"last_week\", from: shiftIso(-7 - mondayOffset), to: shiftIso(-1 - mondayOffset) },\n { key: \"last_7_days\", from: shiftIso(-6), to: todayIso() },\n { key: \"last_30_days\", from: shiftIso(-29), to: todayIso() },\n { key: \"this_month\", from: startOfMonthIso(0), to: todayIso() },\n { key: \"last_month\", from: startOfMonthIso(-1), to: endOfMonthIso(-1) },\n { key: \"last_3_months\", from: shiftIso(-90), to: todayIso() },\n { key: \"ytd\", from: startOfYearIso(0), to: todayIso() },\n { key: \"last_year\", from: startOfYearIso(-1), to: endOfYearIso(-1) },\n ];\n}\n\n/** The last `n` WHOLE calendar months before the current one, as an inclusive\n * `{ from, to }` — on 15 May, `n = 3` is 1 February … 30 April. Local-calendar, like\n * everything here; `now` is for tests and for a host with its own clock. */\nexport function lastFullMonthsRange(n: number, now: Date = new Date()): { from: string; to: string } {\n const from = new Date(now.getFullYear(), now.getMonth() - n, 1);\n // Day 0 of the current month is the last day of the previous one.\n const to = new Date(now.getFullYear(), now.getMonth(), 0);\n return { from: toLocalIso(from), to: toLocalIso(to) };\n}\n\n/** The last `n` WHOLE calendar years before the current one — in 2026, `n = 2` is\n * 1 January 2024 … 31 December 2025. */\nexport function lastFullYearsRange(n: number, now: Date = new Date()): { from: string; to: string } {\n const year = now.getFullYear();\n return { from: `${year - n}-01-01`, to: `${year - 1}-12-31` };\n}\n\nexport interface CalendarMonthPresetOptions {\n /** How many whole months each month preset spans. Default `[3, 6]`. */\n months?: readonly number[];\n /** How many whole years each year preset spans. Default `[2]`. */\n years?: readonly number[];\n /** The day to count back from. Default: now, on the local calendar. */\n now?: Date;\n}\n\n/**\n * Month-ALIGNED presets, beside the rolling-day ones of {@link dateRangePresets}.\n *\n * kastlan's accounting reports are closed by the month: \"the last three months\" there\n * means February, March and April, never \"the 90 days up to this morning\", which\n * starts mid-month and ends with a day that is still being booked. `last_3_months`\n * above is the rolling kind and stays that way for the data table's filter, so these\n * are separate keys rather than a changed meaning under an old one.\n *\n * Keys are `last_<n>_full_months` and `last_<n>_full_years`; the host labels them (the\n * same arrangement as `dateRangePresets`' keys and its `table.preset_*` strings). With\n * `DateRangePicker`, pass the key as the preset's `id` so the choice keeps its\n * identity while the dates are recomputed on every render.\n */\nexport function calendarMonthPresets(options: CalendarMonthPresetOptions = {}): DateRangePreset[] {\n const { months = [3, 6], years = [2], now = new Date() } = options;\n return [\n ...months.map((n) => ({ key: `last_${n}_full_months`, ...lastFullMonthsRange(n, now) })),\n ...years.map((n) => ({ key: `last_${n}_full_years`, ...lastFullYearsRange(n, now) })),\n ];\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/lib/dates.ts"],"sourcesContent":["/**\n * ISO date helpers, collected from copies that were scattered across the pages.\n *\n * **Every \"what day is it\" helper here reads the LOCAL calendar** — one family, not\n * two. It used to be two: these all built a local Date and then read it back through\n * `toISOString()`, which is UTC, so east of Greenwich they all returned YESTERDAY\n * between local midnight and the UTC rollover. The old module note called that\n * deliberate and warned against changing it. It was not defensible, and dev#471 is\n * what it cost:\n *\n * > *\"Just entered a tx now from scheduled. But it still shows as upcoming. Is there\n * > a problem with timezones? It is currently 1 o clock middle european summer time.\"*\n *\n * There was: at 01:00 CEST `todayIso()` answered with the 14th, the row the scheduler\n * had just booked was dated the 15th, and `isUpcoming` compares the two strings — so a\n * transaction entered a second ago read as one still to come. The same slip moved\n * `startOfMonthIso()` onto the last day of the PREVIOUS month for an hour every first\n * of the month, which is a report range nobody would have questioned.\n *\n * A calendar day only means anything to the human reading it, and every caller here —\n * the register's today, the date-range presets, the budget cursor, a new\n * transaction's default date — is asking that question. There is no caller for whom\n * \"the UTC day\" is the right answer; the server compares plain dates with no zone at\n * all, so sending it the user's local day is also what it wants.\n */\n\n/** Zero-pad a number to two digits (\"3\" -> \"03\"). */\nexport function pad(n: number): string {\n return String(n).padStart(2, \"0\");\n}\n\n/** Today as \"YYYY-MM-DD\" on the LOCAL calendar (see the module note). */\nexport function todayIso(): string {\n return toLocalIso(new Date());\n}\n\n/** A Date as a local-time \"YYYY-MM-DD\" string. */\nexport function toLocalIso(d: Date): string {\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;\n}\n\n/** A Date as a local-time \"YYYY-MM\" month key. */\nexport function monthKey(d: Date): string {\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}`;\n}\n\n// ---------- Date-range presets + calendar helpers ----------\n// Local-calendar like todayIso (see the module note); used by the data-table date\n// filter and its MiniCalendar. Each one mutates a local Date and must therefore READ\n// it back locally — going out through toISOString() is what shifted every one of\n// these by a day near midnight (dev#471).\n\n/** Today shifted by `days`, as \"YYYY-MM-DD\". */\nexport function shiftIso(days: number): string {\n const d = new Date();\n d.setDate(d.getDate() + days);\n return toLocalIso(d);\n}\n\n/** First day (Monday) of the current week, as \"YYYY-MM-DD\". */\nexport function startOfWeekIso(): string {\n const d = new Date();\n const day = d.getDay();\n const diff = (day + 6) % 7; // monday = 0\n d.setDate(d.getDate() - diff);\n return toLocalIso(d);\n}\n\n/** First day of the month `offsetMonths` from now, as \"YYYY-MM-DD\". */\nexport function startOfMonthIso(offsetMonths = 0): string {\n const d = new Date();\n d.setDate(1);\n d.setMonth(d.getMonth() + offsetMonths);\n return toLocalIso(d);\n}\n\n/** Last day of the month `offsetMonths` from now, as \"YYYY-MM-DD\". */\nexport function endOfMonthIso(offsetMonths = 0): string {\n const d = new Date();\n d.setDate(1);\n d.setMonth(d.getMonth() + offsetMonths + 1);\n d.setDate(0);\n return toLocalIso(d);\n}\n\n/** First day of the year `offsetYears` from now, as \"YYYY-MM-DD\". */\nexport function startOfYearIso(offsetYears = 0): string {\n const d = new Date();\n d.setMonth(0);\n d.setDate(1);\n d.setFullYear(d.getFullYear() + offsetYears);\n return toLocalIso(d);\n}\n\n/** Last day of the year `offsetYears` from now, as \"YYYY-MM-DD\". */\nexport function endOfYearIso(offsetYears = 0): string {\n const d = new Date();\n d.setMonth(11);\n d.setDate(31);\n d.setFullYear(d.getFullYear() + offsetYears);\n return toLocalIso(d);\n}\n\n/** Parse a \"YYYY-MM-DD\" string to a local-midnight Date, or null if malformed —\n * where \"malformed\" includes a date that does not exist.\n *\n * The falsy check alone was not enough. `new Date(y, m - 1, d)` ROLLS OVER out-of-range\n * components rather than refusing them, so \"2026-13-45\" came back as 2027-02-14 and\n * \"2026-02-30\" as 2026-03-02: a valid-looking Date for a day that never happened. No\n * caller could tell — {@link formatIsoDate} blanks only on `null`, so the invented\n * date formatted and displayed exactly like a real one.\n *\n * The round-trip is the check: build the Date, then read the components back off it.\n * A value that rolled over cannot answer with the numbers it was given. That is the\n * same shape {@link sameYmd} already expresses, applied to a Date and its source. */\nexport function parseIsoDate(s: string): Date | null {\n if (!s) return null;\n const [y, m, d] = s.split(\"-\").map(Number);\n if (!y || !m || !d) return null;\n if (!Number.isInteger(y) || !Number.isInteger(m) || !Number.isInteger(d)) return null;\n const dt = new Date(y, m - 1, d);\n if (Number.isNaN(dt.getTime())) return null;\n if (dt.getFullYear() !== y || dt.getMonth() !== m - 1 || dt.getDate() !== d) return null;\n return dt;\n}\n\n/** Format a \"YYYY-MM-DD\" string for display in `locale` (empty string when\n * unparseable). Defaults to the locale's short numeric date; pass `options` to\n * override (e.g. `{ dateStyle: \"medium\" }`). */\nexport function formatIsoDate(\n iso: string,\n locale: string,\n options?: Intl.DateTimeFormatOptions,\n): string {\n const d = parseIsoDate(iso);\n return d ? d.toLocaleDateString(locale, options) : \"\";\n}\n\n/** A \"YYYY-MM-DD\" string shifted by `days` (negative shifts back), or the input\n * unchanged when it isn't a parseable date.\n *\n * Local-time throughout — {@link parseIsoDate} builds a local-midnight Date and\n * {@link toLocalIso} reads local components back, so this never slides a day the way\n * a `toISOString()` round-trip does west of UTC. */\nexport function addDaysIso(iso: string, days: number): string {\n const d = parseIsoDate(iso);\n if (!d) return iso;\n d.setDate(d.getDate() + days);\n return toLocalIso(d);\n}\n\n/**\n * `d` moved by whole months, keeping the day of the month where the target has one.\n * `setMonth` alone ROLLS OVER — 31 January + 1 month is 3 March — which would skip\n * February entirely for anyone paging through the year with PageDown. Returns a new\n * Date; `d` is not touched.\n */\nexport function addMonthsClamped(d: Date, months: number): Date {\n const day = d.getDate();\n const out = new Date(d.getFullYear(), d.getMonth() + months, 1);\n const lastOfTarget = new Date(out.getFullYear(), out.getMonth() + 1, 0).getDate();\n out.setDate(Math.min(day, lastOfTarget));\n return out;\n}\n\n/**\n * The first day of the week in `locale`, as a `Date#getDay` index (0 = Sunday).\n *\n * `Intl.Locale#getWeekInfo()` is the standard spelling; V8 shipped it first as the\n * `weekInfo` accessor, and Firefox has neither yet — hence both reads and a Monday\n * fallback, which is ISO 8601. Its `firstDay` counts 1 = Monday … 7 = Sunday, so `% 7`\n * maps it onto `getDay`. `undefined` asks for the runtime's default locale.\n */\nexport function localeWeekStart(locale: string | undefined): 0 | 1 | 2 | 3 | 4 | 5 | 6 {\n try {\n const tag = locale ?? new Intl.DateTimeFormat().resolvedOptions().locale;\n const loc = new Intl.Locale(tag) as Intl.Locale & {\n getWeekInfo?: () => { firstDay: number };\n weekInfo?: { firstDay: number };\n };\n const firstDay = (loc.getWeekInfo?.() ?? loc.weekInfo)?.firstDay;\n if (typeof firstDay === \"number\") return (firstDay % 7) as 0 | 1 | 2 | 3 | 4 | 5 | 6;\n } catch {\n // A malformed tag throws from `Intl.Locale`; a calendar still has to render.\n }\n return 1;\n}\n\n/** Whether two Dates fall on the same local calendar day. */\nexport function sameYmd(a: Date, b: Date): boolean {\n return (\n a.getFullYear() === b.getFullYear() &&\n a.getMonth() === b.getMonth() &&\n a.getDate() === b.getDate()\n );\n}\n\nexport interface DateRangePreset {\n key: string;\n from: string;\n to: string;\n}\n\n/**\n * The named ranges offered by the data-table date filter (local-calendar, like\n * {@link todayIso} — see the module note). `key` maps to the consumer's\n * `table.preset_*` labels.\n */\nexport function dateRangePresets(): DateRangePreset[] {\n const mondayOffset = (new Date().getDay() + 6) % 7; // monday = 0\n return [\n { key: \"today\", from: todayIso(), to: todayIso() },\n { key: \"yesterday\", from: shiftIso(-1), to: shiftIso(-1) },\n { key: \"this_week\", from: startOfWeekIso(), to: todayIso() },\n { key: \"last_week\", from: shiftIso(-7 - mondayOffset), to: shiftIso(-1 - mondayOffset) },\n { key: \"last_7_days\", from: shiftIso(-6), to: todayIso() },\n { key: \"last_30_days\", from: shiftIso(-29), to: todayIso() },\n { key: \"this_month\", from: startOfMonthIso(0), to: todayIso() },\n { key: \"last_month\", from: startOfMonthIso(-1), to: endOfMonthIso(-1) },\n { key: \"last_3_months\", from: shiftIso(-90), to: todayIso() },\n { key: \"ytd\", from: startOfYearIso(0), to: todayIso() },\n { key: \"last_year\", from: startOfYearIso(-1), to: endOfYearIso(-1) },\n ];\n}\n\n/** The last `n` WHOLE calendar months before the current one, as an inclusive\n * `{ from, to }` — on 15 May, `n = 3` is 1 February … 30 April. Local-calendar, like\n * everything here; `now` is for tests and for a host with its own clock. */\nexport function lastFullMonthsRange(n: number, now: Date = new Date()): { from: string; to: string } {\n const from = new Date(now.getFullYear(), now.getMonth() - n, 1);\n // Day 0 of the current month is the last day of the previous one.\n const to = new Date(now.getFullYear(), now.getMonth(), 0);\n return { from: toLocalIso(from), to: toLocalIso(to) };\n}\n\n/** The last `n` WHOLE calendar years before the current one — in 2026, `n = 2` is\n * 1 January 2024 … 31 December 2025. */\nexport function lastFullYearsRange(n: number, now: Date = new Date()): { from: string; to: string } {\n const year = now.getFullYear();\n return { from: `${year - n}-01-01`, to: `${year - 1}-12-31` };\n}\n\nexport interface CalendarMonthPresetOptions {\n /** How many whole months each month preset spans. Default `[3, 6]`. */\n months?: readonly number[];\n /** How many whole years each year preset spans. Default `[2]`. */\n years?: readonly number[];\n /** The day to count back from. Default: now, on the local calendar. */\n now?: Date;\n}\n\n/**\n * Month-ALIGNED presets, beside the rolling-day ones of {@link dateRangePresets}.\n *\n * kastlan's accounting reports are closed by the month: \"the last three months\" there\n * means February, March and April, never \"the 90 days up to this morning\", which\n * starts mid-month and ends with a day that is still being booked. `last_3_months`\n * above is the rolling kind and stays that way for the data table's filter, so these\n * are separate keys rather than a changed meaning under an old one.\n *\n * Keys are `last_<n>_full_months` and `last_<n>_full_years`; the host labels them (the\n * same arrangement as `dateRangePresets`' keys and its `table.preset_*` strings). With\n * `DateRangePicker`, pass the key as the preset's `id` so the choice keeps its\n * identity while the dates are recomputed on every render.\n */\nexport function calendarMonthPresets(options: CalendarMonthPresetOptions = {}): DateRangePreset[] {\n const { months = [3, 6], years = [2], now = new Date() } = options;\n return [\n ...months.map((n) => ({ key: `last_${n}_full_months`, ...lastFullMonthsRange(n, now) })),\n ...years.map((n) => ({ key: `last_${n}_full_years`, ...lastFullYearsRange(n, now) })),\n ];\n}\n"],"mappings":"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","names":[]}
@@ -5,6 +5,7 @@ import { FieldValues, FieldPath, ControllerProps } from 'react-hook-form';
5
5
  import { LabelProps } from '../components/ui.js';
6
6
  import '../components/tooltip.js';
7
7
  import '../hooks/use-anchored-rect.js';
8
+ import '../lib/clipping.js';
8
9
 
9
10
  /** The form's `useForm()` return, spread in: `<Form {...form}>`. react-hook-form's own
10
11
  * `FormProvider`, under the name the shadcn file gave it. */
@@ -0,0 +1,22 @@
1
+ import { FieldValues, FieldPath, UseFormReturn } from 'react-hook-form';
2
+
3
+ type RhfWizardStepOptions<TFieldValues extends FieldValues, TTransformed = TFieldValues> = {
4
+ /** Validate only these fields — the step's share of a form that spans steps. */
5
+ fields: readonly FieldPath<TFieldValues>[];
6
+ /** Called with `form.getValues()` when the fields pass, before the wizard
7
+ * advances. */
8
+ onValid?: (values: TFieldValues) => void;
9
+ } | {
10
+ /** Omitted: validate the whole form through `handleSubmit`. */
11
+ fields?: undefined;
12
+ /** Called with the form's (transformed) values when it passes, before the
13
+ * wizard advances. */
14
+ onValid?: (values: TTransformed) => void;
15
+ };
16
+ /**
17
+ * Connects `form` to the wizard's step validation and returns the step's validator.
18
+ * See the module note for the two modes.
19
+ */
20
+ declare function useRhfWizardStep<TFieldValues extends FieldValues, TContext = unknown, TTransformed = TFieldValues>(form: UseFormReturn<TFieldValues, TContext, TTransformed>, options?: RhfWizardStepOptions<TFieldValues, TTransformed> | ((values: TTransformed) => void)): () => Promise<boolean>;
21
+
22
+ export { type RhfWizardStepOptions, useRhfWizardStep };
@@ -0,0 +1,38 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useRef } from "react";
3
+ import { useOptionalWizardContext } from "../wizard/wizard-context.js";
4
+ function useRhfWizardStep(form, options) {
5
+ const opts = typeof options === "function" ? { onValid: options } : options ?? {};
6
+ const optsRef = useRef(opts);
7
+ useEffect(() => {
8
+ optsRef.current = opts;
9
+ });
10
+ const validate = useCallback(async () => {
11
+ const current = optsRef.current;
12
+ if (current.fields) {
13
+ const ok2 = await form.trigger([...current.fields], { shouldFocus: true });
14
+ if (ok2) current.onValid?.(form.getValues());
15
+ return ok2;
16
+ }
17
+ const onValid = current.onValid;
18
+ let ok = false;
19
+ await form.handleSubmit(
20
+ (values) => {
21
+ ok = true;
22
+ onValid?.(values);
23
+ },
24
+ () => {
25
+ ok = false;
26
+ }
27
+ )();
28
+ return ok;
29
+ }, [form]);
30
+ const wizard = useOptionalWizardContext();
31
+ const register = wizard?.registerStepValidate;
32
+ useEffect(() => register?.(validate), [register, validate]);
33
+ return validate;
34
+ }
35
+ export {
36
+ useRhfWizardStep
37
+ };
38
+ //# sourceMappingURL=use-rhf-wizard-step.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/rhf/use-rhf-wizard-step.ts"],"sourcesContent":["/**\n * The bridge between a react-hook-form step and the wizard's Next gate.\n * `@eifi1/ui-kit/rhf`.\n *\n * ```tsx\n * function AddressStep({ onUpdate }: { onUpdate: (v: Address) => void }) {\n * const form = useForm<Address>({ defaultValues });\n * useRhfWizardStep(form, { onValid: onUpdate });\n * return <Form {...form}>…</Form>;\n * }\n * ```\n *\n * Inside a `<StepperNav>` the hook registers the step's validator with the wizard, so\n * Next (and Finish on a form-bearing last step) runs it: on success the values go to\n * `onValid` and the wizard advances; on failure react-hook-form shows its own inline\n * `<FormMessage>`s, focuses the first field in error, and the wizard stays put — and,\n * since the result carries no per-field errors, says its \"fill in the required\n * fields\" line so the button did not seem to do nothing. It also RETURNS the\n * validator, for a nav of your own outside `<StepperNav>` (where there is no wizard\n * context to register with, so nothing is registered).\n *\n * Two modes:\n *\n * - **The whole form** (no `fields`): `form.handleSubmit`, exactly as kastlan's\n * `use-rhf-wizard-step.ts` — every rule and the resolver run, `onValid` receives\n * the TRANSFORMED values (a zod `.transform()` included), and focus follows the\n * form's own `shouldFocusError` (on by default).\n * - **Some fields** (`fields: [\"iban\", \"bic\"]`): `form.trigger(fields)`, for a wizard\n * that keeps one form across several steps, each of which validates only what it\n * shows. The first of those fields in error is focused (`shouldFocus`; the control\n * has to take `field.ref`), errors on fields of later steps are not raised, and\n * `onValid` receives `form.getValues()` — the untransformed values, because a\n * resolver's output is only produced for the whole form.\n *\n * Both callbacks are read through refs, so the validator is registered once per form\n * however the caller writes them (an inline `(v) => onUpdate(v)` is the usual case).\n *\n * The positional `useRhfWizardStep(form, onValid)` of the hook kastlan kept is the\n * whole-form mode, and still accepted.\n *\n * Why it lives here and not in `@eifi1/ui-kit/wizard`: it imports react-hook-form,\n * which only this entry may (see packaging-contract).\n */\nimport { useCallback, useEffect, useRef } from \"react\";\nimport type { FieldPath, FieldValues, UseFormReturn } from \"react-hook-form\";\nimport { useOptionalWizardContext } from \"../wizard/wizard-context\";\n\nexport type RhfWizardStepOptions<\n TFieldValues extends FieldValues,\n TTransformed = TFieldValues,\n> =\n | {\n /** Validate only these fields — the step's share of a form that spans steps. */\n fields: readonly FieldPath<TFieldValues>[];\n /** Called with `form.getValues()` when the fields pass, before the wizard\n * advances. */\n onValid?: (values: TFieldValues) => void;\n }\n | {\n /** Omitted: validate the whole form through `handleSubmit`. */\n fields?: undefined;\n /** Called with the form's (transformed) values when it passes, before the\n * wizard advances. */\n onValid?: (values: TTransformed) => void;\n };\n\n/**\n * Connects `form` to the wizard's step validation and returns the step's validator.\n * See the module note for the two modes.\n */\nexport function useRhfWizardStep<\n TFieldValues extends FieldValues,\n TContext = unknown,\n TTransformed = TFieldValues,\n>(\n form: UseFormReturn<TFieldValues, TContext, TTransformed>,\n options?:\n | RhfWizardStepOptions<TFieldValues, TTransformed>\n | ((values: TTransformed) => void),\n): () => Promise<boolean> {\n const opts = typeof options === \"function\" ? { onValid: options } : (options ?? {});\n const optsRef = useRef(opts);\n useEffect(() => {\n optsRef.current = opts;\n });\n\n const validate = useCallback(async (): Promise<boolean> => {\n const current = optsRef.current;\n if (current.fields) {\n const ok = await form.trigger([...current.fields], { shouldFocus: true });\n if (ok) current.onValid?.(form.getValues());\n return ok;\n }\n const onValid = current.onValid as ((values: TTransformed) => void) | undefined;\n let ok = false;\n await form.handleSubmit(\n (values) => {\n ok = true;\n onValid?.(values);\n },\n () => {\n ok = false;\n },\n )();\n return ok;\n }, [form]);\n\n const wizard = useOptionalWizardContext();\n const register = wizard?.registerStepValidate;\n useEffect(() => register?.(validate), [register, validate]);\n\n return validate;\n}\n"],"mappings":";AA2CA,SAAS,aAAa,WAAW,cAAc;AAE/C,SAAS,gCAAgC;AAyBlC,SAAS,iBAKd,MACA,SAGwB;AACxB,QAAM,OAAO,OAAO,YAAY,aAAa,EAAE,SAAS,QAAQ,IAAK,WAAW,CAAC;AACjF,QAAM,UAAU,OAAO,IAAI;AAC3B,YAAU,MAAM;AACd,YAAQ,UAAU;AAAA,EACpB,CAAC;AAED,QAAM,WAAW,YAAY,YAA8B;AACzD,UAAM,UAAU,QAAQ;AACxB,QAAI,QAAQ,QAAQ;AAClB,YAAMA,MAAK,MAAM,KAAK,QAAQ,CAAC,GAAG,QAAQ,MAAM,GAAG,EAAE,aAAa,KAAK,CAAC;AACxE,UAAIA,IAAI,SAAQ,UAAU,KAAK,UAAU,CAAC;AAC1C,aAAOA;AAAA,IACT;AACA,UAAM,UAAU,QAAQ;AACxB,QAAI,KAAK;AACT,UAAM,KAAK;AAAA,MACT,CAAC,WAAW;AACV,aAAK;AACL,kBAAU,MAAM;AAAA,MAClB;AAAA,MACA,MAAM;AACJ,aAAK;AAAA,MACP;AAAA,IACF,EAAE;AACF,WAAO;AAAA,EACT,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,SAAS,yBAAyB;AACxC,QAAM,WAAW,QAAQ;AACzB,YAAU,MAAM,WAAW,QAAQ,GAAG,CAAC,UAAU,QAAQ,CAAC;AAE1D,SAAO;AACT;","names":["ok"]}
package/dist/rhf.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export { Form, FormControl, FormControlProps, FormDescription, FormDescriptionProps, FormField, FormItem, FormItemProps, FormLabel, FormLabelProps, FormMessage, FormMessageProps, useFormField } from './rhf/form.js';
2
+ export { RhfWizardStepOptions, useRhfWizardStep } from './rhf/use-rhf-wizard-step.js';
2
3
  import 'react';
3
4
  import 'react-hook-form';
4
5
  import './components/ui.js';
5
6
  import './components/tooltip.js';
6
7
  import './hooks/use-anchored-rect.js';
8
+ import './lib/clipping.js';
package/dist/rhf.js CHANGED
@@ -1,3 +1,4 @@
1
1
  "use client";
2
2
  export * from "./rhf/form.js";
3
+ export * from "./rhf/use-rhf-wizard-step.js";
3
4
  //# sourceMappingURL=rhf.js.map
package/dist/rhf.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/rhf.ts"],"sourcesContent":["// `@eifi1/ui-kit/rhf` — the react-hook-form adapter: Form, FormField, FormItem,\n// FormLabel, FormControl, FormDescription, FormMessage and useFormField.\n//\n// The only entry point in the package that requires `react-hook-form`, and an optional\n// peer for that reason: an app without a form library installs nothing for it. NOT\n// re-exported from the main barrel, which would make the peer everyone's — the reason\n// the barrel dropped its last react-hook-form import (see the wizard section of\n// src/index.ts). The packaging contract test holds that line.\nexport * from \"./rhf/form\";\n"],"mappings":";AAQA,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/rhf.ts"],"sourcesContent":["// `@eifi1/ui-kit/rhf` — the react-hook-form adapter: Form, FormField, FormItem,\n// FormLabel, FormControl, FormDescription, FormMessage and useFormField.\n//\n// The only entry point in the package that requires `react-hook-form`, and an optional\n// peer for that reason: an app without a form library installs nothing for it. NOT\n// re-exported from the main barrel, which would make the peer everyone's — the reason\n// the barrel dropped its last react-hook-form import (see the wizard section of\n// src/index.ts). The packaging contract test holds that line.\nexport * from \"./rhf/form\";\n// The react-hook-form bridge to the wizard's Next gate (useWizard lives in /wizard).\nexport * from \"./rhf/use-rhf-wizard-step\";\n"],"mappings":";AAQA,cAAc;AAEd,cAAc;","names":[]}
@@ -62,6 +62,13 @@ interface CommandPaletteLabels {
62
62
  close?: string;
63
63
  }
64
64
  declare const DEFAULT_COMMAND_PALETTE_LABELS: CommandPaletteLabels;
65
+ /**
66
+ * How tall the palette's rows are. `"compact"` (the default) is the desktop list the
67
+ * palette always drew, 36px a row; `"comfortable"` gives every row the kit's 44px touch
68
+ * target (`min-h-11`) and body-size text — keksdose's phone search, where a 36px row
69
+ * under a thumb picks its neighbour.
70
+ */
71
+ type CommandPaletteDensity = "compact" | "comfortable";
65
72
  interface CommandPaletteProps {
66
73
  open: boolean;
67
74
  onClose: () => void;
@@ -70,6 +77,14 @@ interface CommandPaletteProps {
70
77
  * query is allowed — return defaults like pages/recent). Sync or async; stale
71
78
  * async responses are ignored.
72
79
  *
80
+ * A SYNCHRONOUS provider (one returning an array, not a promise) never shows the
81
+ * "Searching…" hint: its answer is there the moment it is asked, and the hint only
82
+ * flashed through the debounce before results that were never late — keksdose's
83
+ * transaction search filters a list already in memory. The palette tells the two
84
+ * apart by what the provider RETURNS, call by call, so there is no prop to keep in
85
+ * step with the implementation: an async provider shows the hint from the first
86
+ * promise on (and during the debounce of every later query), a sync one never.
87
+ *
73
88
  * Its IDENTITY is not a re-run signal — it is read through a ref, so the palette
74
89
  * never re-searches merely because the caller rebuilt the callback. That is
75
90
  * deliberate: a provider closing over a list rendered from a query allocates a new
@@ -133,6 +148,8 @@ interface CommandPaletteProps {
133
148
  * up. `false` keeps the card on every viewport.
134
149
  */
135
150
  fullScreenOnPhone?: boolean;
151
+ /** Row height — see {@link CommandPaletteDensity}. Default `"compact"`. */
152
+ density?: CommandPaletteDensity;
136
153
  labels?: Partial<CommandPaletteLabels>;
137
154
  }
138
155
  /** Registers a global ⌘K / Ctrl-K shortcut that calls `onOpen`. */
@@ -143,6 +160,6 @@ declare function useCommandKey(onOpen: () => void): void;
143
160
  * results via the `search` provider (pages, actions, records, …); each result
144
161
  * carries its own `onSelect`.
145
162
  */
146
- declare function CommandPalette({ open, onClose, search, revision, query: queryProp, onQueryChange, searchOn, redactLabels, fullScreenOnPhone, labels, }: CommandPaletteProps): react.ReactPortal | null;
163
+ declare function CommandPalette({ open, onClose, search, revision, query: queryProp, onQueryChange, searchOn, redactLabels, fullScreenOnPhone, density, labels, }: CommandPaletteProps): react.ReactPortal | null;
147
164
 
148
- export { type CommandItem, CommandPalette, type CommandPaletteLabels, DEFAULT_COMMAND_PALETTE_LABELS, useCommandKey };
165
+ export { type CommandItem, CommandPalette, type CommandPaletteDensity, type CommandPaletteLabels, DEFAULT_COMMAND_PALETTE_LABELS, useCommandKey };
@@ -35,6 +35,7 @@ const DEFAULT_COMMAND_PALETTE_LABELS = {
35
35
  submit: "Search",
36
36
  close: "Close"
37
37
  };
38
+ const isThenable = (value) => typeof value?.then === "function";
38
39
  const { dialog: DEFAULT_DIALOG_NAME, ...DEFAULTS_WITHOUT_DIALOG } = DEFAULT_COMMAND_PALETTE_LABELS;
39
40
  function useCommandKey(onOpen) {
40
41
  useEffect(() => {
@@ -58,6 +59,7 @@ function CommandPalette({
58
59
  searchOn = "input",
59
60
  redactLabels = false,
60
61
  fullScreenOnPhone = true,
62
+ density = "compact",
61
63
  labels
62
64
  }) {
63
65
  const l = useKitLabels("commandPalette", DEFAULTS_WITHOUT_DIALOG, labels);
@@ -97,6 +99,7 @@ function CommandPalette({
97
99
  const searchRef = useRef(search);
98
100
  searchRef.current = search;
99
101
  const reqId = useRef(0);
102
+ const syncRef = useRef(void 0);
100
103
  const shownQuery = useRef(null);
101
104
  const activeIdRef = useRef(void 0);
102
105
  useOverlayHistory(open, onClose);
@@ -112,10 +115,13 @@ function CommandPalette({
112
115
  useEffect(() => {
113
116
  if (!open) return;
114
117
  const id = ++reqId.current;
115
- setLoading(true);
118
+ if (syncRef.current === false) setLoading(true);
116
119
  const run = async () => {
117
120
  try {
118
- const r = await Promise.resolve(searchRef.current(query));
121
+ const answer = searchRef.current(query);
122
+ syncRef.current = !isThenable(answer);
123
+ if (!syncRef.current) setLoading(true);
124
+ const r = await answer;
119
125
  if (reqId.current === id) {
120
126
  const keep = shownQuery.current === query ? activeIdRef.current : void 0;
121
127
  const at = keep === void 0 ? -1 : groupItems(r).flat.findIndex((item) => item.id === keep);
@@ -169,9 +175,10 @@ function CommandPalette({
169
175
  };
170
176
  useEffect(() => {
171
177
  const el = listRef.current?.querySelector(`[data-index="${active}"]`);
172
- el?.scrollIntoView({ block: "nearest" });
178
+ el?.scrollIntoView?.({ block: "nearest" });
173
179
  }, [active]);
174
180
  if (!open) return null;
181
+ const comfortable = density === "comfortable";
175
182
  let flatIndex = -1;
176
183
  return createPortal(
177
184
  /* @__PURE__ */ jsx(
@@ -282,7 +289,10 @@ function CommandPalette({
282
289
  "li",
283
290
  {
284
291
  role: "presentation",
285
- className: "flex items-center gap-2.5 px-3 py-2 text-xs text-[var(--text-placeholder)]",
292
+ className: cn(
293
+ "flex items-center gap-2.5 px-3 text-xs text-[var(--text-placeholder)]",
294
+ comfortable ? "min-h-11 py-2.5" : "py-2"
295
+ ),
286
296
  children: [
287
297
  item.icon && /* @__PURE__ */ jsx("span", { className: "flex size-4 shrink-0 items-center justify-center", children: item.icon }),
288
298
  /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: item.label })
@@ -302,7 +312,8 @@ function CommandPalette({
302
312
  "data-index": idx,
303
313
  onMouseMove: () => setActive(idx),
304
314
  className: cn(
305
- "flex w-full items-center gap-2.5 px-3 py-2 text-start text-sm",
315
+ "flex w-full items-center gap-2.5 px-3 text-start",
316
+ comfortable ? "min-h-11 py-2.5 text-base" : "py-2 text-sm",
306
317
  isActive ? "bg-[var(--bg-active)] text-[var(--text-primary)]" : "text-[var(--text-secondary)]"
307
318
  )
308
319
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/search/command-palette.tsx"],"sourcesContent":["import { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { CornerDownLeft, Search, X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { PHONE_QUERY } from \"../components/ui\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { useOverlayHistory } from \"../hooks/use-overlay-history\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { useBodyScrollLock } from \"../hooks/use-body-scroll-lock\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\n\nexport interface CommandItem {\n id: string;\n /** Primary text. */\n label: string;\n /** Secondary text on the right (e.g. an amount, a date, a group hint). */\n hint?: string;\n /** Heading this item is grouped under. */\n group: string;\n /** Leading icon / element. */\n icon?: ReactNode;\n /** Invoked when the item is chosen (navigate, run an action, …). */\n onSelect: () => void;\n /** Where this item leads, when it is a navigation rather than an action. Renders\n * the row as a real anchor so it can be middle-/⌘-clicked into a new tab\n * (Keksdose feedback #451); `onSelect` still runs for a plain click and for ↵, so\n * in-app navigation stays client-side. `href` only adds what the browser can do\n * with a link and JavaScript cannot fake — the rejected alternative, an\n * `onAuxClick` calling `window.open`, gets the middle click and nothing else. */\n href?: string;\n /** Marks this row's label and hint `data-private`, so session replay and screenshot\n * tooling masks them — a payee, an account name, an amount. Overrides the palette's\n * {@link CommandPaletteProps.redactLabels} either way, so `false` un-masks one\n * harmless row (\"Settings\") in a palette that masks by default. */\n redact?: boolean;\n /**\n * `\"status\"` makes the row a line of text rather than a choice — a group's\n * \"Searching…\" while its source is still out, or its error. It is skipped by the\n * arrow keys and ↵, has no hover highlight, and its `onSelect` is never called.\n * Default `\"option\"`.\n */\n kind?: \"option\" | \"status\";\n}\n\n/** Group items preserving first-seen group order; `flat` is the navigable rows only. */\nfunction groupItems(items: CommandItem[]) {\n const order: string[] = [];\n const byGroup = new Map<string, CommandItem[]>();\n for (const item of items) {\n if (!byGroup.has(item.group)) {\n byGroup.set(item.group, []);\n order.push(item.group);\n }\n byGroup.get(item.group)!.push(item);\n }\n return {\n groups: order.map((g) => ({ group: g, items: byGroup.get(g)! })),\n flat: order.flatMap((g) => byGroup.get(g)!).filter((item) => item.kind !== \"status\"),\n };\n}\n\nexport interface CommandPaletteLabels {\n placeholder: string;\n empty: string;\n loading: string;\n /** Hint shown in the footer, e.g. \"↑↓ to navigate · ↵ to select · esc to close\". */\n hint?: string;\n /**\n * Accessible name of the dialog itself. It used to borrow `placeholder`, which is\n * written to sit greyed-out in an empty box (\"Search…\", \"Type a command\"), not to\n * name a window — and a reader heard the ellipsis read out as part of the name.\n *\n * Optional, unlike `placeholder`, so a translation annotated as\n * `CommandPaletteLabels` keeps compiling; the default always carries it.\n */\n dialog?: string;\n /**\n * Shown in place of the results when the `search` provider throws or its promise\n * rejects. Optional for the same reason as `dialog`; the default always carries it.\n */\n error?: string;\n /** Names the \"×\" that empties the field. Optional like `dialog`. */\n clear?: string;\n /** Names the button that commits the typed text when `searchOn=\"submit\"`. */\n submit?: string;\n /** Names the close button of the full-screen phone presentation, which has no\n * backdrop to tap and, on a phone, no Escape key. */\n close?: string;\n}\n\nexport const DEFAULT_COMMAND_PALETTE_LABELS: CommandPaletteLabels = {\n placeholder: \"Search…\",\n empty: \"No results\",\n loading: \"Searching…\",\n dialog: \"Search\",\n error: \"Search failed. Try again.\",\n clear: \"Clear search\",\n submit: \"Search\",\n close: \"Close\",\n};\n\n/** The defaults minus `dialog`, so a resolved `dialog` means someone SUPPLIED one —\n * see the dialog's `aria-label` for why that distinction matters. */\nconst { dialog: DEFAULT_DIALOG_NAME, ...DEFAULTS_WITHOUT_DIALOG } = DEFAULT_COMMAND_PALETTE_LABELS;\n\ninterface CommandPaletteProps {\n open: boolean;\n onClose: () => void;\n /**\n * Returns results for a query (called debounced as the user types; an empty\n * query is allowed — return defaults like pages/recent). Sync or async; stale\n * async responses are ignored.\n *\n * Its IDENTITY is not a re-run signal — it is read through a ref, so the palette\n * never re-searches merely because the caller rebuilt the callback. That is\n * deliberate: a provider closing over a list rendered from a query allocates a new\n * function on most renders, and re-running the search on each would make typing\n * stutter. Use {@link CommandPaletteProps.revision} when the DATA behind the\n * provider changes.\n */\n search: (query: string) => CommandItem[] | Promise<CommandItem[]>;\n /**\n * Anything whose change means the current results are out of date — typically the\n * arrays the provider searches over. Compared by identity (as an effect dependency\n * would be), so pass stable references: a `data = []` default allocates a fresh\n * array on every render while a query is pending and would re-search forever.\n *\n * Without it, a provider whose data arrives AFTER the palette opened never gets\n * asked again: the user's last keystroke settles the debounce, the queries resolve a\n * moment later, and the groups they would have contributed are simply absent until\n * another keystroke. That is the ordinary first open on a cold cache.\n */\n revision?: unknown;\n /**\n * The search text, CONTROLLED. Keksdose binds it to the URL's `q`, so a search\n * survives a reload, can be shared as a link, and Back walks through it — none of\n * which state inside the palette can do.\n *\n * Leave it out and the palette keeps its own, as before, starting empty on every\n * open. Given, it is never reset on open: the owner decides what an open shows, and\n * a palette that blanked the URL's `q` each time would fight its own address bar.\n * `onQueryChange` is then how a keystroke gets anywhere, since it is the only writer.\n */\n query?: string;\n /** Every edit of the field's text. Fires in both modes — uncontrolled, it only\n * observes. */\n onQueryChange?: (query: string) => void;\n /**\n * When the typed text becomes THE query — the one `search` runs on and\n * `onQueryChange` reports.\n *\n * - `\"input\"` (default): every keystroke, debounced for the search, as before.\n * - `\"submit\"`: typing edits a draft; the query is committed by ↵ or the submit\n * button beside the field. Until then the results stay on the last committed query\n * and `onQueryChange` stays quiet — which is what an owner binding `query` to the\n * URL's `q` needs, or every keystroke re-filters the page behind and pushes history.\n * ↵ with a draft that differs from the committed query COMMITS; ↵ with nothing new\n * typed chooses the highlighted row, as in `\"input\"` mode. The draft follows any\n * outside change of the committed query (Back through the URL), and an uncommitted\n * draft is dropped when the palette is closed.\n *\n * The clear \"×\" commits the empty query in both modes: an emptied box over results\n * (or a page) still filtered by the old text is the stuck state Keksdose #423 was.\n */\n searchOn?: \"input\" | \"submit\";\n /** Mark every row's label and hint `data-private` (see {@link CommandItem.redact},\n * which overrides it per row). Off by default. */\n redactLabels?: boolean;\n /**\n * Below the phone breakpoint ({@link PHONE_QUERY}) the palette fills the screen:\n * no inset, no rounded panel, the field pinned at the top inside the safe areas and\n * the list scrolling under it, with a close button since there is no backdrop to tap.\n * On by default — a top-centred card at 70vh leaves little room once the keyboard is\n * up. `false` keeps the card on every viewport.\n */\n fullScreenOnPhone?: boolean;\n labels?: Partial<CommandPaletteLabels>;\n}\n\n/** Registers a global ⌘K / Ctrl-K shortcut that calls `onOpen`. */\nexport function useCommandKey(onOpen: () => void): void {\n useEffect(() => {\n const handler = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && (e.key === \"k\" || e.key === \"K\")) {\n e.preventDefault();\n onOpen();\n }\n };\n document.addEventListener(\"keydown\", handler);\n return () => document.removeEventListener(\"keydown\", handler);\n }, [onOpen]);\n}\n\n/**\n * A ⌘K-style command palette: a top-centred modal with a search field and a\n * grouped, keyboard-navigable result list. Domain-free — the app supplies\n * results via the `search` provider (pages, actions, records, …); each result\n * carries its own `onSelect`.\n */\nexport function CommandPalette({\n open,\n onClose,\n search,\n revision,\n query: queryProp,\n onQueryChange,\n searchOn = \"input\",\n redactLabels = false,\n fullScreenOnPhone = true,\n labels,\n}: CommandPaletteProps) {\n const l = useKitLabels(\"commandPalette\", DEFAULTS_WITHOUT_DIALOG, labels);\n // `dialog` is new, and until it existed the dialog was named by `placeholder`. An\n // app that translated `placeholder` and has not heard of `dialog` would otherwise\n // go from a German name to an English \"Search\" on update — so a translated\n // placeholder keeps doing the job until a `dialog` is supplied.\n const dialogName =\n l.dialog ??\n (l.placeholder !== DEFAULT_COMMAND_PALETTE_LABELS.placeholder ? l.placeholder : DEFAULT_DIALOG_NAME);\n const [ownQuery, setOwnQuery] = useState(\"\");\n const controlled = queryProp !== undefined;\n const query = controlled ? queryProp : ownQuery;\n const setQuery = (next: string) => {\n if (!controlled) setOwnQuery(next);\n onQueryChange?.(next);\n };\n const submitMode = searchOn === \"submit\";\n // The field's text in `\"submit\"` mode. It follows the committed query whenever that\n // changes — adjusted during render, React's pattern for state derived from a prop,\n // rather than in an effect that would paint the stale draft first.\n const [draft, setDraft] = useState(query);\n const [draftBase, setDraftBase] = useState(query);\n if (draftBase !== query) {\n setDraftBase(query);\n setDraft(query);\n }\n const fieldText = submitMode ? draft : query;\n const commit = (next: string) => {\n setDraft(next);\n setQuery(next);\n };\n const dirty = submitMode && draft !== query;\n const isPhone = useMediaQuery(PHONE_QUERY, false) && fullScreenOnPhone;\n const [results, setResults] = useState<CommandItem[]>([]);\n const [loading, setLoading] = useState(false);\n const [failed, setFailed] = useState(false);\n const [active, setActive] = useState(0);\n // Per instance: these were the literal \"command-palette-list\" / \"command-item-<id>\",\n // so two palettes on one page (an app-wide one and a scoped one) pointed each\n // other's `aria-controls` and `aria-activedescendant` at the wrong list.\n const uid = useId();\n const listId = `${uid}-list`;\n const itemId = (id: string) => `${uid}-item-${id}`;\n const groupId = (index: number) => `${uid}-group-${index}`;\n const dialogRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const listRef = useRef<HTMLUListElement>(null);\n const searchRef = useRef(search);\n searchRef.current = search;\n const reqId = useRef(0);\n /** The query the results on screen answer, and the row highlighted in them — so a\n * re-run for the SAME query (a `revision` change: a source streaming in) keeps the\n * highlight on its row instead of throwing it back to the top mid-arrow. */\n const shownQuery = useRef<string | null>(null);\n const activeIdRef = useRef<string | undefined>(undefined);\n\n // Back dismisses the palette (Keksdose feedback #172). Declared with the other\n // hooks, above the `if (!open) return null` below — a hook past a conditional\n // return changes the hook order between renders. Passing `open` rather than\n // mounting-while-open is what lets it sit here.\n useOverlayHistory(open, onClose);\n\n // A modal like any other: Tab stays inside, the page behind does not scroll, and\n // focus goes back to whatever opened it — the palette used to drop it on <body>.\n // Initial focus is the field, not the container: typing is the only reason to open it.\n useFocusTrap(dialogRef, { active: open, initialFocus: () => inputRef.current });\n useBodyScrollLock(open);\n\n // Reset when opened — the palette's OWN text only; a controlled query belongs to\n // its owner (see the prop).\n useEffect(() => {\n if (!open) return;\n if (!controlled) setOwnQuery(\"\");\n // An uncommitted draft does not outlive the open it was typed in.\n setDraft(controlled ? query : \"\");\n setActive(0);\n shownQuery.current = null;\n // `controlled` is read at the open, not watched: a caller switching modes while\n // the palette is open is not a reason to wipe what is typed.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open]);\n\n // Debounced, race-safe search.\n useEffect(() => {\n if (!open) return;\n const id = ++reqId.current;\n setLoading(true);\n const run = async () => {\n try {\n const r = await Promise.resolve(searchRef.current(query));\n if (reqId.current === id) {\n const keep = shownQuery.current === query ? activeIdRef.current : undefined;\n const at = keep === undefined ? -1 : groupItems(r).flat.findIndex((item) => item.id === keep);\n shownQuery.current = query;\n setResults(r);\n setFailed(false);\n setActive(Math.max(at, 0));\n }\n } catch (err) {\n // There was no catch: the previous query's results stayed on screen as if they\n // answered this one, and the rejection went unhandled. Clear them and say so.\n if (reqId.current === id) {\n shownQuery.current = null;\n setResults([]);\n setFailed(true);\n setActive(0);\n }\n console.error(\"CommandPalette: search failed\", err);\n } finally {\n if (reqId.current === id) setLoading(false);\n }\n };\n const t = setTimeout(run, query ? 150 : 0);\n return () => clearTimeout(t);\n // `revision`, not `search`: see the prop's docstring. The debounce is re-armed on a\n // revision change, which is right — data arriving is not a keystroke and there is\n // nothing to feel laggy about.\n }, [query, open, revision]);\n\n // Group results, preserving first-seen group order; keep a flat list for nav.\n const { groups, flat } = useMemo(() => groupItems(results), [results]);\n useEffect(() => {\n activeIdRef.current = flat[active]?.id;\n });\n\n const choose = (item: CommandItem | undefined) => {\n if (!item) return;\n onClose();\n item.onSelect();\n };\n\n const onKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActive((i) => Math.min(i + 1, flat.length - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => Math.max(i - 1, 0));\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n if (dirty) commit(draft);\n else choose(flat[active]);\n }\n };\n\n // Escape on the whole dialog, not only the field: once Tab has moved focus to a row\n // (or a footer control), Escape used to do nothing at all.\n const onDialogKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"Escape\") {\n e.preventDefault();\n onClose();\n }\n };\n\n // Keep the active row scrolled into view.\n useEffect(() => {\n const el = listRef.current?.querySelector<HTMLElement>(`[data-index=\"${active}\"]`);\n el?.scrollIntoView({ block: \"nearest\" });\n }, [active]);\n\n if (!open) return null;\n\n let flatIndex = -1;\n return createPortal(\n <div\n className={cn(\n \"fixed inset-0 z-[70] flex items-start justify-center bg-black/40\",\n isPhone ? \"p-0\" : \"p-4 pt-[10vh]\",\n )}\n onMouseDown={(e) => {\n if (e.target === e.currentTarget) onClose();\n }}\n >\n {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- Escape from anywhere in the dialog */}\n <div\n ref={dialogRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={dialogName}\n // The trap's fallback target when the field is not there to take focus.\n tabIndex={-1}\n // Escape bubbling up from anything inside — a dialog is where it belongs.\n onKeyDown={onDialogKeyDown}\n data-fullscreen={isPhone || undefined}\n className={cn(\n \"flex w-full flex-col overflow-hidden bg-[var(--bg-surface)] outline-none\",\n isPhone\n ? \"h-full\"\n : \"max-h-[70vh] max-w-xl rounded-xl border border-[var(--border)] shadow-2xl\",\n )}\n // Full screen means edge to edge, so the notch, the home indicator and the\n // rounded corners are the panel's to keep clear of.\n style={\n isPhone\n ? {\n paddingTop: \"max(0px, env(safe-area-inset-top))\",\n paddingBottom: \"max(0px, env(safe-area-inset-bottom))\",\n paddingLeft: \"max(0px, env(safe-area-inset-left))\",\n paddingRight: \"max(0px, env(safe-area-inset-right))\",\n }\n : undefined\n }\n >\n <div className=\"flex shrink-0 items-center gap-2 border-b border-[var(--border)] px-3\">\n <Search className=\"size-4 shrink-0 text-[var(--text-placeholder)]\" />\n <input\n ref={inputRef}\n role=\"combobox\"\n aria-expanded\n aria-controls={listId}\n aria-activedescendant={flat[active] ? itemId(flat[active].id) : undefined}\n value={fieldText}\n onChange={(e) => (submitMode ? setDraft(e.target.value) : setQuery(e.target.value))}\n onKeyDown={onKeyDown}\n placeholder={l.placeholder}\n enterKeyHint={submitMode ? \"search\" : undefined}\n className={cn(\n \"w-full bg-transparent py-3 outline-none placeholder:text-[var(--text-placeholder)] text-[var(--text-primary)]\",\n // 16px on a phone, or iOS zooms the page in on focus.\n isPhone ? \"text-base\" : \"text-sm\",\n )}\n />\n {loading && <span className=\"shrink-0 text-[11px] text-[var(--text-placeholder)]\">{l.loading}</span>}\n {fieldText !== \"\" && (\n <button\n type=\"button\"\n aria-label={l.clear ?? DEFAULT_COMMAND_PALETTE_LABELS.clear}\n onClick={() => {\n commit(\"\");\n // Clearing starts the next query far more often than it ends this one.\n inputRef.current?.focus();\n }}\n className=\"shrink-0 rounded p-1 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-4\" />\n </button>\n )}\n {dirty && (\n <button\n type=\"button\"\n aria-label={l.submit ?? DEFAULT_COMMAND_PALETTE_LABELS.submit}\n onClick={() => {\n commit(draft);\n inputRef.current?.focus();\n }}\n className=\"shrink-0 rounded p-1 text-[var(--text-secondary)] hover:text-[var(--text-primary)]\"\n >\n <CornerDownLeft className=\"size-4\" />\n </button>\n )}\n {isPhone && (\n <button\n type=\"button\"\n onClick={onClose}\n className=\"-me-1 shrink-0 rounded px-1 py-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)]\"\n >\n {l.close ?? DEFAULT_COMMAND_PALETTE_LABELS.close}\n </button>\n )}\n </div>\n\n {/* The status lines sit outside the listbox: a listbox owns options and groups,\n and \"No results\" is neither. */}\n {failed && !loading && (\n <div role=\"alert\" className=\"px-3 py-6 text-center text-sm text-[var(--text-muted)]\">\n {l.error ?? DEFAULT_COMMAND_PALETTE_LABELS.error}\n </div>\n )}\n {/* `results`, not `flat`: a group whose only row is its \"Searching…\" line is not\n an empty result. */}\n {!failed && results.length === 0 && !loading && (\n <div className=\"px-3 py-6 text-center text-sm text-[var(--text-muted)]\">{l.empty}</div>\n )}\n {/* Listbox grouping: each group is a `role=\"group\"` named by its heading, and\n the list markup in between is presentational so the listbox's children are\n groups and options only — as a plain <li>/<ul> tree a reader announced\n \"list, 2 items\" between options and never tied the heading to its rows. */}\n <ul id={listId} ref={listRef} role=\"listbox\" aria-label={dialogName} className=\"min-h-0 flex-1 overflow-y-auto py-1\">\n {groups.map(({ group, items }, groupIndex) => (\n <li key={group} role=\"presentation\">\n <div\n id={groupId(groupIndex)}\n role=\"presentation\"\n className=\"px-3 pb-0.5 pt-2 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]\"\n >\n {group}\n </div>\n <ul role=\"group\" aria-labelledby={groupId(groupIndex)}>\n {items.map((item) => {\n if (item.kind === \"status\") {\n return (\n <li\n key={item.id}\n role=\"presentation\"\n className=\"flex items-center gap-2.5 px-3 py-2 text-xs text-[var(--text-placeholder)]\"\n >\n {item.icon && <span className=\"flex size-4 shrink-0 items-center justify-center\">{item.icon}</span>}\n <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n </li>\n );\n }\n flatIndex += 1;\n const idx = flatIndex;\n const isActive = idx === active;\n const redact = item.redact ?? redactLabels;\n // Written once and worn by either tag below, so the anchor and the\n // button can never drift apart in looks or in listbox semantics.\n // `role=\"option\"` on an <a href> is fine: the browser's middle-click\n // behaviour keys off the element, not the ARIA role.\n const shared = {\n id: itemId(item.id),\n role: \"option\" as const,\n \"aria-selected\": isActive,\n \"data-index\": idx,\n onMouseMove: () => setActive(idx),\n className: cn(\n \"flex w-full items-center gap-2.5 px-3 py-2 text-start text-sm\",\n isActive\n ? \"bg-[var(--bg-active)] text-[var(--text-primary)]\"\n : \"text-[var(--text-secondary)]\",\n ),\n };\n const inner = (\n <>\n {item.icon && (\n <span className=\"flex size-4 shrink-0 items-center justify-center text-[var(--text-placeholder)]\">\n {item.icon}\n </span>\n )}\n <span data-private={redact || undefined} className=\"min-w-0 flex-1 truncate\">\n {item.label}\n </span>\n {item.hint && (\n <span data-private={redact || undefined} className=\"shrink-0 text-xs text-[var(--text-placeholder)]\">\n {item.hint}\n </span>\n )}\n </>\n );\n return (\n <li key={item.id} role=\"presentation\">\n {item.href ? (\n <a\n {...shared}\n href={item.href}\n draggable={false}\n onClick={(e) => {\n // A modified click belongs to the browser (feedback\n // #451) — and the palette deliberately stays OPEN for\n // it: ⌘-clicking three results in a row is the reason to\n // want this, and closing after the first would undo it.\n if (\n e.button !== 0 ||\n e.metaKey ||\n e.ctrlKey ||\n e.shiftKey ||\n e.altKey\n )\n return;\n e.preventDefault();\n choose(item);\n }}\n >\n {inner}\n </a>\n ) : (\n <button type=\"button\" {...shared} onClick={() => choose(item)}>\n {inner}\n </button>\n )}\n </li>\n );\n })}\n </ul>\n </li>\n ))}\n </ul>\n\n {l.hint && (\n <div className=\"border-t border-[var(--border)] px-3 py-1.5 text-[11px] text-[var(--text-placeholder)]\">\n {l.hint}\n </div>\n )}\n </div>\n </div>,\n document.body,\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/search/command-palette.tsx"],"sourcesContent":["import { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { CornerDownLeft, Search, X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { PHONE_QUERY } from \"../components/ui\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { useOverlayHistory } from \"../hooks/use-overlay-history\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { useBodyScrollLock } from \"../hooks/use-body-scroll-lock\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\n\nexport interface CommandItem {\n id: string;\n /** Primary text. */\n label: string;\n /** Secondary text on the right (e.g. an amount, a date, a group hint). */\n hint?: string;\n /** Heading this item is grouped under. */\n group: string;\n /** Leading icon / element. */\n icon?: ReactNode;\n /** Invoked when the item is chosen (navigate, run an action, …). */\n onSelect: () => void;\n /** Where this item leads, when it is a navigation rather than an action. Renders\n * the row as a real anchor so it can be middle-/⌘-clicked into a new tab\n * (Keksdose feedback #451); `onSelect` still runs for a plain click and for ↵, so\n * in-app navigation stays client-side. `href` only adds what the browser can do\n * with a link and JavaScript cannot fake — the rejected alternative, an\n * `onAuxClick` calling `window.open`, gets the middle click and nothing else. */\n href?: string;\n /** Marks this row's label and hint `data-private`, so session replay and screenshot\n * tooling masks them — a payee, an account name, an amount. Overrides the palette's\n * {@link CommandPaletteProps.redactLabels} either way, so `false` un-masks one\n * harmless row (\"Settings\") in a palette that masks by default. */\n redact?: boolean;\n /**\n * `\"status\"` makes the row a line of text rather than a choice — a group's\n * \"Searching…\" while its source is still out, or its error. It is skipped by the\n * arrow keys and ↵, has no hover highlight, and its `onSelect` is never called.\n * Default `\"option\"`.\n */\n kind?: \"option\" | \"status\";\n}\n\n/** Group items preserving first-seen group order; `flat` is the navigable rows only. */\nfunction groupItems(items: CommandItem[]) {\n const order: string[] = [];\n const byGroup = new Map<string, CommandItem[]>();\n for (const item of items) {\n if (!byGroup.has(item.group)) {\n byGroup.set(item.group, []);\n order.push(item.group);\n }\n byGroup.get(item.group)!.push(item);\n }\n return {\n groups: order.map((g) => ({ group: g, items: byGroup.get(g)! })),\n flat: order.flatMap((g) => byGroup.get(g)!).filter((item) => item.kind !== \"status\"),\n };\n}\n\nexport interface CommandPaletteLabels {\n placeholder: string;\n empty: string;\n loading: string;\n /** Hint shown in the footer, e.g. \"↑↓ to navigate · ↵ to select · esc to close\". */\n hint?: string;\n /**\n * Accessible name of the dialog itself. It used to borrow `placeholder`, which is\n * written to sit greyed-out in an empty box (\"Search…\", \"Type a command\"), not to\n * name a window — and a reader heard the ellipsis read out as part of the name.\n *\n * Optional, unlike `placeholder`, so a translation annotated as\n * `CommandPaletteLabels` keeps compiling; the default always carries it.\n */\n dialog?: string;\n /**\n * Shown in place of the results when the `search` provider throws or its promise\n * rejects. Optional for the same reason as `dialog`; the default always carries it.\n */\n error?: string;\n /** Names the \"×\" that empties the field. Optional like `dialog`. */\n clear?: string;\n /** Names the button that commits the typed text when `searchOn=\"submit\"`. */\n submit?: string;\n /** Names the close button of the full-screen phone presentation, which has no\n * backdrop to tap and, on a phone, no Escape key. */\n close?: string;\n}\n\nexport const DEFAULT_COMMAND_PALETTE_LABELS: CommandPaletteLabels = {\n placeholder: \"Search…\",\n empty: \"No results\",\n loading: \"Searching…\",\n dialog: \"Search\",\n error: \"Search failed. Try again.\",\n clear: \"Clear search\",\n submit: \"Search\",\n close: \"Close\",\n};\n\n/**\n * How tall the palette's rows are. `\"compact\"` (the default) is the desktop list the\n * palette always drew, 36px a row; `\"comfortable\"` gives every row the kit's 44px touch\n * target (`min-h-11`) and body-size text — keksdose's phone search, where a 36px row\n * under a thumb picks its neighbour.\n */\nexport type CommandPaletteDensity = \"compact\" | \"comfortable\";\n\n/** A provider's answer is a promise — the only case with anything to wait for. */\nconst isThenable = (value: unknown): value is PromiseLike<unknown> =>\n typeof (value as PromiseLike<unknown> | null)?.then === \"function\";\n\n/** The defaults minus `dialog`, so a resolved `dialog` means someone SUPPLIED one —\n * see the dialog's `aria-label` for why that distinction matters. */\nconst { dialog: DEFAULT_DIALOG_NAME, ...DEFAULTS_WITHOUT_DIALOG } = DEFAULT_COMMAND_PALETTE_LABELS;\n\ninterface CommandPaletteProps {\n open: boolean;\n onClose: () => void;\n /**\n * Returns results for a query (called debounced as the user types; an empty\n * query is allowed — return defaults like pages/recent). Sync or async; stale\n * async responses are ignored.\n *\n * A SYNCHRONOUS provider (one returning an array, not a promise) never shows the\n * \"Searching…\" hint: its answer is there the moment it is asked, and the hint only\n * flashed through the debounce before results that were never late — keksdose's\n * transaction search filters a list already in memory. The palette tells the two\n * apart by what the provider RETURNS, call by call, so there is no prop to keep in\n * step with the implementation: an async provider shows the hint from the first\n * promise on (and during the debounce of every later query), a sync one never.\n *\n * Its IDENTITY is not a re-run signal — it is read through a ref, so the palette\n * never re-searches merely because the caller rebuilt the callback. That is\n * deliberate: a provider closing over a list rendered from a query allocates a new\n * function on most renders, and re-running the search on each would make typing\n * stutter. Use {@link CommandPaletteProps.revision} when the DATA behind the\n * provider changes.\n */\n search: (query: string) => CommandItem[] | Promise<CommandItem[]>;\n /**\n * Anything whose change means the current results are out of date — typically the\n * arrays the provider searches over. Compared by identity (as an effect dependency\n * would be), so pass stable references: a `data = []` default allocates a fresh\n * array on every render while a query is pending and would re-search forever.\n *\n * Without it, a provider whose data arrives AFTER the palette opened never gets\n * asked again: the user's last keystroke settles the debounce, the queries resolve a\n * moment later, and the groups they would have contributed are simply absent until\n * another keystroke. That is the ordinary first open on a cold cache.\n */\n revision?: unknown;\n /**\n * The search text, CONTROLLED. Keksdose binds it to the URL's `q`, so a search\n * survives a reload, can be shared as a link, and Back walks through it — none of\n * which state inside the palette can do.\n *\n * Leave it out and the palette keeps its own, as before, starting empty on every\n * open. Given, it is never reset on open: the owner decides what an open shows, and\n * a palette that blanked the URL's `q` each time would fight its own address bar.\n * `onQueryChange` is then how a keystroke gets anywhere, since it is the only writer.\n */\n query?: string;\n /** Every edit of the field's text. Fires in both modes — uncontrolled, it only\n * observes. */\n onQueryChange?: (query: string) => void;\n /**\n * When the typed text becomes THE query — the one `search` runs on and\n * `onQueryChange` reports.\n *\n * - `\"input\"` (default): every keystroke, debounced for the search, as before.\n * - `\"submit\"`: typing edits a draft; the query is committed by ↵ or the submit\n * button beside the field. Until then the results stay on the last committed query\n * and `onQueryChange` stays quiet — which is what an owner binding `query` to the\n * URL's `q` needs, or every keystroke re-filters the page behind and pushes history.\n * ↵ with a draft that differs from the committed query COMMITS; ↵ with nothing new\n * typed chooses the highlighted row, as in `\"input\"` mode. The draft follows any\n * outside change of the committed query (Back through the URL), and an uncommitted\n * draft is dropped when the palette is closed.\n *\n * The clear \"×\" commits the empty query in both modes: an emptied box over results\n * (or a page) still filtered by the old text is the stuck state Keksdose #423 was.\n */\n searchOn?: \"input\" | \"submit\";\n /** Mark every row's label and hint `data-private` (see {@link CommandItem.redact},\n * which overrides it per row). Off by default. */\n redactLabels?: boolean;\n /**\n * Below the phone breakpoint ({@link PHONE_QUERY}) the palette fills the screen:\n * no inset, no rounded panel, the field pinned at the top inside the safe areas and\n * the list scrolling under it, with a close button since there is no backdrop to tap.\n * On by default — a top-centred card at 70vh leaves little room once the keyboard is\n * up. `false` keeps the card on every viewport.\n */\n fullScreenOnPhone?: boolean;\n /** Row height — see {@link CommandPaletteDensity}. Default `\"compact\"`. */\n density?: CommandPaletteDensity;\n labels?: Partial<CommandPaletteLabels>;\n}\n\n/** Registers a global ⌘K / Ctrl-K shortcut that calls `onOpen`. */\nexport function useCommandKey(onOpen: () => void): void {\n useEffect(() => {\n const handler = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && (e.key === \"k\" || e.key === \"K\")) {\n e.preventDefault();\n onOpen();\n }\n };\n document.addEventListener(\"keydown\", handler);\n return () => document.removeEventListener(\"keydown\", handler);\n }, [onOpen]);\n}\n\n/**\n * A ⌘K-style command palette: a top-centred modal with a search field and a\n * grouped, keyboard-navigable result list. Domain-free — the app supplies\n * results via the `search` provider (pages, actions, records, …); each result\n * carries its own `onSelect`.\n */\nexport function CommandPalette({\n open,\n onClose,\n search,\n revision,\n query: queryProp,\n onQueryChange,\n searchOn = \"input\",\n redactLabels = false,\n fullScreenOnPhone = true,\n density = \"compact\",\n labels,\n}: CommandPaletteProps) {\n const l = useKitLabels(\"commandPalette\", DEFAULTS_WITHOUT_DIALOG, labels);\n // `dialog` is new, and until it existed the dialog was named by `placeholder`. An\n // app that translated `placeholder` and has not heard of `dialog` would otherwise\n // go from a German name to an English \"Search\" on update — so a translated\n // placeholder keeps doing the job until a `dialog` is supplied.\n const dialogName =\n l.dialog ??\n (l.placeholder !== DEFAULT_COMMAND_PALETTE_LABELS.placeholder ? l.placeholder : DEFAULT_DIALOG_NAME);\n const [ownQuery, setOwnQuery] = useState(\"\");\n const controlled = queryProp !== undefined;\n const query = controlled ? queryProp : ownQuery;\n const setQuery = (next: string) => {\n if (!controlled) setOwnQuery(next);\n onQueryChange?.(next);\n };\n const submitMode = searchOn === \"submit\";\n // The field's text in `\"submit\"` mode. It follows the committed query whenever that\n // changes — adjusted during render, React's pattern for state derived from a prop,\n // rather than in an effect that would paint the stale draft first.\n const [draft, setDraft] = useState(query);\n const [draftBase, setDraftBase] = useState(query);\n if (draftBase !== query) {\n setDraftBase(query);\n setDraft(query);\n }\n const fieldText = submitMode ? draft : query;\n const commit = (next: string) => {\n setDraft(next);\n setQuery(next);\n };\n const dirty = submitMode && draft !== query;\n const isPhone = useMediaQuery(PHONE_QUERY, false) && fullScreenOnPhone;\n const [results, setResults] = useState<CommandItem[]>([]);\n const [loading, setLoading] = useState(false);\n const [failed, setFailed] = useState(false);\n const [active, setActive] = useState(0);\n // Per instance: these were the literal \"command-palette-list\" / \"command-item-<id>\",\n // so two palettes on one page (an app-wide one and a scoped one) pointed each\n // other's `aria-controls` and `aria-activedescendant` at the wrong list.\n const uid = useId();\n const listId = `${uid}-list`;\n const itemId = (id: string) => `${uid}-item-${id}`;\n const groupId = (index: number) => `${uid}-group-${index}`;\n const dialogRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const listRef = useRef<HTMLUListElement>(null);\n const searchRef = useRef(search);\n searchRef.current = search;\n const reqId = useRef(0);\n /** What the provider's last answer was: a promise (`false`), an array (`true`), or\n * not known yet. Only a provider known to be async shows \"Searching…\" while the\n * debounce runs — see `search`. */\n const syncRef = useRef<boolean | undefined>(undefined);\n /** The query the results on screen answer, and the row highlighted in them — so a\n * re-run for the SAME query (a `revision` change: a source streaming in) keeps the\n * highlight on its row instead of throwing it back to the top mid-arrow. */\n const shownQuery = useRef<string | null>(null);\n const activeIdRef = useRef<string | undefined>(undefined);\n\n // Back dismisses the palette (Keksdose feedback #172). Declared with the other\n // hooks, above the `if (!open) return null` below — a hook past a conditional\n // return changes the hook order between renders. Passing `open` rather than\n // mounting-while-open is what lets it sit here.\n useOverlayHistory(open, onClose);\n\n // A modal like any other: Tab stays inside, the page behind does not scroll, and\n // focus goes back to whatever opened it — the palette used to drop it on <body>.\n // Initial focus is the field, not the container: typing is the only reason to open it.\n useFocusTrap(dialogRef, { active: open, initialFocus: () => inputRef.current });\n useBodyScrollLock(open);\n\n // Reset when opened — the palette's OWN text only; a controlled query belongs to\n // its owner (see the prop).\n useEffect(() => {\n if (!open) return;\n if (!controlled) setOwnQuery(\"\");\n // An uncommitted draft does not outlive the open it was typed in.\n setDraft(controlled ? query : \"\");\n setActive(0);\n shownQuery.current = null;\n // `controlled` is read at the open, not watched: a caller switching modes while\n // the palette is open is not a reason to wipe what is typed.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open]);\n\n // Debounced, race-safe search.\n useEffect(() => {\n if (!open) return;\n const id = ++reqId.current;\n if (syncRef.current === false) setLoading(true);\n const run = async () => {\n try {\n const answer = searchRef.current(query);\n syncRef.current = !isThenable(answer);\n if (!syncRef.current) setLoading(true);\n const r = await answer;\n if (reqId.current === id) {\n const keep = shownQuery.current === query ? activeIdRef.current : undefined;\n const at = keep === undefined ? -1 : groupItems(r).flat.findIndex((item) => item.id === keep);\n shownQuery.current = query;\n setResults(r);\n setFailed(false);\n setActive(Math.max(at, 0));\n }\n } catch (err) {\n // There was no catch: the previous query's results stayed on screen as if they\n // answered this one, and the rejection went unhandled. Clear them and say so.\n if (reqId.current === id) {\n shownQuery.current = null;\n setResults([]);\n setFailed(true);\n setActive(0);\n }\n console.error(\"CommandPalette: search failed\", err);\n } finally {\n if (reqId.current === id) setLoading(false);\n }\n };\n const t = setTimeout(run, query ? 150 : 0);\n return () => clearTimeout(t);\n // `revision`, not `search`: see the prop's docstring. The debounce is re-armed on a\n // revision change, which is right — data arriving is not a keystroke and there is\n // nothing to feel laggy about.\n }, [query, open, revision]);\n\n // Group results, preserving first-seen group order; keep a flat list for nav.\n const { groups, flat } = useMemo(() => groupItems(results), [results]);\n useEffect(() => {\n activeIdRef.current = flat[active]?.id;\n });\n\n const choose = (item: CommandItem | undefined) => {\n if (!item) return;\n onClose();\n item.onSelect();\n };\n\n const onKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActive((i) => Math.min(i + 1, flat.length - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => Math.max(i - 1, 0));\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n if (dirty) commit(draft);\n else choose(flat[active]);\n }\n };\n\n // Escape on the whole dialog, not only the field: once Tab has moved focus to a row\n // (or a footer control), Escape used to do nothing at all.\n const onDialogKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"Escape\") {\n e.preventDefault();\n onClose();\n }\n };\n\n // Keep the active row scrolled into view.\n useEffect(() => {\n const el = listRef.current?.querySelector<HTMLElement>(`[data-index=\"${active}\"]`);\n // Optional-called, as in combobox-core: jsdom has no scrollIntoView, and every\n // consumer's tests crashed on the first highlighted row (kastlan stubbed it).\n el?.scrollIntoView?.({ block: \"nearest\" });\n }, [active]);\n\n if (!open) return null;\n\n const comfortable = density === \"comfortable\";\n let flatIndex = -1;\n return createPortal(\n <div\n className={cn(\n \"fixed inset-0 z-[70] flex items-start justify-center bg-black/40\",\n isPhone ? \"p-0\" : \"p-4 pt-[10vh]\",\n )}\n onMouseDown={(e) => {\n if (e.target === e.currentTarget) onClose();\n }}\n >\n {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- Escape from anywhere in the dialog */}\n <div\n ref={dialogRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={dialogName}\n // The trap's fallback target when the field is not there to take focus.\n tabIndex={-1}\n // Escape bubbling up from anything inside — a dialog is where it belongs.\n onKeyDown={onDialogKeyDown}\n data-fullscreen={isPhone || undefined}\n className={cn(\n \"flex w-full flex-col overflow-hidden bg-[var(--bg-surface)] outline-none\",\n isPhone\n ? \"h-full\"\n : \"max-h-[70vh] max-w-xl rounded-xl border border-[var(--border)] shadow-2xl\",\n )}\n // Full screen means edge to edge, so the notch, the home indicator and the\n // rounded corners are the panel's to keep clear of.\n style={\n isPhone\n ? {\n paddingTop: \"max(0px, env(safe-area-inset-top))\",\n paddingBottom: \"max(0px, env(safe-area-inset-bottom))\",\n paddingLeft: \"max(0px, env(safe-area-inset-left))\",\n paddingRight: \"max(0px, env(safe-area-inset-right))\",\n }\n : undefined\n }\n >\n <div className=\"flex shrink-0 items-center gap-2 border-b border-[var(--border)] px-3\">\n <Search className=\"size-4 shrink-0 text-[var(--text-placeholder)]\" />\n <input\n ref={inputRef}\n role=\"combobox\"\n aria-expanded\n aria-controls={listId}\n aria-activedescendant={flat[active] ? itemId(flat[active].id) : undefined}\n value={fieldText}\n onChange={(e) => (submitMode ? setDraft(e.target.value) : setQuery(e.target.value))}\n onKeyDown={onKeyDown}\n placeholder={l.placeholder}\n enterKeyHint={submitMode ? \"search\" : undefined}\n className={cn(\n \"w-full bg-transparent py-3 outline-none placeholder:text-[var(--text-placeholder)] text-[var(--text-primary)]\",\n // 16px on a phone, or iOS zooms the page in on focus.\n isPhone ? \"text-base\" : \"text-sm\",\n )}\n />\n {loading && <span className=\"shrink-0 text-[11px] text-[var(--text-placeholder)]\">{l.loading}</span>}\n {fieldText !== \"\" && (\n <button\n type=\"button\"\n aria-label={l.clear ?? DEFAULT_COMMAND_PALETTE_LABELS.clear}\n onClick={() => {\n commit(\"\");\n // Clearing starts the next query far more often than it ends this one.\n inputRef.current?.focus();\n }}\n className=\"shrink-0 rounded p-1 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-4\" />\n </button>\n )}\n {dirty && (\n <button\n type=\"button\"\n aria-label={l.submit ?? DEFAULT_COMMAND_PALETTE_LABELS.submit}\n onClick={() => {\n commit(draft);\n inputRef.current?.focus();\n }}\n className=\"shrink-0 rounded p-1 text-[var(--text-secondary)] hover:text-[var(--text-primary)]\"\n >\n <CornerDownLeft className=\"size-4\" />\n </button>\n )}\n {isPhone && (\n <button\n type=\"button\"\n onClick={onClose}\n className=\"-me-1 shrink-0 rounded px-1 py-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)]\"\n >\n {l.close ?? DEFAULT_COMMAND_PALETTE_LABELS.close}\n </button>\n )}\n </div>\n\n {/* The status lines sit outside the listbox: a listbox owns options and groups,\n and \"No results\" is neither. */}\n {failed && !loading && (\n <div role=\"alert\" className=\"px-3 py-6 text-center text-sm text-[var(--text-muted)]\">\n {l.error ?? DEFAULT_COMMAND_PALETTE_LABELS.error}\n </div>\n )}\n {/* `results`, not `flat`: a group whose only row is its \"Searching…\" line is not\n an empty result. */}\n {!failed && results.length === 0 && !loading && (\n <div className=\"px-3 py-6 text-center text-sm text-[var(--text-muted)]\">{l.empty}</div>\n )}\n {/* Listbox grouping: each group is a `role=\"group\"` named by its heading, and\n the list markup in between is presentational so the listbox's children are\n groups and options only — as a plain <li>/<ul> tree a reader announced\n \"list, 2 items\" between options and never tied the heading to its rows. */}\n <ul id={listId} ref={listRef} role=\"listbox\" aria-label={dialogName} className=\"min-h-0 flex-1 overflow-y-auto py-1\">\n {groups.map(({ group, items }, groupIndex) => (\n <li key={group} role=\"presentation\">\n <div\n id={groupId(groupIndex)}\n role=\"presentation\"\n className=\"px-3 pb-0.5 pt-2 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]\"\n >\n {group}\n </div>\n <ul role=\"group\" aria-labelledby={groupId(groupIndex)}>\n {items.map((item) => {\n if (item.kind === \"status\") {\n return (\n <li\n key={item.id}\n role=\"presentation\"\n className={cn(\n \"flex items-center gap-2.5 px-3 text-xs text-[var(--text-placeholder)]\",\n comfortable ? \"min-h-11 py-2.5\" : \"py-2\",\n )}\n >\n {item.icon && <span className=\"flex size-4 shrink-0 items-center justify-center\">{item.icon}</span>}\n <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n </li>\n );\n }\n flatIndex += 1;\n const idx = flatIndex;\n const isActive = idx === active;\n const redact = item.redact ?? redactLabels;\n // Written once and worn by either tag below, so the anchor and the\n // button can never drift apart in looks or in listbox semantics.\n // `role=\"option\"` on an <a href> is fine: the browser's middle-click\n // behaviour keys off the element, not the ARIA role.\n const shared = {\n id: itemId(item.id),\n role: \"option\" as const,\n \"aria-selected\": isActive,\n \"data-index\": idx,\n onMouseMove: () => setActive(idx),\n className: cn(\n \"flex w-full items-center gap-2.5 px-3 text-start\",\n comfortable ? \"min-h-11 py-2.5 text-base\" : \"py-2 text-sm\",\n isActive\n ? \"bg-[var(--bg-active)] text-[var(--text-primary)]\"\n : \"text-[var(--text-secondary)]\",\n ),\n };\n const inner = (\n <>\n {item.icon && (\n <span className=\"flex size-4 shrink-0 items-center justify-center text-[var(--text-placeholder)]\">\n {item.icon}\n </span>\n )}\n <span data-private={redact || undefined} className=\"min-w-0 flex-1 truncate\">\n {item.label}\n </span>\n {item.hint && (\n <span data-private={redact || undefined} className=\"shrink-0 text-xs text-[var(--text-placeholder)]\">\n {item.hint}\n </span>\n )}\n </>\n );\n return (\n <li key={item.id} role=\"presentation\">\n {item.href ? (\n <a\n {...shared}\n href={item.href}\n draggable={false}\n onClick={(e) => {\n // A modified click belongs to the browser (feedback\n // #451) — and the palette deliberately stays OPEN for\n // it: ⌘-clicking three results in a row is the reason to\n // want this, and closing after the first would undo it.\n if (\n e.button !== 0 ||\n e.metaKey ||\n e.ctrlKey ||\n e.shiftKey ||\n e.altKey\n )\n return;\n e.preventDefault();\n choose(item);\n }}\n >\n {inner}\n </a>\n ) : (\n <button type=\"button\" {...shared} onClick={() => choose(item)}>\n {inner}\n </button>\n )}\n </li>\n );\n })}\n </ul>\n </li>\n ))}\n </ul>\n\n {l.hint && (\n <div className=\"border-t border-[var(--border)] px-3 py-1.5 text-[11px] text-[var(--text-placeholder)]\">\n {l.hint}\n </div>\n )}\n </div>\n </div>,\n document.body,\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,4 +1,4 @@
1
- import { CommandPaletteLabels } from './command-palette.js';
1
+ import { CommandPaletteDensity, CommandPaletteLabels } from './command-palette.js';
2
2
  import { SearchEntry, SearchIndexOptions } from './search-index.js';
3
3
  import * as react from 'react';
4
4
  import { ReactNode } from 'react';
@@ -77,6 +77,14 @@ interface GlobalSearchProps {
77
77
  sources?: readonly GlobalSearchSource[];
78
78
  /** What the empty query shows, under the `suggestions` heading. */
79
79
  suggestions?: readonly GlobalSearchSuggestion[];
80
+ /**
81
+ * Show each suggested ENTRY under its own `group` instead of all of them under the
82
+ * one `suggestions` heading — keksdose's empty palette listed "Pages" and "Actions"
83
+ * apart, and one "Try" heading over both lost the difference. An entry with no
84
+ * `group` and a query suggestion (`{ query }`) stay under `suggestions`. `groupOrder`
85
+ * orders these groups as it orders the results'. Off by default.
86
+ */
87
+ suggestionsKeepGroups?: boolean;
80
88
  /** Groups listed here come first, in this order; the rest follow in ranking order. */
81
89
  groupOrder?: readonly string[];
82
90
  /** Ranking and capping — `groupLimit` (default 8) applies to every group. */
@@ -113,6 +121,23 @@ interface GlobalSearchProps {
113
121
  hideTriggerOnPhone?: boolean;
114
122
  /** Class for the default trigger button, merged over `TOPBAR_TRIGGER_CLASS`. */
115
123
  triggerClassName?: string;
124
+ /**
125
+ * The default trigger's magnifier, in px. Default 16 (`size-4`), as it always was;
126
+ * 20 matches the kit's other top-bar triggers (`TopbarIconButton` draws `size-5`) and
127
+ * keksdose's top bar, which reached into the button with `[&_svg]:size-5` for it.
128
+ */
129
+ triggerIconSize?: number;
130
+ /**
131
+ * Whether the trigger's accessible name includes the shortcut: `"plain"` (default)
132
+ * names it "Search"; `"withShortcut"` names it by `labels.shortcut(keys)` —
133
+ * "Search (⌘K)" on Apple platforms, "Search (Ctrl K)" elsewhere — the same text its
134
+ * tooltip shows, so a speech-control user ("click Search") and a sighted keyboard user
135
+ * meet the shortcut in the name itself (keksdose C25). `aria-keyshortcuts` is set
136
+ * either way. Also reaches a custom trigger as `triggerProps.label`.
137
+ */
138
+ triggerName?: "plain" | "withShortcut";
139
+ /** The palette's row height — `CommandPalette`'s `density`. Default `"compact"`. */
140
+ density?: CommandPaletteDensity;
116
141
  labels?: Partial<GlobalSearchLabels>;
117
142
  /** Passed through to the palette (`empty`, `loading`, `error`, `hint`, …). */
118
143
  paletteLabels?: Partial<CommandPaletteLabels>;