@eifi1/ui-kit 0.51.7 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (775) hide show
  1. package/README.md +178 -67
  2. package/dist/account/account-parts.d.ts +10 -8
  3. package/dist/account/account-parts.js +1 -18
  4. package/dist/account/account-parts.js.map +1 -1
  5. package/dist/account/data-export-setting-labels.js +1 -1
  6. package/dist/account/data-export-setting-labels.js.map +1 -1
  7. package/dist/account/delete-account-setting-labels.js +1 -1
  8. package/dist/account/delete-account-setting-labels.js.map +1 -1
  9. package/dist/account/email-change-setting-labels.js +1 -1
  10. package/dist/account/email-change-setting-labels.js.map +1 -1
  11. package/dist/account/passkeys-setting.d.ts +9 -2
  12. package/dist/account/passkeys-setting.js +41 -29
  13. package/dist/account/passkeys-setting.js.map +1 -1
  14. package/dist/account/sessions-setting.d.ts +3 -2
  15. package/dist/account/sessions-setting.js +10 -8
  16. package/dist/account/sessions-setting.js.map +1 -1
  17. package/dist/admin/admin-action-confirm.js +1 -3
  18. package/dist/admin/admin-action-confirm.js.map +1 -1
  19. package/dist/admin/admin-parts.d.ts +6 -3
  20. package/dist/admin/admin-parts.js +6 -21
  21. package/dist/admin/admin-parts.js.map +1 -1
  22. package/dist/admin/mail-test-card-labels.js +1 -1
  23. package/dist/admin/mail-test-card-labels.js.map +1 -1
  24. package/dist/admin/plan-change-labels.d.ts +2 -2
  25. package/dist/admin/plan-change-labels.js +1 -17
  26. package/dist/admin/plan-change-labels.js.map +1 -1
  27. package/dist/admin/plan-change.d.ts +3 -42
  28. package/dist/admin/plan-change.js +1 -1
  29. package/dist/admin/plan-change.js.map +1 -1
  30. package/dist/admin/reviewer-scope-editor.js +3 -3
  31. package/dist/admin/reviewer-scope-editor.js.map +1 -1
  32. package/dist/auth/auth-errors-labels.d.ts +11 -0
  33. package/dist/auth/auth-errors-labels.js +13 -0
  34. package/dist/auth/auth-errors-labels.js.map +1 -0
  35. package/dist/auth/auth-errors.d.ts +7 -23
  36. package/dist/auth/auth-errors.js +2 -15
  37. package/dist/auth/auth-errors.js.map +1 -1
  38. package/dist/auth/complete-name-dialog.js +2 -1
  39. package/dist/auth/complete-name-dialog.js.map +1 -1
  40. package/dist/auth/forgot-password-form-labels.js +1 -1
  41. package/dist/auth/forgot-password-form-labels.js.map +1 -1
  42. package/dist/auth/forgot-password-form.js +7 -2
  43. package/dist/auth/forgot-password-form.js.map +1 -1
  44. package/dist/auth/register-form-labels.js +1 -1
  45. package/dist/auth/register-form-labels.js.map +1 -1
  46. package/dist/auth/reset-password-form-labels.js +1 -1
  47. package/dist/auth/reset-password-form-labels.js.map +1 -1
  48. package/dist/auth/reset-password-form.js +5 -1
  49. package/dist/auth/reset-password-form.js.map +1 -1
  50. package/dist/auth/sign-in-form-labels.js +1 -1
  51. package/dist/auth/sign-in-form-labels.js.map +1 -1
  52. package/dist/auth/status-parts.d.ts +0 -3
  53. package/dist/auth/status-parts.js +0 -4
  54. package/dist/auth/status-parts.js.map +1 -1
  55. package/dist/auth/verify-email-labels.js +1 -1
  56. package/dist/auth/verify-email-labels.js.map +1 -1
  57. package/dist/auth/verify-email.js +7 -2
  58. package/dist/auth/verify-email.js.map +1 -1
  59. package/dist/billing/cancel-subscription.d.ts +10 -7
  60. package/dist/billing/cancel-subscription.js +6 -6
  61. package/dist/billing/cancel-subscription.js.map +1 -1
  62. package/dist/billing/plan-card.d.ts +5 -3
  63. package/dist/billing/plan-card.js +7 -8
  64. package/dist/billing/plan-card.js.map +1 -1
  65. package/dist/billing/plan-change-labels.d.ts +42 -0
  66. package/dist/billing/plan-change-labels.js +22 -0
  67. package/dist/billing/plan-change-labels.js.map +1 -0
  68. package/dist/billing/plan-switch.d.ts +9 -6
  69. package/dist/billing/plan-switch.js +7 -5
  70. package/dist/billing/plan-switch.js.map +1 -1
  71. package/dist/chart.d.ts +4 -3
  72. package/dist/chart.js +72 -4
  73. package/dist/chart.js.map +1 -1
  74. package/dist/components/account-chips.d.ts +5 -5
  75. package/dist/components/account-chips.js +11 -13
  76. package/dist/components/account-chips.js.map +1 -1
  77. package/dist/components/alert-banner.d.ts +16 -5
  78. package/dist/components/alert-banner.js +46 -1
  79. package/dist/components/alert-banner.js.map +1 -1
  80. package/dist/components/amount-input.d.ts +13 -0
  81. package/dist/components/amount-input.js +1 -0
  82. package/dist/components/amount-input.js.map +1 -1
  83. package/dist/components/autocomplete.js +57 -65
  84. package/dist/components/autocomplete.js.map +1 -1
  85. package/dist/components/bulk-action-bar.d.ts +12 -2
  86. package/dist/components/bulk-action-bar.js +65 -21
  87. package/dist/components/bulk-action-bar.js.map +1 -1
  88. package/dist/components/button-look.d.ts +5 -5
  89. package/dist/components/button-look.js.map +1 -1
  90. package/dist/components/button.d.ts +2 -2
  91. package/dist/components/button.js.map +1 -1
  92. package/dist/components/chart-edge.d.ts +16 -0
  93. package/dist/components/chart-edge.js +14 -1
  94. package/dist/components/chart-edge.js.map +1 -1
  95. package/dist/components/chart-figure.d.ts +13 -0
  96. package/dist/components/chart-figure.js +15 -0
  97. package/dist/components/chart-figure.js.map +1 -0
  98. package/dist/components/chart-zoom-core.js +1 -4
  99. package/dist/components/chart-zoom-core.js.map +1 -1
  100. package/dist/components/chart.d.ts +8 -1
  101. package/dist/components/chart.js +19 -10
  102. package/dist/components/chart.js.map +1 -1
  103. package/dist/components/combobox-core.d.ts +111 -2
  104. package/dist/components/combobox-core.js +148 -42
  105. package/dist/components/combobox-core.js.map +1 -1
  106. package/dist/components/combobox.d.ts +1 -1
  107. package/dist/components/combobox.js +141 -130
  108. package/dist/components/combobox.js.map +1 -1
  109. package/dist/components/copy-button.d.ts +6 -5
  110. package/dist/components/copy-button.js +5 -3
  111. package/dist/components/copy-button.js.map +1 -1
  112. package/dist/components/country-select.js +2 -2
  113. package/dist/components/country-select.js.map +1 -1
  114. package/dist/components/data-table-column-settings.d.ts +1 -1
  115. package/dist/components/data-table-column-settings.js +33 -7
  116. package/dist/components/data-table-column-settings.js.map +1 -1
  117. package/dist/components/data-table-filters.d.ts +4 -2
  118. package/dist/components/data-table-filters.js +2 -2
  119. package/dist/components/data-table-filters.js.map +1 -1
  120. package/dist/components/data-table-fit.d.ts +6 -4
  121. package/dist/components/data-table-fit.js +4 -1
  122. package/dist/components/data-table-fit.js.map +1 -1
  123. package/dist/components/data-table-groups.d.ts +1 -1
  124. package/dist/components/data-table-groups.js.map +1 -1
  125. package/dist/components/data-table-head.d.ts +1 -1
  126. package/dist/components/data-table-head.js +15 -12
  127. package/dist/components/data-table-head.js.map +1 -1
  128. package/dist/components/data-table-mobile.d.ts +12 -7
  129. package/dist/components/data-table-mobile.js +43 -30
  130. package/dist/components/data-table-mobile.js.map +1 -1
  131. package/dist/components/data-table-pagination.js +11 -6
  132. package/dist/components/data-table-pagination.js.map +1 -1
  133. package/dist/components/data-table-resize.d.ts +1 -1
  134. package/dist/components/data-table-resize.js.map +1 -1
  135. package/dist/components/data-table-row-actions.d.ts +2 -2
  136. package/dist/components/data-table-row-actions.js.map +1 -1
  137. package/dist/components/data-table-row.d.ts +6 -2
  138. package/dist/components/data-table-row.js +17 -10
  139. package/dist/components/data-table-row.js.map +1 -1
  140. package/dist/components/data-table-selection.d.ts +1 -1
  141. package/dist/components/data-table-selection.js.map +1 -1
  142. package/dist/components/data-table-shared.d.ts +10 -2
  143. package/dist/components/data-table-shared.js +18 -0
  144. package/dist/components/data-table-shared.js.map +1 -1
  145. package/dist/components/data-table-sort.d.ts +21 -2
  146. package/dist/components/data-table-sort.js +24 -0
  147. package/dist/components/data-table-sort.js.map +1 -1
  148. package/dist/components/data-table-virtual.d.ts +1 -1
  149. package/dist/components/data-table-virtual.js +18 -6
  150. package/dist/components/data-table-virtual.js.map +1 -1
  151. package/dist/components/data-table.d.ts +13 -4
  152. package/dist/components/data-table.js +139 -36
  153. package/dist/components/data-table.js.map +1 -1
  154. package/dist/components/date-field.d.ts +25 -3
  155. package/dist/components/date-field.js +33 -8
  156. package/dist/components/date-field.js.map +1 -1
  157. package/dist/components/date-picker.d.ts +16 -3
  158. package/dist/components/date-picker.js +55 -26
  159. package/dist/components/date-picker.js.map +1 -1
  160. package/dist/components/date-range-picker.d.ts +38 -24
  161. package/dist/components/date-range-picker.js +23 -7
  162. package/dist/components/date-range-picker.js.map +1 -1
  163. package/dist/components/error-boundary.js +37 -30
  164. package/dist/components/error-boundary.js.map +1 -1
  165. package/dist/components/{ui-labels.d.ts → field-base-labels.d.ts} +0 -2
  166. package/dist/components/{ui-labels.js → field-base-labels.js} +2 -9
  167. package/dist/components/field-base-labels.js.map +1 -0
  168. package/dist/components/field-base.d.ts +4 -3
  169. package/dist/components/field-base.js +3 -3
  170. package/dist/components/field-base.js.map +1 -1
  171. package/dist/components/form-actions.js +11 -5
  172. package/dist/components/form-actions.js.map +1 -1
  173. package/dist/components/hover-menu.d.ts +3 -0
  174. package/dist/components/hover-menu.js +5 -3
  175. package/dist/components/hover-menu.js.map +1 -1
  176. package/dist/components/icon-button.js +14 -9
  177. package/dist/components/icon-button.js.map +1 -1
  178. package/dist/components/inline-confirm.d.ts +43 -0
  179. package/dist/components/inline-confirm.js +67 -0
  180. package/dist/components/inline-confirm.js.map +1 -0
  181. package/dist/components/inline-entity-combobox.d.ts +4 -3
  182. package/dist/components/inline-entity-combobox.js +148 -140
  183. package/dist/components/inline-entity-combobox.js.map +1 -1
  184. package/dist/components/input.js +1 -1
  185. package/dist/components/input.js.map +1 -1
  186. package/dist/components/jump-calendar.js +28 -112
  187. package/dist/components/jump-calendar.js.map +1 -1
  188. package/dist/components/legal-page.d.ts +3 -1
  189. package/dist/components/legal-page.js +3 -2
  190. package/dist/components/legal-page.js.map +1 -1
  191. package/dist/components/legal.d.ts +4 -2
  192. package/dist/components/legal.js.map +1 -1
  193. package/dist/components/lightbox.d.ts +2 -2
  194. package/dist/components/lightbox.js +17 -6
  195. package/dist/components/lightbox.js.map +1 -1
  196. package/dist/components/list.d.ts +6 -2
  197. package/dist/components/list.js +3 -2
  198. package/dist/components/list.js.map +1 -1
  199. package/dist/components/measured-grid.d.ts +1 -1
  200. package/dist/components/measured-grid.js +2 -1
  201. package/dist/components/measured-grid.js.map +1 -1
  202. package/dist/components/mini-calendar.js +3 -5
  203. package/dist/components/mini-calendar.js.map +1 -1
  204. package/dist/components/modal.js +3 -1
  205. package/dist/components/modal.js.map +1 -1
  206. package/dist/components/month-grid.d.ts +55 -0
  207. package/dist/components/month-grid.js +148 -0
  208. package/dist/components/month-grid.js.map +1 -0
  209. package/dist/components/month-picker.d.ts +36 -4
  210. package/dist/components/month-picker.js +123 -146
  211. package/dist/components/month-picker.js.map +1 -1
  212. package/dist/components/multi-select.d.ts +2 -2
  213. package/dist/components/multi-select.js +11 -6
  214. package/dist/components/multi-select.js.map +1 -1
  215. package/dist/components/nav-pills.d.ts +1 -1
  216. package/dist/components/nav-pills.js.map +1 -1
  217. package/dist/components/picker-sheet.js +2 -1
  218. package/dist/components/picker-sheet.js.map +1 -1
  219. package/dist/components/pie-chart-labels.d.ts +7 -0
  220. package/dist/components/pie-chart-labels.js +1 -0
  221. package/dist/components/pie-chart-labels.js.map +1 -1
  222. package/dist/components/pie-chart.d.ts +5 -3
  223. package/dist/components/pie-chart.js +7 -22
  224. package/dist/components/pie-chart.js.map +1 -1
  225. package/dist/components/popover.d.ts +3 -0
  226. package/dist/components/popover.js +7 -1
  227. package/dist/components/popover.js.map +1 -1
  228. package/dist/components/row-actions.js +2 -1
  229. package/dist/components/row-actions.js.map +1 -1
  230. package/dist/components/series-chart-axes.d.ts +4 -4
  231. package/dist/components/series-chart-axes.js.map +1 -1
  232. package/dist/components/series-chart-labels.d.ts +7 -4
  233. package/dist/components/series-chart-labels.js.map +1 -1
  234. package/dist/components/series-chart.d.ts +92 -23
  235. package/dist/components/series-chart.js +74 -30
  236. package/dist/components/series-chart.js.map +1 -1
  237. package/dist/components/share-card.js +3 -3
  238. package/dist/components/share-card.js.map +1 -1
  239. package/dist/components/tabs-labels.d.ts +2 -0
  240. package/dist/components/tabs-labels.js +11 -0
  241. package/dist/components/tabs-labels.js.map +1 -0
  242. package/dist/components/tabs.d.ts +1 -1
  243. package/dist/components/tabs.js +1 -1
  244. package/dist/components/tabs.js.map +1 -1
  245. package/dist/components/toggle-legend.d.ts +2 -2
  246. package/dist/components/toggle-legend.js +6 -6
  247. package/dist/components/toggle-legend.js.map +1 -1
  248. package/dist/components/translation-review-editor.d.ts +4 -2
  249. package/dist/components/translation-review-editor.js +2 -3
  250. package/dist/components/translation-review-editor.js.map +1 -1
  251. package/dist/components/translation-review.d.ts +2 -1
  252. package/dist/components/translation-review.js +2 -2
  253. package/dist/components/translation-review.js.map +1 -1
  254. package/dist/components/treemap-labels.d.ts +31 -0
  255. package/dist/components/treemap-labels.js +10 -0
  256. package/dist/components/treemap-labels.js.map +1 -0
  257. package/dist/components/treemap-layout.d.ts +62 -0
  258. package/dist/components/treemap-layout.js +126 -0
  259. package/dist/components/treemap-layout.js.map +1 -0
  260. package/dist/components/treemap.d.ts +167 -19
  261. package/dist/components/treemap.js +340 -100
  262. package/dist/components/treemap.js.map +1 -1
  263. package/dist/components/use-table-state.d.ts +5 -5
  264. package/dist/components/use-table-state.js +4 -1
  265. package/dist/components/use-table-state.js.map +1 -1
  266. package/dist/data-table.d.ts +1 -2
  267. package/dist/data-table.js +1 -4
  268. package/dist/data-table.js.map +1 -1
  269. package/dist/demo/demo-countdown.d.ts +0 -5
  270. package/dist/demo/demo-countdown.js +0 -4
  271. package/dist/demo/demo-countdown.js.map +1 -1
  272. package/dist/eslint/index.js +1 -1
  273. package/dist/eslint/role-utilities.d.ts +36 -7
  274. package/dist/eslint/role-utilities.js +42 -9
  275. package/dist/eslint/role-utilities.js.map +1 -1
  276. package/dist/feedback/feedback-dialog.d.ts +25 -16
  277. package/dist/feedback/feedback-dialog.js +6 -2
  278. package/dist/feedback/feedback-dialog.js.map +1 -1
  279. package/dist/feedback/feedback-mutation.d.ts +18 -0
  280. package/dist/feedback/feedback-mutation.js +8 -0
  281. package/dist/feedback/feedback-mutation.js.map +1 -0
  282. package/dist/feedback/feedback-page-link.d.ts +53 -0
  283. package/dist/feedback/feedback-page-link.js +37 -0
  284. package/dist/feedback/feedback-page-link.js.map +1 -0
  285. package/dist/feedback/feedback-public-list.d.ts +8 -4
  286. package/dist/feedback/feedback-public-list.js +2 -2
  287. package/dist/feedback/feedback-public-list.js.map +1 -1
  288. package/dist/feedback/feedback-replies.d.ts +3 -1
  289. package/dist/feedback/feedback-replies.js +5 -4
  290. package/dist/feedback/feedback-replies.js.map +1 -1
  291. package/dist/feedback/feedback-row-detail-labels.js +0 -3
  292. package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
  293. package/dist/feedback/feedback-row-detail.d.ts +33 -44
  294. package/dist/feedback/feedback-row-detail.js +28 -34
  295. package/dist/feedback/feedback-row-detail.js.map +1 -1
  296. package/dist/feedback/feedback-status-undo.d.ts +17 -14
  297. package/dist/feedback/feedback-status-undo.js +10 -15
  298. package/dist/feedback/feedback-status-undo.js.map +1 -1
  299. package/dist/feedback/feedback-submit-dialog.d.ts +1 -1
  300. package/dist/feedback/feedback-submit-dialog.js +1 -1
  301. package/dist/feedback/feedback-submit-dialog.js.map +1 -1
  302. package/dist/feedback/feedback-submit.js +9 -3
  303. package/dist/feedback/feedback-submit.js.map +1 -1
  304. package/dist/feedback/feedback-swipe.d.ts +1 -1
  305. package/dist/feedback/feedback-swipe.js.map +1 -1
  306. package/dist/feedback/feedback-table.d.ts +21 -9
  307. package/dist/feedback/feedback-table.js +45 -25
  308. package/dist/feedback/feedback-table.js.map +1 -1
  309. package/dist/feedback/feedback-thread-labels.d.ts +2 -2
  310. package/dist/feedback/feedback-thread-labels.js.map +1 -1
  311. package/dist/feedback/feedback-thread.d.ts +15 -23
  312. package/dist/feedback/feedback-thread.js +2 -1
  313. package/dist/feedback/feedback-thread.js.map +1 -1
  314. package/dist/feedback.d.ts +6 -6
  315. package/dist/feedback.js +90 -6
  316. package/dist/feedback.js.map +1 -1
  317. package/dist/hooks/use-focus-trap.d.ts +35 -0
  318. package/dist/hooks/use-focus-trap.js +6 -4
  319. package/dist/hooks/use-focus-trap.js.map +1 -1
  320. package/dist/hooks/use-hotkey.d.ts +5 -2
  321. package/dist/hooks/use-hotkey.js +5 -4
  322. package/dist/hooks/use-hotkey.js.map +1 -1
  323. package/dist/hooks/use-long-press.d.ts +7 -4
  324. package/dist/hooks/use-long-press.js +3 -0
  325. package/dist/hooks/use-long-press.js.map +1 -1
  326. package/dist/hooks/use-media-query.js +12 -4
  327. package/dist/hooks/use-media-query.js.map +1 -1
  328. package/dist/hooks/use-mounted.d.ts +22 -0
  329. package/dist/hooks/use-mounted.js +24 -0
  330. package/dist/hooks/use-mounted.js.map +1 -0
  331. package/dist/hooks/use-overlay-history.js +19 -3
  332. package/dist/hooks/use-overlay-history.js.map +1 -1
  333. package/dist/i18n/defaults.js +4 -9
  334. package/dist/i18n/defaults.js.map +1 -1
  335. package/dist/i18n/german.js +7 -18
  336. package/dist/i18n/german.js.map +1 -1
  337. package/dist/i18n/kit-labels.d.ts +9 -11
  338. package/dist/i18n/kit-labels.js.map +1 -1
  339. package/dist/i18n/locales/en.js +15 -64
  340. package/dist/i18n/locales/en.js.map +3 -3
  341. package/dist/i18n/locales/es.js +7 -18
  342. package/dist/i18n/locales/es.js.map +1 -1
  343. package/dist/i18n/locales/fr.js +20 -27
  344. package/dist/i18n/locales/fr.js.map +1 -1
  345. package/dist/i18n/locales/hu.js +9 -18
  346. package/dist/i18n/locales/hu.js.map +1 -1
  347. package/dist/i18n/locales/it.js +7 -19
  348. package/dist/i18n/locales/it.js.map +1 -1
  349. package/dist/i18n/locales/zh.js +8 -18
  350. package/dist/i18n/locales/zh.js.map +1 -1
  351. package/dist/i18n/review.js +12 -2
  352. package/dist/i18n/review.js.map +1 -1
  353. package/dist/i18n/shared-labels.js +0 -1
  354. package/dist/i18n/shared-labels.js.map +1 -1
  355. package/dist/index.d.ts +21 -23
  356. package/dist/index.js +50 -65
  357. package/dist/index.js.map +1 -1
  358. package/dist/landing/routing.d.ts +3 -1
  359. package/dist/landing/routing.js +4 -7
  360. package/dist/landing/routing.js.map +1 -1
  361. package/dist/lib/broadcast.d.ts +17 -0
  362. package/dist/lib/broadcast.js +30 -0
  363. package/dist/lib/broadcast.js.map +1 -0
  364. package/dist/lib/date-format.d.ts +37 -2
  365. package/dist/lib/date-format.js +34 -8
  366. package/dist/lib/date-format.js.map +1 -1
  367. package/dist/lib/day-format.d.ts +46 -0
  368. package/dist/lib/day-format.js +46 -0
  369. package/dist/lib/day-format.js.map +1 -0
  370. package/dist/lib/describe-failure.d.ts +14 -0
  371. package/dist/lib/describe-failure.js +9 -0
  372. package/dist/lib/describe-failure.js.map +1 -0
  373. package/dist/lib/direction.d.ts +16 -0
  374. package/dist/lib/direction.js +9 -1
  375. package/dist/lib/direction.js.map +1 -1
  376. package/dist/lib/focus-left.d.ts +63 -0
  377. package/dist/lib/focus-left.js +67 -0
  378. package/dist/lib/focus-left.js.map +1 -0
  379. package/dist/lib/format.js +5 -14
  380. package/dist/lib/format.js.map +1 -1
  381. package/dist/lib/http-status.d.ts +24 -0
  382. package/dist/lib/http-status.js +14 -0
  383. package/dist/lib/http-status.js.map +1 -0
  384. package/dist/lib/intl-cache.d.ts +14 -0
  385. package/dist/lib/intl-cache.js +15 -0
  386. package/dist/lib/intl-cache.js.map +1 -0
  387. package/dist/lib/iso-day.d.ts +11 -0
  388. package/dist/lib/iso-day.js +8 -0
  389. package/dist/lib/iso-day.js.map +1 -0
  390. package/dist/lib/lazy-with-fallback.d.ts +31 -0
  391. package/dist/lib/lazy-with-fallback.js +40 -0
  392. package/dist/lib/lazy-with-fallback.js.map +1 -0
  393. package/dist/lib/path-pattern.d.ts +34 -0
  394. package/dist/lib/path-pattern.js +24 -0
  395. package/dist/lib/path-pattern.js.map +1 -0
  396. package/dist/lib/reduced-motion.d.ts +2 -1
  397. package/dist/lib/reduced-motion.js.map +1 -1
  398. package/dist/lib/round.d.ts +2 -1
  399. package/dist/lib/round.js +2 -1
  400. package/dist/lib/round.js.map +1 -1
  401. package/dist/lib/safe-storage.d.ts +24 -5
  402. package/dist/lib/safe-storage.js +27 -2
  403. package/dist/lib/safe-storage.js.map +1 -1
  404. package/dist/lib/session-refresh.js +1 -1
  405. package/dist/lib/session-refresh.js.map +1 -1
  406. package/dist/lib/share-format.d.ts +13 -0
  407. package/dist/lib/share-format.js +10 -0
  408. package/dist/lib/share-format.js.map +1 -0
  409. package/dist/lib/text-case.d.ts +17 -0
  410. package/dist/lib/text-case.js +8 -0
  411. package/dist/lib/text-case.js.map +1 -0
  412. package/dist/lib/text-match.d.ts +29 -0
  413. package/dist/lib/text-match.js +20 -2
  414. package/dist/lib/text-match.js.map +1 -1
  415. package/dist/lib/thenable.d.ts +13 -0
  416. package/dist/lib/thenable.js +8 -0
  417. package/dist/lib/thenable.js.map +1 -0
  418. package/dist/lib/translation-review.d.ts +7 -2
  419. package/dist/lib/translation-review.js +4 -4
  420. package/dist/lib/translation-review.js.map +1 -1
  421. package/dist/notifications/device-zone-report.js +1 -1
  422. package/dist/notifications/device-zone-report.js.map +1 -1
  423. package/dist/notifications/notification-bell.d.ts +2 -1
  424. package/dist/notifications/notification-bell.js +3 -3
  425. package/dist/notifications/notification-bell.js.map +1 -1
  426. package/dist/notifications/notification-inbox.d.ts +10 -6
  427. package/dist/notifications/notification-inbox.js +26 -48
  428. package/dist/notifications/notification-inbox.js.map +1 -1
  429. package/dist/notifications/notification-parts.d.ts +3 -2
  430. package/dist/notifications/notification-parts.js +1 -3
  431. package/dist/notifications/notification-parts.js.map +1 -1
  432. package/dist/notifications/notification-types-setting.js +1 -1
  433. package/dist/notifications/notification-types-setting.js.map +1 -1
  434. package/dist/notifications/notifications-labels.d.ts +6 -0
  435. package/dist/notifications/notifications-labels.js +1 -0
  436. package/dist/notifications/notifications-labels.js.map +1 -1
  437. package/dist/notifications/push-device-setting.js +1 -1
  438. package/dist/notifications/push-device-setting.js.map +1 -1
  439. package/dist/notifications/push-devices-setting.d.ts +2 -1
  440. package/dist/notifications/push-devices-setting.js +27 -49
  441. package/dist/notifications/push-devices-setting.js.map +1 -1
  442. package/dist/notifications/quiet-hours-setting.d.ts +10 -3
  443. package/dist/notifications/quiet-hours-setting.js +65 -26
  444. package/dist/notifications/quiet-hours-setting.js.map +1 -1
  445. package/dist/notifications/unsubscribe-panel.js +4 -7
  446. package/dist/notifications/unsubscribe-panel.js.map +1 -1
  447. package/dist/notifications/use-push-device.d.ts +39 -1
  448. package/dist/notifications/use-push-device.js +28 -7
  449. package/dist/notifications/use-push-device.js.map +1 -1
  450. package/dist/offline/query-persistence.d.ts +3 -1
  451. package/dist/offline/query-persistence.js +19 -2
  452. package/dist/offline/query-persistence.js.map +1 -1
  453. package/dist/rhf/fields.d.ts +11 -4
  454. package/dist/rhf/fields.js.map +1 -1
  455. package/dist/search/command-palette.js +1 -1
  456. package/dist/search/command-palette.js.map +1 -1
  457. package/dist/search/global-search.js +19 -9
  458. package/dist/search/global-search.js.map +1 -1
  459. package/dist/search/search-index.d.ts +0 -6
  460. package/dist/search/search-index.js +0 -4
  461. package/dist/search/search-index.js.map +1 -1
  462. package/dist/search/use-command-key.d.ts +12 -1
  463. package/dist/search/use-command-key.js +3 -12
  464. package/dist/search/use-command-key.js.map +1 -1
  465. package/dist/search.d.ts +1 -1
  466. package/dist/search.js +6 -1
  467. package/dist/search.js.map +1 -1
  468. package/dist/settings/use-settings-focus.js +1 -0
  469. package/dist/settings/use-settings-focus.js.map +1 -1
  470. package/dist/shell/account-menu-language.d.ts +5 -3
  471. package/dist/shell/account-menu-language.js +2 -2
  472. package/dist/shell/account-menu-language.js.map +1 -1
  473. package/dist/shell/account-menu-text-size.d.ts +9 -6
  474. package/dist/shell/account-menu-text-size.js +2 -2
  475. package/dist/shell/account-menu-text-size.js.map +1 -1
  476. package/dist/shell/app-shell.d.ts +0 -10
  477. package/dist/shell/app-shell.js +1 -7
  478. package/dist/shell/app-shell.js.map +1 -1
  479. package/dist/shell/company-switcher.d.ts +1 -1
  480. package/dist/shell/company-switcher.js.map +1 -1
  481. package/dist/shell/dev-menu.d.ts +1 -1
  482. package/dist/shell/dev-menu.js +5 -18
  483. package/dist/shell/dev-menu.js.map +1 -1
  484. package/dist/shell/top-bar-actions.d.ts +1 -1
  485. package/dist/shell/top-bar-actions.js.map +1 -1
  486. package/dist/shell/top-bar-parts-labels.d.ts +1 -3
  487. package/dist/shell/top-bar-parts-labels.js +0 -1
  488. package/dist/shell/top-bar-parts-labels.js.map +1 -1
  489. package/dist/shell.d.ts +7 -10
  490. package/dist/shell.js +14 -23
  491. package/dist/shell.js.map +1 -1
  492. package/dist/sync/engine.d.ts +30 -4
  493. package/dist/sync/engine.js +172 -52
  494. package/dist/sync/engine.js.map +1 -1
  495. package/dist/sync.d.ts +2 -2
  496. package/dist/sync.js +2 -10
  497. package/dist/sync.js.map +1 -1
  498. package/dist/testing/css-variables.js +3 -8
  499. package/dist/testing/css-variables.js.map +1 -1
  500. package/dist/theme/chart-palette.d.ts +0 -5
  501. package/dist/theme/chart-palette.js +0 -2
  502. package/dist/theme/chart-palette.js.map +1 -1
  503. package/dist/theme/contrast.d.ts +2 -1
  504. package/dist/theme/contrast.js +3 -8
  505. package/dist/theme/contrast.js.map +1 -1
  506. package/dist/theme/palette-store.js +6 -6
  507. package/dist/theme/palette-store.js.map +1 -1
  508. package/dist/theme/text-size.js +3 -8
  509. package/dist/theme/text-size.js.map +1 -1
  510. package/dist/theme/theme-store.js +3 -6
  511. package/dist/theme/theme-store.js.map +1 -1
  512. package/dist/tour/tour-catalogue.d.ts +7 -4
  513. package/dist/tour/tour-catalogue.js +12 -10
  514. package/dist/tour/tour-catalogue.js.map +1 -1
  515. package/dist/tour/tour-dock.d.ts +0 -2
  516. package/dist/tour/tour-dock.js +0 -2
  517. package/dist/tour/tour-dock.js.map +1 -1
  518. package/dist/tour/tour-gates.d.ts +4 -0
  519. package/dist/tour/tour-gates.js +14 -0
  520. package/dist/tour/tour-gates.js.map +1 -1
  521. package/dist/tour/tour-overlay.d.ts +27 -0
  522. package/dist/tour/tour-overlay.js +741 -0
  523. package/dist/tour/tour-overlay.js.map +1 -0
  524. package/dist/tour/tour.d.ts +13 -3
  525. package/dist/tour/tour.js +59 -781
  526. package/dist/tour/tour.js.map +1 -1
  527. package/dist/tour/use-tour-progress.js +1 -1
  528. package/dist/tour/use-tour-progress.js.map +1 -1
  529. package/dist/wizard/stepper-nav.d.ts +5 -1
  530. package/dist/wizard/stepper-nav.js.map +1 -1
  531. package/dist/wizard/types.d.ts +6 -2
  532. package/dist/wizard/types.js.map +1 -1
  533. package/dist/wizard/use-wizard.d.ts +3 -1
  534. package/dist/wizard/use-wizard.js.map +1 -1
  535. package/dist/wizard/validation.d.ts +3 -1
  536. package/dist/wizard/validation.js.map +1 -1
  537. package/package.json +10 -6
  538. package/src/account/account-parts.ts +5 -25
  539. package/src/account/data-export-setting-labels.ts +1 -1
  540. package/src/account/delete-account-setting-labels.ts +1 -1
  541. package/src/account/email-change-setting-labels.ts +1 -1
  542. package/src/account/passkeys-setting.tsx +60 -38
  543. package/src/account/sessions-setting.tsx +18 -12
  544. package/src/admin/admin-action-confirm.tsx +1 -5
  545. package/src/admin/admin-parts.ts +11 -25
  546. package/src/admin/mail-test-card-labels.ts +1 -1
  547. package/src/admin/plan-change-labels.ts +5 -23
  548. package/src/admin/plan-change.tsx +7 -44
  549. package/src/admin/reviewer-scope-editor.tsx +3 -3
  550. package/src/auth/auth-errors-labels.ts +23 -0
  551. package/src/auth/auth-errors.ts +11 -38
  552. package/src/auth/complete-name-dialog.tsx +4 -1
  553. package/src/auth/forgot-password-form-labels.ts +1 -1
  554. package/src/auth/forgot-password-form.tsx +11 -6
  555. package/src/auth/register-form-labels.ts +1 -1
  556. package/src/auth/reset-password-form-labels.ts +1 -1
  557. package/src/auth/reset-password-form.tsx +8 -4
  558. package/src/auth/sign-in-form-labels.ts +1 -1
  559. package/src/auth/status-parts.tsx +0 -6
  560. package/src/auth/verify-email-labels.ts +1 -1
  561. package/src/auth/verify-email.tsx +11 -6
  562. package/src/billing/cancel-subscription.tsx +15 -12
  563. package/src/billing/plan-card.tsx +18 -10
  564. package/src/billing/plan-change-labels.ts +66 -0
  565. package/src/billing/plan-switch.tsx +15 -12
  566. package/src/chart.ts +73 -6
  567. package/src/components/account-chips.tsx +20 -23
  568. package/src/components/alert-banner.tsx +82 -5
  569. package/src/components/amount-input.tsx +18 -7
  570. package/src/components/autocomplete.tsx +64 -70
  571. package/src/components/bulk-action-bar.tsx +70 -2
  572. package/src/components/button-look.ts +5 -5
  573. package/src/components/button.tsx +2 -2
  574. package/src/components/chart-edge.ts +34 -1
  575. package/src/components/chart-figure.tsx +35 -0
  576. package/src/components/chart-zoom-core.tsx +6 -5
  577. package/src/components/chart.tsx +26 -5
  578. package/src/components/combobox-core.tsx +293 -62
  579. package/src/components/combobox.tsx +183 -153
  580. package/src/components/copy-button.tsx +34 -23
  581. package/src/components/country-select.tsx +16 -11
  582. package/src/components/data-table-column-settings.tsx +52 -10
  583. package/src/components/data-table-filters.ts +6 -4
  584. package/src/components/data-table-fit.ts +19 -5
  585. package/src/components/data-table-groups.tsx +1 -1
  586. package/src/components/data-table-head.tsx +32 -22
  587. package/src/components/data-table-mobile.tsx +61 -26
  588. package/src/components/data-table-pagination.tsx +21 -6
  589. package/src/components/data-table-resize.ts +1 -1
  590. package/src/components/data-table-row-actions.tsx +3 -3
  591. package/src/components/data-table-row.tsx +25 -14
  592. package/src/components/data-table-selection.ts +1 -1
  593. package/src/components/data-table-shared.tsx +33 -1
  594. package/src/components/data-table-sort.ts +57 -2
  595. package/src/components/data-table-virtual.ts +32 -8
  596. package/src/components/data-table.tsx +239 -68
  597. package/src/components/date-field.tsx +66 -9
  598. package/src/components/date-picker.tsx +125 -49
  599. package/src/components/date-range-picker.tsx +71 -36
  600. package/src/components/error-boundary.tsx +53 -39
  601. package/src/components/field-base-labels.ts +20 -0
  602. package/src/components/field-base.tsx +13 -7
  603. package/src/components/form-actions.tsx +20 -5
  604. package/src/components/hover-menu.tsx +18 -8
  605. package/src/components/icon-button.tsx +14 -9
  606. package/src/components/inline-confirm.tsx +116 -0
  607. package/src/components/inline-entity-combobox.tsx +197 -153
  608. package/src/components/input.tsx +1 -1
  609. package/src/components/jump-calendar.tsx +30 -146
  610. package/src/components/legal-page.tsx +10 -7
  611. package/src/components/legal.tsx +4 -2
  612. package/src/components/lightbox.tsx +21 -8
  613. package/src/components/list.tsx +10 -4
  614. package/src/components/measured-grid.tsx +8 -4
  615. package/src/components/mini-calendar.tsx +6 -5
  616. package/src/components/modal.tsx +6 -1
  617. package/src/components/month-grid.tsx +229 -0
  618. package/src/components/month-picker.tsx +192 -200
  619. package/src/components/multi-select.tsx +21 -9
  620. package/src/components/nav-pills.tsx +1 -1
  621. package/src/components/picker-sheet.tsx +12 -9
  622. package/src/components/pie-chart-labels.ts +8 -0
  623. package/src/components/pie-chart.tsx +22 -28
  624. package/src/components/popover.tsx +18 -1
  625. package/src/components/row-actions.tsx +2 -1
  626. package/src/components/series-chart-axes.ts +9 -4
  627. package/src/components/series-chart-labels.ts +7 -4
  628. package/src/components/series-chart.tsx +253 -87
  629. package/src/components/share-card.tsx +6 -6
  630. package/src/components/tabs-labels.ts +13 -0
  631. package/src/components/tabs.tsx +2 -2
  632. package/src/components/toggle-legend.tsx +16 -11
  633. package/src/components/translation-review-editor.tsx +6 -5
  634. package/src/components/translation-review.tsx +4 -3
  635. package/src/components/treemap-labels.ts +47 -0
  636. package/src/components/treemap-layout.ts +210 -0
  637. package/src/components/treemap.tsx +780 -138
  638. package/src/components/use-table-state.ts +12 -9
  639. package/src/data-table.ts +1 -4
  640. package/src/demo/demo-countdown.ts +3 -8
  641. package/src/eslint/role-utilities.ts +97 -19
  642. package/src/feedback/feedback-dialog.tsx +30 -19
  643. package/src/feedback/feedback-mutation.ts +20 -0
  644. package/src/feedback/feedback-page-link.ts +90 -0
  645. package/src/feedback/feedback-public-list.tsx +13 -7
  646. package/src/feedback/feedback-replies.tsx +13 -6
  647. package/src/feedback/feedback-row-detail-labels.ts +0 -3
  648. package/src/feedback/feedback-row-detail.tsx +70 -84
  649. package/src/feedback/feedback-status-undo.ts +28 -28
  650. package/src/feedback/feedback-submit-dialog.tsx +2 -2
  651. package/src/feedback/feedback-submit.tsx +21 -7
  652. package/src/feedback/feedback-swipe.tsx +1 -1
  653. package/src/feedback/feedback-table.tsx +86 -38
  654. package/src/feedback/feedback-thread-labels.ts +2 -2
  655. package/src/feedback/feedback-thread.tsx +18 -26
  656. package/src/feedback.ts +89 -6
  657. package/src/hooks/use-focus-trap.ts +8 -24
  658. package/src/hooks/use-hotkey.ts +24 -7
  659. package/src/hooks/use-long-press.ts +16 -2
  660. package/src/hooks/use-media-query.ts +20 -4
  661. package/src/hooks/use-mounted.ts +39 -0
  662. package/src/hooks/use-overlay-history.ts +32 -3
  663. package/src/i18n/defaults.ts +4 -9
  664. package/src/i18n/german.ts +8 -19
  665. package/src/i18n/kit-labels.tsx +9 -11
  666. package/src/i18n/locales/es.ts +8 -19
  667. package/src/i18n/locales/fr.ts +21 -30
  668. package/src/i18n/locales/hu.ts +10 -19
  669. package/src/i18n/locales/it.ts +8 -20
  670. package/src/i18n/locales/zh.ts +9 -19
  671. package/src/i18n/review.ts +12 -2
  672. package/src/i18n/shared-labels.ts +0 -1
  673. package/src/index.ts +57 -49
  674. package/src/landing/routing.tsx +14 -10
  675. package/src/lib/broadcast.ts +52 -0
  676. package/src/lib/date-format.ts +89 -13
  677. package/src/lib/day-format.ts +96 -0
  678. package/src/lib/describe-failure.ts +18 -0
  679. package/src/lib/direction.ts +39 -0
  680. package/src/lib/focus-left.ts +156 -0
  681. package/src/lib/format.ts +5 -17
  682. package/src/lib/http-status.ts +38 -0
  683. package/src/lib/intl-cache.ts +25 -0
  684. package/src/lib/iso-day.ts +13 -0
  685. package/src/lib/lazy-with-fallback.ts +82 -0
  686. package/src/lib/path-pattern.ts +55 -0
  687. package/src/lib/reduced-motion.ts +2 -1
  688. package/src/lib/round.ts +7 -2
  689. package/src/lib/safe-storage.ts +48 -6
  690. package/src/lib/session-refresh.ts +1 -1
  691. package/src/lib/share-format.ts +18 -0
  692. package/src/lib/text-case.ts +19 -0
  693. package/src/lib/text-match.ts +57 -2
  694. package/src/lib/thenable.ts +19 -0
  695. package/src/lib/translation-review.ts +13 -5
  696. package/src/notifications/device-zone-report.ts +1 -1
  697. package/src/notifications/notification-bell.tsx +7 -4
  698. package/src/notifications/notification-inbox.tsx +40 -58
  699. package/src/notifications/notification-parts.tsx +3 -4
  700. package/src/notifications/notification-types-setting.tsx +1 -1
  701. package/src/notifications/notifications-labels.ts +7 -0
  702. package/src/notifications/push-device-setting.tsx +1 -1
  703. package/src/notifications/push-devices-setting.tsx +30 -55
  704. package/src/notifications/quiet-hours-setting.tsx +93 -31
  705. package/src/notifications/unsubscribe-panel.tsx +4 -7
  706. package/src/notifications/use-push-device.ts +76 -10
  707. package/src/offline/query-persistence.ts +35 -3
  708. package/src/rhf/fields.tsx +11 -4
  709. package/src/search/command-palette.tsx +2 -4
  710. package/src/search/global-search.tsx +36 -15
  711. package/src/search/search-index.ts +0 -15
  712. package/src/search/use-command-key.ts +13 -12
  713. package/src/search.ts +10 -1
  714. package/src/settings/use-settings-focus.ts +3 -0
  715. package/src/shell/account-menu-language.tsx +6 -4
  716. package/src/shell/account-menu-text-size.tsx +10 -7
  717. package/src/shell/app-shell.tsx +0 -13
  718. package/src/shell/company-switcher.tsx +1 -1
  719. package/src/shell/dev-menu.tsx +8 -22
  720. package/src/shell/top-bar-actions.tsx +1 -1
  721. package/src/shell/top-bar-parts-labels.ts +1 -4
  722. package/src/shell.ts +21 -17
  723. package/src/sync/engine.ts +339 -61
  724. package/src/sync.ts +2 -8
  725. package/src/testing/css-variables.ts +8 -9
  726. package/src/theme/chart-palette.ts +0 -2
  727. package/src/theme/contrast.ts +8 -9
  728. package/src/theme/palette-store.ts +9 -9
  729. package/src/theme/text-size.ts +6 -8
  730. package/src/theme/theme-store.ts +7 -9
  731. package/src/tour/tour-catalogue.tsx +26 -15
  732. package/src/tour/tour-dock.ts +0 -2
  733. package/src/tour/tour-gates.ts +23 -0
  734. package/src/tour/tour-overlay.tsx +1170 -0
  735. package/src/tour/tour.tsx +108 -1202
  736. package/src/tour/use-tour-progress.ts +1 -1
  737. package/src/wizard/stepper-nav.tsx +3 -1
  738. package/src/wizard/types.ts +11 -2
  739. package/src/wizard/use-wizard.ts +2 -1
  740. package/src/wizard/validation.ts +8 -2
  741. package/tokens.css +6 -5
  742. package/dist/components/field-sync-labels.d.ts +0 -2
  743. package/dist/components/field-sync-labels.js +0 -12
  744. package/dist/components/field-sync-labels.js.map +0 -1
  745. package/dist/components/field-sync.d.ts +0 -164
  746. package/dist/components/field-sync.js +0 -269
  747. package/dist/components/field-sync.js.map +0 -1
  748. package/dist/components/section-nav.d.ts +0 -37
  749. package/dist/components/section-nav.js +0 -33
  750. package/dist/components/section-nav.js.map +0 -1
  751. package/dist/components/sign-chip-labels.d.ts +0 -2
  752. package/dist/components/sign-chip-labels.js +0 -11
  753. package/dist/components/sign-chip-labels.js.map +0 -1
  754. package/dist/components/sign-chip.d.ts +0 -68
  755. package/dist/components/sign-chip.js +0 -47
  756. package/dist/components/sign-chip.js.map +0 -1
  757. package/dist/components/ui-labels.js.map +0 -1
  758. package/dist/lib/isin.d.ts +0 -28
  759. package/dist/lib/isin.js +0 -36
  760. package/dist/lib/isin.js.map +0 -1
  761. package/dist/shell/dev-switchers.d.ts +0 -6
  762. package/dist/shell/dev-switchers.js +0 -12
  763. package/dist/shell/dev-switchers.js.map +0 -1
  764. package/dist/shell/role-switcher.d.ts +0 -21
  765. package/dist/shell/role-switcher.js +0 -31
  766. package/dist/shell/role-switcher.js.map +0 -1
  767. package/src/components/field-sync-labels.ts +0 -13
  768. package/src/components/field-sync.tsx +0 -482
  769. package/src/components/section-nav.tsx +0 -65
  770. package/src/components/sign-chip-labels.ts +0 -12
  771. package/src/components/sign-chip.tsx +0 -124
  772. package/src/components/ui-labels.ts +0 -24
  773. package/src/lib/isin.ts +0 -62
  774. package/src/shell/dev-switchers.tsx +0 -18
  775. package/src/shell/role-switcher.tsx +0 -46
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/demo/demo-countdown.ts"],"sourcesContent":["import { useCallback, useEffect, useEffectEvent, useSyncExternalStore } from \"react\";\n\n/**\n * The demo's countdown (docs/landing-demo-harmonization.md §5.3, §5.4): the time left to\n * the demo account's end, from `expires_at` (the `TokenResponse`) or `demo_expires_at`\n * (`/auth/me`). The client never decodes the JWT.\n */\n\n/** When the demo ends: an ISO timestamp from the server, epoch milliseconds, or a\n * `Date`. */\nexport type DemoExpiry = string | number | Date;\n\n/** The last stretch, in minutes, in which the banner turns to the warning tone (§5.4). */\nexport const DEMO_WARNING_MINUTES = 10;\n/** Below this many minutes the countdown shows minutes only (§5.4). */\nexport const DEMO_MINUTES_ONLY = 60;\n\nconst MINUTE = 60_000;\n\nexport interface DemoCountdown {\n /**\n * Whole minutes left, ROUNDED UP — 30 s left is \"1 min\", and 0 only once the demo has\n * ended. `null` when `expiresAt` cannot be read: the countdown then shows no time and\n * never ends by itself (a parse slip must not throw a visitor out).\n */\n minutesLeft: number | null;\n /** The display split: whole hours, and the minutes past them. In the last hour\n * (`lastHour`) `hours` is 0 and `minutes` is `minutesLeft`. */\n hours: number;\n minutes: number;\n /** `minutesLeft <= 60`: show minutes only. */\n lastHour: boolean;\n /** `minutesLeft <= 10`: the warning tone. */\n warning: boolean;\n /** The demo's time is up. */\n ended: boolean;\n}\n\n/** `expiresAt` as epoch milliseconds, or NaN. */\nfunction expiryMs(expiresAt: DemoExpiry | null | undefined): number {\n if (expiresAt === null || expiresAt === undefined) return Number.NaN;\n if (typeof expiresAt === \"number\") return expiresAt;\n if (expiresAt instanceof Date) return expiresAt.getTime();\n return Date.parse(expiresAt);\n}\n\n/** Whole minutes left at `now`, rounded up, never below 0; null for an unreadable end. */\nfunction minutesAt(expiry: number, now: number): number | null {\n if (!Number.isFinite(expiry)) return null;\n return Math.max(0, Math.ceil((expiry - now) / MINUTE));\n}\n\n/**\n * The countdown at `now` — pure, for a test or a server-rendered value. See\n * {@link DemoCountdown} for the rounding and the thresholds.\n */\nexport function demoCountdown(expiresAt: DemoExpiry | null | undefined, now: number): DemoCountdown {\n return countdownOf(minutesAt(expiryMs(expiresAt), now));\n}\n\nfunction countdownOf(minutesLeft: number | null): DemoCountdown {\n if (minutesLeft === null) {\n return { minutesLeft, hours: 0, minutes: 0, lastHour: false, warning: false, ended: false };\n }\n const lastHour = minutesLeft <= DEMO_MINUTES_ONLY;\n return {\n minutesLeft,\n hours: lastHour ? 0 : Math.floor(minutesLeft / 60),\n minutes: lastHour ? minutesLeft : minutesLeft % 60,\n lastHour,\n warning: minutesLeft <= DEMO_WARNING_MINUTES,\n ended: minutesLeft === 0,\n };\n}\n\nexport interface DemoCountdownOptions {\n /**\n * The clock, in epoch milliseconds. Default `Date.now`. A test passes its own (or uses\n * fake timers, which move `Date.now` too). Keep it a stable function — a new one each\n * render re-arms the timer each render.\n */\n now?: () => number;\n /** Called once when the countdown reaches zero — also on mount, if it already has.\n * The app clears the session and replaces to `/demo/ended` (§5.5). */\n onEnded?: () => void;\n}\n\n/**\n * The time left to `expiresAt`, re-rendering when the shown minute changes — one timer\n * to the next minute boundary, not a tick a second — and once more when the tab comes\n * back from the background (a hidden tab's timers are throttled).\n *\n * `onEnded` fires at zero (§5.5). The countdown is the client's, from `expires_at`; the\n * server's answer to a request after the end (a 401) ends the demo as well — the app's\n * 401 handler, not this hook.\n */\nexport function useDemoCountdown(\n expiresAt: DemoExpiry | null | undefined,\n { now = Date.now, onEnded }: DemoCountdownOptions = {},\n): DemoCountdown {\n const expiry = expiryMs(expiresAt);\n const subscribe = useCallback(\n (notify: () => void) => {\n if (!Number.isFinite(expiry)) return () => {};\n let timer: ReturnType<typeof setTimeout> | undefined;\n const arm = () => {\n const left = expiry - now();\n if (left <= 0) return;\n // The rounded-up minute changes when `left` crosses the next whole minute below.\n const delay = left - (Math.ceil(left / MINUTE) - 1) * MINUTE;\n timer = setTimeout(() => {\n notify();\n arm();\n }, delay);\n };\n const onVisible = () => {\n if (document.visibilityState !== \"visible\") return;\n clearTimeout(timer);\n notify();\n arm();\n };\n arm();\n document.addEventListener(\"visibilitychange\", onVisible);\n return () => {\n clearTimeout(timer);\n document.removeEventListener(\"visibilitychange\", onVisible);\n };\n },\n [expiry, now],\n );\n const snapshot = () => minutesAt(expiry, now());\n const minutesLeft = useSyncExternalStore(subscribe, snapshot, snapshot);\n const countdown = countdownOf(minutesLeft);\n\n const fireEnded = useEffectEvent(() => onEnded?.());\n useEffect(() => {\n if (countdown.ended) fireEnded();\n }, [countdown.ended]);\n\n return countdown;\n}\n"],"mappings":";AAAA,SAAS,aAAa,WAAW,gBAAgB,4BAA4B;AAatE,MAAM,uBAAuB;AAE7B,MAAM,oBAAoB;AAEjC,MAAM,SAAS;AAsBf,SAAS,SAAS,WAAkD;AAClE,MAAI,cAAc,QAAQ,cAAc,OAAW,QAAO,OAAO;AACjE,MAAI,OAAO,cAAc,SAAU,QAAO;AAC1C,MAAI,qBAAqB,KAAM,QAAO,UAAU,QAAQ;AACxD,SAAO,KAAK,MAAM,SAAS;AAC7B;AAGA,SAAS,UAAU,QAAgB,KAA4B;AAC7D,MAAI,CAAC,OAAO,SAAS,MAAM,EAAG,QAAO;AACrC,SAAO,KAAK,IAAI,GAAG,KAAK,MAAM,SAAS,OAAO,MAAM,CAAC;AACvD;AAMO,SAAS,cAAc,WAA0C,KAA4B;AAClG,SAAO,YAAY,UAAU,SAAS,SAAS,GAAG,GAAG,CAAC;AACxD;AAEA,SAAS,YAAY,aAA2C;AAC9D,MAAI,gBAAgB,MAAM;AACxB,WAAO,EAAE,aAAa,OAAO,GAAG,SAAS,GAAG,UAAU,OAAO,SAAS,OAAO,OAAO,MAAM;AAAA,EAC5F;AACA,QAAM,WAAW,eAAe;AAChC,SAAO;AAAA,IACL;AAAA,IACA,OAAO,WAAW,IAAI,KAAK,MAAM,cAAc,EAAE;AAAA,IACjD,SAAS,WAAW,cAAc,cAAc;AAAA,IAChD;AAAA,IACA,SAAS,eAAe;AAAA,IACxB,OAAO,gBAAgB;AAAA,EACzB;AACF;AAuBO,SAAS,iBACd,WACA,EAAE,MAAM,KAAK,KAAK,QAAQ,IAA0B,CAAC,GACtC;AACf,QAAM,SAAS,SAAS,SAAS;AACjC,QAAM,YAAY;AAAA,IAChB,CAAC,WAAuB;AACtB,UAAI,CAAC,OAAO,SAAS,MAAM,EAAG,QAAO,MAAM;AAAA,MAAC;AAC5C,UAAI;AACJ,YAAM,MAAM,MAAM;AAChB,cAAM,OAAO,SAAS,IAAI;AAC1B,YAAI,QAAQ,EAAG;AAEf,cAAM,QAAQ,QAAQ,KAAK,KAAK,OAAO,MAAM,IAAI,KAAK;AACtD,gBAAQ,WAAW,MAAM;AACvB,iBAAO;AACP,cAAI;AAAA,QACN,GAAG,KAAK;AAAA,MACV;AACA,YAAM,YAAY,MAAM;AACtB,YAAI,SAAS,oBAAoB,UAAW;AAC5C,qBAAa,KAAK;AAClB,eAAO;AACP,YAAI;AAAA,MACN;AACA,UAAI;AACJ,eAAS,iBAAiB,oBAAoB,SAAS;AACvD,aAAO,MAAM;AACX,qBAAa,KAAK;AAClB,iBAAS,oBAAoB,oBAAoB,SAAS;AAAA,MAC5D;AAAA,IACF;AAAA,IACA,CAAC,QAAQ,GAAG;AAAA,EACd;AACA,QAAM,WAAW,MAAM,UAAU,QAAQ,IAAI,CAAC;AAC9C,QAAM,cAAc,qBAAqB,WAAW,UAAU,QAAQ;AACtE,QAAM,YAAY,YAAY,WAAW;AAEzC,QAAM,YAAY,eAAe,MAAM,UAAU,CAAC;AAClD,YAAU,MAAM;AACd,QAAI,UAAU,MAAO,WAAU;AAAA,EACjC,GAAG,CAAC,UAAU,KAAK,CAAC;AAEpB,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/demo/demo-countdown.ts"],"sourcesContent":["import { useCallback, useEffect, useEffectEvent, useSyncExternalStore } from \"react\";\n\n/**\n * The demo's countdown (docs/landing-demo-harmonization.md §5.3, §5.4): the time left to\n * the demo account's end, from `expires_at` (the `TokenResponse`) or `demo_expires_at`\n * (`/auth/me`). The client never decodes the JWT.\n */\n\n/** When the demo ends: an ISO timestamp from the server, epoch milliseconds, or a\n * `Date`. */\nexport type DemoExpiry = string | number | Date;\n\n/** The last stretch, in minutes, in which the banner turns to the warning tone (§5.4). */\nexport const DEMO_WARNING_MINUTES = 10;\n/** Below this many minutes the countdown shows minutes only (§5.4). */\nexport const DEMO_MINUTES_ONLY = 60;\n\nconst MINUTE = 60_000;\n\nexport interface DemoCountdown {\n /**\n * Whole minutes left, ROUNDED UP — 30 s left is \"1 min\", and 0 only once the demo has\n * ended. `null` when `expiresAt` cannot be read: the countdown then shows no time and\n * never ends by itself (a parse slip must not throw a visitor out).\n */\n minutesLeft: number | null;\n /** The display split: whole hours, and the minutes past them. In the last hour\n * (`lastHour`) `hours` is 0 and `minutes` is `minutesLeft`. */\n hours: number;\n minutes: number;\n /** `minutesLeft <= 60`: show minutes only. */\n lastHour: boolean;\n /** `minutesLeft <= 10`: the warning tone. */\n warning: boolean;\n /** The demo's time is up. */\n ended: boolean;\n}\n\n/** `expiresAt` as epoch milliseconds, or NaN. */\nfunction expiryMs(expiresAt: DemoExpiry | null | undefined): number {\n if (expiresAt === null || expiresAt === undefined) return Number.NaN;\n if (typeof expiresAt === \"number\") return expiresAt;\n if (expiresAt instanceof Date) return expiresAt.getTime();\n return Date.parse(expiresAt);\n}\n\n/** Whole minutes left at `now`, rounded up, never below 0; null for an unreadable end. */\nfunction minutesAt(expiry: number, now: number): number | null {\n if (!Number.isFinite(expiry)) return null;\n return Math.max(0, Math.ceil((expiry - now) / MINUTE));\n}\n\n/** The countdown for `minutesLeft` — see {@link DemoCountdown} for the rounding and the\n * thresholds. (Public as `demoCountdown(expiresAt, now)` until 0.53, deprecated in 0.52,\n * API-4: `useDemoCountdown` with its `now` option is the same countdown.) */\nfunction countdownOf(minutesLeft: number | null): DemoCountdown {\n if (minutesLeft === null) {\n return { minutesLeft, hours: 0, minutes: 0, lastHour: false, warning: false, ended: false };\n }\n const lastHour = minutesLeft <= DEMO_MINUTES_ONLY;\n return {\n minutesLeft,\n hours: lastHour ? 0 : Math.floor(minutesLeft / 60),\n minutes: lastHour ? minutesLeft : minutesLeft % 60,\n lastHour,\n warning: minutesLeft <= DEMO_WARNING_MINUTES,\n ended: minutesLeft === 0,\n };\n}\n\nexport interface DemoCountdownOptions {\n /**\n * The clock, in epoch milliseconds. Default `Date.now`. A test passes its own (or uses\n * fake timers, which move `Date.now` too). Keep it a stable function — a new one each\n * render re-arms the timer each render.\n */\n now?: () => number;\n /** Called once when the countdown reaches zero — also on mount, if it already has.\n * The app clears the session and replaces to `/demo/ended` (§5.5). */\n onEnded?: () => void;\n}\n\n/**\n * The time left to `expiresAt`, re-rendering when the shown minute changes — one timer\n * to the next minute boundary, not a tick a second — and once more when the tab comes\n * back from the background (a hidden tab's timers are throttled).\n *\n * `onEnded` fires at zero (§5.5). The countdown is the client's, from `expires_at`; the\n * server's answer to a request after the end (a 401) ends the demo as well — the app's\n * 401 handler, not this hook.\n */\nexport function useDemoCountdown(\n expiresAt: DemoExpiry | null | undefined,\n { now = Date.now, onEnded }: DemoCountdownOptions = {},\n): DemoCountdown {\n const expiry = expiryMs(expiresAt);\n const subscribe = useCallback(\n (notify: () => void) => {\n if (!Number.isFinite(expiry)) return () => {};\n let timer: ReturnType<typeof setTimeout> | undefined;\n const arm = () => {\n const left = expiry - now();\n if (left <= 0) return;\n // The rounded-up minute changes when `left` crosses the next whole minute below.\n const delay = left - (Math.ceil(left / MINUTE) - 1) * MINUTE;\n timer = setTimeout(() => {\n notify();\n arm();\n }, delay);\n };\n const onVisible = () => {\n if (document.visibilityState !== \"visible\") return;\n clearTimeout(timer);\n notify();\n arm();\n };\n arm();\n document.addEventListener(\"visibilitychange\", onVisible);\n return () => {\n clearTimeout(timer);\n document.removeEventListener(\"visibilitychange\", onVisible);\n };\n },\n [expiry, now],\n );\n const snapshot = () => minutesAt(expiry, now());\n const minutesLeft = useSyncExternalStore(subscribe, snapshot, snapshot);\n const countdown = countdownOf(minutesLeft);\n\n const fireEnded = useEffectEvent(() => onEnded?.());\n useEffect(() => {\n if (countdown.ended) fireEnded();\n }, [countdown.ended]);\n\n return countdown;\n}\n"],"mappings":";AAAA,SAAS,aAAa,WAAW,gBAAgB,4BAA4B;AAatE,MAAM,uBAAuB;AAE7B,MAAM,oBAAoB;AAEjC,MAAM,SAAS;AAsBf,SAAS,SAAS,WAAkD;AAClE,MAAI,cAAc,QAAQ,cAAc,OAAW,QAAO,OAAO;AACjE,MAAI,OAAO,cAAc,SAAU,QAAO;AAC1C,MAAI,qBAAqB,KAAM,QAAO,UAAU,QAAQ;AACxD,SAAO,KAAK,MAAM,SAAS;AAC7B;AAGA,SAAS,UAAU,QAAgB,KAA4B;AAC7D,MAAI,CAAC,OAAO,SAAS,MAAM,EAAG,QAAO;AACrC,SAAO,KAAK,IAAI,GAAG,KAAK,MAAM,SAAS,OAAO,MAAM,CAAC;AACvD;AAKA,SAAS,YAAY,aAA2C;AAC9D,MAAI,gBAAgB,MAAM;AACxB,WAAO,EAAE,aAAa,OAAO,GAAG,SAAS,GAAG,UAAU,OAAO,SAAS,OAAO,OAAO,MAAM;AAAA,EAC5F;AACA,QAAM,WAAW,eAAe;AAChC,SAAO;AAAA,IACL;AAAA,IACA,OAAO,WAAW,IAAI,KAAK,MAAM,cAAc,EAAE;AAAA,IACjD,SAAS,WAAW,cAAc,cAAc;AAAA,IAChD;AAAA,IACA,SAAS,eAAe;AAAA,IACxB,OAAO,gBAAgB;AAAA,EACzB;AACF;AAuBO,SAAS,iBACd,WACA,EAAE,MAAM,KAAK,KAAK,QAAQ,IAA0B,CAAC,GACtC;AACf,QAAM,SAAS,SAAS,SAAS;AACjC,QAAM,YAAY;AAAA,IAChB,CAAC,WAAuB;AACtB,UAAI,CAAC,OAAO,SAAS,MAAM,EAAG,QAAO,MAAM;AAAA,MAAC;AAC5C,UAAI;AACJ,YAAM,MAAM,MAAM;AAChB,cAAM,OAAO,SAAS,IAAI;AAC1B,YAAI,QAAQ,EAAG;AAEf,cAAM,QAAQ,QAAQ,KAAK,KAAK,OAAO,MAAM,IAAI,KAAK;AACtD,gBAAQ,WAAW,MAAM;AACvB,iBAAO;AACP,cAAI;AAAA,QACN,GAAG,KAAK;AAAA,MACV;AACA,YAAM,YAAY,MAAM;AACtB,YAAI,SAAS,oBAAoB,UAAW;AAC5C,qBAAa,KAAK;AAClB,eAAO;AACP,YAAI;AAAA,MACN;AACA,UAAI;AACJ,eAAS,iBAAiB,oBAAoB,SAAS;AACvD,aAAO,MAAM;AACX,qBAAa,KAAK;AAClB,iBAAS,oBAAoB,oBAAoB,SAAS;AAAA,MAC5D;AAAA,IACF;AAAA,IACA,CAAC,QAAQ,GAAG;AAAA,EACd;AACA,QAAM,WAAW,MAAM,UAAU,QAAQ,IAAI,CAAC;AAC9C,QAAM,cAAc,qBAAqB,WAAW,UAAU,QAAQ;AACtE,QAAM,YAAY,YAAY,WAAW;AAEzC,QAAM,YAAY,eAAe,MAAM,UAAU,CAAC;AAClD,YAAU,MAAM;AACd,QAAI,UAAU,MAAO,WAAU;AAAA,EACjC,GAAG,CAAC,UAAU,KAAK,CAAC;AAEpB,SAAO;AACT;","names":[]}
@@ -47,7 +47,7 @@ const roleUtilities = {
47
47
  };
48
48
  }
49
49
  };
50
- const VERSION = true ? "0.51.7" : "0.0.0-source";
50
+ const VERSION = true ? "0.53.0" : "0.0.0-source";
51
51
  const recommended = {
52
52
  name: "@eifi1/ui-kit/recommended",
53
53
  // Not the tests, as check:tokens skips the kit's: a test names the old class to assert it
@@ -43,19 +43,47 @@ export interface ThemeInlineDeclaration {
43
43
  }
44
44
  /** The `@theme inline` block: where it stands, and every declaration in it in block order. */
45
45
  export interface ThemeInlineBlock {
46
- /** Its first and one-past-last offset in `stripCssComments(css)`, not in `css`. */
46
+ /** Its first and one-past-last offset in `stripCssComments(css)`, not in `css`. With
47
+ * several `@theme inline` blocks, the first one's; `ranges` holds each. */
47
48
  start: number;
48
49
  end: number;
50
+ ranges: {
51
+ start: number;
52
+ end: number;
53
+ }[];
54
+ /** Every block's, in stylesheet order. */
49
55
  declarations: ThemeInlineDeclaration[];
50
56
  }
51
57
  /** `css` without its comments: a declaration commented out is not one. */
52
58
  export declare function stripCssComments(css: string): string;
59
+ /** One `@theme … { … }` block: its first and one-past-last offset, and what its braces hold. */
60
+ export interface ThemeBlock {
61
+ start: number;
62
+ end: number;
63
+ body: string;
64
+ }
65
+ /**
66
+ * Every `@theme` block of `css` (comments already stripped) whose prelude names `inline`
67
+ * (`@theme inline`, `@theme inline reference`) — or, with `inline: false`, every other
68
+ * one (`@theme {`, Tailwind's `@theme default {`). Counted brace by brace, so a nested
69
+ * block does not end it early.
70
+ *
71
+ * The one block finder (refactor audit 2026-10-30, DUP-13): this parser read only the
72
+ * first `@theme inline {…}` up to its first `}`, the testing guard's
73
+ * `declaredCustomProperties` every inline block brace-aware, and the README's inventory a
74
+ * plain `@theme {` by a regex of its own — three readers that disagreed on what one block
75
+ * is. It lives here because this module imports nothing, so the scripts load it from
76
+ * source under Node.
77
+ */
78
+ export declare function themeBlocks(css: string, { inline }: {
79
+ inline: boolean;
80
+ }): ThemeBlock[];
53
81
  /**
54
- * tokens.css's `@theme inline` block, read once for every reader: the table below, and
55
- * the README's (scripts/gen-styling-inventory.mjs), which fails on a `property: null`
56
- * where the table leaves it out. `null` when there is no such block — the role utilities
57
- * are gone, or this parser is. Theme blocks hold only declarations, so the first `}`
58
- * closes it; no namespace is another's prefix, so the first that fits is the one.
82
+ * tokens.css's `@theme inline` block (every one, should there be several), read once for
83
+ * every reader: the table below, and the README's (scripts/gen-styling-inventory.mjs),
84
+ * which fails on a `property: null` where the table leaves it out. `null` when there is
85
+ * no such block — the role utilities are gone, or this parser is. No namespace is
86
+ * another's prefix, so the first that fits is the one.
59
87
  */
60
88
  export declare function parseThemeInline(css: string): ThemeInlineBlock | null;
61
89
  /**
@@ -73,7 +101,8 @@ export interface RoleClassHit {
73
101
  utilities: string[];
74
102
  }
75
103
  /**
76
- * Every `[var(--token)]` class in `text` whose token has a role utility on that property.
104
+ * Every `[var(--token)]` class in `text` (or one of v4's spellings of it, above) whose
105
+ * token has a role utility on that property.
77
106
  * A token with no utility there (`text-[var(--chart-1)]`, `--status-*`,
78
107
  * `bg-[var(--border)]` as a divider's fill) is not a hit: `[var(--…)]` is right for it.
79
108
  */
@@ -14,11 +14,28 @@ const ROLE_NAMESPACES = {
14
14
  function stripCssComments(css) {
15
15
  return css.replace(/\/\*[\s\S]*?\*\//g, "");
16
16
  }
17
+ function themeBlocks(css, { inline }) {
18
+ const blocks = [];
19
+ for (const m of css.matchAll(/@theme\b([^{;]*)\{/g)) {
20
+ if (/\binline\b/.test(m[1]) !== inline) continue;
21
+ const open = m.index + m[0].length - 1;
22
+ let depth = 0;
23
+ let close = open;
24
+ for (; close < css.length; close++) {
25
+ if (css[close] === "{") depth++;
26
+ else if (css[close] === "}" && --depth === 0) break;
27
+ }
28
+ blocks.push({ start: m.index, end: close + 1, body: css.slice(open + 1, close) });
29
+ }
30
+ return blocks;
31
+ }
17
32
  const PROPERTIES = Object.keys(ROLE_NAMESPACES);
18
33
  function parseThemeInline(css) {
19
- const block = /@theme\s+inline\s*\{([^}]*)\}/.exec(stripCssComments(css));
20
- if (!block) return null;
21
- const declarations = [...block[1].matchAll(/--([a-z0-9-]+)\s*:\s*([^;]+);/g)].map(([, variable, raw]) => {
34
+ const blocks = themeBlocks(stripCssComments(css), { inline: true });
35
+ if (blocks.length === 0) return null;
36
+ const declarations = [
37
+ ...blocks.map((b) => b.body).join("\n").matchAll(/--([a-z0-9-]+)\s*:\s*([^;]+);/g)
38
+ ].map(([, variable, raw]) => {
22
39
  const value = raw.trim();
23
40
  const property = PROPERTIES.find((p) => variable.startsWith(`${ROLE_NAMESPACES[p]}-`)) ?? null;
24
41
  const name = property && variable.slice(ROLE_NAMESPACES[property].length + 1);
@@ -31,7 +48,8 @@ function parseThemeInline(css) {
31
48
  value
32
49
  };
33
50
  });
34
- return { start: block.index, end: block.index + block[0].length, declarations };
51
+ const ranges = blocks.map(({ start, end }) => ({ start, end }));
52
+ return { start: ranges[0].start, end: ranges[0].end, ranges, declarations };
35
53
  }
36
54
  function roleUtilitiesFromCss(css) {
37
55
  const block = parseThemeInline(css);
@@ -42,13 +60,27 @@ function roleUtilitiesFromCss(css) {
42
60
  }
43
61
  return table;
44
62
  }
45
- const ROLE_CLASS = /(?<=^|[\s"'`:])(text|bg|border(?:-[xytrblse])?|divide|ring-offset|ring|outline|fill|stroke)-\[(length:)?var\((--[a-zA-Z0-9-]+)\)\]/g;
63
+ const TOKEN = "(--[a-zA-Z0-9-]+)";
64
+ const ROLE_CLASS = new RegExp(
65
+ `(?<=^|[\\s"'\`:])(?:(text|bg|border(?:-[xytrblse])?|divide|ring-offset|ring|outline|fill|stroke)-(?:\\[(?:(length|color):)?var\\(${TOKEN}\\)\\]|\\((?:(length|color):)?${TOKEN}\\))|\\[(color|background-color|border-color|outline-color|fill|stroke):var\\(${TOKEN}\\)\\])`,
66
+ "g"
67
+ );
68
+ const ARBITRARY_PROPERTY = {
69
+ color: "text",
70
+ "background-color": "bg",
71
+ "border-color": "border",
72
+ "outline-color": "outline",
73
+ fill: "fill",
74
+ stroke: "stroke"
75
+ };
46
76
  function findRoleClasses(text, table) {
47
- if (!text.includes("var(--")) return [];
77
+ if (!text.includes("--")) return [];
48
78
  const hits = [];
49
79
  for (const m of text.matchAll(ROLE_CLASS)) {
50
- const [found, prop, length, token] = m;
51
- const key = length ? `${prop}-width` : prop.startsWith("border-") || prop === "divide" ? "border" : prop;
80
+ const [found, utility, bracketHint, bracketToken, parenHint, parenToken, cssProperty, propertyToken] = m;
81
+ const prop = utility ?? ARBITRARY_PROPERTY[cssProperty];
82
+ const token = bracketToken ?? parenToken ?? propertyToken;
83
+ const key = (bracketHint ?? parenHint) === "length" ? `${prop}-width` : prop.startsWith("border-") || prop === "divide" ? "border" : prop;
52
84
  const names = table[key]?.[token];
53
85
  if (names?.length) hits.push({ index: m.index, found, utilities: names.map((name) => `${prop}-${name}`) });
54
86
  }
@@ -59,6 +91,7 @@ export {
59
91
  findRoleClasses,
60
92
  parseThemeInline,
61
93
  roleUtilitiesFromCss,
62
- stripCssComments
94
+ stripCssComments,
95
+ themeBlocks
63
96
  };
64
97
  //# sourceMappingURL=role-utilities.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/eslint/role-utilities.ts"],"sourcesContent":["// Which `[var(--token)]` classes have a role utility, and what it is called — the one\n// definition `@eifi1/ui-kit/eslint`'s `role-utilities` rule and `npm run check:tokens`\n// (scripts/check-token-discipline.mjs) both read (round 0.35 R5, Kurvenschmiede's Q6).\n//\n// The table itself is role-utilities.generated.ts, re-rendered from tokens.css's\n// `@theme inline` block by scripts/gen-role-utilities.mjs with the parser below; the\n// check fails while it is stale. So tokens.css stays the source: a role that gains a\n// utility there is flagged in the kit and in the apps from the next release on. The\n// README's role-utility table (scripts/gen-styling-inventory.mjs) reads the block with\n// the same parser since 0.37, so the README, the check and the plugin cannot disagree.\n//\n// This module imports NOTHING, and its TypeScript is erasable only (no enum, no\n// namespace, no parameter property): the check imports it from source under Node's type\n// stripping, before any build, and the plugin ships it to an app's ESLint, which is\n// plain Node with no React and no stylesheet.\n\n/**\n * The properties a role utility exists for, each with the Tailwind theme namespace it is\n * declared in. `ring-width` and `outline-width` are a width typed `length:` —\n * `ring-[length:var(--focus-ring-width)]` is `ring-focus`. `divide-*` and the side borders\n * (`border-t-*`, `border-x-*` …) read `border`'s, as Tailwind does.\n */\nexport const ROLE_NAMESPACES = {\n text: \"text-color\",\n bg: \"background-color\",\n border: \"border-color\",\n ring: \"ring-color\",\n \"ring-offset\": \"ring-offset-color\",\n outline: \"outline-color\",\n fill: \"fill\",\n stroke: \"stroke\",\n \"ring-width\": \"ring-width\",\n \"outline-width\": \"outline-width\",\n} as const;\n\nexport type RoleProperty = keyof typeof ROLE_NAMESPACES;\n\n/**\n * Per property, each token that has a role utility → the utility's name after the\n * property's prefix: `border: { \"--border\": [\"subtle\"] }` is `border-subtle`. A list,\n * because nothing stops two names from reading one token; one name is a fix, several a\n * choice.\n */\nexport type RoleUtilityTable = {\n readonly [P in RoleProperty]: Readonly<Record<string, readonly string[]>>;\n};\n\n/** One `--<namespace>-<name>: <value>;` of the `@theme inline` block. */\nexport interface ThemeInlineDeclaration {\n /** The theme variable without its dashes: `text-color-muted`, `ring-width-focus`. */\n variable: string;\n /** The property whose namespace it is in; `null` for a namespace ROLE_NAMESPACES lacks. */\n property: RoleProperty | null;\n /** The name after the namespace, `muted`; `null` with `property`. */\n name: string | null;\n /** The utility it makes: `text-muted`, `ring-focus` (a width is `ring-*` like a colour). */\n utility: string | null;\n /** The token the value reads, `--text-muted`; `null` unless the value is a bare `var()`. */\n token: string | null;\n /** The value as written, trimmed. */\n value: string;\n}\n\n/** The `@theme inline` block: where it stands, and every declaration in it in block order. */\nexport interface ThemeInlineBlock {\n /** Its first and one-past-last offset in `stripCssComments(css)`, not in `css`. */\n start: number;\n end: number;\n declarations: ThemeInlineDeclaration[];\n}\n\n/** `css` without its comments: a declaration commented out is not one. */\nexport function stripCssComments(css: string): string {\n return css.replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\");\n}\n\nconst PROPERTIES = Object.keys(ROLE_NAMESPACES) as RoleProperty[];\n\n/**\n * tokens.css's `@theme inline` block, read once for every reader: the table below, and\n * the README's (scripts/gen-styling-inventory.mjs), which fails on a `property: null`\n * where the table leaves it out. `null` when there is no such block — the role utilities\n * are gone, or this parser is. Theme blocks hold only declarations, so the first `}`\n * closes it; no namespace is another's prefix, so the first that fits is the one.\n */\nexport function parseThemeInline(css: string): ThemeInlineBlock | null {\n const block = /@theme\\s+inline\\s*\\{([^}]*)\\}/.exec(stripCssComments(css));\n if (!block) return null;\n const declarations = [...block[1].matchAll(/--([a-z0-9-]+)\\s*:\\s*([^;]+);/g)].map(([, variable, raw]) => {\n const value = raw.trim();\n const property = PROPERTIES.find((p) => variable.startsWith(`${ROLE_NAMESPACES[p]}-`)) ?? null;\n const name = property && variable.slice(ROLE_NAMESPACES[property].length + 1);\n return {\n variable,\n property,\n name,\n utility: property && `${property.replace(/-width$/, \"\")}-${name}`,\n token: /^var\\((--[a-z0-9-]+)\\)$/.exec(value)?.[1] ?? null,\n value,\n };\n });\n return { start: block.index, end: block.index + block[0].length, declarations };\n}\n\n/**\n * The table `tokens.css` declares: every `--<namespace>-<name>: var(--<token>)` in its\n * `@theme inline` block, in block order, each property present (in ROLE_NAMESPACES' order)\n * even when it has none. `null` when there is no such block.\n */\nexport function roleUtilitiesFromCss(css: string): RoleUtilityTable | null {\n const block = parseThemeInline(css);\n if (!block) return null;\n const table = Object.fromEntries(PROPERTIES.map((p) => [p, {}])) as Record<RoleProperty, Record<string, string[]>>;\n for (const { property, name, token } of block.declarations) {\n if (property && name && token) (table[property][token] ??= []).push(name);\n }\n return table;\n}\n\n/**\n * `text-[var(--text-muted)]`, `hover:bg-[var(--bg-hover)]`, `border-t-[var(--border)]`,\n * `focus-visible:ring-[var(--brand)]/40`, `ring-offset-[var(--bg-surface)]`,\n * `ring-[length:var(--focus-ring-width)]`. Preceded by a start, a space, a quote or a\n * variant's colon, which the match does not include — nor the variants before it or an\n * opacity or `!` after it — so replacing the match keeps all of them.\n */\nconst ROLE_CLASS =\n /(?<=^|[\\s\"'`:])(text|bg|border(?:-[xytrblse])?|divide|ring-offset|ring|outline|fill|stroke)-\\[(length:)?var\\((--[a-zA-Z0-9-]+)\\)\\]/g;\n\nexport interface RoleClassHit {\n /** Where the class starts in the scanned text, after its variants. */\n index: number;\n /** The class as written, without variants or opacity: `border-t-[var(--border)]`. */\n found: string;\n /** The role utilities that paint the same: `[\"border-t-subtle\"]`. */\n utilities: string[];\n}\n\n/**\n * Every `[var(--token)]` class in `text` whose token has a role utility on that property.\n * A token with no utility there (`text-[var(--chart-1)]`, `--status-*`,\n * `bg-[var(--border)]` as a divider's fill) is not a hit: `[var(--…)]` is right for it.\n */\nexport function findRoleClasses(text: string, table: RoleUtilityTable): RoleClassHit[] {\n if (!text.includes(\"var(--\")) return [];\n const hits: RoleClassHit[] = [];\n for (const m of text.matchAll(ROLE_CLASS)) {\n const [found, prop, length, token] = m;\n // `border-t` and `divide` read `border`'s names; a `length:` hint reads the widths.\n const key = length ? `${prop}-width` : prop.startsWith(\"border-\") || prop === \"divide\" ? \"border\" : prop;\n const names = (table as Partial<Record<string, Readonly<Record<string, readonly string[]>>>>)[key]?.[token];\n if (names?.length) hits.push({ index: m.index, found, utilities: names.map((name) => `${prop}-${name}`) });\n }\n return hits;\n}\n"],"mappings":";AAsBO,MAAM,kBAAkB;AAAA,EAC7B,MAAM;AAAA,EACN,IAAI;AAAA,EACJ,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,eAAe;AAAA,EACf,SAAS;AAAA,EACT,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,iBAAiB;AACnB;AAuCO,SAAS,iBAAiB,KAAqB;AACpD,SAAO,IAAI,QAAQ,qBAAqB,EAAE;AAC5C;AAEA,MAAM,aAAa,OAAO,KAAK,eAAe;AASvC,SAAS,iBAAiB,KAAsC;AACrE,QAAM,QAAQ,gCAAgC,KAAK,iBAAiB,GAAG,CAAC;AACxE,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,eAAe,CAAC,GAAG,MAAM,CAAC,EAAE,SAAS,gCAAgC,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM;AACvG,UAAM,QAAQ,IAAI,KAAK;AACvB,UAAM,WAAW,WAAW,KAAK,CAAC,MAAM,SAAS,WAAW,GAAG,gBAAgB,CAAC,CAAC,GAAG,CAAC,KAAK;AAC1F,UAAM,OAAO,YAAY,SAAS,MAAM,gBAAgB,QAAQ,EAAE,SAAS,CAAC;AAC5E,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,YAAY,GAAG,SAAS,QAAQ,WAAW,EAAE,CAAC,IAAI,IAAI;AAAA,MAC/D,OAAO,0BAA0B,KAAK,KAAK,IAAI,CAAC,KAAK;AAAA,MACrD;AAAA,IACF;AAAA,EACF,CAAC;AACD,SAAO,EAAE,OAAO,MAAM,OAAO,KAAK,MAAM,QAAQ,MAAM,CAAC,EAAE,QAAQ,aAAa;AAChF;AAOO,SAAS,qBAAqB,KAAsC;AACzE,QAAM,QAAQ,iBAAiB,GAAG;AAClC,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,QAAQ,OAAO,YAAY,WAAW,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAC/D,aAAW,EAAE,UAAU,MAAM,MAAM,KAAK,MAAM,cAAc;AAC1D,QAAI,YAAY,QAAQ,MAAO,EAAC,MAAM,QAAQ,EAAE,KAAK,MAAM,CAAC,GAAG,KAAK,IAAI;AAAA,EAC1E;AACA,SAAO;AACT;AASA,MAAM,aACJ;AAgBK,SAAS,gBAAgB,MAAc,OAAyC;AACrF,MAAI,CAAC,KAAK,SAAS,QAAQ,EAAG,QAAO,CAAC;AACtC,QAAM,OAAuB,CAAC;AAC9B,aAAW,KAAK,KAAK,SAAS,UAAU,GAAG;AACzC,UAAM,CAAC,OAAO,MAAM,QAAQ,KAAK,IAAI;AAErC,UAAM,MAAM,SAAS,GAAG,IAAI,WAAW,KAAK,WAAW,SAAS,KAAK,SAAS,WAAW,WAAW;AACpG,UAAM,QAAS,MAA+E,GAAG,IAAI,KAAK;AAC1G,QAAI,OAAO,OAAQ,MAAK,KAAK,EAAE,OAAO,EAAE,OAAO,OAAO,WAAW,MAAM,IAAI,CAAC,SAAS,GAAG,IAAI,IAAI,IAAI,EAAE,EAAE,CAAC;AAAA,EAC3G;AACA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/eslint/role-utilities.ts"],"sourcesContent":["// Which `[var(--token)]` classes have a role utility, and what it is called — the one\n// definition `@eifi1/ui-kit/eslint`'s `role-utilities` rule and `npm run check:tokens`\n// (scripts/check-token-discipline.mjs) both read (round 0.35 R5, Kurvenschmiede's Q6).\n//\n// The table itself is role-utilities.generated.ts, re-rendered from tokens.css's\n// `@theme inline` block by scripts/gen-role-utilities.mjs with the parser below; the\n// check fails while it is stale. So tokens.css stays the source: a role that gains a\n// utility there is flagged in the kit and in the apps from the next release on. The\n// README's role-utility table (scripts/gen-styling-inventory.mjs) reads the block with\n// the same parser since 0.37, so the README, the check and the plugin cannot disagree.\n//\n// This module imports NOTHING, and its TypeScript is erasable only (no enum, no\n// namespace, no parameter property): the check imports it from source under Node's type\n// stripping, before any build, and the plugin ships it to an app's ESLint, which is\n// plain Node with no React and no stylesheet.\n\n/**\n * The properties a role utility exists for, each with the Tailwind theme namespace it is\n * declared in. `ring-width` and `outline-width` are a width typed `length:` —\n * `ring-[length:var(--focus-ring-width)]` is `ring-focus`. `divide-*` and the side borders\n * (`border-t-*`, `border-x-*` …) read `border`'s, as Tailwind does.\n */\nexport const ROLE_NAMESPACES = {\n text: \"text-color\",\n bg: \"background-color\",\n border: \"border-color\",\n ring: \"ring-color\",\n \"ring-offset\": \"ring-offset-color\",\n outline: \"outline-color\",\n fill: \"fill\",\n stroke: \"stroke\",\n \"ring-width\": \"ring-width\",\n \"outline-width\": \"outline-width\",\n} as const;\n\nexport type RoleProperty = keyof typeof ROLE_NAMESPACES;\n\n/**\n * Per property, each token that has a role utility → the utility's name after the\n * property's prefix: `border: { \"--border\": [\"subtle\"] }` is `border-subtle`. A list,\n * because nothing stops two names from reading one token; one name is a fix, several a\n * choice.\n */\nexport type RoleUtilityTable = {\n readonly [P in RoleProperty]: Readonly<Record<string, readonly string[]>>;\n};\n\n/** One `--<namespace>-<name>: <value>;` of the `@theme inline` block. */\nexport interface ThemeInlineDeclaration {\n /** The theme variable without its dashes: `text-color-muted`, `ring-width-focus`. */\n variable: string;\n /** The property whose namespace it is in; `null` for a namespace ROLE_NAMESPACES lacks. */\n property: RoleProperty | null;\n /** The name after the namespace, `muted`; `null` with `property`. */\n name: string | null;\n /** The utility it makes: `text-muted`, `ring-focus` (a width is `ring-*` like a colour). */\n utility: string | null;\n /** The token the value reads, `--text-muted`; `null` unless the value is a bare `var()`. */\n token: string | null;\n /** The value as written, trimmed. */\n value: string;\n}\n\n/** The `@theme inline` block: where it stands, and every declaration in it in block order. */\nexport interface ThemeInlineBlock {\n /** Its first and one-past-last offset in `stripCssComments(css)`, not in `css`. With\n * several `@theme inline` blocks, the first one's; `ranges` holds each. */\n start: number;\n end: number;\n ranges: { start: number; end: number }[];\n /** Every block's, in stylesheet order. */\n declarations: ThemeInlineDeclaration[];\n}\n\n/** `css` without its comments: a declaration commented out is not one. */\nexport function stripCssComments(css: string): string {\n return css.replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\");\n}\n\n/** One `@theme … { … }` block: its first and one-past-last offset, and what its braces hold. */\nexport interface ThemeBlock {\n start: number;\n end: number;\n body: string;\n}\n\n/**\n * Every `@theme` block of `css` (comments already stripped) whose prelude names `inline`\n * (`@theme inline`, `@theme inline reference`) — or, with `inline: false`, every other\n * one (`@theme {`, Tailwind's `@theme default {`). Counted brace by brace, so a nested\n * block does not end it early.\n *\n * The one block finder (refactor audit 2026-10-30, DUP-13): this parser read only the\n * first `@theme inline {…}` up to its first `}`, the testing guard's\n * `declaredCustomProperties` every inline block brace-aware, and the README's inventory a\n * plain `@theme {` by a regex of its own — three readers that disagreed on what one block\n * is. It lives here because this module imports nothing, so the scripts load it from\n * source under Node.\n */\nexport function themeBlocks(css: string, { inline }: { inline: boolean }): ThemeBlock[] {\n const blocks: ThemeBlock[] = [];\n for (const m of css.matchAll(/@theme\\b([^{;]*)\\{/g)) {\n if (/\\binline\\b/.test(m[1]) !== inline) continue;\n const open = m.index + m[0].length - 1;\n let depth = 0;\n let close = open;\n for (; close < css.length; close++) {\n if (css[close] === \"{\") depth++;\n else if (css[close] === \"}\" && --depth === 0) break;\n }\n blocks.push({ start: m.index, end: close + 1, body: css.slice(open + 1, close) });\n }\n return blocks;\n}\n\nconst PROPERTIES = Object.keys(ROLE_NAMESPACES) as RoleProperty[];\n\n/**\n * tokens.css's `@theme inline` block (every one, should there be several), read once for\n * every reader: the table below, and the README's (scripts/gen-styling-inventory.mjs),\n * which fails on a `property: null` where the table leaves it out. `null` when there is\n * no such block — the role utilities are gone, or this parser is. No namespace is\n * another's prefix, so the first that fits is the one.\n */\nexport function parseThemeInline(css: string): ThemeInlineBlock | null {\n const blocks = themeBlocks(stripCssComments(css), { inline: true });\n if (blocks.length === 0) return null;\n const declarations = [\n ...blocks\n .map((b) => b.body)\n .join(\"\\n\")\n .matchAll(/--([a-z0-9-]+)\\s*:\\s*([^;]+);/g),\n ].map(([, variable, raw]) => {\n const value = raw.trim();\n const property = PROPERTIES.find((p) => variable.startsWith(`${ROLE_NAMESPACES[p]}-`)) ?? null;\n const name = property && variable.slice(ROLE_NAMESPACES[property].length + 1);\n return {\n variable,\n property,\n name,\n utility: property && `${property.replace(/-width$/, \"\")}-${name}`,\n token: /^var\\((--[a-z0-9-]+)\\)$/.exec(value)?.[1] ?? null,\n value,\n };\n });\n const ranges = blocks.map(({ start, end }) => ({ start, end }));\n return { start: ranges[0].start, end: ranges[0].end, ranges, declarations };\n}\n\n/**\n * The table `tokens.css` declares: every `--<namespace>-<name>: var(--<token>)` in its\n * `@theme inline` block, in block order, each property present (in ROLE_NAMESPACES' order)\n * even when it has none. `null` when there is no such block.\n */\nexport function roleUtilitiesFromCss(css: string): RoleUtilityTable | null {\n const block = parseThemeInline(css);\n if (!block) return null;\n const table = Object.fromEntries(PROPERTIES.map((p) => [p, {}])) as Record<RoleProperty, Record<string, string[]>>;\n for (const { property, name, token } of block.declarations) {\n if (property && name && token) (table[property][token] ??= []).push(name);\n }\n return table;\n}\n\n/**\n * `text-[var(--text-muted)]`, `hover:bg-[var(--bg-hover)]`, `border-t-[var(--border)]`,\n * `focus-visible:ring-[var(--brand)]/40`, `ring-offset-[var(--bg-surface)]`,\n * `ring-[length:var(--focus-ring-width)]` — and, since 0.52, Tailwind v4's own spellings\n * of the same: the shorthand `text-(--text-muted)`, a type hint `text-[color:var(--x)]` /\n * `text-(color:--x)` / `ring-(length:--x)`, and the arbitrary property\n * `[color:var(--text-muted)]`, which is `text-muted` (refactor audit 2026-10-30, DOC-1:\n * all of them passed the check and every app's lint). Preceded by a start, a space, a\n * quote or a variant's colon, which the match does not include — nor the variants before\n * it or an opacity or `!` after it — so replacing the match keeps all of them.\n *\n * Groups: 1 the property; then 2 the hint and 3 the token of `-[…var()]`, or 4 and 5 of\n * `-(…)`; or 6 the CSS property and 7 the token of `[property:var()]`.\n */\nconst TOKEN = \"(--[a-zA-Z0-9-]+)\";\nconst ROLE_CLASS = new RegExp(\n \"(?<=^|[\\\\s\\\"'`:])(?:\" +\n \"(text|bg|border(?:-[xytrblse])?|divide|ring-offset|ring|outline|fill|stroke)-\" +\n `(?:\\\\[(?:(length|color):)?var\\\\(${TOKEN}\\\\)\\\\]|\\\\((?:(length|color):)?${TOKEN}\\\\))` +\n `|\\\\[(color|background-color|border-color|outline-color|fill|stroke):var\\\\(${TOKEN}\\\\)\\\\]` +\n \")\",\n \"g\",\n);\n\n/** The utility prefix an arbitrary property `[property:var(--x)]` is written as. */\nconst ARBITRARY_PROPERTY: Record<string, string> = {\n color: \"text\",\n \"background-color\": \"bg\",\n \"border-color\": \"border\",\n \"outline-color\": \"outline\",\n fill: \"fill\",\n stroke: \"stroke\",\n};\n\nexport interface RoleClassHit {\n /** Where the class starts in the scanned text, after its variants. */\n index: number;\n /** The class as written, without variants or opacity: `border-t-[var(--border)]`. */\n found: string;\n /** The role utilities that paint the same: `[\"border-t-subtle\"]`. */\n utilities: string[];\n}\n\n/**\n * Every `[var(--token)]` class in `text` (or one of v4's spellings of it, above) whose\n * token has a role utility on that property.\n * A token with no utility there (`text-[var(--chart-1)]`, `--status-*`,\n * `bg-[var(--border)]` as a divider's fill) is not a hit: `[var(--…)]` is right for it.\n */\nexport function findRoleClasses(text: string, table: RoleUtilityTable): RoleClassHit[] {\n // Every form names a token: `var(--x)`, or `(--x)` in v4's shorthand.\n if (!text.includes(\"--\")) return [];\n const hits: RoleClassHit[] = [];\n for (const m of text.matchAll(ROLE_CLASS)) {\n const [found, utility, bracketHint, bracketToken, parenHint, parenToken, cssProperty, propertyToken] = m;\n const prop = utility ?? ARBITRARY_PROPERTY[cssProperty];\n const token = bracketToken ?? parenToken ?? propertyToken;\n // `border-t` and `divide` read `border`'s names; a `length:` hint reads the widths.\n const key =\n (bracketHint ?? parenHint) === \"length\"\n ? `${prop}-width`\n : prop.startsWith(\"border-\") || prop === \"divide\"\n ? \"border\"\n : prop;\n const names = (table as Partial<Record<string, Readonly<Record<string, readonly string[]>>>>)[key]?.[token];\n if (names?.length) hits.push({ index: m.index, found, utilities: names.map((name) => `${prop}-${name}`) });\n }\n return hits;\n}\n"],"mappings":"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","names":[]}
@@ -3,8 +3,10 @@ import type { ButtonSize, ButtonVariant } from "../components/button-look.js";
3
3
  import { type LabelOverride } from "../i18n/kit-labels.js";
4
4
  import { type FeedbackAttachmentError, type FeedbackAttachmentErrorInfo } from "./feedback-attachment.js";
5
5
  import { DEFAULT_FEEDBACK_DIALOG_LABELS } from "./feedback-dialog-labels.js";
6
- export interface FeedbackCategoryOption {
7
- value: string;
6
+ /** One category the dialog offers. `C` (0.52): the app's category type — the kit's
7
+ * `FeedbackCategory`, or `string` (the default) for a vocabulary of the app's own. */
8
+ export interface FeedbackCategoryOption<C extends string = string> {
9
+ value: C;
8
10
  label: string;
9
11
  }
10
12
  export interface FeedbackAttachmentLabels {
@@ -80,10 +82,10 @@ export interface FeedbackDialogTextLabels {
80
82
  publicNotice?: string;
81
83
  }
82
84
  export { DEFAULT_FEEDBACK_DIALOG_LABELS };
83
- export interface FeedbackSubmission {
85
+ export interface FeedbackSubmission<C extends string = string> {
84
86
  title: string;
85
87
  body: string;
86
- category: string;
88
+ category: C;
87
89
  attachment: File | null;
88
90
  }
89
91
  /**
@@ -92,21 +94,28 @@ export interface FeedbackSubmission {
92
94
  * attaches the screenshot to the report and the photos as its evidence list. No
93
95
  * `attachment`: there is no one file it could name.
94
96
  */
95
- export interface FeedbackMultipleSubmission {
97
+ export interface FeedbackMultipleSubmission<C extends string = string> {
96
98
  title: string;
97
99
  body: string;
98
- category: string;
100
+ category: C;
99
101
  /** What `onCaptureScreenshot` returned, or `null` — at most one. */
100
102
  screenshot: File | null;
101
103
  /** Picked and pasted files, in the order they were added; at most `maxAttachments`. */
102
104
  attachments: File[];
103
105
  }
104
- interface FeedbackDialogBaseProps {
106
+ /**
107
+ * `C` (0.52, refactor audit 2026-10-30, API-7): the category type, inferred from
108
+ * `categories` and `category` — `FeedbackCategory` hands `onCategoryChange` and
109
+ * `onSubmit` a `FeedbackCategory` back, where a plain `string` had to be cast (the kit's
110
+ * own `useFeedbackSubmit` did). `string` by default, so an app's own vocabulary fits.
111
+ */
112
+ interface FeedbackDialogBaseProps<C extends string = string> {
105
113
  open: boolean;
106
114
  onClose: () => void;
107
- categories: FeedbackCategoryOption[];
108
- category: string;
109
- onCategoryChange: (value: string) => void;
115
+ /** `readonly` since 0.52: the dialog never changes it. */
116
+ categories: readonly FeedbackCategoryOption<C>[];
117
+ category: C;
118
+ onCategoryChange: (value: C) => void;
110
119
  /** Prop > `<UiKitProvider labels={{ feedbackDialog, feedbackAttachment }}>` >
111
120
  * English. Optional since 0.12.0; a whole `FeedbackDialogLabels` still fits. */
112
121
  labels?: LabelOverride<FeedbackDialogLabels>;
@@ -152,9 +161,9 @@ interface FeedbackDialogBaseProps {
152
161
  }
153
162
  /** One attachment — a screenshot, a picked or a pasted image; a second replaces
154
163
  * nothing, it has to wait for the first to be removed. The default. */
155
- export interface FeedbackDialogSingleProps extends FeedbackDialogBaseProps {
164
+ export interface FeedbackDialogSingleProps<C extends string = string> extends FeedbackDialogBaseProps<C> {
156
165
  attachments?: "single";
157
- onSubmit: (data: FeedbackSubmission) => void | Promise<void>;
166
+ onSubmit: (data: FeedbackSubmission<C>) => void | Promise<void>;
158
167
  /** `info` (0.23.0) names the refused file and the limit — see
159
168
  * {@link FeedbackAttachmentErrorInfo}. */
160
169
  onAttachmentError?: (kind: "type" | "size", info: FeedbackAttachmentErrorInfo) => void;
@@ -166,16 +175,16 @@ export interface FeedbackDialogSingleProps extends FeedbackDialogBaseProps {
166
175
  * dev#578: "pasted two photos, the second overwrote the first"). The capture button is
167
176
  * offered while there is no screenshot yet; the add button while there is room.
168
177
  */
169
- export interface FeedbackDialogMultipleProps extends FeedbackDialogBaseProps {
178
+ export interface FeedbackDialogMultipleProps<C extends string = string> extends FeedbackDialogBaseProps<C> {
170
179
  attachments: "multiple";
171
180
  /** Default `DEFAULT_MAX_ATTACHMENTS` (5). The screenshot does not count. */
172
181
  maxAttachments?: number;
173
- onSubmit: (data: FeedbackMultipleSubmission) => void | Promise<void>;
182
+ onSubmit: (data: FeedbackMultipleSubmission<C>) => void | Promise<void>;
174
183
  /** `"count"`: more files arrived at once than there was room for; the surplus was
175
184
  * dropped. `info` (0.23.0) names the refused file(s) and the limit. */
176
185
  onAttachmentError?: (kind: FeedbackAttachmentError, info: FeedbackAttachmentErrorInfo) => void;
177
186
  }
178
- export type FeedbackDialogProps = FeedbackDialogSingleProps | FeedbackDialogMultipleProps;
187
+ export type FeedbackDialogProps<C extends string = string> = FeedbackDialogSingleProps<C> | FeedbackDialogMultipleProps<C>;
179
188
  /**
180
189
  * The generic feedback form dialog: category + subject + body + an optional image
181
190
  * attachment, with Ctrl/Cmd+Enter to submit. Domain-free — the app supplies the
@@ -196,4 +205,4 @@ export type FeedbackDialogProps = FeedbackDialogSingleProps | FeedbackDialogMult
196
205
  * FeedbackMultipleSubmission}) instead of `attachment`. Opt-in, so a caller typed
197
206
  * against the one-file submission keeps compiling and behaving as before.
198
207
  */
199
- export declare function FeedbackDialog(props: FeedbackDialogProps): import("react").JSX.Element | null;
208
+ export declare function FeedbackDialog<C extends string = string>(props: FeedbackDialogProps<C>): import("react").JSX.Element | null;
@@ -9,6 +9,7 @@ import { Modal } from "../components/modal.js";
9
9
  import { isApplePlatform } from "../hooks/use-hotkey.js";
10
10
  import { usePhoneLayout } from "../hooks/use-breakpoint.js";
11
11
  import { useKitLabels } from "../i18n/kit-labels.js";
12
+ import { hasContent } from "../lib/presence.js";
12
13
  import {
13
14
  DEFAULT_ATTACHMENT_ACCEPT,
14
15
  DEFAULT_FEEDBACK_ATTACHMENT_LABELS,
@@ -41,7 +42,7 @@ function FeedbackDialog(props) {
41
42
  const [files, setFiles] = useState([]);
42
43
  const isMobile = usePhoneLayout();
43
44
  const noticeId = useId();
44
- const hasNotice = publicNotice !== void 0 && publicNotice !== null && publicNotice !== false && publicNotice !== "";
45
+ const hasNotice = hasContent(publicNotice);
45
46
  const labels = useKitLabels("feedbackDialog", DEFAULT_FEEDBACK_DIALOG_LABELS, labelsProp);
46
47
  const attachmentText = useKitLabels("feedbackAttachment", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);
47
48
  const listHeading = labelsProp?.attachmentList ?? attachmentText.attachmentList ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentList;
@@ -67,7 +68,10 @@ function FeedbackDialog(props) {
67
68
  }
68
69
  };
69
70
  if (!open) return null;
70
- const categoryField = /* @__PURE__ */ jsx(Select, { label: labels.category, value: category, onChange: (e) => onCategoryChange(e.target.value), children: categories.map((c) => /* @__PURE__ */ jsx("option", { value: c.value, children: c.label }, c.value)) });
71
+ const categoryField = (
72
+ // The select offers exactly `categories`' values, so what it answers is one of them.
73
+ /* @__PURE__ */ jsx(Select, { label: labels.category, value: category, onChange: (e) => onCategoryChange(e.target.value), children: categories.map((c) => /* @__PURE__ */ jsx("option", { value: c.value, children: c.label }, c.value)) })
74
+ );
71
75
  const subjectField = /* @__PURE__ */ jsx(Input, { label: labels.subject, value: title, onChange: (e) => setTitle(e.target.value), variant: "display" });
72
76
  const bodyTextarea = /* @__PURE__ */ jsx(
73
77
  Textarea,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/feedback/feedback-dialog.tsx"],"sourcesContent":["import { useId, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Button } from \"../components/button\";\nimport { Input } from \"../components/input\";\nimport { Select } from \"../components/select\";\nimport { Textarea } from \"../components/textarea\";\nimport type { ButtonSize, ButtonVariant } from \"../components/button-look\";\nimport { Modal } from \"../components/modal\";\nimport { isApplePlatform } from \"../hooks/use-hotkey\";\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport {\n DEFAULT_ATTACHMENT_ACCEPT,\n DEFAULT_FEEDBACK_ATTACHMENT_LABELS,\n DEFAULT_MAX_ATTACHMENT_BYTES,\n FeedbackAttachmentField,\n type FeedbackAttachmentError,\n type FeedbackAttachmentErrorInfo,\n} from \"./feedback-attachment\";\nimport { DEFAULT_FEEDBACK_DIALOG_LABELS } from \"./feedback-dialog-labels\";\n\nexport interface FeedbackCategoryOption {\n value: string;\n label: string;\n}\n\nexport interface FeedbackAttachmentLabels {\n /** The heading over the field. Optional: a note editor puts the buttons\n * straight under its textarea, where a second heading is noise. */\n attachment?: string;\n attachmentAdd: string;\n /** Label for the \"capture screenshot\" button. Optional — falls back to the provider's\n * `feedbackAttachment.attachmentCapture`, then English. */\n attachmentCapture?: string;\n /** The line under the attachment buttons saying a screenshot can be pasted\n * straight in. Optional — falls back to the provider's `feedbackAttachment`, then English. */\n attachmentPaste?: string;\n attachmentRemove: string;\n /** `multiple` mode (0.15.5): the chip list's accessible name — and, in\n * `<FeedbackDialog attachments=\"multiple\">`, the heading over it (0.16.0). Optional,\n * as are the keys below — they fall back to the provider's `feedbackAttachment`,\n * then English. */\n attachmentList?: string;\n /** `multiple` mode (0.16.0): the screenshot slot's chip title, in place of its file name. */\n attachmentScreenshot?: string;\n /** `multiple` mode: one chip's remove button, given the file's name. */\n attachmentRemoveFile?: (name: string) => string;\n /** `multiple` mode: the line shown once `max` files are attached. */\n attachmentLimit?: (max: number) => string;\n /** `refs` mode (0.22.0): the second line of a chip whose upload is still running. */\n attachmentUploading?: string;\n}\n\nexport interface FeedbackDialogLabels extends FeedbackAttachmentLabels {\n title: string;\n category: string;\n subject: string;\n body: string;\n attachment: string;\n /** See {@link FeedbackDialogTextLabels.submitHint}. */\n submitHint: FeedbackSubmitHint;\n cancel: string;\n save: string;\n}\n\n/** The submit hint: a string, or the line for the platform (`apple`: Mac, iPhone, iPad). */\nexport type FeedbackSubmitHint = string | ((apple: boolean) => string);\n\n/**\n * The dialog's own strings — the `feedbackDialog` namespace of `<UiKitProvider\n * labels>` (0.12.0). The attachment keys are not repeated here: they are the\n * `feedbackAttachment` namespace's, which the field inside already reads.\n *\n * Before, every key was required at the call site, so each app restated ten strings\n * wherever it mounted the dialog (kastlan feedback-button.tsx built the object inline).\n */\nexport interface FeedbackDialogTextLabels {\n title: string;\n category: string;\n subject: string;\n body: string;\n /** The body's label when `requireBody={false}`: \"What happened? (optional)\".\n * Optional, so a complete `UiKitLabels` typed before 0.14.2 still compiles. */\n bodyOptional?: string;\n /** The heading over the attachment buttons. */\n attachment: string;\n /**\n * The line beside the buttons naming the shortcut. Since 0.27.0 a function of the\n * platform, like `form.submitShortcut`: `apple` is true on a Mac, iPhone or iPad, where\n * the key is Cmd — \"⌘ Enter to send\" there, keksdose's \"Ctrl+Enter to send\" elsewhere\n * (§7.12). A plain string is still taken and shown everywhere, as before 0.27.0.\n */\n submitHint: FeedbackSubmitHint;\n cancel: string;\n save: string;\n /** 0.38 (docs/feedback-harmonization.md §9.2 decision 6): the line under the body when\n * reports are public — `useFeedbackSubmit({ publicNotice: true })` shows it. Optional,\n * so a complete `UiKitLabels` typed before 0.38 still compiles. de-CH \"Andere Personen\n * sehen Titel und Text (nie Ihre Screenshots oder Dateien).\" */\n publicNotice?: string;\n}\n\n// Defined in ./feedback-dialog-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_FEEDBACK_DIALOG_LABELS };\n\nexport interface FeedbackSubmission {\n title: string;\n body: string;\n category: string;\n attachment: File | null;\n}\n\n/**\n * What `onSubmit` gets under `attachments=\"multiple\"` (0.15.5): the screenshot and\n * the picked/pasted files apart, because an app files them differently — keksdose\n * attaches the screenshot to the report and the photos as its evidence list. No\n * `attachment`: there is no one file it could name.\n */\nexport interface FeedbackMultipleSubmission {\n title: string;\n body: string;\n category: string;\n /** What `onCaptureScreenshot` returned, or `null` — at most one. */\n screenshot: File | null;\n /** Picked and pasted files, in the order they were added; at most `maxAttachments`. */\n attachments: File[];\n}\n\ninterface FeedbackDialogBaseProps {\n open: boolean;\n onClose: () => void;\n categories: FeedbackCategoryOption[];\n category: string;\n onCategoryChange: (value: string) => void;\n /** Prop > `<UiKitProvider labels={{ feedbackDialog, feedbackAttachment }}>` >\n * English. Optional since 0.12.0; a whole `FeedbackDialogLabels` still fits. */\n labels?: LabelOverride<FeedbackDialogLabels>;\n submitting?: boolean;\n contextSlot?: ReactNode;\n attachmentAccept?: string[];\n maxAttachmentBytes?: number;\n /**\n * Optional: capture a screenshot of the underlying app view and return it as a\n * File. When provided, a \"Capture screenshot\" button is shown next to \"Add\n * attachment\"; the returned file is fed through the same validation + preview.\n */\n onCaptureScreenshot?: () => Promise<File | null>;\n /**\n * `false`: a report with only a subject can be sent, and the body's label says it is\n * optional (`bodyOptional`, unless `labels.body` is passed). Default `true`, the\n * dialog's behaviour until 0.14.2 (keksdose K1: its backend takes title-only reports).\n */\n requireBody?: boolean;\n /**\n * The attachment field's add and capture buttons: its `buttonVariant` (0.25.0),\n * Button's own `variant`. Default `\"secondary\"`, the look they always had.\n *\n * The field took `buttonVariant` / `buttonSize` in 0.24.0 (keksdose's support chat),\n * but the dialog builds its field itself and passed neither, so a host that wanted\n * quieter buttons here — a dialog that already ends in a row of Cancel / Send — had\n * no way to say so short of rebuilding the dialog. Named after the dialog's other\n * attachment options (`attachmentAccept`, `onAttachmentError`) and handed through\n * unchanged, in both `attachments` modes.\n */\n attachmentButtonVariant?: ButtonVariant;\n /** The attachment field's `buttonSize` (0.25.0) — with {@link attachmentButtonVariant}.\n * Default `\"md\"`; `\"sm\"` draws the paperclip and camera at 14px. */\n attachmentButtonSize?: ButtonSize;\n /**\n * A muted line under the body saying who will read it (0.38, docs/feedback-harmonization.md\n * §9.2 decision 6) — the kit's sentence is `feedbackDialog.publicNotice`, \"Other users\n * will see the title and text (never your screenshots or files)\", which\n * `useFeedbackSubmit({ publicNotice: true })` passes. Attached to the body field with\n * `aria-describedby`, so it is heard where the text is written. Left out: no line.\n */\n publicNotice?: ReactNode;\n}\n\n/** One attachment — a screenshot, a picked or a pasted image; a second replaces\n * nothing, it has to wait for the first to be removed. The default. */\nexport interface FeedbackDialogSingleProps extends FeedbackDialogBaseProps {\n attachments?: \"single\";\n onSubmit: (data: FeedbackSubmission) => void | Promise<void>;\n /** `info` (0.23.0) names the refused file and the limit — see\n * {@link FeedbackAttachmentErrorInfo}. */\n onAttachmentError?: (kind: \"type\" | \"size\", info: FeedbackAttachmentErrorInfo) => void;\n /** Multiple mode only — refused here rather than silently ignored. */\n maxAttachments?: never;\n}\n\n/**\n * One screenshot plus up to `maxAttachments` picked or pasted files (0.15.5, keksdose\n * dev#578: \"pasted two photos, the second overwrote the first\"). The capture button is\n * offered while there is no screenshot yet; the add button while there is room.\n */\nexport interface FeedbackDialogMultipleProps extends FeedbackDialogBaseProps {\n attachments: \"multiple\";\n /** Default `DEFAULT_MAX_ATTACHMENTS` (5). The screenshot does not count. */\n maxAttachments?: number;\n onSubmit: (data: FeedbackMultipleSubmission) => void | Promise<void>;\n /** `\"count\"`: more files arrived at once than there was room for; the surplus was\n * dropped. `info` (0.23.0) names the refused file(s) and the limit. */\n onAttachmentError?: (kind: FeedbackAttachmentError, info: FeedbackAttachmentErrorInfo) => void;\n}\n\nexport type FeedbackDialogProps = FeedbackDialogSingleProps | FeedbackDialogMultipleProps;\n\n/**\n * The generic feedback form dialog: category + subject + body + an optional image\n * attachment, with Ctrl/Cmd+Enter to submit. Domain-free — the app supplies the\n * category options, labels and an `onSubmit` that talks to its own backend, plus\n * an optional `contextSlot` for app-specific context (user, current URL, …).\n *\n * **A screenshot can be pasted straight in.** Ctrl/Cmd+V anywhere in the dialog\n * takes an image off the clipboard and makes it the attachment, through the same\n * validation and the same preview as the file picker. It is the gesture the two\n * ways in did not cover: `onCaptureScreenshot` snapshots the *whole* app view,\n * and the file picker needs a file — so somebody who wanted to show one panel,\n * or one region of one, had to save a crop to disk first and then find it again\n * (Steering Design feedback #39). The clipboard is where a region snip already\n * is on every platform.\n *\n * **`attachments=\"multiple\"`** (0.15.5) takes several: every paste and pick adds a\n * chip, and `onSubmit` gets `{ screenshot, attachments }` ({@link\n * FeedbackMultipleSubmission}) instead of `attachment`. Opt-in, so a caller typed\n * against the one-file submission keeps compiling and behaving as before.\n */\nexport function FeedbackDialog(props: FeedbackDialogProps) {\n const {\n open,\n onClose,\n categories,\n category,\n onCategoryChange,\n labels: labelsProp,\n submitting = false,\n contextSlot,\n attachmentAccept = DEFAULT_ATTACHMENT_ACCEPT,\n maxAttachmentBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onCaptureScreenshot,\n requireBody = true,\n attachmentButtonVariant,\n attachmentButtonSize,\n publicNotice,\n } = props;\n const [title, setTitle] = useState(\"\");\n const [body, setBody] = useState(\"\");\n const [attachment, setAttachment] = useState<File | null>(null);\n // `attachments=\"multiple\"` only: the capture, apart from the picked/pasted files.\n const [screenshot, setScreenshot] = useState<File | null>(null);\n const [files, setFiles] = useState<File[]>([]);\n const isMobile = usePhoneLayout();\n const noticeId = useId();\n const hasNotice =\n publicNotice !== undefined && publicNotice !== null && publicNotice !== false && publicNotice !== \"\";\n const labels = useKitLabels(\"feedbackDialog\", DEFAULT_FEEDBACK_DIALOG_LABELS, labelsProp);\n const attachmentText = useKitLabels(\"feedbackAttachment\", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);\n // Under `multiple` the heading is `attachmentList` — a screenshot AND photos — which\n // also names the chip list beneath it: one string, so the two cannot disagree. Its old\n // second name, `feedbackDialog.attachments`, went in 0.34.\n const listHeading =\n labelsProp?.attachmentList ?? attachmentText.attachmentList ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentList!;\n // The attachment field resolves its own keys from `feedbackAttachment`; it gets the\n // prop's (so a caller's `attachmentAdd` still wins) plus the resolved heading.\n const attachmentLabels =\n props.attachments === \"multiple\"\n ? { ...labelsProp, attachment: listHeading, attachmentList: listHeading }\n : { ...labelsProp, attachment: labels.attachment };\n\n // Reset the form whenever the dialog is (re)opened — during render, on the\n // closed-to-open transition, so the old draft never paints for a frame.\n const [wasOpen, setWasOpen] = useState(open);\n if (open !== wasOpen) {\n setWasOpen(open);\n if (open) {\n setTitle(\"\");\n setBody(\"\");\n setAttachment(null);\n setScreenshot(null);\n setFiles([]);\n }\n }\n\n // Trimmed: a subject or body of spaces is empty (keksdose's API answers a blank title\n // with a 422, and a blank report is no report).\n const canSubmit = !!title.trim() && (!requireBody || !!body.trim()) && !submitting;\n const trySubmit = () => {\n if (!canSubmit) return;\n if (props.attachments === \"multiple\") {\n void props.onSubmit({ title, body, category, screenshot, attachments: files });\n } else {\n void props.onSubmit({ title, body, category, attachment });\n }\n };\n\n if (!open) return null;\n\n const categoryField = (\n <Select label={labels.category} value={category} onChange={(e) => onCategoryChange(e.target.value)}>\n {categories.map((c) => (\n <option key={c.value} value={c.value}>\n {c.label}\n </option>\n ))}\n </Select>\n );\n // On a phone the subject leads as a heading rather than as the second of two\n // identical boxes under a select (Keksdose feedback #179). `variant=\"display\"`\n // is a no-op above the phone breakpoint, so the desktop dialog is unchanged.\n const subjectField = (\n <Input label={labels.subject} value={title} onChange={(e) => setTitle(e.target.value)} variant=\"display\" />\n );\n const bodyTextarea = (\n <Textarea\n rows={5}\n label={requireBody ? labels.body : (labelsProp?.body ?? labels.bodyOptional ?? labels.body)}\n value={body}\n onChange={(e) => setBody(e.target.value)}\n aria-describedby={hasNotice ? noticeId : undefined}\n />\n );\n // 0.38: who will read it, right under what they are writing (§9.2 decision 6).\n const bodyField = hasNotice ? (\n <div className=\"space-y-1\">\n {bodyTextarea}\n <p id={noticeId} className=\"text-xs text-muted\" data-feedback-public-notice=\"\">\n {publicNotice}\n </p>\n </div>\n ) : (\n bodyTextarea\n );\n const fieldProps = {\n labels: attachmentLabels,\n accept: attachmentAccept,\n maxBytes: maxAttachmentBytes,\n onCaptureScreenshot,\n // Left undefined they are the field's own defaults (secondary / md).\n buttonVariant: attachmentButtonVariant,\n buttonSize: attachmentButtonSize,\n // On `document`, not on the panel: the Modal focuses its own panel on open\n // and traps Tab inside it, so while this dialog is up every paste in the\n // page is meant for it — including the one made with nothing in\n // particular focused, which never reaches a React `onPaste` on a child.\n documentPaste: open,\n };\n const attachmentField =\n props.attachments === \"multiple\" ? (\n <FeedbackAttachmentField\n {...fieldProps}\n multiple\n value={files}\n onChange={setFiles}\n max={props.maxAttachments}\n // The capture goes to its own slot, so it is offered once and never crowds\n // out a photo; a paste is a photo (a region snip is chosen evidence, not \"the\n // screenshot\").\n screenshot={screenshot}\n onScreenshotChange={setScreenshot}\n onError={props.onAttachmentError}\n />\n ) : (\n <FeedbackAttachmentField\n {...fieldProps}\n value={attachment}\n onChange={setAttachment}\n onError={props.onAttachmentError}\n />\n );\n\n return (\n <Modal\n onClose={onClose}\n // The one dialog whose subject is the page behind it (Keksdose dev#460: \"Make\n // the feedback dialog draggable so I can see behind it if it blocks\n // something\") — you are describing what is under it while you type.\n draggable\n onKeyDown={(e) => {\n if ((e.ctrlKey || e.metaKey) && e.key === \"Enter\") {\n e.preventDefault();\n trySubmit();\n }\n }}\n >\n {/* Also the drag handle: pressing anywhere on the panel's own chrome moves it,\n and the heading is the strip a user reaches for. */}\n <h2 className=\"mb-3 text-lg font-semibold\">{labels.title}</h2>\n <div className=\"space-y-3\">\n {isMobile ? (\n // Phone shape, the same reasoning as the transaction editor's (#177):\n // the desktop order is the DATA MODEL's order — category, then subject,\n // then body — but nobody opens this dialog to pick a category. What they\n // came to do is say the thing, so that leads; the classification and the\n // evidence follow, a tier down.\n <>\n {subjectField}\n {bodyField}\n <div className=\"space-y-3 border-t border-subtle pt-3\">\n {categoryField}\n {attachmentField}\n {contextSlot}\n </div>\n </>\n ) : (\n <>\n {categoryField}\n {subjectField}\n {bodyField}\n {attachmentField}\n {contextSlot}\n </>\n )}\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"text-xs text-placeholder\">\n {typeof labels.submitHint === \"function\" ? labels.submitHint(isApplePlatform()) : labels.submitHint}\n </div>\n <div className=\"flex gap-2\">\n <Button variant=\"ghost\" onClick={onClose}>\n {labels.cancel}\n </Button>\n <Button onClick={trySubmit} disabled={!canSubmit}>\n {labels.save}\n </Button>\n </div>\n </div>\n </div>\n </Modal>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/feedback/feedback-dialog.tsx"],"sourcesContent":["import { useId, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Button } from \"../components/button\";\nimport { Input } from \"../components/input\";\nimport { Select } from \"../components/select\";\nimport { Textarea } from \"../components/textarea\";\nimport type { ButtonSize, ButtonVariant } from \"../components/button-look\";\nimport { Modal } from \"../components/modal\";\nimport { isApplePlatform } from \"../hooks/use-hotkey\";\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { hasContent } from \"../lib/presence\";\nimport {\n DEFAULT_ATTACHMENT_ACCEPT,\n DEFAULT_FEEDBACK_ATTACHMENT_LABELS,\n DEFAULT_MAX_ATTACHMENT_BYTES,\n FeedbackAttachmentField,\n type FeedbackAttachmentError,\n type FeedbackAttachmentErrorInfo,\n} from \"./feedback-attachment\";\nimport { DEFAULT_FEEDBACK_DIALOG_LABELS } from \"./feedback-dialog-labels\";\n\n/** One category the dialog offers. `C` (0.52): the app's category type — the kit's\n * `FeedbackCategory`, or `string` (the default) for a vocabulary of the app's own. */\nexport interface FeedbackCategoryOption<C extends string = string> {\n value: C;\n label: string;\n}\n\nexport interface FeedbackAttachmentLabels {\n /** The heading over the field. Optional: a note editor puts the buttons\n * straight under its textarea, where a second heading is noise. */\n attachment?: string;\n attachmentAdd: string;\n /** Label for the \"capture screenshot\" button. Optional — falls back to the provider's\n * `feedbackAttachment.attachmentCapture`, then English. */\n attachmentCapture?: string;\n /** The line under the attachment buttons saying a screenshot can be pasted\n * straight in. Optional — falls back to the provider's `feedbackAttachment`, then English. */\n attachmentPaste?: string;\n attachmentRemove: string;\n /** `multiple` mode (0.15.5): the chip list's accessible name — and, in\n * `<FeedbackDialog attachments=\"multiple\">`, the heading over it (0.16.0). Optional,\n * as are the keys below — they fall back to the provider's `feedbackAttachment`,\n * then English. */\n attachmentList?: string;\n /** `multiple` mode (0.16.0): the screenshot slot's chip title, in place of its file name. */\n attachmentScreenshot?: string;\n /** `multiple` mode: one chip's remove button, given the file's name. */\n attachmentRemoveFile?: (name: string) => string;\n /** `multiple` mode: the line shown once `max` files are attached. */\n attachmentLimit?: (max: number) => string;\n /** `refs` mode (0.22.0): the second line of a chip whose upload is still running. */\n attachmentUploading?: string;\n}\n\nexport interface FeedbackDialogLabels extends FeedbackAttachmentLabels {\n title: string;\n category: string;\n subject: string;\n body: string;\n attachment: string;\n /** See {@link FeedbackDialogTextLabels.submitHint}. */\n submitHint: FeedbackSubmitHint;\n cancel: string;\n save: string;\n}\n\n/** The submit hint: a string, or the line for the platform (`apple`: Mac, iPhone, iPad). */\nexport type FeedbackSubmitHint = string | ((apple: boolean) => string);\n\n/**\n * The dialog's own strings — the `feedbackDialog` namespace of `<UiKitProvider\n * labels>` (0.12.0). The attachment keys are not repeated here: they are the\n * `feedbackAttachment` namespace's, which the field inside already reads.\n *\n * Before, every key was required at the call site, so each app restated ten strings\n * wherever it mounted the dialog (kastlan feedback-button.tsx built the object inline).\n */\nexport interface FeedbackDialogTextLabels {\n title: string;\n category: string;\n subject: string;\n body: string;\n /** The body's label when `requireBody={false}`: \"What happened? (optional)\".\n * Optional, so a complete `UiKitLabels` typed before 0.14.2 still compiles. */\n bodyOptional?: string;\n /** The heading over the attachment buttons. */\n attachment: string;\n /**\n * The line beside the buttons naming the shortcut. Since 0.27.0 a function of the\n * platform, like `form.submitShortcut`: `apple` is true on a Mac, iPhone or iPad, where\n * the key is Cmd — \"⌘ Enter to send\" there, keksdose's \"Ctrl+Enter to send\" elsewhere\n * (§7.12). A plain string is still taken and shown everywhere, as before 0.27.0.\n */\n submitHint: FeedbackSubmitHint;\n cancel: string;\n save: string;\n /** 0.38 (docs/feedback-harmonization.md §9.2 decision 6): the line under the body when\n * reports are public — `useFeedbackSubmit({ publicNotice: true })` shows it. Optional,\n * so a complete `UiKitLabels` typed before 0.38 still compiles. de-CH \"Andere Personen\n * sehen Titel und Text (nie Ihre Screenshots oder Dateien).\" */\n publicNotice?: string;\n}\n\n// Defined in ./feedback-dialog-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_FEEDBACK_DIALOG_LABELS };\n\nexport interface FeedbackSubmission<C extends string = string> {\n title: string;\n body: string;\n category: C;\n attachment: File | null;\n}\n\n/**\n * What `onSubmit` gets under `attachments=\"multiple\"` (0.15.5): the screenshot and\n * the picked/pasted files apart, because an app files them differently — keksdose\n * attaches the screenshot to the report and the photos as its evidence list. No\n * `attachment`: there is no one file it could name.\n */\nexport interface FeedbackMultipleSubmission<C extends string = string> {\n title: string;\n body: string;\n category: C;\n /** What `onCaptureScreenshot` returned, or `null` — at most one. */\n screenshot: File | null;\n /** Picked and pasted files, in the order they were added; at most `maxAttachments`. */\n attachments: File[];\n}\n\n/**\n * `C` (0.52, refactor audit 2026-10-30, API-7): the category type, inferred from\n * `categories` and `category` — `FeedbackCategory` hands `onCategoryChange` and\n * `onSubmit` a `FeedbackCategory` back, where a plain `string` had to be cast (the kit's\n * own `useFeedbackSubmit` did). `string` by default, so an app's own vocabulary fits.\n */\ninterface FeedbackDialogBaseProps<C extends string = string> {\n open: boolean;\n onClose: () => void;\n /** `readonly` since 0.52: the dialog never changes it. */\n categories: readonly FeedbackCategoryOption<C>[];\n category: C;\n onCategoryChange: (value: C) => void;\n /** Prop > `<UiKitProvider labels={{ feedbackDialog, feedbackAttachment }}>` >\n * English. Optional since 0.12.0; a whole `FeedbackDialogLabels` still fits. */\n labels?: LabelOverride<FeedbackDialogLabels>;\n submitting?: boolean;\n contextSlot?: ReactNode;\n attachmentAccept?: string[];\n maxAttachmentBytes?: number;\n /**\n * Optional: capture a screenshot of the underlying app view and return it as a\n * File. When provided, a \"Capture screenshot\" button is shown next to \"Add\n * attachment\"; the returned file is fed through the same validation + preview.\n */\n onCaptureScreenshot?: () => Promise<File | null>;\n /**\n * `false`: a report with only a subject can be sent, and the body's label says it is\n * optional (`bodyOptional`, unless `labels.body` is passed). Default `true`, the\n * dialog's behaviour until 0.14.2 (keksdose K1: its backend takes title-only reports).\n */\n requireBody?: boolean;\n /**\n * The attachment field's add and capture buttons: its `buttonVariant` (0.25.0),\n * Button's own `variant`. Default `\"secondary\"`, the look they always had.\n *\n * The field took `buttonVariant` / `buttonSize` in 0.24.0 (keksdose's support chat),\n * but the dialog builds its field itself and passed neither, so a host that wanted\n * quieter buttons here — a dialog that already ends in a row of Cancel / Send — had\n * no way to say so short of rebuilding the dialog. Named after the dialog's other\n * attachment options (`attachmentAccept`, `onAttachmentError`) and handed through\n * unchanged, in both `attachments` modes.\n */\n attachmentButtonVariant?: ButtonVariant;\n /** The attachment field's `buttonSize` (0.25.0) — with {@link attachmentButtonVariant}.\n * Default `\"md\"`; `\"sm\"` draws the paperclip and camera at 14px. */\n attachmentButtonSize?: ButtonSize;\n /**\n * A muted line under the body saying who will read it (0.38, docs/feedback-harmonization.md\n * §9.2 decision 6) — the kit's sentence is `feedbackDialog.publicNotice`, \"Other users\n * will see the title and text (never your screenshots or files)\", which\n * `useFeedbackSubmit({ publicNotice: true })` passes. Attached to the body field with\n * `aria-describedby`, so it is heard where the text is written. Left out: no line.\n */\n publicNotice?: ReactNode;\n}\n\n/** One attachment — a screenshot, a picked or a pasted image; a second replaces\n * nothing, it has to wait for the first to be removed. The default. */\nexport interface FeedbackDialogSingleProps<C extends string = string> extends FeedbackDialogBaseProps<C> {\n attachments?: \"single\";\n onSubmit: (data: FeedbackSubmission<C>) => void | Promise<void>;\n /** `info` (0.23.0) names the refused file and the limit — see\n * {@link FeedbackAttachmentErrorInfo}. */\n onAttachmentError?: (kind: \"type\" | \"size\", info: FeedbackAttachmentErrorInfo) => void;\n /** Multiple mode only — refused here rather than silently ignored. */\n maxAttachments?: never;\n}\n\n/**\n * One screenshot plus up to `maxAttachments` picked or pasted files (0.15.5, keksdose\n * dev#578: \"pasted two photos, the second overwrote the first\"). The capture button is\n * offered while there is no screenshot yet; the add button while there is room.\n */\nexport interface FeedbackDialogMultipleProps<C extends string = string> extends FeedbackDialogBaseProps<C> {\n attachments: \"multiple\";\n /** Default `DEFAULT_MAX_ATTACHMENTS` (5). The screenshot does not count. */\n maxAttachments?: number;\n onSubmit: (data: FeedbackMultipleSubmission<C>) => void | Promise<void>;\n /** `\"count\"`: more files arrived at once than there was room for; the surplus was\n * dropped. `info` (0.23.0) names the refused file(s) and the limit. */\n onAttachmentError?: (kind: FeedbackAttachmentError, info: FeedbackAttachmentErrorInfo) => void;\n}\n\nexport type FeedbackDialogProps<C extends string = string> =\n FeedbackDialogSingleProps<C> | FeedbackDialogMultipleProps<C>;\n\n/**\n * The generic feedback form dialog: category + subject + body + an optional image\n * attachment, with Ctrl/Cmd+Enter to submit. Domain-free — the app supplies the\n * category options, labels and an `onSubmit` that talks to its own backend, plus\n * an optional `contextSlot` for app-specific context (user, current URL, …).\n *\n * **A screenshot can be pasted straight in.** Ctrl/Cmd+V anywhere in the dialog\n * takes an image off the clipboard and makes it the attachment, through the same\n * validation and the same preview as the file picker. It is the gesture the two\n * ways in did not cover: `onCaptureScreenshot` snapshots the *whole* app view,\n * and the file picker needs a file — so somebody who wanted to show one panel,\n * or one region of one, had to save a crop to disk first and then find it again\n * (Steering Design feedback #39). The clipboard is where a region snip already\n * is on every platform.\n *\n * **`attachments=\"multiple\"`** (0.15.5) takes several: every paste and pick adds a\n * chip, and `onSubmit` gets `{ screenshot, attachments }` ({@link\n * FeedbackMultipleSubmission}) instead of `attachment`. Opt-in, so a caller typed\n * against the one-file submission keeps compiling and behaving as before.\n */\nexport function FeedbackDialog<C extends string = string>(props: FeedbackDialogProps<C>) {\n const {\n open,\n onClose,\n categories,\n category,\n onCategoryChange,\n labels: labelsProp,\n submitting = false,\n contextSlot,\n attachmentAccept = DEFAULT_ATTACHMENT_ACCEPT,\n maxAttachmentBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onCaptureScreenshot,\n requireBody = true,\n attachmentButtonVariant,\n attachmentButtonSize,\n publicNotice,\n } = props;\n const [title, setTitle] = useState(\"\");\n const [body, setBody] = useState(\"\");\n const [attachment, setAttachment] = useState<File | null>(null);\n // `attachments=\"multiple\"` only: the capture, apart from the picked/pasted files.\n const [screenshot, setScreenshot] = useState<File | null>(null);\n const [files, setFiles] = useState<File[]>([]);\n const isMobile = usePhoneLayout();\n const noticeId = useId();\n const hasNotice = hasContent(publicNotice);\n const labels = useKitLabels(\"feedbackDialog\", DEFAULT_FEEDBACK_DIALOG_LABELS, labelsProp);\n const attachmentText = useKitLabels(\"feedbackAttachment\", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);\n // Under `multiple` the heading is `attachmentList` — a screenshot AND photos — which\n // also names the chip list beneath it: one string, so the two cannot disagree. Its old\n // second name, `feedbackDialog.attachments`, went in 0.34.\n const listHeading =\n labelsProp?.attachmentList ?? attachmentText.attachmentList ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentList!;\n // The attachment field resolves its own keys from `feedbackAttachment`; it gets the\n // prop's (so a caller's `attachmentAdd` still wins) plus the resolved heading.\n const attachmentLabels =\n props.attachments === \"multiple\"\n ? { ...labelsProp, attachment: listHeading, attachmentList: listHeading }\n : { ...labelsProp, attachment: labels.attachment };\n\n // Reset the form whenever the dialog is (re)opened — during render, on the\n // closed-to-open transition, so the old draft never paints for a frame.\n const [wasOpen, setWasOpen] = useState(open);\n if (open !== wasOpen) {\n setWasOpen(open);\n if (open) {\n setTitle(\"\");\n setBody(\"\");\n setAttachment(null);\n setScreenshot(null);\n setFiles([]);\n }\n }\n\n // Trimmed: a subject or body of spaces is empty (keksdose's API answers a blank title\n // with a 422, and a blank report is no report).\n const canSubmit = !!title.trim() && (!requireBody || !!body.trim()) && !submitting;\n const trySubmit = () => {\n if (!canSubmit) return;\n if (props.attachments === \"multiple\") {\n void props.onSubmit({ title, body, category, screenshot, attachments: files });\n } else {\n void props.onSubmit({ title, body, category, attachment });\n }\n };\n\n if (!open) return null;\n\n const categoryField = (\n // The select offers exactly `categories`' values, so what it answers is one of them.\n <Select label={labels.category} value={category} onChange={(e) => onCategoryChange(e.target.value as C)}>\n {categories.map((c) => (\n <option key={c.value} value={c.value}>\n {c.label}\n </option>\n ))}\n </Select>\n );\n // On a phone the subject leads as a heading rather than as the second of two\n // identical boxes under a select (Keksdose feedback #179). `variant=\"display\"`\n // is a no-op above the phone breakpoint, so the desktop dialog is unchanged.\n const subjectField = (\n <Input label={labels.subject} value={title} onChange={(e) => setTitle(e.target.value)} variant=\"display\" />\n );\n const bodyTextarea = (\n <Textarea\n rows={5}\n label={requireBody ? labels.body : (labelsProp?.body ?? labels.bodyOptional ?? labels.body)}\n value={body}\n onChange={(e) => setBody(e.target.value)}\n aria-describedby={hasNotice ? noticeId : undefined}\n />\n );\n // 0.38: who will read it, right under what they are writing (§9.2 decision 6).\n const bodyField = hasNotice ? (\n <div className=\"space-y-1\">\n {bodyTextarea}\n <p id={noticeId} className=\"text-xs text-muted\" data-feedback-public-notice=\"\">\n {publicNotice}\n </p>\n </div>\n ) : (\n bodyTextarea\n );\n const fieldProps = {\n labels: attachmentLabels,\n accept: attachmentAccept,\n maxBytes: maxAttachmentBytes,\n onCaptureScreenshot,\n // Left undefined they are the field's own defaults (secondary / md).\n buttonVariant: attachmentButtonVariant,\n buttonSize: attachmentButtonSize,\n // On `document`, not on the panel: the Modal focuses its own panel on open\n // and traps Tab inside it, so while this dialog is up every paste in the\n // page is meant for it — including the one made with nothing in\n // particular focused, which never reaches a React `onPaste` on a child.\n documentPaste: open,\n };\n const attachmentField =\n props.attachments === \"multiple\" ? (\n <FeedbackAttachmentField\n {...fieldProps}\n multiple\n value={files}\n onChange={setFiles}\n max={props.maxAttachments}\n // The capture goes to its own slot, so it is offered once and never crowds\n // out a photo; a paste is a photo (a region snip is chosen evidence, not \"the\n // screenshot\").\n screenshot={screenshot}\n onScreenshotChange={setScreenshot}\n onError={props.onAttachmentError}\n />\n ) : (\n <FeedbackAttachmentField\n {...fieldProps}\n value={attachment}\n onChange={setAttachment}\n onError={props.onAttachmentError}\n />\n );\n\n return (\n <Modal\n onClose={onClose}\n // The one dialog whose subject is the page behind it (Keksdose dev#460: \"Make\n // the feedback dialog draggable so I can see behind it if it blocks\n // something\") — you are describing what is under it while you type.\n draggable\n onKeyDown={(e) => {\n if ((e.ctrlKey || e.metaKey) && e.key === \"Enter\") {\n e.preventDefault();\n trySubmit();\n }\n }}\n >\n {/* Also the drag handle: pressing anywhere on the panel's own chrome moves it,\n and the heading is the strip a user reaches for. */}\n <h2 className=\"mb-3 text-lg font-semibold\">{labels.title}</h2>\n <div className=\"space-y-3\">\n {isMobile ? (\n // Phone shape, the same reasoning as the transaction editor's (#177):\n // the desktop order is the DATA MODEL's order — category, then subject,\n // then body — but nobody opens this dialog to pick a category. What they\n // came to do is say the thing, so that leads; the classification and the\n // evidence follow, a tier down.\n <>\n {subjectField}\n {bodyField}\n <div className=\"space-y-3 border-t border-subtle pt-3\">\n {categoryField}\n {attachmentField}\n {contextSlot}\n </div>\n </>\n ) : (\n <>\n {categoryField}\n {subjectField}\n {bodyField}\n {attachmentField}\n {contextSlot}\n </>\n )}\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"text-xs text-placeholder\">\n {typeof labels.submitHint === \"function\" ? labels.submitHint(isApplePlatform()) : labels.submitHint}\n </div>\n <div className=\"flex gap-2\">\n <Button variant=\"ghost\" onClick={onClose}>\n {labels.cancel}\n </Button>\n <Button onClick={trySubmit} disabled={!canSubmit}>\n {labels.save}\n </Button>\n </div>\n </div>\n </div>\n </Modal>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * A page's mutation, run once (refactor audit 2026-10-30, FLW-3): TanStack Query's
3
+ * `mutateAsync`, or any `(patch) => Promise` that resolves once the PATCH has landed.
4
+ *
5
+ * The parts took TanStack Query's `mutate` with a per-call `onSuccess` until 0.53
6
+ * (deprecated in 0.52). TanStack v5 calls per-call callbacks for the LATEST `mutate()` on
7
+ * an observer only — it overwrites the options and detaches from the earlier mutation —
8
+ * so a second change sent before the first PATCH answered (another swipe, another pill,
9
+ * the Undo itself) lost the first change's Undo toast, and the detail's editor stayed
10
+ * open. The promise `mutateAsync` returns settles for every call, whatever the observer
11
+ * does next.
12
+ *
13
+ * A rejected promise is the mutation's own to report (its `onError`), so it is caught here
14
+ * and nothing more happens.
15
+ *
16
+ * @internal Not part of `/feedback` or the barrel.
17
+ */
18
+ export declare function runMutation<P>(call: (patch: P) => Promise<unknown>, patch: P, landed: () => void): void;
@@ -0,0 +1,8 @@
1
+ "use client";
2
+ function runMutation(call, patch, landed) {
3
+ call(patch).then(landed, () => void 0);
4
+ }
5
+ export {
6
+ runMutation
7
+ };
8
+ //# sourceMappingURL=feedback-mutation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/feedback/feedback-mutation.ts"],"sourcesContent":["/**\n * A page's mutation, run once (refactor audit 2026-10-30, FLW-3): TanStack Query's\n * `mutateAsync`, or any `(patch) => Promise` that resolves once the PATCH has landed.\n *\n * The parts took TanStack Query's `mutate` with a per-call `onSuccess` until 0.53\n * (deprecated in 0.52). TanStack v5 calls per-call callbacks for the LATEST `mutate()` on\n * an observer only — it overwrites the options and detaches from the earlier mutation —\n * so a second change sent before the first PATCH answered (another swipe, another pill,\n * the Undo itself) lost the first change's Undo toast, and the detail's editor stayed\n * open. The promise `mutateAsync` returns settles for every call, whatever the observer\n * does next.\n *\n * A rejected promise is the mutation's own to report (its `onError`), so it is caught here\n * and nothing more happens.\n *\n * @internal Not part of `/feedback` or the barrel.\n */\nexport function runMutation<P>(call: (patch: P) => Promise<unknown>, patch: P, landed: () => void): void {\n call(patch).then(landed, () => undefined);\n}\n"],"mappings":";AAiBO,SAAS,YAAe,MAAsC,OAAU,QAA0B;AACvG,OAAK,KAAK,EAAE,KAAK,QAAQ,MAAM,MAAS;AAC1C;","names":[]}
@@ -0,0 +1,53 @@
1
+ import type { PathPattern } from "../landing/routing.js";
2
+ import type { FeedbackContext } from "./feedback-record.js";
3
+ /**
4
+ * The page a report was filed on, and which of it the feedback parts link (refactor audit
5
+ * 2026-10-30, FLW-7).
6
+ *
7
+ * The page comes from the REPORTER's client context: its origin is always this app's
8
+ * (only the path is ever linked), but its path and query are whatever the row says. A
9
+ * signed-in user could file a report whose link takes the admin to a route that acts on
10
+ * load — keksdose's `/join?token=` accepts an invitation through `AcceptInvitation`,
11
+ * kastlan's and Kurvenschmiede's `/register?invite=` likewise.
12
+ *
13
+ * **Only the PATH is linked** (0.52 code review, this file). The tokens and the forward
14
+ * targets travel in the query: a guard on the path alone let `/login?next=%2Fjoin%3Ftoken%3D…`
15
+ * through as a link, and `RedirectIfAuthed` sent the signed-in admin on to the invitation —
16
+ * as `/forgot-password`, `/demo` and `/welcome` would, and whatever forwarder an app adds
17
+ * next (`?returnTo=`, a share target). No list of forwarders can know them all; a link
18
+ * without a query reaches none. The query and hash are shown as text beside it, and "Copy
19
+ * URL" copies the whole address, for whoever wants that exact state and opens it knowingly.
20
+ *
21
+ * **A one-shot PATH is not linked at all** — the kit's `/join`, `/register`,
22
+ * `/verify-email`, `/reset-password` (`/reset-password/<token>` carries it in the path),
23
+ * and the app's `pageLinkExclude` on top: shown as text with Copy.
24
+ *
25
+ * @internal Not part of `/feedback` or the barrel.
26
+ */
27
+ /** The kit's one-shot routes: the invitation accepts and the two token pages. */
28
+ export declare const FEEDBACK_ONE_SHOT_PAGES: readonly PathPattern[];
29
+ /** Whether `href` (a path, with or without its query) names a one-shot route — the kit's,
30
+ * or one of the app's `exclude` on top. Read as the ROUTER reads it, decoded and
31
+ * case-insensitive: `/Join?token=` and `/j%6Fin?token=` render `/join` too (0.52 code
32
+ * review, this file). */
33
+ export declare function isOneShotPage(href: string, exclude?: readonly PathPattern[]): boolean;
34
+ /** The page a report was filed on, read once — the URL cell and the detail each parsed it
35
+ * three times, once per part (0.52 code review, feedback-table.tsx: 810 `URL`s per render
36
+ * of a 270-row inbox, for one column). */
37
+ export interface FeedbackPage {
38
+ /** The path: what the URL column shows, and the only part ever linked. */
39
+ path: string;
40
+ /** Path + query + hash: the page as the reporter had it (keksdose `localHref`: the query
41
+ * and hash carry the state, `?tab=import`, a drill-down) — shown, never followed. */
42
+ href: string;
43
+ /** What "Copy URL" copies: the FULL `context.url` as the reporter's browser had it —
44
+ * origin included, the one part that says which copy of the app it was (feedback
45
+ * #278) — else `context.route`. */
46
+ url: string;
47
+ }
48
+ /** `context.url`, else `context.route`, parsed — or null when neither is set or the value
49
+ * is no http(s) address or path (a `javascript:` value is not a page). */
50
+ export declare function feedbackPage(context: FeedbackContext | null | undefined): FeedbackPage | null;
51
+ /** Where a click on the page goes: its path, or `null` — show it as text — when that path
52
+ * is one-shot. */
53
+ export declare function feedbackPageLinkTarget(page: FeedbackPage, exclude?: readonly PathPattern[]): string | null;