@eifi1/ui-kit 0.51.4 → 0.51.5

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 (757) hide show
  1. package/README.md +3 -3
  2. package/dist/account/account-settings-labels.js.map +1 -1
  3. package/dist/account/account-settings.js +11 -9
  4. package/dist/account/account-settings.js.map +1 -1
  5. package/dist/account/delete-account-setting.js +56 -65
  6. package/dist/account/delete-account-setting.js.map +1 -1
  7. package/dist/account/email-change-setting-labels.js.map +1 -1
  8. package/dist/account/email-change-setting.js.map +1 -1
  9. package/dist/account/passkeys-setting.js.map +1 -1
  10. package/dist/account/sessions-setting.js.map +1 -1
  11. package/dist/admin/admin-action-confirm.js +1 -3
  12. package/dist/admin/admin-action-confirm.js.map +1 -1
  13. package/dist/admin/admin-action-log.js.map +1 -1
  14. package/dist/admin/admin-area-labels.js +4 -1
  15. package/dist/admin/admin-area-labels.js.map +1 -1
  16. package/dist/admin/admin-overview.js +19 -11
  17. package/dist/admin/admin-overview.js.map +1 -1
  18. package/dist/admin/admin-sections.js.map +1 -1
  19. package/dist/admin/admin-tool-links.d.ts +1 -1
  20. package/dist/admin/admin-tool-links.js +7 -1
  21. package/dist/admin/admin-tool-links.js.map +1 -1
  22. package/dist/admin/invitations-panel.js +2 -13
  23. package/dist/admin/invitations-panel.js.map +1 -1
  24. package/dist/admin/mail-test-card.js.map +1 -1
  25. package/dist/admin/reviewer-scope-editor.js +4 -11
  26. package/dist/admin/reviewer-scope-editor.js.map +1 -1
  27. package/dist/admin/role-select.js.map +1 -1
  28. package/dist/admin/user-roster.js +1 -9
  29. package/dist/admin/user-roster.js.map +1 -1
  30. package/dist/auth/accept-invitation.js.map +1 -1
  31. package/dist/auth/auth-errors.js.map +1 -1
  32. package/dist/auth/forgot-password-form.js +23 -31
  33. package/dist/auth/forgot-password-form.js.map +1 -1
  34. package/dist/auth/register-form.js.map +1 -1
  35. package/dist/auth/reset-password-form.js.map +1 -1
  36. package/dist/auth/sign-in-form.js +13 -5
  37. package/dist/auth/sign-in-form.js.map +1 -1
  38. package/dist/auth/verify-email.js +1 -8
  39. package/dist/auth/verify-email.js.map +1 -1
  40. package/dist/billing/billing-banner.js.map +1 -1
  41. package/dist/billing/billing-labels.js.map +1 -1
  42. package/dist/billing/checkout-processing.js.map +1 -1
  43. package/dist/billing/plan-card.js +91 -85
  44. package/dist/billing/plan-card.js.map +1 -1
  45. package/dist/billing/plan-limit-toast.js.map +1 -1
  46. package/dist/billing/plan-limit.js.map +1 -1
  47. package/dist/billing/plan-price.js.map +1 -1
  48. package/dist/billing/plan-switch.js.map +1 -1
  49. package/dist/billing/subscription-actions.js.map +1 -1
  50. package/dist/bin/eifi1-pay-page.mjs +6 -1
  51. package/dist/components/account-chips.js.map +1 -1
  52. package/dist/components/alert-banner.js +1 -5
  53. package/dist/components/alert-banner.js.map +1 -1
  54. package/dist/components/amount-input.js +29 -12
  55. package/dist/components/amount-input.js.map +1 -1
  56. package/dist/components/authed-image.js.map +1 -1
  57. package/dist/components/autocomplete.js.map +1 -1
  58. package/dist/components/bulk-action-bar.js +1 -4
  59. package/dist/components/bulk-action-bar.js.map +1 -1
  60. package/dist/components/button-group.js.map +1 -1
  61. package/dist/components/button-look.js.map +1 -1
  62. package/dist/components/button.js +1 -7
  63. package/dist/components/button.js.map +1 -1
  64. package/dist/components/calculator.js +5 -5
  65. package/dist/components/calculator.js.map +1 -1
  66. package/dist/components/calendar-heatmap.js +4 -10
  67. package/dist/components/calendar-heatmap.js.map +1 -1
  68. package/dist/components/card.js +1 -8
  69. package/dist/components/card.js.map +1 -1
  70. package/dist/components/chart-zoom-core.js +3 -17
  71. package/dist/components/chart-zoom-core.js.map +1 -1
  72. package/dist/components/chart.d.ts +1 -1
  73. package/dist/components/chart.js +4 -22
  74. package/dist/components/chart.js.map +1 -1
  75. package/dist/components/checkbox-group.js +1 -4
  76. package/dist/components/checkbox-group.js.map +1 -1
  77. package/dist/components/chip-input.js +4 -1
  78. package/dist/components/chip-input.js.map +1 -1
  79. package/dist/components/chip.js +10 -5
  80. package/dist/components/chip.js.map +1 -1
  81. package/dist/components/choice-card.js +58 -74
  82. package/dist/components/choice-card.js.map +1 -1
  83. package/dist/components/column-mapper-labels.js.map +1 -1
  84. package/dist/components/column-mapper.js +167 -227
  85. package/dist/components/column-mapper.js.map +1 -1
  86. package/dist/components/combobox-core.js +1 -4
  87. package/dist/components/combobox-core.js.map +1 -1
  88. package/dist/components/combobox.js +1 -4
  89. package/dist/components/combobox.js.map +1 -1
  90. package/dist/components/confirm-dialog.js.map +1 -1
  91. package/dist/components/copy-button.js.map +1 -1
  92. package/dist/components/country-select.d.ts +5 -4
  93. package/dist/components/country-select.js +3 -19
  94. package/dist/components/country-select.js.map +1 -1
  95. package/dist/components/currency-option-list.js +10 -8
  96. package/dist/components/currency-option-list.js.map +1 -1
  97. package/dist/components/currency-select.js +1 -6
  98. package/dist/components/currency-select.js.map +1 -1
  99. package/dist/components/danger-confirm.js.map +1 -1
  100. package/dist/components/data-table-cells.js.map +1 -1
  101. package/dist/components/data-table-column-settings.js.map +1 -1
  102. package/dist/components/data-table-filter-popover.js +2 -18
  103. package/dist/components/data-table-filter-popover.js.map +1 -1
  104. package/dist/components/data-table-filters.js.map +1 -1
  105. package/dist/components/data-table-fit.js.map +1 -1
  106. package/dist/components/data-table-groups.js +3 -1
  107. package/dist/components/data-table-groups.js.map +1 -1
  108. package/dist/components/data-table-head.js +44 -56
  109. package/dist/components/data-table-head.js.map +1 -1
  110. package/dist/components/data-table-mobile.js +31 -44
  111. package/dist/components/data-table-mobile.js.map +1 -1
  112. package/dist/components/data-table-pagination.js.map +1 -1
  113. package/dist/components/data-table-row-actions.js +19 -16
  114. package/dist/components/data-table-row-actions.js.map +1 -1
  115. package/dist/components/data-table-row.js.map +1 -1
  116. package/dist/components/data-table-shared.js.map +1 -1
  117. package/dist/components/data-table-sort.js.map +1 -1
  118. package/dist/components/data-table-virtual.js +4 -1
  119. package/dist/components/data-table-virtual.js.map +1 -1
  120. package/dist/components/data-table.js +30 -39
  121. package/dist/components/data-table.js.map +1 -1
  122. package/dist/components/date-picker.js +1 -6
  123. package/dist/components/date-picker.js.map +1 -1
  124. package/dist/components/date-range-picker.js.map +1 -1
  125. package/dist/components/description-list.js +10 -10
  126. package/dist/components/description-list.js.map +1 -1
  127. package/dist/components/dialog-frame.js +9 -2
  128. package/dist/components/dialog-frame.js.map +1 -1
  129. package/dist/components/disclosure.js +17 -27
  130. package/dist/components/disclosure.js.map +1 -1
  131. package/dist/components/dropdown.js +25 -35
  132. package/dist/components/dropdown.js.map +1 -1
  133. package/dist/components/empty-state.js.map +1 -1
  134. package/dist/components/entity-combobox.js.map +1 -1
  135. package/dist/components/error-boundary.js.map +1 -1
  136. package/dist/components/field-base.js +5 -5
  137. package/dist/components/field-base.js.map +1 -1
  138. package/dist/components/field-parts.d.ts +1 -1
  139. package/dist/components/field-parts.js +10 -14
  140. package/dist/components/field-parts.js.map +1 -1
  141. package/dist/components/field-sync.js +8 -10
  142. package/dist/components/field-sync.js.map +1 -1
  143. package/dist/components/file-button.js.map +1 -1
  144. package/dist/components/file-dropzone.js.map +1 -1
  145. package/dist/components/floating-panel.js +4 -1
  146. package/dist/components/floating-panel.js.map +1 -1
  147. package/dist/components/focus-ring.js.map +1 -1
  148. package/dist/components/form-actions.js.map +1 -1
  149. package/dist/components/grouped-picker.js +6 -8
  150. package/dist/components/grouped-picker.js.map +1 -1
  151. package/dist/components/hover-menu.js +1 -4
  152. package/dist/components/hover-menu.js.map +1 -1
  153. package/dist/components/iban-input-labels.js.map +1 -1
  154. package/dist/components/icon-button.js +4 -1
  155. package/dist/components/icon-button.js.map +1 -1
  156. package/dist/components/icon-picker.js +2 -7
  157. package/dist/components/icon-picker.js.map +1 -1
  158. package/dist/components/image-grid.js +41 -50
  159. package/dist/components/image-grid.js.map +1 -1
  160. package/dist/components/inline-edit-field.js.map +1 -1
  161. package/dist/components/inline-entity-combobox.js +2 -8
  162. package/dist/components/inline-entity-combobox.js.map +1 -1
  163. package/dist/components/input.js +14 -2
  164. package/dist/components/input.js.map +1 -1
  165. package/dist/components/jump-calendar.js +1 -7
  166. package/dist/components/jump-calendar.js.map +1 -1
  167. package/dist/components/legal-labels.js.map +1 -1
  168. package/dist/components/legal-page.js +14 -2
  169. package/dist/components/legal-page.js.map +1 -1
  170. package/dist/components/legal.js +12 -2
  171. package/dist/components/legal.js.map +1 -1
  172. package/dist/components/lightbox.js.map +1 -1
  173. package/dist/components/line-items.js +26 -36
  174. package/dist/components/line-items.js.map +1 -1
  175. package/dist/components/list.js +22 -10
  176. package/dist/components/list.js.map +1 -1
  177. package/dist/components/measured-grid.js +29 -53
  178. package/dist/components/measured-grid.js.map +1 -1
  179. package/dist/components/menu-item.js.map +1 -1
  180. package/dist/components/mini-calendar.js +13 -31
  181. package/dist/components/mini-calendar.js.map +1 -1
  182. package/dist/components/money-field.js.map +1 -1
  183. package/dist/components/month-picker.js +10 -21
  184. package/dist/components/month-picker.js.map +1 -1
  185. package/dist/components/multi-entity-combobox.js +3 -16
  186. package/dist/components/multi-entity-combobox.js.map +1 -1
  187. package/dist/components/multi-select.js.map +1 -1
  188. package/dist/components/nav-pills.js +10 -1
  189. package/dist/components/nav-pills.js.map +1 -1
  190. package/dist/components/number-field.js.map +1 -1
  191. package/dist/components/number-input.js +1 -7
  192. package/dist/components/number-input.js.map +1 -1
  193. package/dist/components/numpad-sheet.js +1 -4
  194. package/dist/components/numpad-sheet.js.map +1 -1
  195. package/dist/components/one-time-code-input.js +1 -7
  196. package/dist/components/one-time-code-input.js.map +1 -1
  197. package/dist/components/page-contents.js +13 -22
  198. package/dist/components/page-contents.js.map +1 -1
  199. package/dist/components/page-header.js +1 -11
  200. package/dist/components/page-header.js.map +1 -1
  201. package/dist/components/password-strength-labels.js.map +1 -1
  202. package/dist/components/password-strength.js +2 -8
  203. package/dist/components/password-strength.js.map +1 -1
  204. package/dist/components/phone-input.js +42 -57
  205. package/dist/components/phone-input.js.map +1 -1
  206. package/dist/components/picker-sheet.js.map +1 -1
  207. package/dist/components/pie-chart.js +1 -8
  208. package/dist/components/pie-chart.js.map +1 -1
  209. package/dist/components/popover.js.map +1 -1
  210. package/dist/components/progress-bar.js +16 -9
  211. package/dist/components/progress-bar.js.map +1 -1
  212. package/dist/components/qr-code.js +1 -8
  213. package/dist/components/qr-code.js.map +1 -1
  214. package/dist/components/row-actions.js +5 -1
  215. package/dist/components/row-actions.js.map +1 -1
  216. package/dist/components/search-field.js.map +1 -1
  217. package/dist/components/select.js +28 -38
  218. package/dist/components/select.js.map +1 -1
  219. package/dist/components/separator.js +1 -5
  220. package/dist/components/separator.js.map +1 -1
  221. package/dist/components/series-chart-axes.js +4 -1
  222. package/dist/components/series-chart-axes.js.map +1 -1
  223. package/dist/components/series-chart-budget.js.map +1 -1
  224. package/dist/components/series-chart-ticks.js.map +1 -1
  225. package/dist/components/series-chart.js +29 -45
  226. package/dist/components/series-chart.js.map +1 -1
  227. package/dist/components/settings-fields.js +21 -17
  228. package/dist/components/settings-fields.js.map +1 -1
  229. package/dist/components/share-card.js +4 -9
  230. package/dist/components/share-card.js.map +1 -1
  231. package/dist/components/sign-chip.js.map +1 -1
  232. package/dist/components/signature-pad.js +4 -13
  233. package/dist/components/signature-pad.js.map +1 -1
  234. package/dist/components/signed-amount.js +12 -5
  235. package/dist/components/signed-amount.js.map +1 -1
  236. package/dist/components/skeleton.js +2 -17
  237. package/dist/components/skeleton.js.map +1 -1
  238. package/dist/components/slider.js +13 -36
  239. package/dist/components/slider.js.map +1 -1
  240. package/dist/components/sparkline.js.map +1 -1
  241. package/dist/components/stat-tile.js +14 -10
  242. package/dist/components/stat-tile.js.map +1 -1
  243. package/dist/components/swatch-picker.js +14 -4
  244. package/dist/components/swatch-picker.js.map +1 -1
  245. package/dist/components/swipeable-row.d.ts +1 -1
  246. package/dist/components/swipeable-row.js +1 -8
  247. package/dist/components/swipeable-row.js.map +1 -1
  248. package/dist/components/table-skeleton.js +4 -1
  249. package/dist/components/table-skeleton.js.map +1 -1
  250. package/dist/components/table.js +12 -9
  251. package/dist/components/table.js.map +1 -1
  252. package/dist/components/tabs.js +5 -24
  253. package/dist/components/tabs.js.map +1 -1
  254. package/dist/components/text-link.js.map +1 -1
  255. package/dist/components/text.js.map +1 -1
  256. package/dist/components/tile-radio.js +1 -7
  257. package/dist/components/tile-radio.js.map +1 -1
  258. package/dist/components/time-input.js.map +1 -1
  259. package/dist/components/toast.js.map +1 -1
  260. package/dist/components/toggle-group.js +6 -1
  261. package/dist/components/toggle-group.js.map +1 -1
  262. package/dist/components/toggle-legend.js +2 -11
  263. package/dist/components/toggle-legend.js.map +1 -1
  264. package/dist/components/tooltip.js +2 -22
  265. package/dist/components/tooltip.js.map +1 -1
  266. package/dist/components/translation-review-editor.js +1 -4
  267. package/dist/components/translation-review-editor.js.map +1 -1
  268. package/dist/components/translation-review-parts.d.ts +1 -1
  269. package/dist/components/translation-review-parts.js +7 -1
  270. package/dist/components/translation-review-parts.js.map +1 -1
  271. package/dist/components/translation-review.js +18 -7
  272. package/dist/components/translation-review.js.map +1 -1
  273. package/dist/components/tree-view.js +14 -10
  274. package/dist/components/tree-view.js.map +1 -1
  275. package/dist/components/treemap.js +1 -13
  276. package/dist/components/treemap.js.map +1 -1
  277. package/dist/components/trigger-aria.js.map +1 -1
  278. package/dist/components/use-table-state.js +14 -11
  279. package/dist/components/use-table-state.js.map +1 -1
  280. package/dist/components/user-avatar.js +3 -26
  281. package/dist/components/user-avatar.js.map +1 -1
  282. package/dist/demo/demo-start.js +8 -4
  283. package/dist/demo/demo-start.js.map +1 -1
  284. package/dist/eslint/index.js +1 -1
  285. package/dist/feedback/feedback-attachment-labels.js.map +1 -1
  286. package/dist/feedback/feedback-attachment.js +1 -8
  287. package/dist/feedback/feedback-attachment.js.map +1 -1
  288. package/dist/feedback/feedback-capture.d.ts +1 -1
  289. package/dist/feedback/feedback-capture.js +3 -1
  290. package/dist/feedback/feedback-capture.js.map +1 -1
  291. package/dist/feedback/feedback-comment-thread.js +27 -13
  292. package/dist/feedback/feedback-comment-thread.js.map +1 -1
  293. package/dist/feedback/feedback-context.js +1 -4
  294. package/dist/feedback/feedback-context.js.map +1 -1
  295. package/dist/feedback/feedback-dialog.js.map +1 -1
  296. package/dist/feedback/feedback-inbox.js.map +1 -1
  297. package/dist/feedback/feedback-public-list.js +7 -1
  298. package/dist/feedback/feedback-public-list.js.map +1 -1
  299. package/dist/feedback/feedback-record.js +1 -5
  300. package/dist/feedback/feedback-record.js.map +1 -1
  301. package/dist/feedback/feedback-replies.js +25 -10
  302. package/dist/feedback/feedback-replies.js.map +1 -1
  303. package/dist/feedback/feedback-row-detail.js.map +1 -1
  304. package/dist/feedback/feedback-status-undo.js.map +1 -1
  305. package/dist/feedback/feedback-submit-dialog.js.map +1 -1
  306. package/dist/feedback/feedback-table.js +4 -1
  307. package/dist/feedback/feedback-table.js.map +1 -1
  308. package/dist/feedback/feedback-thread.js +2 -13
  309. package/dist/feedback/feedback-thread.js.map +1 -1
  310. package/dist/feedback/feedback-vocabulary.js.map +1 -1
  311. package/dist/feedback/feedback-vote-button.js.map +1 -1
  312. package/dist/hooks/use-anchored-rect.js.map +1 -1
  313. package/dist/hooks/use-document-title.js.map +1 -1
  314. package/dist/hooks/use-file-drop.js.map +1 -1
  315. package/dist/hooks/use-file-picker-labels.js.map +1 -1
  316. package/dist/hooks/use-file-picker.js.map +1 -1
  317. package/dist/hooks/use-focus-trap.js.map +1 -1
  318. package/dist/hooks/use-overlay-history.js +10 -5
  319. package/dist/hooks/use-overlay-history.js.map +1 -1
  320. package/dist/hooks/use-row-swipe.d.ts +1 -1
  321. package/dist/hooks/use-row-swipe.js +1 -6
  322. package/dist/hooks/use-row-swipe.js.map +1 -1
  323. package/dist/i18n/defaults.js +1 -4
  324. package/dist/i18n/defaults.js.map +1 -1
  325. package/dist/i18n/german.js +4 -1
  326. package/dist/i18n/german.js.map +1 -1
  327. package/dist/i18n/kit-labels.js +2 -8
  328. package/dist/i18n/kit-labels.js.map +1 -1
  329. package/dist/i18n/languages.js.map +1 -1
  330. package/dist/i18n/locales/en.js +4 -1
  331. package/dist/i18n/locales/en.js.map +2 -2
  332. package/dist/i18n/locales/es.js +4 -1
  333. package/dist/i18n/locales/es.js.map +1 -1
  334. package/dist/i18n/locales/fr.js +4 -1
  335. package/dist/i18n/locales/fr.js.map +1 -1
  336. package/dist/i18n/locales/hu.js +4 -1
  337. package/dist/i18n/locales/hu.js.map +1 -1
  338. package/dist/i18n/locales/it.js +4 -1
  339. package/dist/i18n/locales/it.js.map +1 -1
  340. package/dist/i18n/locales/zh.js.map +1 -1
  341. package/dist/i18n/plurals.js.map +1 -1
  342. package/dist/i18n/review.js +13 -7
  343. package/dist/i18n/review.js.map +1 -1
  344. package/dist/i18n/shared-labels.js.map +1 -1
  345. package/dist/index.d.ts +16 -16
  346. package/dist/index.js +40 -23
  347. package/dist/index.js.map +1 -1
  348. package/dist/landing/access.js +1 -4
  349. package/dist/landing/access.js.map +1 -1
  350. package/dist/landing/landing-sections.js +35 -38
  351. package/dist/landing/landing-sections.js.map +1 -1
  352. package/dist/landing/page-seo.js.map +1 -1
  353. package/dist/lib/calc.js.map +1 -1
  354. package/dist/lib/clipboard-minus.js.map +1 -1
  355. package/dist/lib/dates.js.map +1 -1
  356. package/dist/lib/decimal-marks.js.map +1 -1
  357. package/dist/lib/device-settings.js +2 -1
  358. package/dist/lib/device-settings.js.map +1 -1
  359. package/dist/lib/direction.js.map +1 -1
  360. package/dist/lib/iban.js.map +1 -1
  361. package/dist/lib/logger.js +1 -5
  362. package/dist/lib/logger.js.map +1 -1
  363. package/dist/lib/person-name.js.map +1 -1
  364. package/dist/lib/phone.js.map +1 -1
  365. package/dist/lib/qr-encode.js +344 -8
  366. package/dist/lib/qr-encode.js.map +1 -1
  367. package/dist/lib/session-refresh.js.map +1 -1
  368. package/dist/lib/table-text.js +15 -3
  369. package/dist/lib/table-text.js.map +1 -1
  370. package/dist/lib/text-match.d.ts +4 -0
  371. package/dist/lib/text-match.js.map +1 -1
  372. package/dist/lib/translation-review.js.map +1 -1
  373. package/dist/notifications/notification-inbox.js +63 -44
  374. package/dist/notifications/notification-inbox.js.map +1 -1
  375. package/dist/notifications/notification-parts.js.map +1 -1
  376. package/dist/notifications/notification-types-setting.js.map +1 -1
  377. package/dist/notifications/notifications-labels.js.map +1 -1
  378. package/dist/notifications/push-device-setting.js +6 -1
  379. package/dist/notifications/push-device-setting.js.map +1 -1
  380. package/dist/notifications/push-devices-setting.js.map +1 -1
  381. package/dist/notifications/push-worker.js.map +1 -1
  382. package/dist/notifications/quiet-hours-setting.js.map +1 -1
  383. package/dist/notifications/use-push-device.js +4 -1
  384. package/dist/notifications/use-push-device.js.map +1 -1
  385. package/dist/offline/pwa-update-prompt.d.ts +1 -1
  386. package/dist/offline/pwa-update-prompt.js +7 -1
  387. package/dist/offline/pwa-update-prompt.js.map +1 -1
  388. package/dist/offline/query-persistence.js.map +1 -1
  389. package/dist/offline.d.ts +1 -1
  390. package/dist/offline.js +6 -1
  391. package/dist/offline.js.map +1 -1
  392. package/dist/pay/pay.css +8 -1
  393. package/dist/rhf/fields.d.ts +1 -2
  394. package/dist/rhf/fields.js +1 -10
  395. package/dist/rhf/fields.js.map +1 -1
  396. package/dist/rhf/form-core.js +1 -4
  397. package/dist/rhf/form-core.js.map +1 -1
  398. package/dist/rhf/line-items.js.map +1 -1
  399. package/dist/rhf/use-rhf-wizard-step.js.map +1 -1
  400. package/dist/search/command-palette.js +71 -62
  401. package/dist/search/command-palette.js.map +1 -1
  402. package/dist/search/global-search.js +34 -5
  403. package/dist/search/global-search.js.map +1 -1
  404. package/dist/search/search-index.js.map +1 -1
  405. package/dist/settings/device-reset-setting.js +1 -6
  406. package/dist/settings/device-reset-setting.js.map +1 -1
  407. package/dist/settings/settings-heading.js.map +1 -1
  408. package/dist/settings/settings-layout.js +9 -20
  409. package/dist/settings/settings-layout.js.map +1 -1
  410. package/dist/settings/use-settings-route.js.map +1 -1
  411. package/dist/shell/app-shell.js +62 -31
  412. package/dist/shell/app-shell.js.map +1 -1
  413. package/dist/shell/auth-layout.js +1 -10
  414. package/dist/shell/auth-layout.js.map +1 -1
  415. package/dist/shell/dev-menu.js +12 -6
  416. package/dist/shell/dev-menu.js.map +1 -1
  417. package/dist/shell/option-switcher-menu.js.map +1 -1
  418. package/dist/shell/sync-status-indicator.js +7 -6
  419. package/dist/shell/sync-status-indicator.js.map +1 -1
  420. package/dist/shell/top-bar-parts-labels.js.map +1 -1
  421. package/dist/shell/topbar-action-menu.js.map +1 -1
  422. package/dist/shell/topbar-controls.js +1 -10
  423. package/dist/shell/topbar-controls.js.map +1 -1
  424. package/dist/shell/tour-menu.js +9 -1
  425. package/dist/shell/tour-menu.js.map +1 -1
  426. package/dist/sync/db.js.map +1 -1
  427. package/dist/sync/engine.js +24 -6
  428. package/dist/sync/engine.js.map +1 -1
  429. package/dist/sync.d.ts +2 -2
  430. package/dist/sync.js +7 -1
  431. package/dist/sync.js.map +1 -1
  432. package/dist/testing/css-variables.js.map +1 -1
  433. package/dist/testing/index.d.ts +1 -1
  434. package/dist/testing/index.js +1 -3
  435. package/dist/testing/index.js.map +1 -1
  436. package/dist/testing/react.js.map +1 -1
  437. package/dist/testing/viewport.js +7 -4
  438. package/dist/testing/viewport.js.map +1 -1
  439. package/dist/theme/contrast-tokens.js +13 -2
  440. package/dist/theme/contrast-tokens.js.map +1 -1
  441. package/dist/theme/palette-derive.d.ts +1 -1
  442. package/dist/theme/palette-derive.js +18 -29
  443. package/dist/theme/palette-derive.js.map +1 -1
  444. package/dist/theme/palette-presets.js.map +1 -1
  445. package/dist/theme/palette-store.js.map +1 -1
  446. package/dist/theme/text-size.js.map +1 -1
  447. package/dist/theme/theme-store.js +1 -3
  448. package/dist/theme/theme-store.js.map +1 -1
  449. package/dist/tour/tour-catalogue.js.map +1 -1
  450. package/dist/tour/tour-dock.js +5 -1
  451. package/dist/tour/tour-dock.js.map +1 -1
  452. package/dist/tour/tour-engine.js +1 -4
  453. package/dist/tour/tour-engine.js.map +1 -1
  454. package/dist/tour/tour.js +21 -12
  455. package/dist/tour/tour.js.map +1 -1
  456. package/dist/tour/use-tour-progress.js.map +1 -1
  457. package/dist/tour/use-tour-url-sync.js.map +1 -1
  458. package/dist/wizard/stepper-nav.js +3 -24
  459. package/dist/wizard/stepper-nav.js.map +1 -1
  460. package/dist/wizard/types.js.map +1 -1
  461. package/dist/wizard/use-wizard.js +24 -28
  462. package/dist/wizard/use-wizard.js.map +1 -1
  463. package/dist/wizard/validation.js.map +1 -1
  464. package/dist/wizard/wizard-context.js.map +1 -1
  465. package/dist/wizard/wizard-step.d.ts +1 -1
  466. package/dist/wizard/wizard-step.js +1 -8
  467. package/dist/wizard/wizard-step.js.map +1 -1
  468. package/dist/wizard/wizard-summary.js.map +1 -1
  469. package/package.json +5 -2
  470. package/src/account/account-settings-labels.ts +1 -2
  471. package/src/account/account-settings.tsx +11 -17
  472. package/src/account/delete-account-setting.tsx +1 -6
  473. package/src/account/email-change-setting-labels.ts +1 -2
  474. package/src/account/email-change-setting.tsx +3 -1
  475. package/src/account/passkeys-setting.tsx +1 -3
  476. package/src/account/sessions-setting.tsx +4 -2
  477. package/src/admin/admin-action-confirm.tsx +4 -7
  478. package/src/admin/admin-action-log.tsx +29 -29
  479. package/src/admin/admin-area-labels.ts +4 -1
  480. package/src/admin/admin-overview.tsx +8 -2
  481. package/src/admin/admin-sections.tsx +3 -1
  482. package/src/admin/admin-tool-links.tsx +7 -1
  483. package/src/admin/invitations-panel.tsx +5 -11
  484. package/src/admin/mail-test-card.tsx +1 -2
  485. package/src/admin/reviewer-scope-editor.tsx +4 -9
  486. package/src/admin/role-select.tsx +4 -2
  487. package/src/admin/user-roster.tsx +2 -11
  488. package/src/auth/accept-invitation.tsx +1 -7
  489. package/src/auth/auth-errors.ts +1 -6
  490. package/src/auth/forgot-password-form.tsx +2 -7
  491. package/src/auth/register-form.tsx +7 -8
  492. package/src/auth/reset-password-form.tsx +2 -5
  493. package/src/auth/sign-in-form.tsx +15 -14
  494. package/src/auth/verify-email.tsx +2 -7
  495. package/src/billing/billing-banner.tsx +3 -7
  496. package/src/billing/billing-labels.ts +6 -4
  497. package/src/billing/checkout-processing.ts +0 -1
  498. package/src/billing/plan-card.tsx +9 -8
  499. package/src/billing/plan-limit-toast.tsx +1 -2
  500. package/src/billing/plan-limit.ts +1 -2
  501. package/src/billing/plan-price.ts +1 -4
  502. package/src/billing/plan-switch.tsx +1 -2
  503. package/src/billing/subscription-actions.tsx +5 -2
  504. package/src/components/account-chips.tsx +1 -7
  505. package/src/components/alert-banner.tsx +2 -8
  506. package/src/components/amount-input.tsx +206 -178
  507. package/src/components/authed-image.tsx +7 -4
  508. package/src/components/autocomplete.tsx +7 -9
  509. package/src/components/bulk-action-bar.tsx +2 -7
  510. package/src/components/button-group.tsx +1 -2
  511. package/src/components/button-look.ts +6 -17
  512. package/src/components/button.tsx +1 -4
  513. package/src/components/calculator.tsx +6 -9
  514. package/src/components/calendar-heatmap.tsx +5 -10
  515. package/src/components/card.tsx +9 -13
  516. package/src/components/chart-zoom-core.tsx +6 -33
  517. package/src/components/chart.tsx +8 -31
  518. package/src/components/checkbox-group.tsx +1 -4
  519. package/src/components/chip-input.tsx +6 -6
  520. package/src/components/chip.tsx +14 -26
  521. package/src/components/choice-card.tsx +4 -18
  522. package/src/components/column-mapper-labels.ts +3 -10
  523. package/src/components/column-mapper.tsx +33 -122
  524. package/src/components/combobox-core.tsx +12 -42
  525. package/src/components/combobox.tsx +2 -6
  526. package/src/components/confirm-dialog.tsx +1 -3
  527. package/src/components/copy-button.tsx +2 -4
  528. package/src/components/country-select.tsx +9 -21
  529. package/src/components/currency-option-list.tsx +13 -14
  530. package/src/components/currency-select.tsx +2 -7
  531. package/src/components/danger-confirm.tsx +8 -9
  532. package/src/components/data-table-cells.tsx +1 -2
  533. package/src/components/data-table-column-settings.tsx +1 -3
  534. package/src/components/data-table-filter-popover.tsx +5 -19
  535. package/src/components/data-table-filters.ts +1 -6
  536. package/src/components/data-table-fit.ts +1 -3
  537. package/src/components/data-table-groups.tsx +3 -1
  538. package/src/components/data-table-head.tsx +27 -45
  539. package/src/components/data-table-mobile.tsx +99 -108
  540. package/src/components/data-table-pagination.tsx +2 -6
  541. package/src/components/data-table-row-actions.tsx +25 -24
  542. package/src/components/data-table-row.tsx +16 -17
  543. package/src/components/data-table-shared.tsx +10 -16
  544. package/src/components/data-table-sort.ts +1 -2
  545. package/src/components/data-table-virtual.ts +9 -4
  546. package/src/components/data-table.tsx +489 -501
  547. package/src/components/date-picker.tsx +1 -7
  548. package/src/components/date-range-picker.tsx +3 -13
  549. package/src/components/description-list.tsx +7 -13
  550. package/src/components/dialog-frame.tsx +12 -3
  551. package/src/components/disclosure.tsx +8 -12
  552. package/src/components/dropdown.tsx +3 -11
  553. package/src/components/empty-state.tsx +1 -3
  554. package/src/components/entity-combobox.tsx +2 -6
  555. package/src/components/error-boundary.tsx +7 -7
  556. package/src/components/field-base.tsx +8 -14
  557. package/src/components/field-parts.tsx +12 -23
  558. package/src/components/field-sync.tsx +7 -11
  559. package/src/components/file-button.tsx +7 -3
  560. package/src/components/file-dropzone.tsx +7 -12
  561. package/src/components/floating-panel.tsx +44 -51
  562. package/src/components/focus-ring.ts +1 -2
  563. package/src/components/form-actions.tsx +1 -4
  564. package/src/components/grouped-picker.tsx +6 -8
  565. package/src/components/hover-menu.tsx +1 -4
  566. package/src/components/iban-input-labels.ts +1 -2
  567. package/src/components/icon-button.tsx +46 -48
  568. package/src/components/icon-picker.tsx +4 -17
  569. package/src/components/image-grid.tsx +1 -6
  570. package/src/components/inline-edit-field.tsx +1 -5
  571. package/src/components/inline-entity-combobox.tsx +6 -21
  572. package/src/components/input.tsx +14 -2
  573. package/src/components/jump-calendar.tsx +2 -6
  574. package/src/components/legal-labels.ts +1 -2
  575. package/src/components/legal-page.tsx +18 -7
  576. package/src/components/legal.tsx +14 -5
  577. package/src/components/lightbox.tsx +1 -3
  578. package/src/components/line-items.tsx +7 -15
  579. package/src/components/list.tsx +29 -25
  580. package/src/components/measured-grid.tsx +8 -30
  581. package/src/components/menu-item.tsx +1 -3
  582. package/src/components/mini-calendar.tsx +129 -159
  583. package/src/components/money-field.tsx +4 -2
  584. package/src/components/month-picker.tsx +14 -21
  585. package/src/components/multi-entity-combobox.tsx +6 -26
  586. package/src/components/multi-select.tsx +5 -9
  587. package/src/components/nav-pills.tsx +10 -2
  588. package/src/components/number-field.tsx +18 -15
  589. package/src/components/number-input.tsx +3 -19
  590. package/src/components/numpad-sheet.tsx +9 -13
  591. package/src/components/one-time-code-input.tsx +6 -11
  592. package/src/components/page-contents.tsx +2 -9
  593. package/src/components/page-header.tsx +5 -10
  594. package/src/components/password-strength-labels.ts +1 -2
  595. package/src/components/password-strength.tsx +6 -9
  596. package/src/components/phone-input.tsx +2 -13
  597. package/src/components/picker-sheet.tsx +2 -5
  598. package/src/components/pie-chart.tsx +6 -12
  599. package/src/components/popover.tsx +2 -1
  600. package/src/components/progress-bar.tsx +16 -25
  601. package/src/components/qr-code.tsx +2 -9
  602. package/src/components/row-actions.tsx +9 -1
  603. package/src/components/search-field.tsx +4 -5
  604. package/src/components/select.tsx +2 -10
  605. package/src/components/separator.tsx +1 -5
  606. package/src/components/series-chart-axes.ts +5 -5
  607. package/src/components/series-chart-budget.ts +1 -4
  608. package/src/components/series-chart-ticks.ts +1 -4
  609. package/src/components/series-chart.tsx +43 -83
  610. package/src/components/settings-fields.tsx +20 -15
  611. package/src/components/share-card.tsx +6 -14
  612. package/src/components/sign-chip.tsx +16 -16
  613. package/src/components/signature-pad.tsx +21 -13
  614. package/src/components/signed-amount.tsx +6 -14
  615. package/src/components/skeleton.tsx +2 -13
  616. package/src/components/slider.tsx +9 -20
  617. package/src/components/sparkline.tsx +2 -13
  618. package/src/components/stat-tile.tsx +13 -24
  619. package/src/components/swatch-picker.tsx +8 -3
  620. package/src/components/swipeable-row.tsx +2 -11
  621. package/src/components/table-skeleton.tsx +4 -1
  622. package/src/components/table.tsx +12 -8
  623. package/src/components/tabs.tsx +24 -39
  624. package/src/components/text-link.tsx +6 -6
  625. package/src/components/text.tsx +1 -2
  626. package/src/components/tile-radio.tsx +6 -19
  627. package/src/components/time-input.tsx +4 -5
  628. package/src/components/toast.tsx +3 -12
  629. package/src/components/toggle-group.tsx +9 -9
  630. package/src/components/toggle-legend.tsx +4 -16
  631. package/src/components/tooltip.tsx +9 -48
  632. package/src/components/translation-review-editor.tsx +4 -4
  633. package/src/components/translation-review-parts.tsx +7 -1
  634. package/src/components/translation-review.tsx +15 -16
  635. package/src/components/tree-view.tsx +17 -20
  636. package/src/components/treemap.tsx +3 -15
  637. package/src/components/trigger-aria.ts +1 -4
  638. package/src/components/use-table-state.ts +16 -17
  639. package/src/components/user-avatar.tsx +4 -21
  640. package/src/demo/demo-start.tsx +8 -4
  641. package/src/feedback/feedback-attachment-labels.ts +2 -1
  642. package/src/feedback/feedback-attachment.tsx +16 -29
  643. package/src/feedback/feedback-capture.ts +3 -1
  644. package/src/feedback/feedback-comment-thread.tsx +9 -6
  645. package/src/feedback/feedback-context.tsx +1 -4
  646. package/src/feedback/feedback-dialog.tsx +3 -4
  647. package/src/feedback/feedback-inbox.tsx +3 -9
  648. package/src/feedback/feedback-public-list.tsx +7 -1
  649. package/src/feedback/feedback-record.ts +6 -14
  650. package/src/feedback/feedback-replies.tsx +10 -5
  651. package/src/feedback/feedback-row-detail.tsx +2 -5
  652. package/src/feedback/feedback-status-undo.ts +1 -4
  653. package/src/feedback/feedback-submit-dialog.tsx +4 -4
  654. package/src/feedback/feedback-table.tsx +12 -8
  655. package/src/feedback/feedback-thread.tsx +4 -16
  656. package/src/feedback/feedback-vocabulary.ts +72 -82
  657. package/src/feedback/feedback-vote-button.tsx +9 -8
  658. package/src/hooks/use-anchored-rect.ts +1 -4
  659. package/src/hooks/use-document-title.ts +3 -1
  660. package/src/hooks/use-file-drop.ts +1 -3
  661. package/src/hooks/use-file-picker-labels.ts +1 -2
  662. package/src/hooks/use-file-picker.tsx +1 -5
  663. package/src/hooks/use-focus-trap.ts +1 -2
  664. package/src/hooks/use-overlay-history.ts +10 -5
  665. package/src/hooks/use-row-swipe.ts +1 -6
  666. package/src/i18n/defaults.ts +1 -4
  667. package/src/i18n/german.ts +33 -49
  668. package/src/i18n/kit-labels.tsx +5 -20
  669. package/src/i18n/languages.ts +2 -8
  670. package/src/i18n/locales/en.ts +11 -31
  671. package/src/i18n/locales/es.ts +35 -49
  672. package/src/i18n/locales/fr.ts +34 -51
  673. package/src/i18n/locales/hu.ts +26 -37
  674. package/src/i18n/locales/it.ts +35 -52
  675. package/src/i18n/locales/zh.ts +14 -32
  676. package/src/i18n/plurals.ts +4 -1
  677. package/src/i18n/review.ts +18 -7
  678. package/src/i18n/shared-labels.ts +1 -2
  679. package/src/index.ts +106 -32
  680. package/src/landing/access.ts +2 -7
  681. package/src/landing/landing-sections.tsx +14 -15
  682. package/src/landing/page-seo.ts +1 -3
  683. package/src/lib/calc.ts +4 -1
  684. package/src/lib/clipboard-minus.ts +4 -1
  685. package/src/lib/dates.ts +2 -10
  686. package/src/lib/decimal-marks.ts +1 -3
  687. package/src/lib/device-settings.ts +2 -1
  688. package/src/lib/direction.ts +1 -3
  689. package/src/lib/iban.ts +89 -9
  690. package/src/lib/logger.ts +1 -5
  691. package/src/lib/person-name.ts +1 -3
  692. package/src/lib/phone.ts +5 -8
  693. package/src/lib/qr-encode.ts +32 -8
  694. package/src/lib/session-refresh.ts +5 -6
  695. package/src/lib/table-text.ts +24 -6
  696. package/src/lib/text-match.ts +15 -3
  697. package/src/lib/translation-review.ts +1 -3
  698. package/src/notifications/notification-inbox.tsx +17 -4
  699. package/src/notifications/notification-parts.tsx +1 -2
  700. package/src/notifications/notification-types-setting.tsx +19 -18
  701. package/src/notifications/notifications-labels.ts +1 -2
  702. package/src/notifications/push-device-setting.tsx +7 -1
  703. package/src/notifications/push-devices-setting.tsx +7 -3
  704. package/src/notifications/push-worker.ts +10 -2
  705. package/src/notifications/quiet-hours-setting.tsx +4 -1
  706. package/src/notifications/use-push-device.ts +4 -1
  707. package/src/offline/pwa-update-prompt.tsx +7 -1
  708. package/src/offline/query-persistence.ts +9 -6
  709. package/src/offline.ts +6 -1
  710. package/src/rhf/fields.tsx +50 -19
  711. package/src/rhf/form-core.tsx +2 -6
  712. package/src/rhf/line-items.tsx +5 -7
  713. package/src/rhf/use-rhf-wizard-step.ts +3 -12
  714. package/src/search/command-palette.tsx +15 -23
  715. package/src/search/global-search.tsx +38 -8
  716. package/src/search/search-index.ts +5 -1
  717. package/src/settings/device-reset-setting.tsx +5 -8
  718. package/src/settings/settings-heading.tsx +3 -1
  719. package/src/settings/settings-layout.tsx +6 -13
  720. package/src/settings/use-settings-route.ts +4 -3
  721. package/src/shell/app-shell.tsx +48 -24
  722. package/src/shell/auth-layout.tsx +1 -6
  723. package/src/shell/dev-menu.tsx +12 -6
  724. package/src/shell/option-switcher-menu.tsx +1 -3
  725. package/src/shell/sync-status-indicator.tsx +14 -12
  726. package/src/shell/top-bar-parts-labels.ts +1 -3
  727. package/src/shell/topbar-action-menu.tsx +9 -8
  728. package/src/shell/topbar-controls.tsx +5 -19
  729. package/src/shell/tour-menu.tsx +9 -1
  730. package/src/sync/db.ts +5 -1
  731. package/src/sync/engine.ts +44 -12
  732. package/src/sync.ts +16 -2
  733. package/src/testing/css-variables.ts +1 -2
  734. package/src/testing/index.ts +1 -6
  735. package/src/testing/react.ts +1 -2
  736. package/src/testing/viewport.ts +9 -9
  737. package/src/theme/contrast-tokens.ts +13 -2
  738. package/src/theme/palette-derive.ts +24 -50
  739. package/src/theme/palette-presets.ts +0 -1
  740. package/src/theme/palette-store.ts +1 -4
  741. package/src/theme/text-size.ts +1 -1
  742. package/src/theme/theme-store.ts +2 -6
  743. package/src/tour/tour-catalogue.tsx +4 -2
  744. package/src/tour/tour-dock.ts +5 -1
  745. package/src/tour/tour-engine.ts +2 -6
  746. package/src/tour/tour.tsx +28 -12
  747. package/src/tour/use-tour-progress.ts +8 -2
  748. package/src/tour/use-tour-url-sync.ts +3 -1
  749. package/src/vite-env.d.ts +1 -4
  750. package/src/wizard/stepper-nav.tsx +10 -35
  751. package/src/wizard/types.ts +1 -4
  752. package/src/wizard/use-wizard.ts +28 -43
  753. package/src/wizard/validation.ts +1 -5
  754. package/src/wizard/wizard-context.tsx +2 -6
  755. package/src/wizard/wizard-step.tsx +1 -8
  756. package/src/wizard/wizard-summary.tsx +2 -6
  757. package/tokens.css +393 -152
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/search/global-search.tsx"],"sourcesContent":["import { lazy, Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { useHref, useInRouterContext, useNavigate } from \"react-router\";\nimport { AlertCircle, Loader2, Search } from \"lucide-react\";\nimport { Tooltip } from \"../components/tooltip\";\nimport { TOPBAR_TRIGGER_CLASS } from \"../shell/topbar-controls\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\n// Types only from ./command-palette, gone from the built module: the palette loads late.\nimport type { CommandItem, CommandPaletteDensity, CommandPaletteLabels } from \"./command-palette\";\nimport { DEFAULT_COMMAND_PALETTE_LABELS } from \"./command-palette-labels\";\nimport { useCommandKey } from \"./use-command-key\";\nimport { createSearchIndex } from \"./search-index\";\nimport type { SearchEntry, SearchIndexOptions } from \"./search-index\";\nimport { DEFAULT_GLOBAL_SEARCH_LABELS } from \"./global-search-labels\";\n\n/**\n * The palette, loaded when the search is first opened rather than with the top bar\n * (0.34, MOD-1 of the refactor audit 2026-10): ~5.5 kB gzip of keksdose's and kastlan's\n * entry chunks, for a dialog drawn only once somebody searches. A LITERAL relative\n * `import()`, as the catalogue loaders are (i18n/languages.ts), so the library build keeps\n * the module its own and an app's bundler splits it off.\n */\nconst LazyCommandPalette = lazy(() =>\n import(\"./command-palette\").then((module) => ({ default: module.CommandPalette })),\n);\n\nexport interface GlobalSearchLabels {\n /** The trigger's accessible name, and the dialog's. */\n trigger: string;\n /** The field's placeholder — says WHAT can be found, which \"Search…\" does not. */\n placeholder: string;\n /** The trigger's tooltip, given the shortcut as the platform writes it (\"⌘K\",\n * \"Ctrl K\"). A function so a language can put the keys where its grammar wants. */\n shortcut: (keys: string) => string;\n /** Heading of the suggestions shown for an empty query. */\n suggestions: string;\n /** Heading for entries that name no `group`. */\n results: string;\n}\n\n// Defined in ./global-search-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_GLOBAL_SEARCH_LABELS };\n\n/**\n * A place results come from that is not the static index — a server search, a\n * local-first store too large to hand over whole. Each source is its own group, with its\n * own debounce, its own \"Searching…\" line and its own failure: the static groups never\n * wait for it, and one source failing never takes another's results with it.\n */\nexport interface GlobalSearchSource {\n /** Stable key — the effect that runs the source is keyed by it, not by identity. */\n id: string;\n /** The group its results are shown under (an entry's own `group` is ignored) — unless\n * `groups` is set, where it is only the home of entries that name no declared group.\n * If every entry names a declared group, it is never shown: a catch-all heading such\n * as \"Other\" is the honest value, so a stray type would still surface, labelled. */\n group: string;\n /**\n * Split ONE request into several groups — kastlan's record search answers with\n * properties, units, contacts, leases and invoices at once, and one source per group\n * meant a dedupe layer so the network still saw one request. Each entry is then shown\n * under the group its own `group` names: a declared `key` (shown by its `label`) or a\n * plain string (key and heading at once). The groups appear in THIS order whatever the\n * answer's order, each kept to `limit` rows; an entry naming no declared group falls\n * under `group`. While the request runs, or when it fails, the source shows one status\n * line under its first group, not one per group. Unset: one group, as before.\n */\n groups?: readonly (string | GlobalSearchSourceGroup)[];\n /** Called with the trimmed query. Its results are shown AS GIVEN — the source ranked\n * them — and never re-filtered by the index's matcher. `signal` aborts when the query\n * moves on or the palette closes; a stale answer is dropped either way. */\n search: (query: string, signal: AbortSignal) => Promise<readonly SearchEntry[]> | readonly SearchEntry[];\n /** Shortest query it is asked for. Default 2 — one letter matches half a database. */\n minChars?: number;\n /** Quiet period after the last keystroke before it is asked. Default 200ms. */\n debounceMs?: number;\n /** Rows kept — per group, when `groups` is set. Default: `indexOptions.groupLimit` (8). */\n limit?: number;\n /** Mask every row of this source for session replay, hints (amounts) included. An\n * entry's own `redact` still wins. */\n redact?: boolean;\n /** Told when `search` throws or rejects (not when it is aborted). The group then\n * shows the palette's `error` line instead of rows. */\n onError?: (error: unknown) => void;\n}\n\n/** One of a multi-group source's groups: the `key` its entries name in `group`, and the\n * heading shown — so a server's type (\"unit\") need not be translated per entry. */\nexport interface GlobalSearchSourceGroup {\n key: string;\n label: string;\n}\n\n/** What a custom trigger gets: everything the default button uses. */\nexport interface GlobalSearchTriggerProps {\n open: () => void;\n /** Resolved `trigger` label — the accessible name. */\n label: string;\n /** Resolved `shortcut(keys)` — the tooltip. */\n tooltip: string;\n /** \"⌘K\" on Apple platforms, \"Ctrl K\" elsewhere. */\n keys: string;\n /** For the trigger's `aria-keyshortcuts`. */\n ariaKeyShortcuts: string;\n}\n\n/** A suggestion for the empty query: an entry, the `id` of one in `entries`, or a query\n * to type into the field. */\nexport type GlobalSearchSuggestion = SearchEntry | string | { query: string };\n\nexport interface GlobalSearchProps {\n /**\n * The static index: pages, actions, settings rows — already filtered by the app (by\n * role, by privacy mode). Matched and ranked by {@link createSearchIndex}.\n *\n * Changing it re-runs the open search by itself. Nothing has to be stable: the index is\n * rebuilt only when the entries' TEXT changes (ids, titles, keywords, …), so a list\n * rebuilt on every render — `data = []` while a query is pending, icons as fresh JSX —\n * neither re-indexes nor loops, and the icons and handlers used are always the latest.\n */\n entries?: readonly SearchEntry[];\n /** The async sources, each streamed into its own group — or several (`groups`). */\n sources?: readonly GlobalSearchSource[];\n /** What the empty query shows, under the `suggestions` heading. */\n suggestions?: readonly GlobalSearchSuggestion[];\n /**\n * Show each suggested ENTRY under its own `group` instead of all of them under the\n * one `suggestions` heading — keksdose's empty palette listed \"Pages\" and \"Actions\"\n * apart, and one \"Try\" heading over both lost the difference. An entry with no\n * `group` and a query suggestion (`{ query }`) stay under `suggestions`. `groupOrder`\n * orders these groups as it orders the results'. Off by default.\n */\n suggestionsKeepGroups?: boolean;\n /** Groups listed here come first, in this order; the rest follow in ranking order. */\n groupOrder?: readonly string[];\n /** Ranking and capping — `groupLimit` (default 8) applies to every group. */\n indexOptions?: SearchIndexOptions;\n /**\n * How an entry's `href` is followed on a plain click or ↵. Default: react-router's\n * `navigate` when the search is rendered inside a router, else a full page load.\n */\n navigate?: (href: string) => void;\n /**\n * The browser URL of an `href`, for the row's real link (middle-/⌘-click opens a tab).\n * Default inside a router: the router's own (`#/page` under a hash router, the\n * basename prefixed under a browser router); outside one, the href itself.\n */\n hrefFor?: (href: string) => string;\n /** Register ⌘K / Ctrl K. Default true; turn it off for a second, scoped search on a\n * page that already has the app-wide one. */\n shortcut?: boolean;\n /** Controlled open state — so a button anywhere can open it. */\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n /** Controlled query, as on `CommandPalette` (keksdose binds it to the URL's `q`). */\n query?: string;\n onQueryChange?: (query: string) => void;\n searchOn?: \"input\" | \"submit\";\n /** Mask every row for session replay (see `CommandPalette`). A source's or an entry's\n * own `redact` wins. */\n redactLabels?: boolean;\n fullScreenOnPhone?: boolean;\n /** `\"button\"` (default): the top-bar icon button. `\"none\"`: no trigger — the shortcut\n * or a controlled `open` opens it. A function renders a trigger of your own. */\n trigger?: \"button\" | \"none\" | ((props: GlobalSearchTriggerProps) => ReactNode);\n /** Hide the default trigger below `sm` (kastlan's phone top bar has no room). */\n hideTriggerOnPhone?: boolean;\n /** Class for the default trigger button, merged over `TOPBAR_TRIGGER_CLASS`. */\n triggerClassName?: string;\n /**\n * The default trigger's magnifier, in px. Default 16 (`size-4`), as it always was;\n * 20 matches the kit's other top-bar triggers (`TopbarIconButton` draws `size-5`) and\n * keksdose's top bar, which reached into the button with `[&_svg]:size-5` for it.\n */\n triggerIconSize?: number;\n /**\n * Whether the trigger's accessible name includes the shortcut: `\"plain\"` (default)\n * names it \"Search\"; `\"withShortcut\"` names it by `labels.shortcut(keys)` —\n * \"Search (⌘K)\" on Apple platforms, \"Search (Ctrl K)\" elsewhere — the same text its\n * tooltip shows, so a speech-control user (\"click Search\") and a sighted keyboard user\n * meet the shortcut in the name itself (keksdose C25). `aria-keyshortcuts` is set\n * either way. Also reaches a custom trigger as `triggerProps.label`.\n */\n triggerName?: \"plain\" | \"withShortcut\";\n /** The palette's row height — `CommandPalette`'s `density`. Default `\"compact\"`. */\n density?: CommandPaletteDensity;\n labels?: LabelOverride<GlobalSearchLabels>;\n /** Passed through to the palette (`empty`, `loading`, `error`, `hint`, …). */\n paletteLabels?: Partial<CommandPaletteLabels>;\n /** 0.43: `data-*` attributes go on the kit's trigger button (`trigger=\"button\"`) — a\n * tour's anchor (`data-tour=\"search\"`), a test hook. A function trigger is the app's. */\n [key: `data-${string}`]: string | number | boolean | undefined;\n}\n\nfunction isApplePlatform(): boolean {\n if (typeof navigator === \"undefined\") return false;\n const nav = navigator as Navigator & { userAgentData?: { platform?: string } };\n return /mac|iphone|ipad|ipod/i.test(nav.userAgentData?.platform || nav.platform || nav.userAgent);\n}\n\nconst pageLoad = (href: string) => window.location.assign(href);\nconst identity = (href: string) => href;\n\n/** The entries' text, which is all the index reads — see `entries`. */\nfunction signature(entries: readonly SearchEntry[]): string {\n return entries\n .map((e) =>\n [e.id, e.title, (e.keywords ?? []).join(\"\\u0003\"), e.description, e.group, e.hint, e.href, e.weight, e.redact].join(\n \"\\u0001\",\n ),\n )\n .join(\"\\u0002\");\n}\n\n/** Focus the open palette's field again — after a suggestion typed itself into it, the\n * row that was clicked (and held focus) is gone. */\nfunction refocusField() {\n requestAnimationFrame(() => {\n const fields = document.querySelectorAll<HTMLInputElement>('[role=\"dialog\"][aria-modal=\"true\"] input[role=\"combobox\"]');\n fields[fields.length - 1]?.focus();\n });\n}\n\ninterface SourceResult {\n query: string;\n status: \"loading\" | \"done\" | \"error\";\n entries: readonly SearchEntry[];\n}\n\n/**\n * The app-wide ⌘K search, whole: the top-bar trigger, the shortcut, and a\n * {@link CommandPalette} fed from a ranked index ({@link createSearchIndex}) and any\n * number of async sources.\n *\n * Every consumer had built this out of `CommandPalette` by hand, and each copy had the\n * same holes — a substring match, no ranking, no typo tolerance, results in declaration\n * order, one monolithic provider in which the static groups waited on the slowest\n * request. This is that wrapper once:\n *\n * <GlobalSearch\n * entries={pages} // { id, title, keywords, group, href, icon }\n * sources={[{ id: \"tx\", group: \"Transactions\", search: (q, signal) => api.search(q, { signal }) }]}\n * suggestions={[\"page-budget\", \"action-new-account\"]}\n * />\n *\n * Inside a react-router tree it navigates with the router and builds its links with it;\n * outside one, pass `navigate` (or accept full page loads).\n *\n * Since 0.34 the palette's module loads on the first open (MOD-1), so the dialog appears a\n * moment after the trigger or ⌘K: a test finds it with `await screen.findBy…`.\n */\nexport function GlobalSearch(props: GlobalSearchProps) {\n // Two components rather than a conditional hook: `useNavigate` throws outside a\n // router, and whether we are in one does not change over a component's life.\n const inRouter = useInRouterContext();\n if (inRouter && (!props.navigate || !props.hrefFor)) return <RoutedGlobalSearch {...props} />;\n return <GlobalSearchImpl {...props} navigate={props.navigate ?? pageLoad} hrefFor={props.hrefFor ?? identity} />;\n}\n\nfunction RoutedGlobalSearch(props: GlobalSearchProps) {\n const routerNavigate = useNavigate();\n // The router's href for \"/\" is its base (\"#/\" under a hash router, \"/app/\" under a\n // basename); every href is that base plus the path.\n const base = useHref(\"/\").replace(/\\/$/, \"\");\n const navigate = useCallback((href: string) => void routerNavigate(href), [routerNavigate]);\n const hrefFor = useCallback((href: string) => (href.startsWith(\"/\") ? base + href : href), [base]);\n return <GlobalSearchImpl {...props} navigate={props.navigate ?? navigate} hrefFor={props.hrefFor ?? hrefFor} />;\n}\n\nfunction GlobalSearchImpl({\n entries,\n sources,\n suggestions,\n suggestionsKeepGroups = false,\n groupOrder,\n indexOptions,\n navigate,\n hrefFor,\n shortcut = true,\n open: openProp,\n onOpenChange,\n query: queryProp,\n onQueryChange,\n searchOn,\n redactLabels = false,\n fullScreenOnPhone,\n trigger = \"button\",\n hideTriggerOnPhone = false,\n triggerClassName,\n triggerIconSize,\n triggerName = \"plain\",\n density,\n labels,\n paletteLabels,\n ...attributes\n}: GlobalSearchProps & { navigate: (href: string) => void; hrefFor: (href: string) => string }) {\n const l = useKitLabels(\"globalSearch\", DEFAULT_GLOBAL_SEARCH_LABELS, labels);\n const pl = useKitLabels(\"commandPalette\", DEFAULT_COMMAND_PALETTE_LABELS, paletteLabels);\n const [ownOpen, setOwnOpen] = useState(false);\n const open = openProp ?? ownOpen;\n const [ownQuery, setOwnQuery] = useState(\"\");\n const queryControlled = queryProp !== undefined;\n const query = queryControlled ? queryProp : ownQuery;\n const q = query.trim();\n\n const setOpen = useCallback(\n (next: boolean) => {\n if (openProp === undefined) setOwnOpen(next);\n onOpenChange?.(next);\n },\n [openProp, onOpenChange],\n );\n const setQuery = useCallback(\n (next: string) => {\n if (!queryControlled) setOwnQuery(next);\n onQueryChange?.(next);\n },\n [queryControlled, onQueryChange],\n );\n\n // Each open starts blank — unless the owner controls the query (a URL `q`). Adjusted\n // during render, React's pattern for state that follows a prop, so a controlled `open`\n // gets the same reset as the trigger does.\n const [wasOpen, setWasOpen] = useState(open);\n if (wasOpen !== open) {\n setWasOpen(open);\n if (open && !queryControlled) setOwnQuery(\"\");\n }\n\n // The palette mounts on the first open — its module loads then — and stays mounted\n // after, closed, as it always was (MOD-1): a closed palette keeps the last results, and\n // a reopen over an owner-held query shows them at once, where a palette mounted anew\n // would say \"No results\" through its debounce first.\n const [paletteMounted, setPaletteMounted] = useState(open);\n if (open && !paletteMounted) setPaletteMounted(true);\n\n const openSearch = useCallback(() => setOpen(true), [setOpen]);\n const noop = useCallback(() => {}, []);\n useCommandKey(shortcut ? openSearch : noop);\n\n /* ── the static index ─────────────────────────────────────────────────── */\n\n // Everything below is keyed by CONTENT, never by the identity of what the app passed:\n // inline arrays and `data = []` defaults are the norm, and an identity key would\n // re-index and re-search on every render of the app. Handlers, which can close over\n // anything, are read from refs at the moment a row is chosen — never stale.\n const list = entries ?? EMPTY;\n const sig = signature(list);\n const optionsKey = JSON.stringify(indexOptions ?? {});\n const groupLimit = indexOptions?.groupLimit ?? 8;\n // `sig` and `optionsKey` stand for `list` and `indexOptions`: same text, same index.\n /* eslint-disable react-hooks/exhaustive-deps */\n const index = useMemo(() => createSearchIndex(list, indexOptions), [sig, optionsKey]);\n const byId = useMemo(() => new Map(list.map((e) => [e.id, e])), [sig]);\n\n const sourceKey = (sources ?? [])\n .map((s) =>\n [\n s.id,\n s.group,\n (s.groups ?? []).map((g) => (typeof g === \"string\" ? g : `${g.key}\\u0003${g.label}`)).join(\"\\u0004\"),\n s.minChars ?? 2,\n s.debounceMs ?? 200,\n s.limit,\n s.redact,\n ].join(\"\\u0001\"),\n )\n .join(\"\\u0002\");\n const sourceList = useMemo(() => sources ?? [], [sourceKey]);\n\n const suggestionsKey = (suggestions ?? [])\n .map((s) => (typeof s === \"string\" ? `i:${s}` : \"query\" in s ? `q:${s.query}` : `e:${signature([s])}`))\n .join(\"\\u0002\");\n const suggestionList = useMemo(() => suggestions ?? [], [suggestionsKey]);\n\n const groupOrderKey = (groupOrder ?? []).join(\"\\u0002\");\n const order = useMemo(() => groupOrder ?? [], [groupOrderKey]);\n /* eslint-enable react-hooks/exhaustive-deps */\n\n // The live objects, for the moment a row is chosen or a source is asked.\n const live = useRef({ entries: list, sources: sources ?? [], suggestions: suggestions ?? [], navigate });\n useLayoutEffect(() => {\n live.current = { entries: list, sources: sources ?? [], suggestions: suggestions ?? [], navigate };\n });\n\n /* ── the async sources ────────────────────────────────────────────────── */\n\n const [sourceResults, setSourceResults] = useState<Record<string, SourceResult>>({});\n useEffect(() => {\n const controllers: AbortController[] = [];\n const timers: ReturnType<typeof setTimeout>[] = [];\n const asked = open ? live.current.sources.filter((s) => q.length >= (s.minChars ?? 2)) : [];\n for (const source of asked) {\n const id = source.id;\n const controller = new AbortController();\n controllers.push(controller);\n timers.push(\n setTimeout(async () => {\n const settle = (result: SourceResult) => {\n if (!controller.signal.aborted) setSourceResults((prev) => ({ ...prev, [id]: result }));\n };\n try {\n const found = await source.search(q, controller.signal);\n settle({ query: q, status: \"done\", entries: found });\n } catch (error) {\n if (controller.signal.aborted) return;\n source.onError?.(error);\n settle({ query: q, status: \"error\", entries: [] });\n }\n }, source.debounceMs ?? 200),\n );\n }\n return () => {\n timers.forEach(clearTimeout);\n controllers.forEach((c) => c.abort());\n };\n }, [open, q, sourceKey]);\n\n /* ── rows ─────────────────────────────────────────────────────────────── */\n\n const toItem = useCallback(\n (prefix: string, entry: SearchEntry, group: string, redact = redactLabels): CommandItem => ({\n id: `${prefix}${entry.id}`,\n label: entry.title,\n hint: entry.hint,\n group,\n icon: entry.icon,\n redact: entry.redact ?? redact,\n href: entry.href !== undefined ? hrefFor(entry.href) : undefined,\n onSelect: () => {\n // The entry as the app has it NOW — its handler may close over newer state\n // than the one this row was built from.\n const current = live.current.entries.find((e) => e.id === entry.id) ?? entry;\n if (current.onSelect) current.onSelect();\n else if (current.href !== undefined) live.current.navigate(current.href);\n },\n }),\n [hrefFor, redactLabels],\n );\n\n // `toItem` only BUILDS `onSelect` closures here; `live` is read when a row is chosen,\n // which the compiler lint cannot tell from a read during render.\n /* eslint-disable react-hooks/refs */\n const items = useMemo((): CommandItem[] => {\n const ordered = (out: CommandItem[]) => {\n if (!order.length) return out;\n const rank = (group: string) => {\n const i = order.indexOf(group);\n return i === -1 ? order.length : i;\n };\n // Stable: within a rank, groups keep the order their best hit gave them.\n return out\n .map((item, i) => ({ item, i }))\n .sort((a, b) => rank(a.item.group) - rank(b.item.group) || a.i - b.i)\n .map(({ item }) => item);\n };\n\n if (!q) {\n const out: CommandItem[] = [];\n const groupOf = (entry: SearchEntry) =>\n suggestionsKeepGroups ? (entry.group ?? l.suggestions) : l.suggestions;\n suggestionList.forEach((s, i) => {\n if (typeof s === \"string\") {\n const entry = byId.get(s);\n if (entry) out.push(toItem(\"try:\", entry, groupOf(entry)));\n } else if (\"query\" in s) {\n out.push({\n id: `try:q${i}`,\n label: s.query,\n group: l.suggestions,\n icon: <Search className=\"size-4\" />,\n onSelect: () => {\n // The palette closes before it runs a row; a query suggestion keeps it\n // open and types itself in instead.\n setOpen(true);\n setQuery(s.query);\n refocusField();\n },\n });\n } else out.push(toItem(\"try:\", s, groupOf(s)));\n });\n // One heading has nothing to order; kept groups are ordered like results.\n return suggestionsKeepGroups ? ordered(out) : out;\n }\n\n const out = index.search(q).map((hit) => toItem(\"s:\", hit.entry, hit.entry.group ?? l.results));\n for (const source of sourceList) {\n if (q.length < (source.minChars ?? 2)) continue;\n const result = sourceResults[source.id];\n const declared = (source.groups ?? []).map((g) => (typeof g === \"string\" ? { key: g, label: g } : g));\n const status = (label: string, icon: ReactNode): CommandItem => ({\n id: `src:${source.id}:status`,\n label,\n // One line for the one request, under the group that will head its answer.\n group: declared[0]?.label ?? source.group,\n icon,\n kind: \"status\",\n onSelect: noop,\n });\n // Anything but an answer to THIS query is still loading — including the previous\n // query's rows, which would otherwise sit under the new query as if they matched.\n if (!result || result.query !== q) {\n out.push(status(pl.loading, <Loader2 className=\"size-3.5 animate-spin\" />));\n } else if (result.status === \"error\") {\n out.push(status(pl.error ?? DEFAULT_COMMAND_PALETTE_LABELS.error!, <AlertCircle className=\"size-3.5\" />));\n } else if (!declared.length) {\n for (const entry of result.entries.slice(0, source.limit ?? groupLimit)) {\n out.push(toItem(`src:${source.id}:`, entry, source.group, source.redact ?? redactLabels));\n }\n } else {\n // Bucket by the entry's own group, then emit in the DECLARED order — the answer's\n // order must not reshuffle the headings between keystrokes. Within a group the\n // source's ranking stands. The group key is in the row id: two record types may\n // share an id (\"1\"), and the palette keys rows by it.\n const buckets = new Map<string, { label: string; entries: SearchEntry[] }>();\n for (const g of declared) buckets.set(g.key, { label: g.label, entries: [] });\n const rest: SearchEntry[] = [];\n for (const entry of result.entries) {\n const bucket = entry.group === undefined ? undefined : buckets.get(entry.group);\n (bucket ? bucket.entries : rest).push(entry);\n }\n const limit = source.limit ?? groupLimit;\n const emit = (key: string, label: string, list: SearchEntry[]) => {\n for (const entry of list.slice(0, limit)) {\n out.push(toItem(`src:${source.id}:${key}:`, entry, label, source.redact ?? redactLabels));\n }\n };\n for (const [key, { label, entries: list }] of buckets) emit(key, label, list);\n emit(\"~\", source.group, rest);\n }\n }\n\n return ordered(out);\n }, [q, suggestionList, suggestionsKeepGroups, byId, index, sourceList, sourceResults, order, groupLimit, toItem, l.suggestions, l.results, pl.loading, pl.error, setOpen, setQuery, noop, redactLabels]);\n /* eslint-enable react-hooks/refs */\n\n // The palette re-runs its provider on a `revision` change, never on the provider's\n // identity — and `items` is everything the provider answers with.\n const provider = useCallback(() => items, [items]);\n\n const resolvedPaletteLabels = useMemo(\n () => ({ dialog: l.trigger, placeholder: l.placeholder, ...paletteLabels }),\n [l.trigger, l.placeholder, paletteLabels],\n );\n\n const apple = useMemo(() => isApplePlatform(), []);\n const keys = apple ? \"⌘K\" : \"Ctrl K\";\n const triggerLabel = triggerName === \"withShortcut\" ? l.shortcut(keys) : l.trigger;\n const triggerProps: GlobalSearchTriggerProps = {\n open: openSearch,\n label: triggerLabel,\n tooltip: l.shortcut(keys),\n keys,\n ariaKeyShortcuts: \"Meta+K Control+K\",\n };\n\n return (\n <>\n {typeof trigger === \"function\"\n ? trigger(triggerProps)\n : trigger === \"button\" && (\n <Tooltip\n label={triggerProps.tooltip}\n side=\"bottom\"\n portal\n className={hideTriggerOnPhone ? \"hidden sm:block\" : undefined}\n >\n <button\n {...Object.fromEntries(Object.entries(attributes).filter(([k]) => k.startsWith(\"data-\")))}\n type=\"button\"\n onClick={openSearch}\n aria-label={triggerLabel}\n aria-keyshortcuts={triggerProps.ariaKeyShortcuts}\n aria-haspopup=\"dialog\"\n className={cn(TOPBAR_TRIGGER_CLASS, triggerClassName)}\n >\n <Search\n className={triggerIconSize === undefined ? \"size-4\" : undefined}\n style={triggerIconSize === undefined ? undefined : { width: triggerIconSize, height: triggerIconSize }}\n />\n </button>\n </Tooltip>\n )}\n {/* Nothing is drawn while the palette's module is on its way (a few milliseconds,\n once per page load). A failed load is a chunk-load error for the app's error\n boundary, as a lazy route's is. */}\n {paletteMounted && (\n <Suspense fallback={null}>\n <LazyCommandPalette\n open={open}\n onClose={() => setOpen(false)}\n search={provider}\n revision={items}\n query={query}\n onQueryChange={setQuery}\n searchOn={searchOn}\n redactLabels={redactLabels}\n fullScreenOnPhone={fullScreenOnPhone}\n density={density}\n labels={resolvedPaletteLabels}\n />\n </Suspense>\n )}\n </>\n );\n}\n\nconst EMPTY: readonly SearchEntry[] = [];\n"],"mappings":"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","names":["out","list"]}
1
+ {"version":3,"sources":["../../src/search/global-search.tsx"],"sourcesContent":["import { lazy, Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { useHref, useInRouterContext, useNavigate } from \"react-router\";\nimport { AlertCircle, Loader2, Search } from \"lucide-react\";\nimport { Tooltip } from \"../components/tooltip\";\nimport { TOPBAR_TRIGGER_CLASS } from \"../shell/topbar-controls\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\n// Types only from ./command-palette, gone from the built module: the palette loads late.\nimport type { CommandItem, CommandPaletteDensity, CommandPaletteLabels } from \"./command-palette\";\nimport { DEFAULT_COMMAND_PALETTE_LABELS } from \"./command-palette-labels\";\nimport { useCommandKey } from \"./use-command-key\";\nimport { createSearchIndex } from \"./search-index\";\nimport type { SearchEntry, SearchIndexOptions } from \"./search-index\";\nimport { DEFAULT_GLOBAL_SEARCH_LABELS } from \"./global-search-labels\";\n\n/**\n * The palette, loaded when the search is first opened rather than with the top bar\n * (0.34, MOD-1 of the refactor audit 2026-10): ~5.5 kB gzip of keksdose's and kastlan's\n * entry chunks, for a dialog drawn only once somebody searches. A LITERAL relative\n * `import()`, as the catalogue loaders are (i18n/languages.ts), so the library build keeps\n * the module its own and an app's bundler splits it off.\n */\nconst LazyCommandPalette = lazy(() =>\n import(\"./command-palette\").then((module) => ({ default: module.CommandPalette })),\n);\n\nexport interface GlobalSearchLabels {\n /** The trigger's accessible name, and the dialog's. */\n trigger: string;\n /** The field's placeholder — says WHAT can be found, which \"Search…\" does not. */\n placeholder: string;\n /** The trigger's tooltip, given the shortcut as the platform writes it (\"⌘K\",\n * \"Ctrl K\"). A function so a language can put the keys where its grammar wants. */\n shortcut: (keys: string) => string;\n /** Heading of the suggestions shown for an empty query. */\n suggestions: string;\n /** Heading for entries that name no `group`. */\n results: string;\n}\n\n// Defined in ./global-search-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_GLOBAL_SEARCH_LABELS };\n\n/**\n * A place results come from that is not the static index — a server search, a\n * local-first store too large to hand over whole. Each source is its own group, with its\n * own debounce, its own \"Searching…\" line and its own failure: the static groups never\n * wait for it, and one source failing never takes another's results with it.\n */\nexport interface GlobalSearchSource {\n /** Stable key — the effect that runs the source is keyed by it, not by identity. */\n id: string;\n /** The group its results are shown under (an entry's own `group` is ignored) — unless\n * `groups` is set, where it is only the home of entries that name no declared group.\n * If every entry names a declared group, it is never shown: a catch-all heading such\n * as \"Other\" is the honest value, so a stray type would still surface, labelled. */\n group: string;\n /**\n * Split ONE request into several groups — kastlan's record search answers with\n * properties, units, contacts, leases and invoices at once, and one source per group\n * meant a dedupe layer so the network still saw one request. Each entry is then shown\n * under the group its own `group` names: a declared `key` (shown by its `label`) or a\n * plain string (key and heading at once). The groups appear in THIS order whatever the\n * answer's order, each kept to `limit` rows; an entry naming no declared group falls\n * under `group`. While the request runs, or when it fails, the source shows one status\n * line under its first group, not one per group. Unset: one group, as before.\n */\n groups?: readonly (string | GlobalSearchSourceGroup)[];\n /** Called with the trimmed query. Its results are shown AS GIVEN — the source ranked\n * them — and never re-filtered by the index's matcher. `signal` aborts when the query\n * moves on or the palette closes; a stale answer is dropped either way. */\n search: (query: string, signal: AbortSignal) => Promise<readonly SearchEntry[]> | readonly SearchEntry[];\n /** Shortest query it is asked for. Default 2 — one letter matches half a database. */\n minChars?: number;\n /** Quiet period after the last keystroke before it is asked. Default 200ms. */\n debounceMs?: number;\n /** Rows kept — per group, when `groups` is set. Default: `indexOptions.groupLimit` (8). */\n limit?: number;\n /** Mask every row of this source for session replay, hints (amounts) included. An\n * entry's own `redact` still wins. */\n redact?: boolean;\n /** Told when `search` throws or rejects (not when it is aborted). The group then\n * shows the palette's `error` line instead of rows. */\n onError?: (error: unknown) => void;\n}\n\n/** One of a multi-group source's groups: the `key` its entries name in `group`, and the\n * heading shown — so a server's type (\"unit\") need not be translated per entry. */\nexport interface GlobalSearchSourceGroup {\n key: string;\n label: string;\n}\n\n/** What a custom trigger gets: everything the default button uses. */\nexport interface GlobalSearchTriggerProps {\n open: () => void;\n /** Resolved `trigger` label — the accessible name. */\n label: string;\n /** Resolved `shortcut(keys)` — the tooltip. */\n tooltip: string;\n /** \"⌘K\" on Apple platforms, \"Ctrl K\" elsewhere. */\n keys: string;\n /** For the trigger's `aria-keyshortcuts`. */\n ariaKeyShortcuts: string;\n}\n\n/** A suggestion for the empty query: an entry, the `id` of one in `entries`, or a query\n * to type into the field. */\nexport type GlobalSearchSuggestion = SearchEntry | string | { query: string };\n\nexport interface GlobalSearchProps {\n /**\n * The static index: pages, actions, settings rows — already filtered by the app (by\n * role, by privacy mode). Matched and ranked by {@link createSearchIndex}.\n *\n * Changing it re-runs the open search by itself. Nothing has to be stable: the index is\n * rebuilt only when the entries' TEXT changes (ids, titles, keywords, …), so a list\n * rebuilt on every render — `data = []` while a query is pending, icons as fresh JSX —\n * neither re-indexes nor loops, and the icons and handlers used are always the latest.\n */\n entries?: readonly SearchEntry[];\n /** The async sources, each streamed into its own group — or several (`groups`). */\n sources?: readonly GlobalSearchSource[];\n /** What the empty query shows, under the `suggestions` heading. */\n suggestions?: readonly GlobalSearchSuggestion[];\n /**\n * Show each suggested ENTRY under its own `group` instead of all of them under the\n * one `suggestions` heading — keksdose's empty palette listed \"Pages\" and \"Actions\"\n * apart, and one \"Try\" heading over both lost the difference. An entry with no\n * `group` and a query suggestion (`{ query }`) stay under `suggestions`. `groupOrder`\n * orders these groups as it orders the results'. Off by default.\n */\n suggestionsKeepGroups?: boolean;\n /** Groups listed here come first, in this order; the rest follow in ranking order. */\n groupOrder?: readonly string[];\n /** Ranking and capping — `groupLimit` (default 8) applies to every group. */\n indexOptions?: SearchIndexOptions;\n /**\n * How an entry's `href` is followed on a plain click or ↵. Default: react-router's\n * `navigate` when the search is rendered inside a router, else a full page load.\n */\n navigate?: (href: string) => void;\n /**\n * The browser URL of an `href`, for the row's real link (middle-/⌘-click opens a tab).\n * Default inside a router: the router's own (`#/page` under a hash router, the\n * basename prefixed under a browser router); outside one, the href itself.\n */\n hrefFor?: (href: string) => string;\n /** Register ⌘K / Ctrl K. Default true; turn it off for a second, scoped search on a\n * page that already has the app-wide one. */\n shortcut?: boolean;\n /** Controlled open state — so a button anywhere can open it. */\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n /** Controlled query, as on `CommandPalette` (keksdose binds it to the URL's `q`). */\n query?: string;\n onQueryChange?: (query: string) => void;\n searchOn?: \"input\" | \"submit\";\n /** Mask every row for session replay (see `CommandPalette`). A source's or an entry's\n * own `redact` wins. */\n redactLabels?: boolean;\n fullScreenOnPhone?: boolean;\n /** `\"button\"` (default): the top-bar icon button. `\"none\"`: no trigger — the shortcut\n * or a controlled `open` opens it. A function renders a trigger of your own. */\n trigger?: \"button\" | \"none\" | ((props: GlobalSearchTriggerProps) => ReactNode);\n /** Hide the default trigger below `sm` (kastlan's phone top bar has no room). */\n hideTriggerOnPhone?: boolean;\n /** Class for the default trigger button, merged over `TOPBAR_TRIGGER_CLASS`. */\n triggerClassName?: string;\n /**\n * The default trigger's magnifier, in px. Default 16 (`size-4`), as it always was;\n * 20 matches the kit's other top-bar triggers (`TopbarIconButton` draws `size-5`) and\n * keksdose's top bar, which reached into the button with `[&_svg]:size-5` for it.\n */\n triggerIconSize?: number;\n /**\n * Whether the trigger's accessible name includes the shortcut: `\"plain\"` (default)\n * names it \"Search\"; `\"withShortcut\"` names it by `labels.shortcut(keys)` —\n * \"Search (⌘K)\" on Apple platforms, \"Search (Ctrl K)\" elsewhere — the same text its\n * tooltip shows, so a speech-control user (\"click Search\") and a sighted keyboard user\n * meet the shortcut in the name itself (keksdose C25). `aria-keyshortcuts` is set\n * either way. Also reaches a custom trigger as `triggerProps.label`.\n */\n triggerName?: \"plain\" | \"withShortcut\";\n /** The palette's row height — `CommandPalette`'s `density`. Default `\"compact\"`. */\n density?: CommandPaletteDensity;\n labels?: LabelOverride<GlobalSearchLabels>;\n /** Passed through to the palette (`empty`, `loading`, `error`, `hint`, …). */\n paletteLabels?: Partial<CommandPaletteLabels>;\n /** 0.43: `data-*` attributes go on the kit's trigger button (`trigger=\"button\"`) — a\n * tour's anchor (`data-tour=\"search\"`), a test hook. A function trigger is the app's. */\n [key: `data-${string}`]: string | number | boolean | undefined;\n}\n\nfunction isApplePlatform(): boolean {\n if (typeof navigator === \"undefined\") return false;\n const nav = navigator as Navigator & { userAgentData?: { platform?: string } };\n return /mac|iphone|ipad|ipod/i.test(nav.userAgentData?.platform || nav.platform || nav.userAgent);\n}\n\nconst pageLoad = (href: string) => window.location.assign(href);\nconst identity = (href: string) => href;\n\n/** The entries' text, which is all the index reads — see `entries`. */\nfunction signature(entries: readonly SearchEntry[]): string {\n return entries\n .map((e) =>\n [\n e.id,\n e.title,\n (e.keywords ?? []).join(\"\\u0003\"),\n e.description,\n e.group,\n e.hint,\n e.href,\n e.weight,\n e.redact,\n ].join(\"\\u0001\"),\n )\n .join(\"\\u0002\");\n}\n\n/** Focus the open palette's field again — after a suggestion typed itself into it, the\n * row that was clicked (and held focus) is gone. */\nfunction refocusField() {\n requestAnimationFrame(() => {\n const fields = document.querySelectorAll<HTMLInputElement>(\n '[role=\"dialog\"][aria-modal=\"true\"] input[role=\"combobox\"]',\n );\n fields[fields.length - 1]?.focus();\n });\n}\n\ninterface SourceResult {\n query: string;\n status: \"loading\" | \"done\" | \"error\";\n entries: readonly SearchEntry[];\n}\n\n/**\n * The app-wide ⌘K search, whole: the top-bar trigger, the shortcut, and a\n * {@link CommandPalette} fed from a ranked index ({@link createSearchIndex}) and any\n * number of async sources.\n *\n * Every consumer had built this out of `CommandPalette` by hand, and each copy had the\n * same holes — a substring match, no ranking, no typo tolerance, results in declaration\n * order, one monolithic provider in which the static groups waited on the slowest\n * request. This is that wrapper once:\n *\n * <GlobalSearch\n * entries={pages} // { id, title, keywords, group, href, icon }\n * sources={[{ id: \"tx\", group: \"Transactions\", search: (q, signal) => api.search(q, { signal }) }]}\n * suggestions={[\"page-budget\", \"action-new-account\"]}\n * />\n *\n * Inside a react-router tree it navigates with the router and builds its links with it;\n * outside one, pass `navigate` (or accept full page loads).\n *\n * Since 0.34 the palette's module loads on the first open (MOD-1), so the dialog appears a\n * moment after the trigger or ⌘K: a test finds it with `await screen.findBy…`.\n */\nexport function GlobalSearch(props: GlobalSearchProps) {\n // Two components rather than a conditional hook: `useNavigate` throws outside a\n // router, and whether we are in one does not change over a component's life.\n const inRouter = useInRouterContext();\n if (inRouter && (!props.navigate || !props.hrefFor)) return <RoutedGlobalSearch {...props} />;\n return <GlobalSearchImpl {...props} navigate={props.navigate ?? pageLoad} hrefFor={props.hrefFor ?? identity} />;\n}\n\nfunction RoutedGlobalSearch(props: GlobalSearchProps) {\n const routerNavigate = useNavigate();\n // The router's href for \"/\" is its base (\"#/\" under a hash router, \"/app/\" under a\n // basename); every href is that base plus the path.\n const base = useHref(\"/\").replace(/\\/$/, \"\");\n const navigate = useCallback((href: string) => void routerNavigate(href), [routerNavigate]);\n const hrefFor = useCallback((href: string) => (href.startsWith(\"/\") ? base + href : href), [base]);\n return <GlobalSearchImpl {...props} navigate={props.navigate ?? navigate} hrefFor={props.hrefFor ?? hrefFor} />;\n}\n\nfunction GlobalSearchImpl({\n entries,\n sources,\n suggestions,\n suggestionsKeepGroups = false,\n groupOrder,\n indexOptions,\n navigate,\n hrefFor,\n shortcut = true,\n open: openProp,\n onOpenChange,\n query: queryProp,\n onQueryChange,\n searchOn,\n redactLabels = false,\n fullScreenOnPhone,\n trigger = \"button\",\n hideTriggerOnPhone = false,\n triggerClassName,\n triggerIconSize,\n triggerName = \"plain\",\n density,\n labels,\n paletteLabels,\n ...attributes\n}: GlobalSearchProps & { navigate: (href: string) => void; hrefFor: (href: string) => string }) {\n const l = useKitLabels(\"globalSearch\", DEFAULT_GLOBAL_SEARCH_LABELS, labels);\n const pl = useKitLabels(\"commandPalette\", DEFAULT_COMMAND_PALETTE_LABELS, paletteLabels);\n const [ownOpen, setOwnOpen] = useState(false);\n const open = openProp ?? ownOpen;\n const [ownQuery, setOwnQuery] = useState(\"\");\n const queryControlled = queryProp !== undefined;\n const query = queryControlled ? queryProp : ownQuery;\n const q = query.trim();\n\n const setOpen = useCallback(\n (next: boolean) => {\n if (openProp === undefined) setOwnOpen(next);\n onOpenChange?.(next);\n },\n [openProp, onOpenChange],\n );\n const setQuery = useCallback(\n (next: string) => {\n if (!queryControlled) setOwnQuery(next);\n onQueryChange?.(next);\n },\n [queryControlled, onQueryChange],\n );\n\n // Each open starts blank — unless the owner controls the query (a URL `q`). Adjusted\n // during render, React's pattern for state that follows a prop, so a controlled `open`\n // gets the same reset as the trigger does.\n const [wasOpen, setWasOpen] = useState(open);\n if (wasOpen !== open) {\n setWasOpen(open);\n if (open && !queryControlled) setOwnQuery(\"\");\n }\n\n // The palette mounts on the first open — its module loads then — and stays mounted\n // after, closed, as it always was (MOD-1): a closed palette keeps the last results, and\n // a reopen over an owner-held query shows them at once, where a palette mounted anew\n // would say \"No results\" through its debounce first.\n const [paletteMounted, setPaletteMounted] = useState(open);\n if (open && !paletteMounted) setPaletteMounted(true);\n\n const openSearch = useCallback(() => setOpen(true), [setOpen]);\n const noop = useCallback(() => {}, []);\n useCommandKey(shortcut ? openSearch : noop);\n\n /* ── the static index ─────────────────────────────────────────────────── */\n\n // Everything below is keyed by CONTENT, never by the identity of what the app passed:\n // inline arrays and `data = []` defaults are the norm, and an identity key would\n // re-index and re-search on every render of the app. Handlers, which can close over\n // anything, are read from refs at the moment a row is chosen — never stale.\n const list = entries ?? EMPTY;\n const sig = signature(list);\n const optionsKey = JSON.stringify(indexOptions ?? {});\n const groupLimit = indexOptions?.groupLimit ?? 8;\n // `sig` and `optionsKey` stand for `list` and `indexOptions`: same text, same index.\n /* eslint-disable react-hooks/exhaustive-deps */\n const index = useMemo(() => createSearchIndex(list, indexOptions), [sig, optionsKey]);\n const byId = useMemo(() => new Map(list.map((e) => [e.id, e])), [sig]);\n\n const sourceKey = (sources ?? [])\n .map((s) =>\n [\n s.id,\n s.group,\n (s.groups ?? []).map((g) => (typeof g === \"string\" ? g : `${g.key}\\u0003${g.label}`)).join(\"\\u0004\"),\n s.minChars ?? 2,\n s.debounceMs ?? 200,\n s.limit,\n s.redact,\n ].join(\"\\u0001\"),\n )\n .join(\"\\u0002\");\n const sourceList = useMemo(() => sources ?? [], [sourceKey]);\n\n const suggestionsKey = (suggestions ?? [])\n .map((s) => (typeof s === \"string\" ? `i:${s}` : \"query\" in s ? `q:${s.query}` : `e:${signature([s])}`))\n .join(\"\\u0002\");\n const suggestionList = useMemo(() => suggestions ?? [], [suggestionsKey]);\n\n const groupOrderKey = (groupOrder ?? []).join(\"\\u0002\");\n const order = useMemo(() => groupOrder ?? [], [groupOrderKey]);\n /* eslint-enable react-hooks/exhaustive-deps */\n\n // The live objects, for the moment a row is chosen or a source is asked.\n const live = useRef({ entries: list, sources: sources ?? [], suggestions: suggestions ?? [], navigate });\n useLayoutEffect(() => {\n live.current = { entries: list, sources: sources ?? [], suggestions: suggestions ?? [], navigate };\n });\n\n /* ── the async sources ────────────────────────────────────────────────── */\n\n const [sourceResults, setSourceResults] = useState<Record<string, SourceResult>>({});\n useEffect(() => {\n const controllers: AbortController[] = [];\n const timers: ReturnType<typeof setTimeout>[] = [];\n const asked = open ? live.current.sources.filter((s) => q.length >= (s.minChars ?? 2)) : [];\n for (const source of asked) {\n const id = source.id;\n const controller = new AbortController();\n controllers.push(controller);\n timers.push(\n setTimeout(async () => {\n const settle = (result: SourceResult) => {\n if (!controller.signal.aborted) setSourceResults((prev) => ({ ...prev, [id]: result }));\n };\n try {\n const found = await source.search(q, controller.signal);\n settle({ query: q, status: \"done\", entries: found });\n } catch (error) {\n if (controller.signal.aborted) return;\n source.onError?.(error);\n settle({ query: q, status: \"error\", entries: [] });\n }\n }, source.debounceMs ?? 200),\n );\n }\n return () => {\n timers.forEach(clearTimeout);\n controllers.forEach((c) => c.abort());\n };\n }, [open, q, sourceKey]);\n\n /* ── rows ─────────────────────────────────────────────────────────────── */\n\n const toItem = useCallback(\n (prefix: string, entry: SearchEntry, group: string, redact = redactLabels): CommandItem => ({\n id: `${prefix}${entry.id}`,\n label: entry.title,\n hint: entry.hint,\n group,\n icon: entry.icon,\n redact: entry.redact ?? redact,\n href: entry.href !== undefined ? hrefFor(entry.href) : undefined,\n onSelect: () => {\n // The entry as the app has it NOW — its handler may close over newer state\n // than the one this row was built from.\n const current = live.current.entries.find((e) => e.id === entry.id) ?? entry;\n if (current.onSelect) current.onSelect();\n else if (current.href !== undefined) live.current.navigate(current.href);\n },\n }),\n [hrefFor, redactLabels],\n );\n\n // `toItem` only BUILDS `onSelect` closures here; `live` is read when a row is chosen,\n // which the compiler lint cannot tell from a read during render.\n /* eslint-disable react-hooks/refs */\n const items = useMemo((): CommandItem[] => {\n const ordered = (out: CommandItem[]) => {\n if (!order.length) return out;\n const rank = (group: string) => {\n const i = order.indexOf(group);\n return i === -1 ? order.length : i;\n };\n // Stable: within a rank, groups keep the order their best hit gave them.\n return out\n .map((item, i) => ({ item, i }))\n .sort((a, b) => rank(a.item.group) - rank(b.item.group) || a.i - b.i)\n .map(({ item }) => item);\n };\n\n if (!q) {\n const out: CommandItem[] = [];\n const groupOf = (entry: SearchEntry) => (suggestionsKeepGroups ? (entry.group ?? l.suggestions) : l.suggestions);\n suggestionList.forEach((s, i) => {\n if (typeof s === \"string\") {\n const entry = byId.get(s);\n if (entry) out.push(toItem(\"try:\", entry, groupOf(entry)));\n } else if (\"query\" in s) {\n out.push({\n id: `try:q${i}`,\n label: s.query,\n group: l.suggestions,\n icon: <Search className=\"size-4\" />,\n onSelect: () => {\n // The palette closes before it runs a row; a query suggestion keeps it\n // open and types itself in instead.\n setOpen(true);\n setQuery(s.query);\n refocusField();\n },\n });\n } else out.push(toItem(\"try:\", s, groupOf(s)));\n });\n // One heading has nothing to order; kept groups are ordered like results.\n return suggestionsKeepGroups ? ordered(out) : out;\n }\n\n const out = index.search(q).map((hit) => toItem(\"s:\", hit.entry, hit.entry.group ?? l.results));\n for (const source of sourceList) {\n if (q.length < (source.minChars ?? 2)) continue;\n const result = sourceResults[source.id];\n const declared = (source.groups ?? []).map((g) => (typeof g === \"string\" ? { key: g, label: g } : g));\n const status = (label: string, icon: ReactNode): CommandItem => ({\n id: `src:${source.id}:status`,\n label,\n // One line for the one request, under the group that will head its answer.\n group: declared[0]?.label ?? source.group,\n icon,\n kind: \"status\",\n onSelect: noop,\n });\n // Anything but an answer to THIS query is still loading — including the previous\n // query's rows, which would otherwise sit under the new query as if they matched.\n if (!result || result.query !== q) {\n out.push(status(pl.loading, <Loader2 className=\"size-3.5 animate-spin\" />));\n } else if (result.status === \"error\") {\n out.push(status(pl.error ?? DEFAULT_COMMAND_PALETTE_LABELS.error!, <AlertCircle className=\"size-3.5\" />));\n } else if (!declared.length) {\n for (const entry of result.entries.slice(0, source.limit ?? groupLimit)) {\n out.push(toItem(`src:${source.id}:`, entry, source.group, source.redact ?? redactLabels));\n }\n } else {\n // Bucket by the entry's own group, then emit in the DECLARED order — the answer's\n // order must not reshuffle the headings between keystrokes. Within a group the\n // source's ranking stands. The group key is in the row id: two record types may\n // share an id (\"1\"), and the palette keys rows by it.\n const buckets = new Map<string, { label: string; entries: SearchEntry[] }>();\n for (const g of declared) buckets.set(g.key, { label: g.label, entries: [] });\n const rest: SearchEntry[] = [];\n for (const entry of result.entries) {\n const bucket = entry.group === undefined ? undefined : buckets.get(entry.group);\n (bucket ? bucket.entries : rest).push(entry);\n }\n const limit = source.limit ?? groupLimit;\n const emit = (key: string, label: string, list: SearchEntry[]) => {\n for (const entry of list.slice(0, limit)) {\n out.push(toItem(`src:${source.id}:${key}:`, entry, label, source.redact ?? redactLabels));\n }\n };\n for (const [key, { label, entries: list }] of buckets) emit(key, label, list);\n emit(\"~\", source.group, rest);\n }\n }\n\n return ordered(out);\n }, [\n q,\n suggestionList,\n suggestionsKeepGroups,\n byId,\n index,\n sourceList,\n sourceResults,\n order,\n groupLimit,\n toItem,\n l.suggestions,\n l.results,\n pl.loading,\n pl.error,\n setOpen,\n setQuery,\n noop,\n redactLabels,\n ]);\n /* eslint-enable react-hooks/refs */\n\n // The palette re-runs its provider on a `revision` change, never on the provider's\n // identity — and `items` is everything the provider answers with.\n const provider = useCallback(() => items, [items]);\n\n const resolvedPaletteLabels = useMemo(\n () => ({ dialog: l.trigger, placeholder: l.placeholder, ...paletteLabels }),\n [l.trigger, l.placeholder, paletteLabels],\n );\n\n const apple = useMemo(() => isApplePlatform(), []);\n const keys = apple ? \"⌘K\" : \"Ctrl K\";\n const triggerLabel = triggerName === \"withShortcut\" ? l.shortcut(keys) : l.trigger;\n const triggerProps: GlobalSearchTriggerProps = {\n open: openSearch,\n label: triggerLabel,\n tooltip: l.shortcut(keys),\n keys,\n ariaKeyShortcuts: \"Meta+K Control+K\",\n };\n\n return (\n <>\n {typeof trigger === \"function\"\n ? trigger(triggerProps)\n : trigger === \"button\" && (\n <Tooltip\n label={triggerProps.tooltip}\n side=\"bottom\"\n portal\n className={hideTriggerOnPhone ? \"hidden sm:block\" : undefined}\n >\n <button\n {...Object.fromEntries(Object.entries(attributes).filter(([k]) => k.startsWith(\"data-\")))}\n type=\"button\"\n onClick={openSearch}\n aria-label={triggerLabel}\n aria-keyshortcuts={triggerProps.ariaKeyShortcuts}\n aria-haspopup=\"dialog\"\n className={cn(TOPBAR_TRIGGER_CLASS, triggerClassName)}\n >\n <Search\n className={triggerIconSize === undefined ? \"size-4\" : undefined}\n style={\n triggerIconSize === undefined ? undefined : { width: triggerIconSize, height: triggerIconSize }\n }\n />\n </button>\n </Tooltip>\n )}\n {/* Nothing is drawn while the palette's module is on its way (a few milliseconds,\n once per page load). A failed load is a chunk-load error for the app's error\n boundary, as a lazy route's is. */}\n {paletteMounted && (\n <Suspense fallback={null}>\n <LazyCommandPalette\n open={open}\n onClose={() => setOpen(false)}\n search={provider}\n revision={items}\n query={query}\n onQueryChange={setQuery}\n searchOn={searchOn}\n redactLabels={redactLabels}\n fullScreenOnPhone={fullScreenOnPhone}\n density={density}\n labels={resolvedPaletteLabels}\n />\n </Suspense>\n )}\n </>\n );\n}\n\nconst EMPTY: readonly SearchEntry[] = [];\n"],"mappings":"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","names":["out","list"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/search/search-index.ts"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { foldForSearch } from \"../lib/text-match\";\n\n/**\n * The matcher behind {@link GlobalSearch} — pure, React-free, testable on its own.\n *\n * WHY IT EXISTS. `CommandPalette` is deliberately domain-free: it shows whatever a\n * `search(query)` provider returns. So every app wrote that provider, and every app wrote\n * the same one — `label.toLowerCase().includes(q)` over its pages and actions (kastlan's\n * command palette, keksdose's global search) plus a second copy with keywords for the\n * settings catalogue (keksdose's `matchSettingsEntries`). A substring test answers \"does\n * this string contain that one\", which is not what a search box is asked:\n *\n * - \"range date\" finds nothing, because the words are in the other order;\n * - \"Uberweisung\" misses \"Überweisung\", and \"Tooltp\" misses \"Tooltip\";\n * - \"del\" matches \"model\" as happily as \"delete\", and every hit ranks the same, so the\n * exact page name can sit under six unrelated rows.\n *\n * What it does instead, per entry:\n *\n * - NORMALISES both sides: lower case, accents stripped (NFD minus combining marks),\n * `ß` → `ss`, punctuation as a word break, and camelCase split so `DateRangePicker`\n * is also the words date, range, picker.\n * - Requires EVERY query word to match SOMEWHERE in the entry — title, keywords or\n * description, in any order.\n * - A word matches a word of the entry exactly, as its prefix, inside it (four letters or\n * more, for compounds: \"bereich\" in \"Datumsbereich\"), or — for words of five letters\n * or more — at edit distance one, transpositions included (\"tooltp\", \"calender\").\n * - RANKS in tiers: exact title > title prefix > all words in the title > some word only\n * in the keywords > some word only in the description. Within a tier, better word\n * matches (exact over prefix over infix over typo) score higher, and `weight` is added\n * last. The tiers are {@link SEARCH_TIER_POINTS} points apart, so a weight below that reorders within\n * a tier and never across one.\n * - Caps each group (`groupLimit`, default 8) after ranking, so one broad group cannot\n * push every other group off the list.\n */\n\n/** One searchable thing: a page, an action, a record, a settings row. */\nexport interface SearchEntry {\n /** Unique across the entries handed to one index. */\n id: string;\n /** What the row says, and what ranks highest. */\n title: string;\n /** Other words or phrases that should find it — synonyms, a translated name, tags.\n * Matched like the title, ranked below it. */\n keywords?: readonly string[];\n /** Longer text, matched last and ranked lowest. Not shown in the row. */\n description?: string;\n /** Heading the row is grouped under in the palette. */\n group?: string;\n /** Secondary text on the row's trailing side (the page it lives on, an amount). */\n hint?: string;\n /** Leading icon. */\n icon?: ReactNode;\n /** Where it leads — the app's own path, handed to `GlobalSearch`'s `navigate`. */\n href?: string;\n /** Run instead of navigating. Wins over `href` for a plain click and ↵; `href` still\n * makes the row a real link for a middle-/⌘-click. */\n onSelect?: () => void;\n /** Added to the score. Below {@link SEARCH_TIER_POINTS} it reorders within a tier. */\n weight?: number;\n /** Masks the row for session replay (see `CommandItem.redact`). */\n redact?: boolean;\n}\n\nexport type SearchMatchField = \"title\" | \"keywords\" | \"description\";\n\nexport interface SearchHit<E extends SearchEntry = SearchEntry> {\n entry: E;\n score: number;\n /** The weakest field any query word had to be found in. */\n field: SearchMatchField;\n}\n\nexport interface SearchIndexOptions {\n /** Hits kept per `group` after ranking. Default 8; `Infinity` for no cap. */\n groupLimit?: number;\n /** Hits kept in total. Default: no cap beyond `groupLimit`. */\n limit?: number;\n /** Shortest query word allowed an edit-distance-1 match. Default 5 — below that a\n * single typo is a different word (\"cat\" / \"car\"). */\n typoMinLength?: number;\n /** Shortest query word matched INSIDE an entry word. Default 4. */\n infixMinLength?: number;\n}\n\nexport interface SearchIndex<E extends SearchEntry = SearchEntry> {\n /** Ranked hits for `query`; an empty query returns none. */\n search: (query: string) => SearchHit<E>[];\n readonly entries: readonly E[];\n}\n\n/** Points between two ranking tiers. */\nexport const SEARCH_TIER_POINTS = 1000;\n\nconst TIERS = {\n exact: 5,\n prefix: 4,\n title: 3,\n keywords: 2,\n description: 1,\n} as const;\n\n/** How well one query word matched one entry word — higher is better. */\nconst WORD_SCORE = { exact: 40, prefix: 30, infix: 15, typo: 10 } as const;\n/** How much the field a word matched in is worth, multiplied with the above. */\nconst FIELD_FACTOR: Record<SearchMatchField, number> = { title: 3, keywords: 2, description: 1 };\n\n/**\n * Lower case, accents off, punctuation to spaces — the form both sides are compared in.\n * Exported so an app can normalise its own keys the same way. The fold is every kit\n * list's {@link foldForSearch} (DUP-5, refactor audit 2026-10); the index adds the\n * punctuation-to-space step its word split needs.\n */\nexport function normalizeSearchText(text: string): string {\n return foldForSearch(text)\n .replace(/[^\\p{L}\\p{N}]+/gu, \" \")\n .trim();\n}\n\n/** The words of a text, camelCase split too: \"DateRangePicker\" → daterangepicker, date,\n * range, picker. The joined word stays so \"daterange\" still prefixes it. */\nfunction words(text: string): string[] {\n const out = new Set<string>();\n // Split camelCase BEFORE lower-casing, or the boundaries are gone.\n const spaced = text.replace(/(\\p{Ll}|\\p{N})(\\p{Lu})/gu, \"$1 $2\").replace(/(\\p{Lu})(\\p{Lu}\\p{Ll})/gu, \"$1 $2\");\n for (const w of normalizeSearchText(text).split(\" \")) if (w) out.add(w);\n for (const w of normalizeSearchText(spaced).split(\" \")) if (w) out.add(w);\n return [...out];\n}\n\n/** Restricted Damerau–Levenshtein distance, early-exiting once it must exceed `max`. */\nfunction withinDistance(a: string, b: string, max: number): boolean {\n if (Math.abs(a.length - b.length) > max) return false;\n if (a === b) return true;\n const n = a.length;\n const m = b.length;\n let prev2: number[] = [];\n let prev = Array.from({ length: m + 1 }, (_, j) => j);\n for (let i = 1; i <= n; i++) {\n const cur = [i];\n let rowMin = i;\n for (let j = 1; j <= m; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n let v = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);\n if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {\n v = Math.min(v, prev2[j - 2] + 1);\n }\n cur.push(v);\n if (v < rowMin) rowMin = v;\n }\n if (rowMin > max) return false;\n prev2 = prev;\n prev = cur;\n }\n return prev[m] <= max;\n}\n\ntype WordKind = keyof typeof WORD_SCORE;\n\nfunction matchWord(q: string, word: string, opts: Required<Pick<SearchIndexOptions, \"typoMinLength\" | \"infixMinLength\">>): WordKind | null {\n if (word === q) return \"exact\";\n if (word.startsWith(q)) return \"prefix\";\n if (q.length >= opts.infixMinLength && word.includes(q)) return \"infix\";\n if (q.length >= opts.typoMinLength) {\n // The whole word, or the word's start as long as the query (a typo in a prefix:\n // \"calender\" is one edit from \"calendar\", the start of \"calendars\").\n if (withinDistance(q, word, 1)) return \"typo\";\n if (word.length > q.length && withinDistance(q, word.slice(0, q.length), 1)) return \"typo\";\n }\n return null;\n}\n\ninterface Prepared<E> {\n entry: E;\n title: string;\n titleCompact: string;\n fields: Array<[SearchMatchField, string[]]>;\n}\n\n/**\n * Build an index over `entries`. Normalisation happens once here, so `search` is a scan\n * over prepared words — a few thousand entries per keystroke is nothing.\n */\nexport function createSearchIndex<E extends SearchEntry>(\n entries: readonly E[],\n options: SearchIndexOptions = {},\n): SearchIndex<E> {\n const { groupLimit = 8, limit = Infinity, typoMinLength = 5, infixMinLength = 4 } = options;\n const wordOpts = { typoMinLength, infixMinLength };\n const prepared: Prepared<E>[] = entries.map((entry) => {\n const title = normalizeSearchText(entry.title);\n return {\n entry,\n title,\n titleCompact: title.replace(/ /g, \"\"),\n fields: [\n [\"title\", words(entry.title)],\n [\"keywords\", (entry.keywords ?? []).flatMap(words)],\n [\"description\", entry.description ? words(entry.description) : []],\n ],\n };\n });\n\n const search = (query: string): SearchHit<E>[] => {\n const q = normalizeSearchText(query);\n if (!q) return [];\n const qWords = [...new Set(q.split(\" \"))];\n const qCompact = q.replace(/ /g, \"\");\n const hits: SearchHit<E>[] = [];\n\n for (const p of prepared) {\n let quality = 0;\n let weakest: SearchMatchField = \"title\";\n let ok = true;\n for (const qw of qWords) {\n let best = 0;\n let bestField: SearchMatchField | null = null;\n for (const [field, fieldWords] of p.fields) {\n for (const w of fieldWords) {\n const kind = matchWord(qw, w, wordOpts);\n if (!kind) continue;\n const s = WORD_SCORE[kind] * FIELD_FACTOR[field];\n if (s > best) {\n best = s;\n bestField = field;\n }\n }\n }\n if (!bestField) {\n ok = false;\n break;\n }\n quality += best;\n if (FIELD_FACTOR[bestField] < FIELD_FACTOR[weakest]) weakest = bestField;\n }\n if (!ok) continue;\n\n let tier: number = TIERS[weakest];\n if (p.title === q || p.titleCompact === qCompact) tier = TIERS.exact;\n else if (\n p.title.startsWith(q) ||\n p.titleCompact.startsWith(qCompact) ||\n // The whole title, mistyped once (\"Tooltp\") — as good as a prefix, not exact.\n (qCompact.length >= typoMinLength && withinDistance(qCompact, p.titleCompact, 1))\n ) {\n tier = TIERS.prefix;\n }\n // Within a tier: how well the words matched (≤ 120 each, averaged so a long query\n // cannot climb by word count), and how much of the title the query covers — so\n // \"Tooltip\" beats \"Popovers, menus & tooltips\" for the same one-word match.\n const coverage = Math.min(1, qCompact.length / Math.max(1, p.titleCompact.length)) * 40;\n const score =\n tier * SEARCH_TIER_POINTS +\n Math.min(quality / qWords.length + coverage, SEARCH_TIER_POINTS / 2 - 1) +\n (p.entry.weight ?? 0);\n hits.push({ entry: p.entry, score, field: weakest });\n }\n\n hits.sort((a, b) => b.score - a.score || a.entry.title.localeCompare(b.entry.title));\n return capByGroup(hits, groupLimit, limit);\n };\n\n return { search, entries };\n}\n\nfunction capByGroup<E extends SearchEntry>(hits: SearchHit<E>[], groupLimit: number, limit: number): SearchHit<E>[] {\n const perGroup = new Map<string, number>();\n const out: SearchHit<E>[] = [];\n for (const hit of hits) {\n if (out.length >= limit) break;\n const g = hit.entry.group ?? \"\";\n const n = perGroup.get(g) ?? 0;\n if (n >= groupLimit) continue;\n perGroup.set(g, n + 1);\n out.push(hit);\n }\n return out;\n}\n\n/**\n * One-shot form of {@link createSearchIndex}: the matching entries, best first. For a\n * list that changes on every call (a server response, a store selector); build an index\n * once when the entries are stable.\n */\nexport function matchEntries<E extends SearchEntry>(\n entries: readonly E[],\n query: string,\n options?: SearchIndexOptions,\n): E[] {\n return createSearchIndex(entries, options)\n .search(query)\n .map((hit) => hit.entry);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/search/search-index.ts"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { foldForSearch } from \"../lib/text-match\";\n\n/**\n * The matcher behind {@link GlobalSearch} — pure, React-free, testable on its own.\n *\n * WHY IT EXISTS. `CommandPalette` is deliberately domain-free: it shows whatever a\n * `search(query)` provider returns. So every app wrote that provider, and every app wrote\n * the same one — `label.toLowerCase().includes(q)` over its pages and actions (kastlan's\n * command palette, keksdose's global search) plus a second copy with keywords for the\n * settings catalogue (keksdose's `matchSettingsEntries`). A substring test answers \"does\n * this string contain that one\", which is not what a search box is asked:\n *\n * - \"range date\" finds nothing, because the words are in the other order;\n * - \"Uberweisung\" misses \"Überweisung\", and \"Tooltp\" misses \"Tooltip\";\n * - \"del\" matches \"model\" as happily as \"delete\", and every hit ranks the same, so the\n * exact page name can sit under six unrelated rows.\n *\n * What it does instead, per entry:\n *\n * - NORMALISES both sides: lower case, accents stripped (NFD minus combining marks),\n * `ß` → `ss`, punctuation as a word break, and camelCase split so `DateRangePicker`\n * is also the words date, range, picker.\n * - Requires EVERY query word to match SOMEWHERE in the entry — title, keywords or\n * description, in any order.\n * - A word matches a word of the entry exactly, as its prefix, inside it (four letters or\n * more, for compounds: \"bereich\" in \"Datumsbereich\"), or — for words of five letters\n * or more — at edit distance one, transpositions included (\"tooltp\", \"calender\").\n * - RANKS in tiers: exact title > title prefix > all words in the title > some word only\n * in the keywords > some word only in the description. Within a tier, better word\n * matches (exact over prefix over infix over typo) score higher, and `weight` is added\n * last. The tiers are {@link SEARCH_TIER_POINTS} points apart, so a weight below that reorders within\n * a tier and never across one.\n * - Caps each group (`groupLimit`, default 8) after ranking, so one broad group cannot\n * push every other group off the list.\n */\n\n/** One searchable thing: a page, an action, a record, a settings row. */\nexport interface SearchEntry {\n /** Unique across the entries handed to one index. */\n id: string;\n /** What the row says, and what ranks highest. */\n title: string;\n /** Other words or phrases that should find it — synonyms, a translated name, tags.\n * Matched like the title, ranked below it. */\n keywords?: readonly string[];\n /** Longer text, matched last and ranked lowest. Not shown in the row. */\n description?: string;\n /** Heading the row is grouped under in the palette. */\n group?: string;\n /** Secondary text on the row's trailing side (the page it lives on, an amount). */\n hint?: string;\n /** Leading icon. */\n icon?: ReactNode;\n /** Where it leads — the app's own path, handed to `GlobalSearch`'s `navigate`. */\n href?: string;\n /** Run instead of navigating. Wins over `href` for a plain click and ↵; `href` still\n * makes the row a real link for a middle-/⌘-click. */\n onSelect?: () => void;\n /** Added to the score. Below {@link SEARCH_TIER_POINTS} it reorders within a tier. */\n weight?: number;\n /** Masks the row for session replay (see `CommandItem.redact`). */\n redact?: boolean;\n}\n\nexport type SearchMatchField = \"title\" | \"keywords\" | \"description\";\n\nexport interface SearchHit<E extends SearchEntry = SearchEntry> {\n entry: E;\n score: number;\n /** The weakest field any query word had to be found in. */\n field: SearchMatchField;\n}\n\nexport interface SearchIndexOptions {\n /** Hits kept per `group` after ranking. Default 8; `Infinity` for no cap. */\n groupLimit?: number;\n /** Hits kept in total. Default: no cap beyond `groupLimit`. */\n limit?: number;\n /** Shortest query word allowed an edit-distance-1 match. Default 5 — below that a\n * single typo is a different word (\"cat\" / \"car\"). */\n typoMinLength?: number;\n /** Shortest query word matched INSIDE an entry word. Default 4. */\n infixMinLength?: number;\n}\n\nexport interface SearchIndex<E extends SearchEntry = SearchEntry> {\n /** Ranked hits for `query`; an empty query returns none. */\n search: (query: string) => SearchHit<E>[];\n readonly entries: readonly E[];\n}\n\n/** Points between two ranking tiers. */\nexport const SEARCH_TIER_POINTS = 1000;\n\nconst TIERS = {\n exact: 5,\n prefix: 4,\n title: 3,\n keywords: 2,\n description: 1,\n} as const;\n\n/** How well one query word matched one entry word — higher is better. */\nconst WORD_SCORE = { exact: 40, prefix: 30, infix: 15, typo: 10 } as const;\n/** How much the field a word matched in is worth, multiplied with the above. */\nconst FIELD_FACTOR: Record<SearchMatchField, number> = { title: 3, keywords: 2, description: 1 };\n\n/**\n * Lower case, accents off, punctuation to spaces — the form both sides are compared in.\n * Exported so an app can normalise its own keys the same way. The fold is every kit\n * list's {@link foldForSearch} (DUP-5, refactor audit 2026-10); the index adds the\n * punctuation-to-space step its word split needs.\n */\nexport function normalizeSearchText(text: string): string {\n return foldForSearch(text)\n .replace(/[^\\p{L}\\p{N}]+/gu, \" \")\n .trim();\n}\n\n/** The words of a text, camelCase split too: \"DateRangePicker\" → daterangepicker, date,\n * range, picker. The joined word stays so \"daterange\" still prefixes it. */\nfunction words(text: string): string[] {\n const out = new Set<string>();\n // Split camelCase BEFORE lower-casing, or the boundaries are gone.\n const spaced = text.replace(/(\\p{Ll}|\\p{N})(\\p{Lu})/gu, \"$1 $2\").replace(/(\\p{Lu})(\\p{Lu}\\p{Ll})/gu, \"$1 $2\");\n for (const w of normalizeSearchText(text).split(\" \")) if (w) out.add(w);\n for (const w of normalizeSearchText(spaced).split(\" \")) if (w) out.add(w);\n return [...out];\n}\n\n/** Restricted Damerau–Levenshtein distance, early-exiting once it must exceed `max`. */\nfunction withinDistance(a: string, b: string, max: number): boolean {\n if (Math.abs(a.length - b.length) > max) return false;\n if (a === b) return true;\n const n = a.length;\n const m = b.length;\n let prev2: number[] = [];\n let prev = Array.from({ length: m + 1 }, (_, j) => j);\n for (let i = 1; i <= n; i++) {\n const cur = [i];\n let rowMin = i;\n for (let j = 1; j <= m; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n let v = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);\n if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {\n v = Math.min(v, prev2[j - 2] + 1);\n }\n cur.push(v);\n if (v < rowMin) rowMin = v;\n }\n if (rowMin > max) return false;\n prev2 = prev;\n prev = cur;\n }\n return prev[m] <= max;\n}\n\ntype WordKind = keyof typeof WORD_SCORE;\n\nfunction matchWord(\n q: string,\n word: string,\n opts: Required<Pick<SearchIndexOptions, \"typoMinLength\" | \"infixMinLength\">>,\n): WordKind | null {\n if (word === q) return \"exact\";\n if (word.startsWith(q)) return \"prefix\";\n if (q.length >= opts.infixMinLength && word.includes(q)) return \"infix\";\n if (q.length >= opts.typoMinLength) {\n // The whole word, or the word's start as long as the query (a typo in a prefix:\n // \"calender\" is one edit from \"calendar\", the start of \"calendars\").\n if (withinDistance(q, word, 1)) return \"typo\";\n if (word.length > q.length && withinDistance(q, word.slice(0, q.length), 1)) return \"typo\";\n }\n return null;\n}\n\ninterface Prepared<E> {\n entry: E;\n title: string;\n titleCompact: string;\n fields: Array<[SearchMatchField, string[]]>;\n}\n\n/**\n * Build an index over `entries`. Normalisation happens once here, so `search` is a scan\n * over prepared words — a few thousand entries per keystroke is nothing.\n */\nexport function createSearchIndex<E extends SearchEntry>(\n entries: readonly E[],\n options: SearchIndexOptions = {},\n): SearchIndex<E> {\n const { groupLimit = 8, limit = Infinity, typoMinLength = 5, infixMinLength = 4 } = options;\n const wordOpts = { typoMinLength, infixMinLength };\n const prepared: Prepared<E>[] = entries.map((entry) => {\n const title = normalizeSearchText(entry.title);\n return {\n entry,\n title,\n titleCompact: title.replace(/ /g, \"\"),\n fields: [\n [\"title\", words(entry.title)],\n [\"keywords\", (entry.keywords ?? []).flatMap(words)],\n [\"description\", entry.description ? words(entry.description) : []],\n ],\n };\n });\n\n const search = (query: string): SearchHit<E>[] => {\n const q = normalizeSearchText(query);\n if (!q) return [];\n const qWords = [...new Set(q.split(\" \"))];\n const qCompact = q.replace(/ /g, \"\");\n const hits: SearchHit<E>[] = [];\n\n for (const p of prepared) {\n let quality = 0;\n let weakest: SearchMatchField = \"title\";\n let ok = true;\n for (const qw of qWords) {\n let best = 0;\n let bestField: SearchMatchField | null = null;\n for (const [field, fieldWords] of p.fields) {\n for (const w of fieldWords) {\n const kind = matchWord(qw, w, wordOpts);\n if (!kind) continue;\n const s = WORD_SCORE[kind] * FIELD_FACTOR[field];\n if (s > best) {\n best = s;\n bestField = field;\n }\n }\n }\n if (!bestField) {\n ok = false;\n break;\n }\n quality += best;\n if (FIELD_FACTOR[bestField] < FIELD_FACTOR[weakest]) weakest = bestField;\n }\n if (!ok) continue;\n\n let tier: number = TIERS[weakest];\n if (p.title === q || p.titleCompact === qCompact) tier = TIERS.exact;\n else if (\n p.title.startsWith(q) ||\n p.titleCompact.startsWith(qCompact) ||\n // The whole title, mistyped once (\"Tooltp\") — as good as a prefix, not exact.\n (qCompact.length >= typoMinLength && withinDistance(qCompact, p.titleCompact, 1))\n ) {\n tier = TIERS.prefix;\n }\n // Within a tier: how well the words matched (≤ 120 each, averaged so a long query\n // cannot climb by word count), and how much of the title the query covers — so\n // \"Tooltip\" beats \"Popovers, menus & tooltips\" for the same one-word match.\n const coverage = Math.min(1, qCompact.length / Math.max(1, p.titleCompact.length)) * 40;\n const score =\n tier * SEARCH_TIER_POINTS +\n Math.min(quality / qWords.length + coverage, SEARCH_TIER_POINTS / 2 - 1) +\n (p.entry.weight ?? 0);\n hits.push({ entry: p.entry, score, field: weakest });\n }\n\n hits.sort((a, b) => b.score - a.score || a.entry.title.localeCompare(b.entry.title));\n return capByGroup(hits, groupLimit, limit);\n };\n\n return { search, entries };\n}\n\nfunction capByGroup<E extends SearchEntry>(hits: SearchHit<E>[], groupLimit: number, limit: number): SearchHit<E>[] {\n const perGroup = new Map<string, number>();\n const out: SearchHit<E>[] = [];\n for (const hit of hits) {\n if (out.length >= limit) break;\n const g = hit.entry.group ?? \"\";\n const n = perGroup.get(g) ?? 0;\n if (n >= groupLimit) continue;\n perGroup.set(g, n + 1);\n out.push(hit);\n }\n return out;\n}\n\n/**\n * One-shot form of {@link createSearchIndex}: the matching entries, best first. For a\n * list that changes on every call (a server response, a store selector); build an index\n * once when the entries are stable.\n */\nexport function matchEntries<E extends SearchEntry>(\n entries: readonly E[],\n query: string,\n options?: SearchIndexOptions,\n): E[] {\n return createSearchIndex(entries, options)\n .search(query)\n .map((hit) => hit.entry);\n}\n"],"mappings":"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","names":[]}
@@ -8,12 +8,7 @@ import { toast } from "../components/toast.js";
8
8
  import { SettingsSection } from "./settings-section.js";
9
9
  import { DEFAULT_DEVICE_RESET_LABELS } from "./device-reset-setting-labels.js";
10
10
  const DEVICE_RESET_ANCHOR = "settings-device-reset";
11
- function DeviceResetSetting({
12
- description,
13
- labels: labelsProp,
14
- onReset,
15
- ...rest
16
- }) {
11
+ function DeviceResetSetting({ description, labels: labelsProp, onReset, ...rest }) {
17
12
  const labels = useKitLabels("deviceReset", DEFAULT_DEVICE_RESET_LABELS, labelsProp);
18
13
  const confirm = useConfirm();
19
14
  const reset = async () => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/settings/device-reset-setting.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\n\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { resetDeviceSettings } from \"../lib/device-settings\";\nimport { Button } from \"../components/button\";\nimport { useConfirm } from \"../components/confirm-dialog\";\nimport { toast } from \"../components/toast\";\nimport { SettingsSection, type SettingsSectionProps } from \"./settings-section\";\nimport { DEFAULT_DEVICE_RESET_LABELS } from \"./device-reset-setting-labels\";\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string {@link DeviceResetSetting} renders — the `deviceReset` namespace of\n * `<UiKitProvider labels>`, overridable per instance through `labels`\n * (docs/settings-harmonization.md §11.3). The confirm's Cancel is `confirmDialog.cancel`. */\nexport interface DeviceResetLabels {\n /** The card's title, and the app's catalogue entry's. */\n title: string;\n /** The line under it: what goes back, and that the account is not touched. */\n description: string;\n /** The button that asks first. */\n action: string;\n /** The question it asks. */\n confirmTitle: string;\n /** What goes, what follows the account again, and what stays. */\n confirmBody: string;\n /** The button that answers it. */\n confirmAction: string;\n /** The toast once it is done. */\n done: string;\n /** For the app's catalogue entry (`keywords`); the card never reads it. */\n keywords: string;\n}\n\n// Defined in ./device-reset-setting-labels, which holds no component code.\nexport { DEFAULT_DEVICE_RESET_LABELS };\n\n/* ── The card ────────────────────────────────────────────────────────────── */\n\n/** The card's DOM id: its catalogue entry's `anchor`, the `?focus=` target. */\nexport const DEVICE_RESET_ANCHOR = \"settings-device-reset\";\n\n// `onReset` is the card's own: the DOM's form-reset handler never fires on a div.\nexport interface DeviceResetSettingProps\n extends Omit<SettingsSectionProps, \"anchor\" | \"title\" | \"description\" | \"children\" | \"onReset\"> {\n /** The app's sentence after the kit's description: what of its own goes back, and what\n * stays (keksdose: the date format, the feature set and the gestures go; presentation\n * mode and the home position stay). */\n description?: ReactNode;\n labels?: LabelOverride<DeviceResetLabels>;\n /** Runs after the reset and before the toast, with the keys reset or removed. */\n onReset?: (keys: string[]) => void;\n}\n\n/**\n * \"Reset this device's settings\" (docs/settings-harmonization.md §11): the LAST card of\n * the appearance group. It puts this device's appearance — theme, palette, language, text\n * size, contrast and the app's own appearance choices — and its views — table views, the\n * sidebar, folded groups, saved filters — back to what a new device gets, through\n * {@link resetDeviceSettings}. The account, the sign-in, the session and anything waiting\n * to be sent are never touched: they never join the registry.\n *\n * - **A confirm, not an undo** (§11.2 decision 9): `useConfirm` in the neutral tone,\n * then a toast. An app mounts a `ConfirmProvider` and a `Toaster` already.\n * - **Not a commit**: nothing is written to the account, so a demo or a lapsed plan's\n * write lock does not disable it (decision 3).\n * - **Live, no reload** (decision 10): each registered store resets through its own\n * setter; the card stays where it is.\n *\n * The app adds the catalogue entry, `{ id, group: \"appearance\", anchor: DEVICE_RESET_ANCHOR,\n * title, keywords }` (the id in its catalogue's own style, e.g. `device_reset` or\n * `deviceReset`; `title` and `keywords` from these labels), last in its appearance group:\n * inside a `SettingsLayout`, `SettingsSection` throws in development without it.\n */\nexport function DeviceResetSetting({\n description,\n labels: labelsProp,\n onReset,\n ...rest\n}: DeviceResetSettingProps) {\n const labels = useKitLabels(\"deviceReset\", DEFAULT_DEVICE_RESET_LABELS, labelsProp);\n const confirm = useConfirm();\n\n const reset = async () => {\n const ok = await confirm({\n title: labels.confirmTitle,\n body: labels.confirmBody,\n confirmLabel: labels.confirmAction,\n tone: \"neutral\",\n });\n if (!ok) return;\n const keys = resetDeviceSettings();\n onReset?.(keys);\n toast.success(labels.done);\n };\n\n return (\n <SettingsSection\n {...rest}\n anchor={DEVICE_RESET_ANCHOR}\n title={labels.title}\n description={\n description != null ? (\n <>\n {labels.description} {description}\n </>\n ) : (\n labels.description\n )\n }\n >\n <div>\n <Button variant=\"secondary\" onClick={() => void reset()}>\n {labels.action}\n </Button>\n </div>\n </SettingsSection>\n );\n}\n"],"mappings":";AAuGU,mBASF,KATE;AArGV,SAAS,oBAAwC;AACjD,SAAS,2BAA2B;AACpC,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,aAAa;AACtB,SAAS,uBAAkD;AAC3D,SAAS,mCAAmC;AAgCrC,MAAM,sBAAsB;AAkC5B,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA,GAAG;AACL,GAA4B;AAC1B,QAAM,SAAS,aAAa,eAAe,6BAA6B,UAAU;AAClF,QAAM,UAAU,WAAW;AAE3B,QAAM,QAAQ,YAAY;AACxB,UAAM,KAAK,MAAM,QAAQ;AAAA,MACvB,OAAO,OAAO;AAAA,MACd,MAAM,OAAO;AAAA,MACb,cAAc,OAAO;AAAA,MACrB,MAAM;AAAA,IACR,CAAC;AACD,QAAI,CAAC,GAAI;AACT,UAAM,OAAO,oBAAoB;AACjC,cAAU,IAAI;AACd,UAAM,QAAQ,OAAO,IAAI;AAAA,EAC3B;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,QAAQ;AAAA,MACR,OAAO,OAAO;AAAA,MACd,aACE,eAAe,OACb,iCACG;AAAA,eAAO;AAAA,QAAY;AAAA,QAAE;AAAA,SACxB,IAEA,OAAO;AAAA,MAIX,8BAAC,SACC,8BAAC,UAAO,SAAQ,aAAY,SAAS,MAAM,KAAK,MAAM,GACnD,iBAAO,QACV,GACF;AAAA;AAAA,EACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/settings/device-reset-setting.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\n\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { resetDeviceSettings } from \"../lib/device-settings\";\nimport { Button } from \"../components/button\";\nimport { useConfirm } from \"../components/confirm-dialog\";\nimport { toast } from \"../components/toast\";\nimport { SettingsSection, type SettingsSectionProps } from \"./settings-section\";\nimport { DEFAULT_DEVICE_RESET_LABELS } from \"./device-reset-setting-labels\";\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string {@link DeviceResetSetting} renders — the `deviceReset` namespace of\n * `<UiKitProvider labels>`, overridable per instance through `labels`\n * (docs/settings-harmonization.md §11.3). The confirm's Cancel is `confirmDialog.cancel`. */\nexport interface DeviceResetLabels {\n /** The card's title, and the app's catalogue entry's. */\n title: string;\n /** The line under it: what goes back, and that the account is not touched. */\n description: string;\n /** The button that asks first. */\n action: string;\n /** The question it asks. */\n confirmTitle: string;\n /** What goes, what follows the account again, and what stays. */\n confirmBody: string;\n /** The button that answers it. */\n confirmAction: string;\n /** The toast once it is done. */\n done: string;\n /** For the app's catalogue entry (`keywords`); the card never reads it. */\n keywords: string;\n}\n\n// Defined in ./device-reset-setting-labels, which holds no component code.\nexport { DEFAULT_DEVICE_RESET_LABELS };\n\n/* ── The card ────────────────────────────────────────────────────────────── */\n\n/** The card's DOM id: its catalogue entry's `anchor`, the `?focus=` target. */\nexport const DEVICE_RESET_ANCHOR = \"settings-device-reset\";\n\n// `onReset` is the card's own: the DOM's form-reset handler never fires on a div.\nexport interface DeviceResetSettingProps extends Omit<\n SettingsSectionProps,\n \"anchor\" | \"title\" | \"description\" | \"children\" | \"onReset\"\n> {\n /** The app's sentence after the kit's description: what of its own goes back, and what\n * stays (keksdose: the date format, the feature set and the gestures go; presentation\n * mode and the home position stay). */\n description?: ReactNode;\n labels?: LabelOverride<DeviceResetLabels>;\n /** Runs after the reset and before the toast, with the keys reset or removed. */\n onReset?: (keys: string[]) => void;\n}\n\n/**\n * \"Reset this device's settings\" (docs/settings-harmonization.md §11): the LAST card of\n * the appearance group. It puts this device's appearance — theme, palette, language, text\n * size, contrast and the app's own appearance choices — and its views — table views, the\n * sidebar, folded groups, saved filters — back to what a new device gets, through\n * {@link resetDeviceSettings}. The account, the sign-in, the session and anything waiting\n * to be sent are never touched: they never join the registry.\n *\n * - **A confirm, not an undo** (§11.2 decision 9): `useConfirm` in the neutral tone,\n * then a toast. An app mounts a `ConfirmProvider` and a `Toaster` already.\n * - **Not a commit**: nothing is written to the account, so a demo or a lapsed plan's\n * write lock does not disable it (decision 3).\n * - **Live, no reload** (decision 10): each registered store resets through its own\n * setter; the card stays where it is.\n *\n * The app adds the catalogue entry, `{ id, group: \"appearance\", anchor: DEVICE_RESET_ANCHOR,\n * title, keywords }` (the id in its catalogue's own style, e.g. `device_reset` or\n * `deviceReset`; `title` and `keywords` from these labels), last in its appearance group:\n * inside a `SettingsLayout`, `SettingsSection` throws in development without it.\n */\nexport function DeviceResetSetting({ description, labels: labelsProp, onReset, ...rest }: DeviceResetSettingProps) {\n const labels = useKitLabels(\"deviceReset\", DEFAULT_DEVICE_RESET_LABELS, labelsProp);\n const confirm = useConfirm();\n\n const reset = async () => {\n const ok = await confirm({\n title: labels.confirmTitle,\n body: labels.confirmBody,\n confirmLabel: labels.confirmAction,\n tone: \"neutral\",\n });\n if (!ok) return;\n const keys = resetDeviceSettings();\n onReset?.(keys);\n toast.success(labels.done);\n };\n\n return (\n <SettingsSection\n {...rest}\n anchor={DEVICE_RESET_ANCHOR}\n title={labels.title}\n description={\n description != null ? (\n <>\n {labels.description} {description}\n </>\n ) : (\n labels.description\n )\n }\n >\n <div>\n <Button variant=\"secondary\" onClick={() => void reset()}>\n {labels.action}\n </Button>\n </div>\n </SettingsSection>\n );\n}\n"],"mappings":";AAoGU,mBASF,KATE;AAlGV,SAAS,oBAAwC;AACjD,SAAS,2BAA2B;AACpC,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,aAAa;AACtB,SAAS,uBAAkD;AAC3D,SAAS,mCAAmC;AAgCrC,MAAM,sBAAsB;AAoC5B,SAAS,mBAAmB,EAAE,aAAa,QAAQ,YAAY,SAAS,GAAG,KAAK,GAA4B;AACjH,QAAM,SAAS,aAAa,eAAe,6BAA6B,UAAU;AAClF,QAAM,UAAU,WAAW;AAE3B,QAAM,QAAQ,YAAY;AACxB,UAAM,KAAK,MAAM,QAAQ;AAAA,MACvB,OAAO,OAAO;AAAA,MACd,MAAM,OAAO;AAAA,MACb,cAAc,OAAO;AAAA,MACrB,MAAM;AAAA,IACR,CAAC;AACD,QAAI,CAAC,GAAI;AACT,UAAM,OAAO,oBAAoB;AACjC,cAAU,IAAI;AACd,UAAM,QAAQ,OAAO,IAAI;AAAA,EAC3B;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,QAAQ;AAAA,MACR,OAAO,OAAO;AAAA,MACd,aACE,eAAe,OACb,iCACG;AAAA,eAAO;AAAA,QAAY;AAAA,QAAE;AAAA,SACxB,IAEA,OAAO;AAAA,MAIX,8BAAC,SACC,8BAAC,UAAO,SAAQ,aAAY,SAAS,MAAM,KAAK,MAAM,GACnD,iBAAO,QACV,GACF;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/settings/settings-heading.tsx"],"sourcesContent":["import { createContext, useContext } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { CARD_TITLE_CLASS } from \"./settings-card-classes\";\n\n/** The heading a settings card's title becomes. */\nexport type SettingsHeadingTag = \"h2\" | \"h3\" | \"h4\";\n\nconst SettingsHeadingLevelContext = createContext<SettingsHeadingTag | null>(null);\n\nexport interface SettingsHeadingLevelProps {\n /** The element every kit setting card below titles itself with. */\n level: SettingsHeadingTag;\n children?: ReactNode;\n}\n\n/**\n * The level the kit's setting cards title themselves at (docs/settings-harmonization.md\n * §3.7). Inside {@link SettingsLayout} a card's title is a heading under the group's —\n * `h3` under the desktop's `h2`, `h2` under a phone group page's `h1` — and the layout\n * sets this for every card it renders. Outside one, nothing is set and the same cards\n * keep the plain `div` title they always had, so a card on an app's own page does not\n * suddenly put an `h3` into an outline that has no `h2`.\n *\n * Set it yourself only where an app lays out settings cards without the layout and\n * wants them in its outline: `<SettingsHeadingLevel level=\"h3\">…</SettingsHeadingLevel>`.\n * `ProfileSetting`, `PasswordSetting`, `TwoFactorSetting`, `PasskeysSetting`,\n * `EmailChangeSetting`, `SessionsSetting`, `DataExportSetting`, `DeleteAccountSetting`\n * and `SettingsSection` read it.\n */\nexport function SettingsHeadingLevel({ level, children }: SettingsHeadingLevelProps) {\n return <SettingsHeadingLevelContext.Provider value={level}>{children}</SettingsHeadingLevelContext.Provider>;\n}\n\n/** The nearest {@link SettingsHeadingLevel}, or `null` outside one. */\nexport function useSettingsHeadingLevel(): SettingsHeadingTag | null {\n return useContext(SettingsHeadingLevelContext);\n}\n\n/**\n * A setting card's title at the settings type scale: a heading inside a\n * {@link SettingsHeadingLevel}, the plain `div` outside one.\n *\n * As a heading it takes `tabIndex={-1}`: `?focus=` moves focus to the card's heading so a\n * screen reader lands on the card it was sent to (§3.5), and a heading is not focusable\n * otherwise. No outline of its own — the ring round the card marks it.\n *\n * @internal The kit's cards use it; not part of the barrel.\n */\nexport function SettingsCardTitle({ className, ...props }: ComponentPropsWithoutRef<\"div\">) {\n const level = useSettingsHeadingLevel();\n if (!level) return <div {...props} className={cn(CARD_TITLE_CLASS, className)} />;\n return <SettingsHeading as={level} tabIndex={-1} {...props} className={cn(CARD_TITLE_CLASS, \"outline-none\", className)} />;\n}\n\n/**\n * A heading at a level chosen at run time. Its own component, taking the tag as a prop\n * (as `CardTitle` does), so the element type is a prop and not a value computed in the\n * caller's render.\n *\n * @internal\n */\nexport function SettingsHeading({ as: Tag, ...props }: { as: SettingsHeadingTag } & ComponentPropsWithoutRef<\"h2\">) {\n return <Tag {...props} />;\n}\n"],"mappings":";AA+BS;AA/BT,SAAS,eAAe,kBAAkB;AAE1C,SAAS,UAAU;AACnB,SAAS,wBAAwB;AAKjC,MAAM,8BAA8B,cAAyC,IAAI;AAsB1E,SAAS,qBAAqB,EAAE,OAAO,SAAS,GAA8B;AACnF,SAAO,oBAAC,4BAA4B,UAA5B,EAAqC,OAAO,OAAQ,UAAS;AACvE;AAGO,SAAS,0BAAqD;AACnE,SAAO,WAAW,2BAA2B;AAC/C;AAYO,SAAS,kBAAkB,EAAE,WAAW,GAAG,MAAM,GAAoC;AAC1F,QAAM,QAAQ,wBAAwB;AACtC,MAAI,CAAC,MAAO,QAAO,oBAAC,SAAK,GAAG,OAAO,WAAW,GAAG,kBAAkB,SAAS,GAAG;AAC/E,SAAO,oBAAC,mBAAgB,IAAI,OAAO,UAAU,IAAK,GAAG,OAAO,WAAW,GAAG,kBAAkB,gBAAgB,SAAS,GAAG;AAC1H;AASO,SAAS,gBAAgB,EAAE,IAAI,KAAK,GAAG,MAAM,GAAgE;AAClH,SAAO,oBAAC,OAAK,GAAG,OAAO;AACzB;","names":[]}
1
+ {"version":3,"sources":["../../src/settings/settings-heading.tsx"],"sourcesContent":["import { createContext, useContext } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { CARD_TITLE_CLASS } from \"./settings-card-classes\";\n\n/** The heading a settings card's title becomes. */\nexport type SettingsHeadingTag = \"h2\" | \"h3\" | \"h4\";\n\nconst SettingsHeadingLevelContext = createContext<SettingsHeadingTag | null>(null);\n\nexport interface SettingsHeadingLevelProps {\n /** The element every kit setting card below titles itself with. */\n level: SettingsHeadingTag;\n children?: ReactNode;\n}\n\n/**\n * The level the kit's setting cards title themselves at (docs/settings-harmonization.md\n * §3.7). Inside {@link SettingsLayout} a card's title is a heading under the group's —\n * `h3` under the desktop's `h2`, `h2` under a phone group page's `h1` — and the layout\n * sets this for every card it renders. Outside one, nothing is set and the same cards\n * keep the plain `div` title they always had, so a card on an app's own page does not\n * suddenly put an `h3` into an outline that has no `h2`.\n *\n * Set it yourself only where an app lays out settings cards without the layout and\n * wants them in its outline: `<SettingsHeadingLevel level=\"h3\">…</SettingsHeadingLevel>`.\n * `ProfileSetting`, `PasswordSetting`, `TwoFactorSetting`, `PasskeysSetting`,\n * `EmailChangeSetting`, `SessionsSetting`, `DataExportSetting`, `DeleteAccountSetting`\n * and `SettingsSection` read it.\n */\nexport function SettingsHeadingLevel({ level, children }: SettingsHeadingLevelProps) {\n return <SettingsHeadingLevelContext.Provider value={level}>{children}</SettingsHeadingLevelContext.Provider>;\n}\n\n/** The nearest {@link SettingsHeadingLevel}, or `null` outside one. */\nexport function useSettingsHeadingLevel(): SettingsHeadingTag | null {\n return useContext(SettingsHeadingLevelContext);\n}\n\n/**\n * A setting card's title at the settings type scale: a heading inside a\n * {@link SettingsHeadingLevel}, the plain `div` outside one.\n *\n * As a heading it takes `tabIndex={-1}`: `?focus=` moves focus to the card's heading so a\n * screen reader lands on the card it was sent to (§3.5), and a heading is not focusable\n * otherwise. No outline of its own — the ring round the card marks it.\n *\n * @internal The kit's cards use it; not part of the barrel.\n */\nexport function SettingsCardTitle({ className, ...props }: ComponentPropsWithoutRef<\"div\">) {\n const level = useSettingsHeadingLevel();\n if (!level) return <div {...props} className={cn(CARD_TITLE_CLASS, className)} />;\n return (\n <SettingsHeading as={level} tabIndex={-1} {...props} className={cn(CARD_TITLE_CLASS, \"outline-none\", className)} />\n );\n}\n\n/**\n * A heading at a level chosen at run time. Its own component, taking the tag as a prop\n * (as `CardTitle` does), so the element type is a prop and not a value computed in the\n * caller's render.\n *\n * @internal\n */\nexport function SettingsHeading({ as: Tag, ...props }: { as: SettingsHeadingTag } & ComponentPropsWithoutRef<\"h2\">) {\n return <Tag {...props} />;\n}\n"],"mappings":";AA+BS;AA/BT,SAAS,eAAe,kBAAkB;AAE1C,SAAS,UAAU;AACnB,SAAS,wBAAwB;AAKjC,MAAM,8BAA8B,cAAyC,IAAI;AAsB1E,SAAS,qBAAqB,EAAE,OAAO,SAAS,GAA8B;AACnF,SAAO,oBAAC,4BAA4B,UAA5B,EAAqC,OAAO,OAAQ,UAAS;AACvE;AAGO,SAAS,0BAAqD;AACnE,SAAO,WAAW,2BAA2B;AAC/C;AAYO,SAAS,kBAAkB,EAAE,WAAW,GAAG,MAAM,GAAoC;AAC1F,QAAM,QAAQ,wBAAwB;AACtC,MAAI,CAAC,MAAO,QAAO,oBAAC,SAAK,GAAG,OAAO,WAAW,GAAG,kBAAkB,SAAS,GAAG;AAC/E,SACE,oBAAC,mBAAgB,IAAI,OAAO,UAAU,IAAK,GAAG,OAAO,WAAW,GAAG,kBAAkB,gBAAgB,SAAS,GAAG;AAErH;AASO,SAAS,gBAAgB,EAAE,IAAI,KAAK,GAAG,MAAM,GAAgE;AAClH,SAAO,oBAAC,OAAK,GAAG,OAAO;AACzB;","names":[]}
@@ -15,12 +15,7 @@ import { SearchField } from "../components/search-field.js";
15
15
  import { cn } from "../lib/cn.js";
16
16
  import { useKitLabels } from "../i18n/kit-labels.js";
17
17
  import { createSearchIndex } from "../search/search-index.js";
18
- import {
19
- normalizeSettingsBase,
20
- settingsHref,
21
- settingsKeywords,
22
- visibleSettingsEntries
23
- } from "./settings-catalogue.js";
18
+ import { normalizeSettingsBase, settingsHref, settingsKeywords, visibleSettingsEntries } from "./settings-catalogue.js";
24
19
  import { SettingsLayoutContext } from "./settings-context.js";
25
20
  import { SettingsHeadingLevel } from "./settings-heading.js";
26
21
  import { DEFAULT_SETTINGS_LABELS } from "./settings-labels.js";
@@ -173,7 +168,10 @@ function SettingsLayout({
173
168
  e.preventDefault();
174
169
  route.back();
175
170
  },
176
- className: cn("-ms-1 inline-flex items-center gap-0.5 self-start rounded-md pe-1 text-sm font-medium text-muted hover:text-primary", FOCUS_RING),
171
+ className: cn(
172
+ "-ms-1 inline-flex items-center gap-0.5 self-start rounded-md pe-1 text-sm font-medium text-muted hover:text-primary",
173
+ FOCUS_RING
174
+ ),
177
175
  children: [
178
176
  /* @__PURE__ */ jsx(ChevronLeft, { "aria-hidden": true, className: "size-4 rtl:-scale-x-100" }),
179
177
  pageTitle
@@ -255,19 +253,10 @@ function SettingsLayout({
255
253
  }
256
254
  )
257
255
  ),
258
- /* @__PURE__ */ jsx("div", { className: "min-w-0 space-y-4", children: searching ? results : open ? /* @__PURE__ */ jsxs(
259
- "div",
260
- {
261
- id: panelId,
262
- role: "tabpanel",
263
- "aria-labelledby": `${panelId}-tab`,
264
- className: "space-y-4",
265
- children: [
266
- /* @__PURE__ */ jsx(PageHeader, { as: "h2", size: "sm", title: open.title, description: open.help }),
267
- cards(open.id)
268
- ]
269
- }
270
- ) : null })
256
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 space-y-4", children: searching ? results : open ? /* @__PURE__ */ jsxs("div", { id: panelId, role: "tabpanel", "aria-labelledby": `${panelId}-tab`, className: "space-y-4", children: [
257
+ /* @__PURE__ */ jsx(PageHeader, { as: "h2", size: "sm", title: open.title, description: open.help }),
258
+ cards(open.id)
259
+ ] }) : null })
271
260
  ] })
272
261
  ] });
273
262
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/settings/settings-layout.tsx"],"sourcesContent":["import { useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { Link, useHref, useLocation } from \"react-router\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { Button } from \"../components/button\";\nimport { Card } from \"../components/card\";\nimport { EmptyState } from \"../components/empty-state\";\nimport { Tabs } from \"../components/tabs\";\nimport { FOCUS_RING } from \"../components/focus-ring\";\nimport { Chip } from \"../components/chip\";\nimport { List, ListItem } from \"../components/list\";\nimport type { ListItemLinkProps } from \"../components/list\";\nimport { PageHeader } from \"../components/page-header\";\nimport { SearchField } from \"../components/search-field\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { createSearchIndex } from \"../search/search-index\";\nimport type { SearchEntry } from \"../search/search-index\";\nimport {\n normalizeSettingsBase,\n settingsHref,\n settingsKeywords,\n visibleSettingsEntries,\n} from \"./settings-catalogue\";\nimport type { SettingsEntry, SettingsGroup } from \"./settings-catalogue\";\nimport { SettingsLayoutContext } from \"./settings-context\";\nimport type { SettingsLayoutContextValue } from \"./settings-context\";\nimport { SettingsHeadingLevel } from \"./settings-heading\";\nimport { DEFAULT_SETTINGS_LABELS } from \"./settings-labels\";\nimport type { SettingsLabels } from \"./settings-labels\";\nimport { settingsFromListState, useSettingsRoute } from \"./use-settings-route\";\nimport type { SettingsLayoutMode } from \"./use-settings-route\";\nimport { useSettingsFocus } from \"./use-settings-focus\";\n\n/** The page's width cap (§3.2): `6xl` for settings, `7xl` for keksdose's admin, whose\n * user table scrolled sideways at 6xl (dev#488). */\nexport type SettingsLayoutWidth = \"6xl\" | \"7xl\";\n\n/** More entries than this and the search field shows by itself (§3.4): Kurvenschmiede has\n * about twelve, keksdose 26; a page of six needs none. */\nexport const SETTINGS_SEARCH_THRESHOLD = 8;\n\n/** The desktop sidebar's column: sticky beside a long group (§3.2). */\nconst SIDEBAR_COLUMN = \"mb-4 md:sticky md:top-6 md:mb-0 md:self-start\";\n\nexport interface SettingsLayoutProps<G extends string> {\n /** The page's `h1`: \"Settings\", \"Administration\". Default: `labels.title`. */\n title?: ReactNode;\n /** The groups, in order (§4.2). */\n groups: readonly SettingsGroup<G>[];\n /** One entry per setting (§4.1). Hidden entries leave search; a group left without a\n * visible one is hidden. */\n entries: readonly SettingsEntry<G>[];\n /** `/settings` or `/admin`. The layout owns every path below it. */\n basePath: string;\n /** The group `/settings` opens on a desktop. Default: the first visible one. */\n defaultGroup?: G;\n /** Retired group ids and their successors, replaced in the address (§3.1). */\n aliases?: Readonly<Partial<Record<string, G>>>;\n /**\n * The search field. Left out, it shows once more than {@link SETTINGS_SEARCH_THRESHOLD}\n * entries are visible; `true` or `false` wins over the count — keksdose's admin keeps\n * none (§3.4, §10.3). ⌘K gets the entries either way ({@link settingsSearchEntries}).\n */\n search?: boolean;\n /** See {@link SettingsLayoutWidth}. Default `\"6xl\"`. */\n width?: SettingsLayoutWidth;\n /** The group's cards, in one column. `sub` is the path segment after the group, which\n * a card owns (`useSettingsLayout().selectSub`). */\n renderGroup: (group: G, sub: string | null) => ReactNode;\n /**\n * Rows under the phone's group list that LEAVE settings — kastlan's \"Administration\",\n * its import page (§3.3, §5). `ListItem`s with an `href`; the layout puts them in a list\n * of their own, so they never read as a group. Not shown on a desktop, where the app's\n * own navigation carries them.\n */\n phoneFooter?: ReactNode;\n /**\n * 0.42.0 (docs/admin-harmonization.md §2.5 decision 16): content under the section\n * list on a desktop, in the same sticky column — the admin area's Tools list\n * (`AdminToolLinks`). Not shown on a phone, where `phoneFooter` carries the same links.\n */\n sidebarFooter?: ReactNode;\n /** Prop > `<UiKitProvider labels={{ settings }}>` > English. */\n labels?: LabelOverride<SettingsLabels>;\n /** See {@link SettingsLayoutMode}. Default `\"auto\"`: the screen decides. */\n layout?: SettingsLayoutMode;\n /**\n * The app's name, the last part of the window's title: \"Security · Settings ·\n * Kastlan\" (§3.1). Left out, the title ends at the page: \"Security · Settings\".\n */\n appName?: string;\n /**\n * The window's title (`document.title`, §3.1) — round 0.34 C8, KS U7. By default the\n * layout writes \"<group> · <page> · <appName>\" while a group is open and\n * \"<page> · <appName>\" on the phone's list, and puts back the title it found when it\n * unmounts. `<page>` is `title` when that is a string, else `labels.title`.\n *\n * A function writes another order or another separator from the same parts; `false`\n * leaves `document.title` to the app. Kastlan's `SettingsChrome` and Kurvenschmiede's\n * `GroupTitle` were this effect written by hand, and Kurvenschmiede's admin page, on\n * the same layout, had none — every admin section was titled \"Kurvenschmiede\".\n */\n documentTitle?: false | ((parts: SettingsDocumentTitleParts) => string);\n className?: string;\n}\n\n/** What {@link SettingsLayoutProps.documentTitle} is given to write the window's title. */\nexport interface SettingsDocumentTitleParts {\n /** The open group's name, or `null` on the phone's list of groups. */\n group: string | null;\n /** The page: `title` when it is a string, else `labels.title` (\"Settings\"). */\n page: string;\n /** The `appName` prop, or `undefined`. */\n appName: string | undefined;\n}\n\n/** \"Security · Settings · Kastlan\" (§3.1); a part that is missing is left out. */\nfunction defaultDocumentTitle({ group, page, appName }: SettingsDocumentTitleParts): string {\n return [group, page, appName].filter((part): part is string => Boolean(part)).join(\" · \");\n}\n\ninterface Searchable<G extends string> extends SearchEntry {\n anchor: string;\n settingsGroup: G;\n}\n\n/**\n * The settings page (docs/settings-harmonization.md §3), and the admin page built the\n * same way (§5). keksdose's page is the reference for the desktop; the phone pattern and\n * the path segments are new for all three apps.\n *\n * **Desktop** (§3.2): the page's `PageHeader`, the search field, then a 14rem sidebar —\n * the kit's vertical `Tabs`, a link per group, sticky so a long group keeps the list in\n * view — beside the open group: its heading (`h2`), its help line and its cards in ONE\n * column, never a masonry (keksdose dev#490/#493).\n *\n * **Phone** (§3.3, below `md`): a drill-down. `/settings` is a list of the groups (icon,\n * name, help line, chevron) with `phoneFooter` under it; a group is a page of its own with\n * a back link, its name as the `h1` and the cards under it. It replaces keksdose's chip\n * strip.\n *\n * **Routes, history and `?focus=`** are {@link useSettingsRoute}'s and\n * {@link useSettingsFocus}'s. **Search** (§3.4) runs the kit's matcher over the catalogue:\n * accents folded, ranked, over the title, the group's name and the keywords. On a desktop\n * the sidebar dims the groups without hits and counts the others' while the hits replace\n * the group; on a phone they replace the list. A hit is a link to its card.\n *\n * The cards inside read {@link useSettingsLayout} for `selectSub`, and their titles are\n * headings under the group's ({@link SettingsHeadingLevel}).\n */\nexport function SettingsLayout<G extends string>({\n title,\n groups,\n entries,\n basePath,\n defaultGroup,\n aliases,\n search,\n width = \"6xl\",\n renderGroup,\n phoneFooter,\n sidebarFooter,\n labels: labelsProp,\n layout = \"auto\",\n appName,\n documentTitle,\n className,\n}: SettingsLayoutProps<G>): ReactElement {\n const labels = useKitLabels(\"settings\", DEFAULT_SETTINGS_LABELS, labelsProp);\n const base = normalizeSettingsBase(basePath);\n const route = useSettingsRoute({ groups, entries, basePath: base, defaultGroup, aliases, layout });\n const location = useLocation();\n // The browser URL of an app path, for the sidebar's real links (a middle click opens a\n // tab): `#/settings/…` under a hash router, the basename in front under a browser one.\n const root = useHref(\"/\").replace(/\\/$/, \"\");\n const panelId = useId();\n const searchRef = useRef<HTMLInputElement>(null);\n const [query, setQuery] = useState(\"\");\n\n const visibleEntries = useMemo(() => visibleSettingsEntries(groups, entries), [groups, entries]);\n const showSearch = search ?? visibleEntries.length > SETTINGS_SEARCH_THRESHOLD;\n const trimmed = query.trim();\n const searching = showSearch && trimmed !== \"\";\n\n const groupById = useMemo(() => new Map(route.groups.map((g) => [g.id, g])), [route.groups]);\n const index = useMemo(\n () =>\n createSearchIndex<Searchable<G>>(\n visibleEntries.map((e) => ({\n id: e.id,\n title: e.title,\n keywords: [groupById.get(e.group)?.title ?? e.group, ...settingsKeywords(e.keywords)],\n group: e.group,\n anchor: e.anchor,\n settingsGroup: e.group,\n })),\n // Every hit: the list is the page's whole answer, not a palette's top few.\n { groupLimit: Infinity },\n ),\n [visibleEntries, groupById],\n );\n const hits = useMemo(() => (searching ? index.search(trimmed).map((h) => h.entry) : []), [searching, index, trimmed]);\n const hitsByGroup = useMemo(() => {\n const counts = new Map<G, number>();\n for (const hit of hits) counts.set(hit.settingsGroup, (counts.get(hit.settingsGroup) ?? 0) + 1);\n return counts;\n }, [hits]);\n\n const anchors = useMemo(() => new Set(entries.map((e) => e.anchor)), [entries]);\n const { phone, group, sub, selectSub, select } = route;\n const headingLevel = phone ? \"h2\" : \"h3\";\n const context = useMemo<SettingsLayoutContextValue>(\n () => ({\n basePath: base,\n group,\n sub,\n selectSub,\n headingLevel,\n hasAnchor: (anchor) => anchors.has(anchor),\n }),\n [base, group, sub, selectSub, headingLevel, anchors],\n );\n\n // Not while the address is about to be replaced (its removal of `focus` would undo the\n // replace), not on the list, and not while the hits stand where the cards would be (a\n // phone's group page has no search: a query left from its list does not count there).\n const hitsShown = searching && (!phone || group === null);\n useSettingsFocus(route.redirecting || group === null || hitsShown ? null : route.focus);\n\n // A link inside the page. On a phone it opens a page of its own — a push that marks the\n // list as the entry before it, so the group page's back is a real back. On a desktop\n // it switches the group in place — a replace.\n const { state } = location;\n const renderLink = useCallback(\n ({ href, ...props }: ListItemLinkProps) => (\n <Link to={href} replace={!phone} state={phone ? settingsFromListState() : state} {...props} />\n ),\n [phone, state],\n );\n\n const clearSearch = () => {\n setQuery(\"\");\n // The button that was pressed goes with the empty state; the field is where the\n // next query is typed.\n searchRef.current?.focus();\n };\n\n const pageTitle = title ?? labels.title;\n\n // The window's title (§3.1). Computed to a string during render so the effect below\n // runs when the words change, not whenever an inline `documentTitle` function is new.\n // Each run puts back the title it found before writing its own, so a group switch\n // ends where it began and the unmount leaves the title the page had before settings.\n const windowTitle =\n documentTitle === false\n ? null\n : (documentTitle ?? defaultDocumentTitle)({\n group: group === null ? null : (groupById.get(group)?.title ?? null),\n page: typeof title === \"string\" ? title : labels.title,\n appName,\n });\n useEffect(() => {\n if (windowTitle === null) return;\n const before = document.title;\n document.title = windowTitle;\n return () => {\n document.title = before;\n };\n }, [windowTitle]);\n\n const pageClass = cn(\"mx-auto w-full space-y-4 p-4 md:p-6\", width === \"7xl\" ? \"max-w-7xl\" : \"max-w-6xl\", className);\n\n const searchField = showSearch ? (\n <SearchField\n ref={searchRef}\n className=\"max-w-md\"\n value={query}\n onChange={setQuery}\n aria-label={labels.search}\n placeholder={labels.searchPlaceholder}\n />\n ) : null;\n\n const results =\n hits.length === 0 ? (\n <EmptyState\n title={labels.noMatches(trimmed)}\n action={\n <Button variant=\"secondary\" size=\"sm\" onClick={clearSearch}>\n {labels.clearSearch}\n </Button>\n }\n />\n ) : (\n <Card className=\"p-1\">\n <List separator=\"divider\" density=\"comfortable\" aria-label={labels.results}>\n {hits.map((hit) => (\n <ListItem\n key={hit.id}\n title={hit.title}\n trailing={groupById.get(hit.settingsGroup)?.title}\n href={settingsHref(base, hit.settingsGroup, hit.anchor)}\n renderLink={renderLink}\n // Leave search on the way: the hit's card is what the user asked for.\n onClick={() => setQuery(\"\")}\n />\n ))}\n </List>\n </Card>\n );\n\n const cards = (open: G) => (\n <SettingsHeadingLevel level={headingLevel}>\n <div className=\"space-y-4\">{renderGroup(open, sub)}</div>\n </SettingsHeadingLevel>\n );\n\n let body: ReactNode;\n if (phone) {\n const open = group === null ? undefined : groupById.get(group);\n body = open ? (\n <div className={pageClass} data-settings-view=\"group\">\n <PageHeader\n size=\"compact\"\n documentTitle={false}\n title={open.title}\n description={open.help}\n breadcrumbs={\n <Link\n to={base || \"/\"}\n aria-label={labels.back}\n onClick={(e) => {\n // A real link for a middle click; a plain click is the page's own back,\n // which is `navigate(-1)` when the list is the entry before this one.\n if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n e.preventDefault();\n route.back();\n }}\n className={cn(\"-ms-1 inline-flex items-center gap-0.5 self-start rounded-md pe-1 text-sm font-medium text-muted hover:text-primary\", FOCUS_RING)}\n >\n <ChevronLeft aria-hidden className=\"size-4 rtl:-scale-x-100\" />\n {pageTitle}\n </Link>\n }\n />\n {cards(open.id)}\n </div>\n ) : (\n <div className={pageClass} data-settings-view=\"list\">\n <PageHeader size=\"compact\" documentTitle={false} title={pageTitle} />\n {searchField}\n {searching ? (\n results\n ) : (\n <Card className=\"p-1\">\n <List separator=\"divider\" density=\"comfortable\" aria-label={labels.sections}>\n {route.groups.map((g) => (\n <ListItem\n key={g.id}\n href={settingsHref(base, g.id)}\n renderLink={renderLink}\n leading={\n <span\n aria-hidden\n className=\"flex size-8 shrink-0 items-center justify-center rounded-md bg-surface-2 text-secondary [&_svg]:size-4\"\n >\n {g.icon}\n </span>\n }\n title={g.title}\n subtitle={g.help}\n trailing={\n g.badge == null ? (\n <ChevronRight aria-hidden className=\"size-4 rtl:-scale-x-100\" />\n ) : (\n // The group's badge (0.48) before the chevron, the two as one end.\n <span className=\"flex items-center gap-2\">\n {g.badge}\n <ChevronRight aria-hidden className=\"size-4 rtl:-scale-x-100\" />\n </span>\n )\n }\n />\n ))}\n </List>\n </Card>\n )}\n {!searching && phoneFooter != null && (\n <Card className=\"p-1\">\n <List separator=\"divider\" density=\"comfortable\">\n {phoneFooter}\n </List>\n </Card>\n )}\n </div>\n );\n } else {\n const open = group === null ? undefined : groupById.get(group);\n // The section list alone, or (0.42) with the app's `sidebarFooter` under it in one\n // sticky column.\n const withSidebarFooter = (tabs: ReactNode) =>\n sidebarFooter == null ? (\n tabs\n ) : (\n <div className={cn(SIDEBAR_COLUMN, \"space-y-6\")}>\n {tabs}\n <div data-slot=\"settings-sidebar-footer\">{sidebarFooter}</div>\n </div>\n );\n body = (\n <div className={pageClass} data-settings-view=\"desktop\">\n <PageHeader size=\"compact\" documentTitle={false} title={pageTitle} />\n {searchField}\n <div className=\"md:grid md:grid-cols-[14rem_minmax(0,1fr)] md:gap-6\">\n {withSidebarFooter(\n <Tabs<G>\n orientation=\"vertical\"\n aria-label={labels.sections}\n // Sticky (new in the harmonisation, §3.2): keksdose's notifications group\n // scrolls for screens, and the list should stay in view beside it. With a\n // `sidebarFooter` the column round both is the sticky one.\n className={sidebarFooter == null ? SIDEBAR_COLUMN : undefined}\n panelId={open && !searching ? panelId : undefined}\n tabs={route.groups.map((g) => {\n const count = hitsByGroup.get(g.id) ?? 0;\n // While searching, the groups with nothing to show step back (keksdose).\n const dim = searching && count === 0;\n return {\n id: g.id,\n href: `${root}${settingsHref(base, g.id)}`,\n name: g.title,\n label: <span className={cn(dim && \"opacity-40\")}>{g.title}</span>,\n icon: <span className={cn(\"inline-flex [&_svg]:size-4\", dim && \"opacity-40\")}>{g.icon}</span>,\n // While searching, the hit count; otherwise the group's own badge (0.48).\n badge: searching ? (\n count > 0 ? (\n <Chip size=\"sm\" tone=\"brand\">\n <span aria-hidden>{count}</span>\n <span className=\"sr-only-fixed\">{labels.matchCount(count)}</span>\n </Chip>\n ) : undefined\n ) : (\n g.badge\n ),\n };\n })}\n active={(open?.id ?? route.groups[0]?.id) as G}\n onChange={(id) => {\n // While searching the hits stand where the group would; a click on a group\n // means \"go there\", so the search makes way (keksdose).\n setQuery(\"\");\n select(id);\n }}\n />,\n )}\n <div className=\"min-w-0 space-y-4\">\n {searching ? (\n results\n ) : open ? (\n <div\n id={panelId}\n role=\"tabpanel\"\n aria-labelledby={`${panelId}-tab`}\n className=\"space-y-4\"\n >\n <PageHeader as=\"h2\" size=\"sm\" title={open.title} description={open.help} />\n {cards(open.id)}\n </div>\n ) : null}\n </div>\n </div>\n </div>\n );\n }\n\n return <SettingsLayoutContext.Provider value={context}>{body}</SettingsLayoutContext.Provider>;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/settings/settings-layout.tsx"],"sourcesContent":["import { useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { Link, useHref, useLocation } from \"react-router\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { Button } from \"../components/button\";\nimport { Card } from \"../components/card\";\nimport { EmptyState } from \"../components/empty-state\";\nimport { Tabs } from \"../components/tabs\";\nimport { FOCUS_RING } from \"../components/focus-ring\";\nimport { Chip } from \"../components/chip\";\nimport { List, ListItem } from \"../components/list\";\nimport type { ListItemLinkProps } from \"../components/list\";\nimport { PageHeader } from \"../components/page-header\";\nimport { SearchField } from \"../components/search-field\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { createSearchIndex } from \"../search/search-index\";\nimport type { SearchEntry } from \"../search/search-index\";\nimport { normalizeSettingsBase, settingsHref, settingsKeywords, visibleSettingsEntries } from \"./settings-catalogue\";\nimport type { SettingsEntry, SettingsGroup } from \"./settings-catalogue\";\nimport { SettingsLayoutContext } from \"./settings-context\";\nimport type { SettingsLayoutContextValue } from \"./settings-context\";\nimport { SettingsHeadingLevel } from \"./settings-heading\";\nimport { DEFAULT_SETTINGS_LABELS } from \"./settings-labels\";\nimport type { SettingsLabels } from \"./settings-labels\";\nimport { settingsFromListState, useSettingsRoute } from \"./use-settings-route\";\nimport type { SettingsLayoutMode } from \"./use-settings-route\";\nimport { useSettingsFocus } from \"./use-settings-focus\";\n\n/** The page's width cap (§3.2): `6xl` for settings, `7xl` for keksdose's admin, whose\n * user table scrolled sideways at 6xl (dev#488). */\nexport type SettingsLayoutWidth = \"6xl\" | \"7xl\";\n\n/** More entries than this and the search field shows by itself (§3.4): Kurvenschmiede has\n * about twelve, keksdose 26; a page of six needs none. */\nexport const SETTINGS_SEARCH_THRESHOLD = 8;\n\n/** The desktop sidebar's column: sticky beside a long group (§3.2). */\nconst SIDEBAR_COLUMN = \"mb-4 md:sticky md:top-6 md:mb-0 md:self-start\";\n\nexport interface SettingsLayoutProps<G extends string> {\n /** The page's `h1`: \"Settings\", \"Administration\". Default: `labels.title`. */\n title?: ReactNode;\n /** The groups, in order (§4.2). */\n groups: readonly SettingsGroup<G>[];\n /** One entry per setting (§4.1). Hidden entries leave search; a group left without a\n * visible one is hidden. */\n entries: readonly SettingsEntry<G>[];\n /** `/settings` or `/admin`. The layout owns every path below it. */\n basePath: string;\n /** The group `/settings` opens on a desktop. Default: the first visible one. */\n defaultGroup?: G;\n /** Retired group ids and their successors, replaced in the address (§3.1). */\n aliases?: Readonly<Partial<Record<string, G>>>;\n /**\n * The search field. Left out, it shows once more than {@link SETTINGS_SEARCH_THRESHOLD}\n * entries are visible; `true` or `false` wins over the count — keksdose's admin keeps\n * none (§3.4, §10.3). ⌘K gets the entries either way ({@link settingsSearchEntries}).\n */\n search?: boolean;\n /** See {@link SettingsLayoutWidth}. Default `\"6xl\"`. */\n width?: SettingsLayoutWidth;\n /** The group's cards, in one column. `sub` is the path segment after the group, which\n * a card owns (`useSettingsLayout().selectSub`). */\n renderGroup: (group: G, sub: string | null) => ReactNode;\n /**\n * Rows under the phone's group list that LEAVE settings — kastlan's \"Administration\",\n * its import page (§3.3, §5). `ListItem`s with an `href`; the layout puts them in a list\n * of their own, so they never read as a group. Not shown on a desktop, where the app's\n * own navigation carries them.\n */\n phoneFooter?: ReactNode;\n /**\n * 0.42.0 (docs/admin-harmonization.md §2.5 decision 16): content under the section\n * list on a desktop, in the same sticky column — the admin area's Tools list\n * (`AdminToolLinks`). Not shown on a phone, where `phoneFooter` carries the same links.\n */\n sidebarFooter?: ReactNode;\n /** Prop > `<UiKitProvider labels={{ settings }}>` > English. */\n labels?: LabelOverride<SettingsLabels>;\n /** See {@link SettingsLayoutMode}. Default `\"auto\"`: the screen decides. */\n layout?: SettingsLayoutMode;\n /**\n * The app's name, the last part of the window's title: \"Security · Settings ·\n * Kastlan\" (§3.1). Left out, the title ends at the page: \"Security · Settings\".\n */\n appName?: string;\n /**\n * The window's title (`document.title`, §3.1) — round 0.34 C8, KS U7. By default the\n * layout writes \"<group> · <page> · <appName>\" while a group is open and\n * \"<page> · <appName>\" on the phone's list, and puts back the title it found when it\n * unmounts. `<page>` is `title` when that is a string, else `labels.title`.\n *\n * A function writes another order or another separator from the same parts; `false`\n * leaves `document.title` to the app. Kastlan's `SettingsChrome` and Kurvenschmiede's\n * `GroupTitle` were this effect written by hand, and Kurvenschmiede's admin page, on\n * the same layout, had none — every admin section was titled \"Kurvenschmiede\".\n */\n documentTitle?: false | ((parts: SettingsDocumentTitleParts) => string);\n className?: string;\n}\n\n/** What {@link SettingsLayoutProps.documentTitle} is given to write the window's title. */\nexport interface SettingsDocumentTitleParts {\n /** The open group's name, or `null` on the phone's list of groups. */\n group: string | null;\n /** The page: `title` when it is a string, else `labels.title` (\"Settings\"). */\n page: string;\n /** The `appName` prop, or `undefined`. */\n appName: string | undefined;\n}\n\n/** \"Security · Settings · Kastlan\" (§3.1); a part that is missing is left out. */\nfunction defaultDocumentTitle({ group, page, appName }: SettingsDocumentTitleParts): string {\n return [group, page, appName].filter((part): part is string => Boolean(part)).join(\" · \");\n}\n\ninterface Searchable<G extends string> extends SearchEntry {\n anchor: string;\n settingsGroup: G;\n}\n\n/**\n * The settings page (docs/settings-harmonization.md §3), and the admin page built the\n * same way (§5). keksdose's page is the reference for the desktop; the phone pattern and\n * the path segments are new for all three apps.\n *\n * **Desktop** (§3.2): the page's `PageHeader`, the search field, then a 14rem sidebar —\n * the kit's vertical `Tabs`, a link per group, sticky so a long group keeps the list in\n * view — beside the open group: its heading (`h2`), its help line and its cards in ONE\n * column, never a masonry (keksdose dev#490/#493).\n *\n * **Phone** (§3.3, below `md`): a drill-down. `/settings` is a list of the groups (icon,\n * name, help line, chevron) with `phoneFooter` under it; a group is a page of its own with\n * a back link, its name as the `h1` and the cards under it. It replaces keksdose's chip\n * strip.\n *\n * **Routes, history and `?focus=`** are {@link useSettingsRoute}'s and\n * {@link useSettingsFocus}'s. **Search** (§3.4) runs the kit's matcher over the catalogue:\n * accents folded, ranked, over the title, the group's name and the keywords. On a desktop\n * the sidebar dims the groups without hits and counts the others' while the hits replace\n * the group; on a phone they replace the list. A hit is a link to its card.\n *\n * The cards inside read {@link useSettingsLayout} for `selectSub`, and their titles are\n * headings under the group's ({@link SettingsHeadingLevel}).\n */\nexport function SettingsLayout<G extends string>({\n title,\n groups,\n entries,\n basePath,\n defaultGroup,\n aliases,\n search,\n width = \"6xl\",\n renderGroup,\n phoneFooter,\n sidebarFooter,\n labels: labelsProp,\n layout = \"auto\",\n appName,\n documentTitle,\n className,\n}: SettingsLayoutProps<G>): ReactElement {\n const labels = useKitLabels(\"settings\", DEFAULT_SETTINGS_LABELS, labelsProp);\n const base = normalizeSettingsBase(basePath);\n const route = useSettingsRoute({ groups, entries, basePath: base, defaultGroup, aliases, layout });\n const location = useLocation();\n // The browser URL of an app path, for the sidebar's real links (a middle click opens a\n // tab): `#/settings/…` under a hash router, the basename in front under a browser one.\n const root = useHref(\"/\").replace(/\\/$/, \"\");\n const panelId = useId();\n const searchRef = useRef<HTMLInputElement>(null);\n const [query, setQuery] = useState(\"\");\n\n const visibleEntries = useMemo(() => visibleSettingsEntries(groups, entries), [groups, entries]);\n const showSearch = search ?? visibleEntries.length > SETTINGS_SEARCH_THRESHOLD;\n const trimmed = query.trim();\n const searching = showSearch && trimmed !== \"\";\n\n const groupById = useMemo(() => new Map(route.groups.map((g) => [g.id, g])), [route.groups]);\n const index = useMemo(\n () =>\n createSearchIndex<Searchable<G>>(\n visibleEntries.map((e) => ({\n id: e.id,\n title: e.title,\n keywords: [groupById.get(e.group)?.title ?? e.group, ...settingsKeywords(e.keywords)],\n group: e.group,\n anchor: e.anchor,\n settingsGroup: e.group,\n })),\n // Every hit: the list is the page's whole answer, not a palette's top few.\n { groupLimit: Infinity },\n ),\n [visibleEntries, groupById],\n );\n const hits = useMemo(() => (searching ? index.search(trimmed).map((h) => h.entry) : []), [searching, index, trimmed]);\n const hitsByGroup = useMemo(() => {\n const counts = new Map<G, number>();\n for (const hit of hits) counts.set(hit.settingsGroup, (counts.get(hit.settingsGroup) ?? 0) + 1);\n return counts;\n }, [hits]);\n\n const anchors = useMemo(() => new Set(entries.map((e) => e.anchor)), [entries]);\n const { phone, group, sub, selectSub, select } = route;\n const headingLevel = phone ? \"h2\" : \"h3\";\n const context = useMemo<SettingsLayoutContextValue>(\n () => ({\n basePath: base,\n group,\n sub,\n selectSub,\n headingLevel,\n hasAnchor: (anchor) => anchors.has(anchor),\n }),\n [base, group, sub, selectSub, headingLevel, anchors],\n );\n\n // Not while the address is about to be replaced (its removal of `focus` would undo the\n // replace), not on the list, and not while the hits stand where the cards would be (a\n // phone's group page has no search: a query left from its list does not count there).\n const hitsShown = searching && (!phone || group === null);\n useSettingsFocus(route.redirecting || group === null || hitsShown ? null : route.focus);\n\n // A link inside the page. On a phone it opens a page of its own — a push that marks the\n // list as the entry before it, so the group page's back is a real back. On a desktop\n // it switches the group in place — a replace.\n const { state } = location;\n const renderLink = useCallback(\n ({ href, ...props }: ListItemLinkProps) => (\n <Link to={href} replace={!phone} state={phone ? settingsFromListState() : state} {...props} />\n ),\n [phone, state],\n );\n\n const clearSearch = () => {\n setQuery(\"\");\n // The button that was pressed goes with the empty state; the field is where the\n // next query is typed.\n searchRef.current?.focus();\n };\n\n const pageTitle = title ?? labels.title;\n\n // The window's title (§3.1). Computed to a string during render so the effect below\n // runs when the words change, not whenever an inline `documentTitle` function is new.\n // Each run puts back the title it found before writing its own, so a group switch\n // ends where it began and the unmount leaves the title the page had before settings.\n const windowTitle =\n documentTitle === false\n ? null\n : (documentTitle ?? defaultDocumentTitle)({\n group: group === null ? null : (groupById.get(group)?.title ?? null),\n page: typeof title === \"string\" ? title : labels.title,\n appName,\n });\n useEffect(() => {\n if (windowTitle === null) return;\n const before = document.title;\n document.title = windowTitle;\n return () => {\n document.title = before;\n };\n }, [windowTitle]);\n\n const pageClass = cn(\"mx-auto w-full space-y-4 p-4 md:p-6\", width === \"7xl\" ? \"max-w-7xl\" : \"max-w-6xl\", className);\n\n const searchField = showSearch ? (\n <SearchField\n ref={searchRef}\n className=\"max-w-md\"\n value={query}\n onChange={setQuery}\n aria-label={labels.search}\n placeholder={labels.searchPlaceholder}\n />\n ) : null;\n\n const results =\n hits.length === 0 ? (\n <EmptyState\n title={labels.noMatches(trimmed)}\n action={\n <Button variant=\"secondary\" size=\"sm\" onClick={clearSearch}>\n {labels.clearSearch}\n </Button>\n }\n />\n ) : (\n <Card className=\"p-1\">\n <List separator=\"divider\" density=\"comfortable\" aria-label={labels.results}>\n {hits.map((hit) => (\n <ListItem\n key={hit.id}\n title={hit.title}\n trailing={groupById.get(hit.settingsGroup)?.title}\n href={settingsHref(base, hit.settingsGroup, hit.anchor)}\n renderLink={renderLink}\n // Leave search on the way: the hit's card is what the user asked for.\n onClick={() => setQuery(\"\")}\n />\n ))}\n </List>\n </Card>\n );\n\n const cards = (open: G) => (\n <SettingsHeadingLevel level={headingLevel}>\n <div className=\"space-y-4\">{renderGroup(open, sub)}</div>\n </SettingsHeadingLevel>\n );\n\n let body: ReactNode;\n if (phone) {\n const open = group === null ? undefined : groupById.get(group);\n body = open ? (\n <div className={pageClass} data-settings-view=\"group\">\n <PageHeader\n size=\"compact\"\n documentTitle={false}\n title={open.title}\n description={open.help}\n breadcrumbs={\n <Link\n to={base || \"/\"}\n aria-label={labels.back}\n onClick={(e) => {\n // A real link for a middle click; a plain click is the page's own back,\n // which is `navigate(-1)` when the list is the entry before this one.\n if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n e.preventDefault();\n route.back();\n }}\n className={cn(\n \"-ms-1 inline-flex items-center gap-0.5 self-start rounded-md pe-1 text-sm font-medium text-muted hover:text-primary\",\n FOCUS_RING,\n )}\n >\n <ChevronLeft aria-hidden className=\"size-4 rtl:-scale-x-100\" />\n {pageTitle}\n </Link>\n }\n />\n {cards(open.id)}\n </div>\n ) : (\n <div className={pageClass} data-settings-view=\"list\">\n <PageHeader size=\"compact\" documentTitle={false} title={pageTitle} />\n {searchField}\n {searching ? (\n results\n ) : (\n <Card className=\"p-1\">\n <List separator=\"divider\" density=\"comfortable\" aria-label={labels.sections}>\n {route.groups.map((g) => (\n <ListItem\n key={g.id}\n href={settingsHref(base, g.id)}\n renderLink={renderLink}\n leading={\n <span\n aria-hidden\n className=\"flex size-8 shrink-0 items-center justify-center rounded-md bg-surface-2 text-secondary [&_svg]:size-4\"\n >\n {g.icon}\n </span>\n }\n title={g.title}\n subtitle={g.help}\n trailing={\n g.badge == null ? (\n <ChevronRight aria-hidden className=\"size-4 rtl:-scale-x-100\" />\n ) : (\n // The group's badge (0.48) before the chevron, the two as one end.\n <span className=\"flex items-center gap-2\">\n {g.badge}\n <ChevronRight aria-hidden className=\"size-4 rtl:-scale-x-100\" />\n </span>\n )\n }\n />\n ))}\n </List>\n </Card>\n )}\n {!searching && phoneFooter != null && (\n <Card className=\"p-1\">\n <List separator=\"divider\" density=\"comfortable\">\n {phoneFooter}\n </List>\n </Card>\n )}\n </div>\n );\n } else {\n const open = group === null ? undefined : groupById.get(group);\n // The section list alone, or (0.42) with the app's `sidebarFooter` under it in one\n // sticky column.\n const withSidebarFooter = (tabs: ReactNode) =>\n sidebarFooter == null ? (\n tabs\n ) : (\n <div className={cn(SIDEBAR_COLUMN, \"space-y-6\")}>\n {tabs}\n <div data-slot=\"settings-sidebar-footer\">{sidebarFooter}</div>\n </div>\n );\n body = (\n <div className={pageClass} data-settings-view=\"desktop\">\n <PageHeader size=\"compact\" documentTitle={false} title={pageTitle} />\n {searchField}\n <div className=\"md:grid md:grid-cols-[14rem_minmax(0,1fr)] md:gap-6\">\n {withSidebarFooter(\n <Tabs<G>\n orientation=\"vertical\"\n aria-label={labels.sections}\n // Sticky (new in the harmonisation, §3.2): keksdose's notifications group\n // scrolls for screens, and the list should stay in view beside it. With a\n // `sidebarFooter` the column round both is the sticky one.\n className={sidebarFooter == null ? SIDEBAR_COLUMN : undefined}\n panelId={open && !searching ? panelId : undefined}\n tabs={route.groups.map((g) => {\n const count = hitsByGroup.get(g.id) ?? 0;\n // While searching, the groups with nothing to show step back (keksdose).\n const dim = searching && count === 0;\n return {\n id: g.id,\n href: `${root}${settingsHref(base, g.id)}`,\n name: g.title,\n label: <span className={cn(dim && \"opacity-40\")}>{g.title}</span>,\n icon: <span className={cn(\"inline-flex [&_svg]:size-4\", dim && \"opacity-40\")}>{g.icon}</span>,\n // While searching, the hit count; otherwise the group's own badge (0.48).\n badge: searching ? (\n count > 0 ? (\n <Chip size=\"sm\" tone=\"brand\">\n <span aria-hidden>{count}</span>\n <span className=\"sr-only-fixed\">{labels.matchCount(count)}</span>\n </Chip>\n ) : undefined\n ) : (\n g.badge\n ),\n };\n })}\n active={(open?.id ?? route.groups[0]?.id) as G}\n onChange={(id) => {\n // While searching the hits stand where the group would; a click on a group\n // means \"go there\", so the search makes way (keksdose).\n setQuery(\"\");\n select(id);\n }}\n />,\n )}\n <div className=\"min-w-0 space-y-4\">\n {searching ? (\n results\n ) : open ? (\n <div id={panelId} role=\"tabpanel\" aria-labelledby={`${panelId}-tab`} className=\"space-y-4\">\n <PageHeader as=\"h2\" size=\"sm\" title={open.title} description={open.help} />\n {cards(open.id)}\n </div>\n ) : null}\n </div>\n </div>\n </div>\n );\n }\n\n return <SettingsLayoutContext.Provider value={context}>{body}</SettingsLayoutContext.Provider>;\n}\n"],"mappings":"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","names":[]}