@eifi1/ui-kit 0.44.0 → 0.46.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 (285) hide show
  1. package/README.md +29 -14
  2. package/dist/admin/admin-action-log-labels.js +9 -0
  3. package/dist/admin/admin-action-log-labels.js.map +1 -1
  4. package/dist/admin/admin-action-log.d.ts +18 -1
  5. package/dist/admin/admin-action-log.js +25 -16
  6. package/dist/admin/admin-action-log.js.map +1 -1
  7. package/dist/admin/admin-area-labels.d.ts +15 -0
  8. package/dist/admin/admin-area-labels.js +6 -1
  9. package/dist/admin/admin-area-labels.js.map +1 -1
  10. package/dist/admin/admin-overview.d.ts +16 -1
  11. package/dist/admin/admin-overview.js +12 -1
  12. package/dist/admin/admin-overview.js.map +1 -1
  13. package/dist/admin/mail-test-card-labels.d.ts +3 -0
  14. package/dist/admin/mail-test-card-labels.js +17 -0
  15. package/dist/admin/mail-test-card-labels.js.map +1 -0
  16. package/dist/admin/mail-test-card.d.ts +75 -0
  17. package/dist/admin/mail-test-card.js +82 -0
  18. package/dist/admin/mail-test-card.js.map +1 -0
  19. package/dist/components/appearance-labels.d.ts +10 -0
  20. package/dist/components/appearance-labels.js +6 -1
  21. package/dist/components/appearance-labels.js.map +1 -1
  22. package/dist/components/data-table-column-settings.js +2 -2
  23. package/dist/components/data-table-column-settings.js.map +1 -1
  24. package/dist/components/data-table-head.js +2 -2
  25. package/dist/components/data-table-head.js.map +1 -1
  26. package/dist/components/data-table-mobile.js +17 -2
  27. package/dist/components/data-table-mobile.js.map +1 -1
  28. package/dist/components/data-table-row-actions.d.ts +18 -2
  29. package/dist/components/data-table-row-actions.js +86 -2
  30. package/dist/components/data-table-row-actions.js.map +1 -1
  31. package/dist/components/data-table-shared.d.ts +2 -0
  32. package/dist/components/data-table-shared.js +17 -2
  33. package/dist/components/data-table-shared.js.map +1 -1
  34. package/dist/components/data-table.d.ts +43 -2
  35. package/dist/components/data-table.js +10 -10
  36. package/dist/components/data-table.js.map +1 -1
  37. package/dist/components/date-format-setting.d.ts +29 -0
  38. package/dist/components/date-format-setting.js +36 -0
  39. package/dist/components/date-format-setting.js.map +1 -0
  40. package/dist/components/swipeable-row.d.ts +8 -0
  41. package/dist/components/swipeable-row.js.map +1 -1
  42. package/dist/components/tabs.d.ts +3 -2
  43. package/dist/components/tabs.js +1 -1
  44. package/dist/components/tabs.js.map +1 -1
  45. package/dist/components/translation-review-editor.js +1 -0
  46. package/dist/components/translation-review-editor.js.map +1 -1
  47. package/dist/components/translation-review-labels.d.ts +5 -0
  48. package/dist/components/translation-review-labels.js +2 -0
  49. package/dist/components/translation-review-labels.js.map +1 -1
  50. package/dist/components/translation-review.d.ts +18 -8
  51. package/dist/components/translation-review.js +64 -7
  52. package/dist/components/translation-review.js.map +1 -1
  53. package/dist/components/write-lock-labels.js +2 -1
  54. package/dist/components/write-lock-labels.js.map +1 -1
  55. package/dist/components/write-lock.d.ts +33 -3
  56. package/dist/components/write-lock.js +9 -0
  57. package/dist/components/write-lock.js.map +1 -1
  58. package/dist/data-table.d.ts +1 -1
  59. package/dist/data-table.js.map +1 -1
  60. package/dist/eslint/index.js +1 -1
  61. package/dist/feedback/feedback-row-detail.d.ts +4 -2
  62. package/dist/feedback/feedback-row-detail.js +17 -26
  63. package/dist/feedback/feedback-row-detail.js.map +1 -1
  64. package/dist/feedback/feedback-swipe.d.ts +18 -0
  65. package/dist/feedback/feedback-swipe.js +10 -1
  66. package/dist/feedback/feedback-swipe.js.map +1 -1
  67. package/dist/i18n/defaults.js +9 -1
  68. package/dist/i18n/defaults.js.map +1 -1
  69. package/dist/i18n/german.js +81 -5
  70. package/dist/i18n/german.js.map +1 -1
  71. package/dist/i18n/kit-labels.d.ts +25 -1
  72. package/dist/i18n/kit-labels.js +8 -2
  73. package/dist/i18n/kit-labels.js.map +1 -1
  74. package/dist/i18n/locales/en.js +97 -7
  75. package/dist/i18n/locales/en.js.map +3 -3
  76. package/dist/i18n/locales/es.js +81 -5
  77. package/dist/i18n/locales/es.js.map +1 -1
  78. package/dist/i18n/locales/fr.js +81 -5
  79. package/dist/i18n/locales/fr.js.map +1 -1
  80. package/dist/i18n/locales/hu.js +81 -5
  81. package/dist/i18n/locales/hu.js.map +1 -1
  82. package/dist/i18n/locales/it.js +81 -5
  83. package/dist/i18n/locales/it.js.map +1 -1
  84. package/dist/i18n/locales/zh.js +81 -5
  85. package/dist/i18n/locales/zh.js.map +1 -1
  86. package/dist/i18n/review.js +16 -2
  87. package/dist/i18n/review.js.map +1 -1
  88. package/dist/index.d.ts +17 -2
  89. package/dist/index.js +40 -0
  90. package/dist/index.js.map +1 -1
  91. package/dist/lib/date-format.d.ts +81 -0
  92. package/dist/lib/date-format.js +56 -0
  93. package/dist/lib/date-format.js.map +1 -0
  94. package/dist/lib/format.d.ts +5 -1
  95. package/dist/lib/format.js +11 -5
  96. package/dist/lib/format.js.map +1 -1
  97. package/dist/notifications/device-zone-report.d.ts +27 -0
  98. package/dist/notifications/device-zone-report.js +24 -0
  99. package/dist/notifications/device-zone-report.js.map +1 -0
  100. package/dist/notifications/feedback-notification-types-labels.d.ts +7 -1
  101. package/dist/notifications/feedback-notification-types-labels.js +4 -1
  102. package/dist/notifications/feedback-notification-types-labels.js.map +1 -1
  103. package/dist/notifications/feedback-notification-types.d.ts +11 -5
  104. package/dist/notifications/feedback-notification-types.js +2 -1
  105. package/dist/notifications/feedback-notification-types.js.map +1 -1
  106. package/dist/notifications/notification-bell.d.ts +33 -0
  107. package/dist/notifications/notification-bell.js +74 -0
  108. package/dist/notifications/notification-bell.js.map +1 -0
  109. package/dist/notifications/notification-inbox.d.ts +77 -0
  110. package/dist/notifications/notification-inbox.js +206 -0
  111. package/dist/notifications/notification-inbox.js.map +1 -0
  112. package/dist/notifications/notification-types-setting.d.ts +6 -4
  113. package/dist/notifications/notification-types-setting.js +103 -45
  114. package/dist/notifications/notification-types-setting.js.map +1 -1
  115. package/dist/notifications/notification-types.d.ts +19 -7
  116. package/dist/notifications/notifications-labels.d.ts +41 -7
  117. package/dist/notifications/notifications-labels.js +18 -1
  118. package/dist/notifications/notifications-labels.js.map +1 -1
  119. package/dist/notifications/push-devices-setting.d.ts +20 -3
  120. package/dist/notifications/push-devices-setting.js +93 -33
  121. package/dist/notifications/push-devices-setting.js.map +1 -1
  122. package/dist/notifications/push-worker.d.ts +108 -0
  123. package/dist/notifications/push-worker.js +62 -0
  124. package/dist/notifications/push-worker.js.map +1 -0
  125. package/dist/notifications/quiet-hours-setting.d.ts +29 -17
  126. package/dist/notifications/quiet-hours-setting.js +9 -41
  127. package/dist/notifications/quiet-hours-setting.js.map +1 -1
  128. package/dist/notifications/time-zone-select.d.ts +5 -2
  129. package/dist/notifications/time-zone-select.js.map +1 -1
  130. package/dist/notifications/use-push-device.d.ts +4 -17
  131. package/dist/notifications/use-push-device.js +7 -15
  132. package/dist/notifications/use-push-device.js.map +1 -1
  133. package/dist/offline/local-first.d.ts +33 -0
  134. package/dist/offline/local-first.js +39 -0
  135. package/dist/offline/local-first.js.map +1 -0
  136. package/dist/offline/pwa-update-prompt-labels.d.ts +2 -0
  137. package/dist/offline/pwa-update-prompt-labels.js +9 -0
  138. package/dist/offline/pwa-update-prompt-labels.js.map +1 -0
  139. package/dist/offline/pwa-update-prompt.d.ts +32 -0
  140. package/dist/offline/pwa-update-prompt.js +36 -0
  141. package/dist/offline/pwa-update-prompt.js.map +1 -0
  142. package/dist/offline/query-persistence.d.ts +125 -0
  143. package/dist/offline/query-persistence.js +183 -0
  144. package/dist/offline/query-persistence.js.map +1 -0
  145. package/dist/offline/service-worker-updates.d.ts +46 -0
  146. package/dist/offline/service-worker-updates.js +39 -0
  147. package/dist/offline/service-worker-updates.js.map +1 -0
  148. package/dist/offline/shared-computer-labels.d.ts +2 -0
  149. package/dist/offline/shared-computer-labels.js +9 -0
  150. package/dist/offline/shared-computer-labels.js.map +1 -0
  151. package/dist/offline/shared-computer.d.ts +44 -0
  152. package/dist/offline/shared-computer.js +51 -0
  153. package/dist/offline/shared-computer.js.map +1 -0
  154. package/dist/offline/sign-out-pending-dialog-labels.d.ts +2 -0
  155. package/dist/offline/sign-out-pending-dialog-labels.js +14 -0
  156. package/dist/offline/sign-out-pending-dialog-labels.js.map +1 -0
  157. package/dist/offline/sign-out-pending-dialog.d.ts +58 -0
  158. package/dist/offline/sign-out-pending-dialog.js +66 -0
  159. package/dist/offline/sign-out-pending-dialog.js.map +1 -0
  160. package/dist/offline.d.ts +11 -0
  161. package/dist/offline.js +33 -0
  162. package/dist/offline.js.map +1 -0
  163. package/dist/push-worker.d.ts +2 -0
  164. package/dist/push-worker.js +14 -0
  165. package/dist/push-worker.js.map +1 -0
  166. package/dist/shell/account-menu-text-size.d.ts +33 -0
  167. package/dist/shell/account-menu-text-size.js +51 -0
  168. package/dist/shell/account-menu-text-size.js.map +1 -0
  169. package/dist/shell/admin-menu.d.ts +24 -0
  170. package/dist/shell/admin-menu.js +12 -3
  171. package/dist/shell/admin-menu.js.map +1 -1
  172. package/dist/shell/top-bar-actions.d.ts +6 -2
  173. package/dist/shell/top-bar-actions.js +1 -0
  174. package/dist/shell/top-bar-actions.js.map +1 -1
  175. package/dist/shell.d.ts +4 -0
  176. package/dist/shell.js +4 -0
  177. package/dist/shell.js.map +1 -1
  178. package/dist/sync/db.d.ts +76 -0
  179. package/dist/sync/db.js +24 -0
  180. package/dist/sync/db.js.map +1 -0
  181. package/dist/sync/engine.d.ts +124 -0
  182. package/dist/sync/engine.js +474 -0
  183. package/dist/sync/engine.js.map +1 -0
  184. package/dist/sync/hooks.d.ts +30 -0
  185. package/dist/sync/hooks.js +67 -0
  186. package/dist/sync/hooks.js.map +1 -0
  187. package/dist/sync/testing.d.ts +28 -0
  188. package/dist/sync/testing.js +41 -0
  189. package/dist/sync/testing.js.map +1 -0
  190. package/dist/sync/wire.d.ts +50 -0
  191. package/dist/sync/wire.js +2 -0
  192. package/dist/sync/wire.js.map +1 -0
  193. package/dist/sync.d.ts +7 -0
  194. package/dist/sync.js +22 -0
  195. package/dist/sync.js.map +1 -0
  196. package/dist/tour/tour-catalogue.d.ts +7 -1
  197. package/dist/tour/tour-catalogue.js.map +1 -1
  198. package/dist/tour/tour-labels.js +1 -0
  199. package/dist/tour/tour-labels.js.map +1 -1
  200. package/dist/tour/tour.d.ts +30 -0
  201. package/dist/tour/tour.js +54 -8
  202. package/dist/tour/tour.js.map +1 -1
  203. package/dist/tour/use-tour-progress.d.ts +99 -1
  204. package/dist/tour/use-tour-progress.js +228 -12
  205. package/dist/tour/use-tour-progress.js.map +1 -1
  206. package/dist/tour.d.ts +2 -0
  207. package/dist/tour.js +2 -0
  208. package/dist/tour.js.map +1 -1
  209. package/package.json +43 -1
  210. package/src/admin/admin-action-log-labels.ts +9 -0
  211. package/src/admin/admin-action-log.tsx +41 -5
  212. package/src/admin/admin-area-labels.ts +20 -0
  213. package/src/admin/admin-overview.tsx +37 -2
  214. package/src/admin/mail-test-card-labels.ts +19 -0
  215. package/src/admin/mail-test-card.tsx +190 -0
  216. package/src/components/appearance-labels.ts +16 -0
  217. package/src/components/data-table-column-settings.tsx +2 -2
  218. package/src/components/data-table-head.tsx +2 -2
  219. package/src/components/data-table-mobile.tsx +26 -3
  220. package/src/components/data-table-row-actions.tsx +118 -2
  221. package/src/components/data-table-shared.tsx +39 -3
  222. package/src/components/data-table.tsx +52 -21
  223. package/src/components/date-format-setting.tsx +64 -0
  224. package/src/components/swipeable-row.tsx +8 -0
  225. package/src/components/tabs.tsx +4 -3
  226. package/src/components/translation-review-editor.tsx +2 -0
  227. package/src/components/translation-review-labels.ts +7 -0
  228. package/src/components/translation-review.tsx +102 -14
  229. package/src/components/write-lock-labels.ts +1 -0
  230. package/src/components/write-lock.tsx +36 -3
  231. package/src/data-table.ts +1 -0
  232. package/src/feedback/feedback-row-detail.tsx +18 -23
  233. package/src/feedback/feedback-swipe.tsx +28 -1
  234. package/src/i18n/defaults.ts +8 -0
  235. package/src/i18n/german.ts +90 -4
  236. package/src/i18n/kit-labels.tsx +31 -1
  237. package/src/i18n/locales/en.ts +13 -0
  238. package/src/i18n/locales/es.ts +90 -4
  239. package/src/i18n/locales/fr.ts +90 -4
  240. package/src/i18n/locales/hu.ts +86 -4
  241. package/src/i18n/locales/it.ts +90 -4
  242. package/src/i18n/locales/zh.ts +84 -2
  243. package/src/i18n/review.ts +15 -1
  244. package/src/index.ts +45 -0
  245. package/src/lib/date-format.ts +151 -0
  246. package/src/lib/format.ts +23 -6
  247. package/src/notifications/device-zone-report.ts +54 -0
  248. package/src/notifications/feedback-notification-types-labels.ts +10 -1
  249. package/src/notifications/feedback-notification-types.ts +12 -5
  250. package/src/notifications/notification-bell.tsx +116 -0
  251. package/src/notifications/notification-inbox.tsx +304 -0
  252. package/src/notifications/notification-types-setting.tsx +151 -55
  253. package/src/notifications/notification-types.ts +20 -8
  254. package/src/notifications/notifications-labels.ts +69 -11
  255. package/src/notifications/push-devices-setting.tsx +123 -34
  256. package/src/notifications/push-worker.ts +160 -0
  257. package/src/notifications/quiet-hours-setting.tsx +41 -58
  258. package/src/notifications/time-zone-select.tsx +5 -2
  259. package/src/notifications/use-push-device.ts +20 -33
  260. package/src/offline/local-first.ts +76 -0
  261. package/src/offline/pwa-update-prompt-labels.ts +9 -0
  262. package/src/offline/pwa-update-prompt.tsx +72 -0
  263. package/src/offline/query-persistence.ts +356 -0
  264. package/src/offline/service-worker-updates.ts +81 -0
  265. package/src/offline/shared-computer-labels.ts +8 -0
  266. package/src/offline/shared-computer.tsx +87 -0
  267. package/src/offline/sign-out-pending-dialog-labels.ts +21 -0
  268. package/src/offline/sign-out-pending-dialog.tsx +135 -0
  269. package/src/offline.ts +32 -0
  270. package/src/push-worker.ts +25 -0
  271. package/src/shell/account-menu-text-size.tsx +77 -0
  272. package/src/shell/admin-menu.tsx +40 -3
  273. package/src/shell/top-bar-actions.tsx +6 -1
  274. package/src/shell.ts +7 -0
  275. package/src/sync/db.ts +103 -0
  276. package/src/sync/engine.ts +699 -0
  277. package/src/sync/hooks.ts +93 -0
  278. package/src/sync/testing.ts +73 -0
  279. package/src/sync/wire.ts +61 -0
  280. package/src/sync.ts +29 -0
  281. package/src/tour/tour-catalogue.tsx +7 -1
  282. package/src/tour/tour-labels.ts +1 -0
  283. package/src/tour/tour.tsx +105 -9
  284. package/src/tour/use-tour-progress.ts +378 -12
  285. package/src/tour.ts +3 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/admin/admin-overview.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Button } from \"../components/button\";\nimport { StatTile, StatTileGrid } from \"../components/stat-tile\";\nimport type { StatTileProps } from \"../components/stat-tile\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { useAdminAreaLabels } from \"./admin-area-labels\";\nimport type { AdminAreaLabels } from \"./admin-area-labels\";\n\n/**\n * The admin's start page (0.42.0, docs/admin-harmonization.md §2.4, §6.1–§6.5): a grid\n * of tiles, each a count of what is waiting and a link to where the work is, fed by one\n * server answer.\n */\n\n/**\n * `GET /admin/attention` (§2.4 decision 14, §6.4) — server-kit's `AdminAttention`\n * (`eifi1_server_kit.user_admin.attention`), as the API sends it.\n *\n * Each count is `null` where the app has no such thing or this viewer cannot act on it,\n * and every count is `null` in a demo session. Demo accounts never count. `extra` holds\n * the app's own counts, keyed as its overview names them (keksdose `support_waiting`,\n * Kurvenschmiede `released_for_work`).\n */\nexport interface AdminAttention {\n /** \"To triage\": OPEN reports, crash reports included (§6.1). */\n feedback_waiting: number | null;\n /** Accounts that asked to be deleted — only where this viewer acts on them (§6.2). */\n deletion_requests: number | null;\n invitations_pending: number | null;\n users_total: number | null;\n /** Accounts made in the last seven days. */\n users_new_7d: number | null;\n extra: Record<string, number | null>;\n}\n\n/** The shared counts that are a tile of their own (`users_new_7d` is the users tile's line). */\nexport type AdminAttentionTile = \"feedback_waiting\" | \"deletion_requests\" | \"invitations_pending\" | \"users_total\";\n\n/** Where each shared tile links when the app names nothing else (§2.4 decision 13). */\nexport const DEFAULT_ADMIN_ATTENTION_HREFS: Readonly<Record<AdminAttentionTile, string>> = Object.freeze({\n feedback_waiting: \"/feedback\",\n deletion_requests: \"/admin/users?state=deletion_requested\",\n invitations_pending: \"/admin/invitations\",\n users_total: \"/admin/users\",\n});\n\n/**\n * The account menu's badge (§2.4 decision 15, §6.3): the sum of the counts that are work\n * waiting on THIS admin — each app names its own (keksdose `[\"feedback_waiting\",\n * \"support_waiting\", \"awaiting_verdict\"]`, kastlan `[\"feedback_waiting\"]`, Kurvenschmiede\n * `[\"feedback_waiting\", \"deletion_requests\"]`). A name is a shared count or an `extra`\n * key; a `null` or missing count adds nothing. Invitations and users never belong in it.\n * `null` while there is no answer yet, so the badge shows nothing rather than a 0.\n */\nexport function adminWaitingCount(\n attention: AdminAttention | null | undefined,\n counts: readonly (AdminAttentionTile | (string & {}))[],\n): number | null {\n if (!attention) return null;\n let sum = 0;\n for (const key of counts) {\n const shared = (attention as unknown as Record<string, unknown>)[key];\n const value = typeof shared === \"number\" ? shared : attention.extra?.[key];\n if (typeof value === \"number\" && value > 0) sum += value;\n }\n return sum;\n}\n\nexport interface AttentionTileProps extends Omit<StatTileProps, \"href\" | \"onClick\" | \"value\"> {\n /** The count. `null` shows the tile's dash; {@link AdminOverview} leaves such a tile out. */\n count: number | null | undefined;\n /** Where the work is — the tile is a link to it. Since 0.43 it may be left out: a\n * quiet count, no link (Kurvenschmiede). */\n href?: string;\n}\n\n/**\n * One tile of the overview: a {@link StatTile} that is a LINK to where the work is, its\n * count the headline. The whole tile is the link, named by its label; route it through\n * `renderLink` or the provider's link component, as any StatTile. Without `href` (0.43) it\n * is a quiet count, no link.\n */\nexport function AttentionTile({ count, href, ...tile }: AttentionTileProps) {\n return <StatTile data-slot=\"attention-tile\" {...tile} value={count ?? null} href={href} />;\n}\n\n/** One of the app's own tiles (§2.4 decision 13: keksdose's \"Support waiting\"). */\nexport interface AdminExtraTile {\n /** Unique among the tiles; usually the `extra` key it shows. */\n key: string;\n label: ReactNode;\n /** `null` or `undefined` leaves the tile out, as for a shared one. */\n count: number | null | undefined;\n /** Where the work is. Left out (0.43, Kurvenschmiede): a quiet count with no link —\n * a number worth seeing that has no page of its own. */\n href?: string;\n /** A line under the count. */\n description?: ReactNode;\n}\n\nexport interface AdminOverviewProps {\n /** The server's answer; `undefined` while it loads (the grid shows its skeletons). */\n attention: AdminAttention | null | undefined;\n /**\n * Where the shared tiles link, over {@link DEFAULT_ADMIN_ATTENTION_HREFS} — the\n * inbox's address filtered to OPEN (§6.1) is the app's own:\n * `{ feedback_waiting: \"/feedback?status=OPEN\" }`.\n */\n hrefs?: Partial<Record<AdminAttentionTile, string>>;\n /** The app's own tiles, after the shared ones, in this order. */\n extraTiles?: readonly AdminExtraTile[];\n /** Passed to every tile — `renderLink` for the router, `size`, `variant`. */\n tileProps?: Pick<StatTileProps, \"renderLink\" | \"size\" | \"variant\" | \"locale\">;\n /**\n * 0.43 (keksdose): the answer failed — the query's `error`, or `true`. Anything truthy\n * shows `adminArea.overviewError` in a danger box, with \"Try again\" when\n * {@link onRetry} is given. A failed poll keeps the last good tiles: with `attention`\n * still there (a query keeps its last data), the box shows ALONGSIDE them, above, not\n * instead (build review 0.43, keksdose N7) — pass `attention.data` as it is. Without\n * an answer yet, the box alone. Without `error`: `undefined` is the skeletons and\n * `null` nothing, as before.\n */\n error?: unknown;\n /** \"Try again\" in the error box — the query's `refetch`. */\n onRetry?: () => void;\n /** Prop > `<UiKitProvider labels={{ adminArea }}>` > English. */\n labels?: LabelOverride<AdminAreaLabels>;\n className?: string;\n}\n\n/**\n * The overview section (§2.4): the shared tiles in their order — To triage, Deletion\n * requests, Pending invitations, Users (with the new accounts of the last 7 days under\n * the total) — then the app's `extraTiles`. A tile whose count is `null` is left out,\n * so each app shows only what it has; a `0` is a quiet tile, \"nothing waiting\".\n *\n * The overview is the admin area's landing page, standalone: it is NOT wrapped in a\n * `SettingsSection` (`adminSections` gives it no entry; render it as the page's body, as\n * Kurvenschmiede's `OverviewPanel` does). keksdose links its metrics from here with at\n * most two tiles (§6.5).\n */\nexport function AdminOverview({\n attention,\n hrefs,\n extraTiles = [],\n tileProps,\n error,\n onRetry,\n labels: labelsProp,\n className,\n}: AdminOverviewProps) {\n const labels = useAdminAreaLabels(labelsProp);\n /** The error box: on its own (`alone`) it is the overview's root. */\n const errorBox = (alone: boolean) => (\n <AlertBanner\n data-slot={alone ? \"admin-overview\" : undefined}\n data-state={alone ? \"error\" : undefined}\n variant=\"box\"\n tone=\"danger\"\n className={alone ? className : undefined}\n action={\n onRetry ? (\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" onClick={() => onRetry()}>\n {labels.retry}\n </Button>\n ) : undefined\n }\n >\n {labels.overviewError}\n </AlertBanner>\n );\n // Nothing good to keep: the box alone.\n if (error && !attention) return errorBox(true);\n const href = { ...DEFAULT_ADMIN_ATTENTION_HREFS, ...hrefs };\n const loading = attention === undefined;\n const shared: { key: AdminAttentionTile; label: string; description?: ReactNode }[] = [\n { key: \"feedback_waiting\", label: labels.tiles.feedbackWaiting },\n { key: \"deletion_requests\", label: labels.tiles.deletionRequests },\n { key: \"invitations_pending\", label: labels.tiles.invitationsPending },\n {\n key: \"users_total\",\n label: labels.tiles.users,\n description:\n attention && typeof attention.users_new_7d === \"number\" ? labels.tiles.usersNew(attention.users_new_7d) : undefined,\n },\n ];\n const tiles = [\n ...shared.map((t) => ({ ...t, count: attention ? attention[t.key] : undefined, href: href[t.key] })),\n ...extraTiles,\n ].filter((t) => loading || (t.count !== null && t.count !== undefined));\n if (!loading && tiles.length === 0) return error ? errorBox(true) : null;\n const grid = (\n <StatTileGrid data-slot={error ? undefined : \"admin-overview\"} loading={loading} className={error ? undefined : className}>\n {tiles.map((t) => (\n <AttentionTile\n key={t.key}\n {...tileProps}\n data-attention={t.key}\n label={t.label}\n count={t.count}\n href={t.href}\n description={t.description}\n />\n ))}\n </StatTileGrid>\n );\n if (!error) return grid;\n // A failed poll: the error above the last good tiles.\n return (\n <div data-slot=\"admin-overview\" data-state=\"error\" className={cn(\"space-y-4\", className)}>\n {errorBox(false)}\n {grid}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/admin/admin-overview.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Button } from \"../components/button\";\nimport { StatTile, StatTileGrid } from \"../components/stat-tile\";\nimport type { StatTileProps } from \"../components/stat-tile\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { useAdminAreaLabels } from \"./admin-area-labels\";\nimport type { AdminAreaLabels } from \"./admin-area-labels\";\n\n/**\n * The admin's start page (0.42.0, docs/admin-harmonization.md §2.4, §6.1–§6.5): a grid\n * of tiles, each a count of what is waiting and a link to where the work is, fed by one\n * server answer.\n */\n\n/**\n * `GET /admin/attention` (§2.4 decision 14, §6.4) — server-kit's `AdminAttention`\n * (`eifi1_server_kit.user_admin.attention`), as the API sends it.\n *\n * Each count is `null` where the app has no such thing or this viewer cannot act on it,\n * and every count is `null` in a demo session. Demo accounts never count. `extra` holds\n * the app's own counts, keyed as its overview names them (keksdose `support_waiting`,\n * Kurvenschmiede `released_for_work`).\n */\nexport interface AdminAttention {\n /** \"To triage\": OPEN reports, crash reports included (§6.1). */\n feedback_waiting: number | null;\n /** Accounts that asked to be deleted — only where this viewer acts on them (§6.2). */\n deletion_requests: number | null;\n invitations_pending: number | null;\n users_total: number | null;\n /** Accounts made in the last seven days. */\n users_new_7d: number | null;\n extra: Record<string, number | null>;\n}\n\n/** The shared counts that are a tile of their own (`users_new_7d` is the users tile's line). */\nexport type AdminAttentionTile = \"feedback_waiting\" | \"deletion_requests\" | \"invitations_pending\" | \"users_total\";\n\n/** Where each shared tile links when the app names nothing else (§2.4 decision 13). */\nexport const DEFAULT_ADMIN_ATTENTION_HREFS: Readonly<Record<AdminAttentionTile, string>> = Object.freeze({\n feedback_waiting: \"/feedback\",\n deletion_requests: \"/admin/users?state=deletion_requested\",\n invitations_pending: \"/admin/invitations\",\n users_total: \"/admin/users\",\n});\n\n/**\n * The account menu's badge (§2.4 decision 15, §6.3): the sum of the counts that are work\n * waiting on THIS admin — each app names its own (keksdose `[\"feedback_waiting\",\n * \"support_waiting\", \"awaiting_verdict\"]`, kastlan `[\"feedback_waiting\"]`, Kurvenschmiede\n * `[\"feedback_waiting\", \"deletion_requests\"]`). A name is a shared count or an `extra`\n * key; a `null` or missing count adds nothing. Invitations and users never belong in it.\n * `null` while there is no answer yet, so the badge shows nothing rather than a 0.\n */\nexport function adminWaitingCount(\n attention: AdminAttention | null | undefined,\n counts: readonly (AdminAttentionTile | (string & {}))[],\n): number | null {\n if (!attention) return null;\n let sum = 0;\n for (const key of counts) {\n const shared = (attention as unknown as Record<string, unknown>)[key];\n const value = typeof shared === \"number\" ? shared : attention.extra?.[key];\n if (typeof value === \"number\" && value > 0) sum += value;\n }\n return sum;\n}\n\nexport interface AttentionTileProps extends Omit<StatTileProps, \"href\" | \"onClick\" | \"value\"> {\n /** The count. `null` shows the tile's dash; {@link AdminOverview} leaves such a tile out. */\n count: number | null | undefined;\n /** Where the work is — the tile is a link to it. Since 0.43 it may be left out: a\n * quiet count, no link (Kurvenschmiede). */\n href?: string;\n}\n\n/**\n * One tile of the overview: a {@link StatTile} that is a LINK to where the work is, its\n * count the headline. The whole tile is the link, named by its label; route it through\n * `renderLink` or the provider's link component, as any StatTile. Without `href` (0.43) it\n * is a quiet count, no link.\n */\nexport function AttentionTile({ count, href, ...tile }: AttentionTileProps) {\n return <StatTile data-slot=\"attention-tile\" {...tile} value={count ?? null} href={href} />;\n}\n\n/**\n * 0.46 (Kurvenschmiede 1, admin §9.5): the `extra` keys server-kit's\n * `feedback_attention` fills, whose tiles have the kit's words\n * (`adminArea.tiles.awaitingVerdict`, `releasedForWork`, with a line each).\n */\nexport type AdminFeedbackTileKey = \"awaiting_verdict\" | \"released_for_work\";\n\n/** One of the app's own tiles (§2.4 decision 13: keksdose's \"Support waiting\"). */\nexport type AdminExtraTile = AdminOwnTile | AdminFeedbackTile;\n\n/** A tile of the app's own: it brings its words. */\nexport interface AdminOwnTile {\n /** Unique among the tiles; usually the `extra` key it shows. */\n key: string;\n label: ReactNode;\n /** `null` or `undefined` leaves the tile out, as for a shared one. */\n count: number | null | undefined;\n /** Where the work is. Left out (0.43, Kurvenschmiede): a quiet count with no link —\n * a number worth seeing that has no page of its own. */\n href?: string;\n /** A line under the count. */\n description?: ReactNode;\n}\n\n/** 0.46: a tile of server-kit's feedback counts — `label` and `description` default to\n * the kit's words; an app's own still win. The kit's `description` fills in only when\n * `label` is left out too: an app that names the tile gets no hint line it did not ask for. */\nexport interface AdminFeedbackTile extends Omit<AdminOwnTile, \"key\" | \"label\"> {\n key: AdminFeedbackTileKey;\n label?: ReactNode;\n}\n\nexport interface AdminOverviewProps {\n /** The server's answer; `undefined` while it loads (the grid shows its skeletons). */\n attention: AdminAttention | null | undefined;\n /**\n * Where the shared tiles link, over {@link DEFAULT_ADMIN_ATTENTION_HREFS} — the\n * inbox's address filtered to OPEN (§6.1) is the app's own:\n * `{ feedback_waiting: \"/feedback?status=OPEN\" }`.\n */\n hrefs?: Partial<Record<AdminAttentionTile, string>>;\n /** The app's own tiles, after the shared ones, in this order. */\n extraTiles?: readonly AdminExtraTile[];\n /** Passed to every tile — `renderLink` for the router, `size`, `variant`. */\n tileProps?: Pick<StatTileProps, \"renderLink\" | \"size\" | \"variant\" | \"locale\">;\n /**\n * 0.43 (keksdose): the answer failed — the query's `error`, or `true`. Anything truthy\n * shows `adminArea.overviewError` in a danger box, with \"Try again\" when\n * {@link onRetry} is given. A failed poll keeps the last good tiles: with `attention`\n * still there (a query keeps its last data), the box shows ALONGSIDE them, above, not\n * instead (build review 0.43, keksdose N7) — pass `attention.data` as it is. Without\n * an answer yet, the box alone. Without `error`: `undefined` is the skeletons and\n * `null` nothing, as before.\n */\n error?: unknown;\n /** \"Try again\" in the error box — the query's `refetch`. */\n onRetry?: () => void;\n /** Prop > `<UiKitProvider labels={{ adminArea }}>` > English. */\n labels?: LabelOverride<AdminAreaLabels>;\n className?: string;\n}\n\n/**\n * The overview section (§2.4): the shared tiles in their order — To triage, Deletion\n * requests, Pending invitations, Users (with the new accounts of the last 7 days under\n * the total) — then the app's `extraTiles`. A tile whose count is `null` is left out,\n * so each app shows only what it has; a `0` is a quiet tile, \"nothing waiting\".\n *\n * The overview is the admin area's landing page, standalone: it is NOT wrapped in a\n * `SettingsSection` (`adminSections` gives it no entry; render it as the page's body, as\n * Kurvenschmiede's `OverviewPanel` does). keksdose links its metrics from here with at\n * most two tiles (§6.5).\n */\nexport function AdminOverview({\n attention,\n hrefs,\n extraTiles = [],\n tileProps,\n error,\n onRetry,\n labels: labelsProp,\n className,\n}: AdminOverviewProps) {\n const labels = useAdminAreaLabels(labelsProp);\n /** The error box: on its own (`alone`) it is the overview's root. */\n const errorBox = (alone: boolean) => (\n <AlertBanner\n data-slot={alone ? \"admin-overview\" : undefined}\n data-state={alone ? \"error\" : undefined}\n variant=\"box\"\n tone=\"danger\"\n className={alone ? className : undefined}\n action={\n onRetry ? (\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" onClick={() => onRetry()}>\n {labels.retry}\n </Button>\n ) : undefined\n }\n >\n {labels.overviewError}\n </AlertBanner>\n );\n // Nothing good to keep: the box alone.\n if (error && !attention) return errorBox(true);\n const href = { ...DEFAULT_ADMIN_ATTENTION_HREFS, ...hrefs };\n const loading = attention === undefined;\n const shared: { key: AdminAttentionTile; label: string; description?: ReactNode }[] = [\n { key: \"feedback_waiting\", label: labels.tiles.feedbackWaiting },\n { key: \"deletion_requests\", label: labels.tiles.deletionRequests },\n { key: \"invitations_pending\", label: labels.tiles.invitationsPending },\n {\n key: \"users_total\",\n label: labels.tiles.users,\n description:\n attention && typeof attention.users_new_7d === \"number\" ? labels.tiles.usersNew(attention.users_new_7d) : undefined,\n },\n ];\n const feedbackWords: Record<AdminFeedbackTileKey, { label: string; description: string }> = {\n awaiting_verdict: { label: labels.tiles.awaitingVerdict, description: labels.tiles.awaitingVerdictHint },\n released_for_work: { label: labels.tiles.releasedForWork, description: labels.tiles.releasedForWorkHint },\n };\n const tiles = [\n ...shared.map((t) => ({ ...t, count: attention ? attention[t.key] : undefined, href: href[t.key] })),\n ...extraTiles.map((t) => {\n const words = Object.prototype.hasOwnProperty.call(feedbackWords, t.key)\n ? feedbackWords[t.key as AdminFeedbackTileKey]\n : undefined;\n // The kit's hint comes with the kit's label only: an app that names the tile\n // itself gets no line it did not ask for (keksdose S5, round 0.46).\n return words\n ? {\n ...t,\n label: t.label ?? words.label,\n description: t.description ?? (t.label === undefined ? words.description : undefined),\n }\n : t;\n }),\n ].filter((t) => loading || (t.count !== null && t.count !== undefined));\n if (!loading && tiles.length === 0) return error ? errorBox(true) : null;\n const grid = (\n <StatTileGrid data-slot={error ? undefined : \"admin-overview\"} loading={loading} className={error ? undefined : className}>\n {tiles.map((t) => (\n <AttentionTile\n key={t.key}\n {...tileProps}\n data-attention={t.key}\n label={t.label}\n count={t.count}\n href={t.href}\n description={t.description}\n />\n ))}\n </StatTileGrid>\n );\n if (!error) return grid;\n // A failed poll: the error above the last good tiles.\n return (\n <div data-slot=\"admin-overview\" data-state=\"error\" className={cn(\"space-y-4\", className)}>\n {errorBox(false)}\n {grid}\n </div>\n );\n}\n"],"mappings":";AAqFS,cAiKL,YAjKK;AApFT,SAAS,mBAAmB;AAC5B,SAAS,cAAc;AACvB,SAAS,UAAU,oBAAoB;AAGvC,SAAS,UAAU;AACnB,SAAS,0BAA0B;AAkC5B,MAAM,gCAA8E,OAAO,OAAO;AAAA,EACvG,kBAAkB;AAAA,EAClB,mBAAmB;AAAA,EACnB,qBAAqB;AAAA,EACrB,aAAa;AACf,CAAC;AAUM,SAAS,kBACd,WACA,QACe;AACf,MAAI,CAAC,UAAW,QAAO;AACvB,MAAI,MAAM;AACV,aAAW,OAAO,QAAQ;AACxB,UAAM,SAAU,UAAiD,GAAG;AACpE,UAAM,QAAQ,OAAO,WAAW,WAAW,SAAS,UAAU,QAAQ,GAAG;AACzE,QAAI,OAAO,UAAU,YAAY,QAAQ,EAAG,QAAO;AAAA,EACrD;AACA,SAAO;AACT;AAgBO,SAAS,cAAc,EAAE,OAAO,MAAM,GAAG,KAAK,GAAuB;AAC1E,SAAO,oBAAC,YAAS,aAAU,kBAAkB,GAAG,MAAM,OAAO,SAAS,MAAM,MAAY;AAC1F;AA2EO,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA,aAAa,CAAC;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AACF,GAAuB;AACrB,QAAM,SAAS,mBAAmB,UAAU;AAE5C,QAAM,WAAW,CAAC,UAChB;AAAA,IAAC;AAAA;AAAA,MACC,aAAW,QAAQ,mBAAmB;AAAA,MACtC,cAAY,QAAQ,UAAU;AAAA,MAC9B,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,WAAW,QAAQ,YAAY;AAAA,MAC/B,QACE,UACE,oBAAC,UAAO,MAAK,UAAS,SAAQ,aAAY,MAAK,MAAK,SAAS,MAAM,QAAQ,GACxE,iBAAO,OACV,IACE;AAAA,MAGL,iBAAO;AAAA;AAAA,EACV;AAGF,MAAI,SAAS,CAAC,UAAW,QAAO,SAAS,IAAI;AAC7C,QAAM,OAAO,EAAE,GAAG,+BAA+B,GAAG,MAAM;AAC1D,QAAM,UAAU,cAAc;AAC9B,QAAM,SAAgF;AAAA,IACpF,EAAE,KAAK,oBAAoB,OAAO,OAAO,MAAM,gBAAgB;AAAA,IAC/D,EAAE,KAAK,qBAAqB,OAAO,OAAO,MAAM,iBAAiB;AAAA,IACjE,EAAE,KAAK,uBAAuB,OAAO,OAAO,MAAM,mBAAmB;AAAA,IACrE;AAAA,MACE,KAAK;AAAA,MACL,OAAO,OAAO,MAAM;AAAA,MACpB,aACE,aAAa,OAAO,UAAU,iBAAiB,WAAW,OAAO,MAAM,SAAS,UAAU,YAAY,IAAI;AAAA,IAC9G;AAAA,EACF;AACA,QAAM,gBAAsF;AAAA,IAC1F,kBAAkB,EAAE,OAAO,OAAO,MAAM,iBAAiB,aAAa,OAAO,MAAM,oBAAoB;AAAA,IACvG,mBAAmB,EAAE,OAAO,OAAO,MAAM,iBAAiB,aAAa,OAAO,MAAM,oBAAoB;AAAA,EAC1G;AACA,QAAM,QAAQ;AAAA,IACZ,GAAG,OAAO,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,YAAY,UAAU,EAAE,GAAG,IAAI,QAAW,MAAM,KAAK,EAAE,GAAG,EAAE,EAAE;AAAA,IACnG,GAAG,WAAW,IAAI,CAAC,MAAM;AACvB,YAAM,QAAQ,OAAO,UAAU,eAAe,KAAK,eAAe,EAAE,GAAG,IACnE,cAAc,EAAE,GAA2B,IAC3C;AAGJ,aAAO,QACH;AAAA,QACE,GAAG;AAAA,QACH,OAAO,EAAE,SAAS,MAAM;AAAA,QACxB,aAAa,EAAE,gBAAgB,EAAE,UAAU,SAAY,MAAM,cAAc;AAAA,MAC7E,IACA;AAAA,IACN,CAAC;AAAA,EACH,EAAE,OAAO,CAAC,MAAM,WAAY,EAAE,UAAU,QAAQ,EAAE,UAAU,MAAU;AACtE,MAAI,CAAC,WAAW,MAAM,WAAW,EAAG,QAAO,QAAQ,SAAS,IAAI,IAAI;AACpE,QAAM,OACJ,oBAAC,gBAAa,aAAW,QAAQ,SAAY,kBAAkB,SAAkB,WAAW,QAAQ,SAAY,WAC7G,gBAAM,IAAI,CAAC,MACV;AAAA,IAAC;AAAA;AAAA,MAEE,GAAG;AAAA,MACJ,kBAAgB,EAAE;AAAA,MAClB,OAAO,EAAE;AAAA,MACT,OAAO,EAAE;AAAA,MACT,MAAM,EAAE;AAAA,MACR,aAAa,EAAE;AAAA;AAAA,IANV,EAAE;AAAA,EAOT,CACD,GACH;AAEF,MAAI,CAAC,MAAO,QAAO;AAEnB,SACE,qBAAC,SAAI,aAAU,kBAAiB,cAAW,SAAQ,WAAW,GAAG,aAAa,SAAS,GACpF;AAAA,aAAS,KAAK;AAAA,IACd;AAAA,KACH;AAEJ;","names":[]}
@@ -0,0 +1,3 @@
1
+ import type { MailTestCardLabels } from "./mail-test-card.js";
2
+ /** docs/admin-harmonization.md §9.2.4, word for word. */
3
+ export declare const DEFAULT_MAIL_TEST_CARD_LABELS: MailTestCardLabels;
@@ -0,0 +1,17 @@
1
+ "use client";
2
+ import { englishRateLimited } from "../auth/auth-errors.js";
3
+ const DEFAULT_MAIL_TEST_CARD_LABELS = {
4
+ title: "Test mail",
5
+ description: "Sends one mail to your own address, through the transport this installation sends all its mail with.",
6
+ transport: (backend) => `Transport: ${backend}`,
7
+ action: "Send a test mail to me",
8
+ sent: (address) => `Sent to ${address}. Check the inbox.`,
9
+ console: "The console transport only logs mail; nothing was sent.",
10
+ failed: "Could not send. The server log has the provider\u2019s answer.",
11
+ rateLimited: englishRateLimited,
12
+ keywords: "test mail email e-mail transport smtp resend console send deliver"
13
+ };
14
+ export {
15
+ DEFAULT_MAIL_TEST_CARD_LABELS
16
+ };
17
+ //# sourceMappingURL=mail-test-card-labels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/admin/mail-test-card-labels.ts"],"sourcesContent":["// The English words of ./mail-test-card, in a module with no component code: the English\n// catalogue (src/i18n/defaults.ts) imports them without the component, so `en` loads as one\n// chunk like the other languages (0.34, KS U8). ./mail-test-card re-exports them.\n\nimport { englishRateLimited } from \"../auth/auth-errors\";\nimport type { MailTestCardLabels } from \"./mail-test-card\";\n\n/** docs/admin-harmonization.md §9.2.4, word for word. */\nexport const DEFAULT_MAIL_TEST_CARD_LABELS: MailTestCardLabels = {\n title: \"Test mail\",\n description: \"Sends one mail to your own address, through the transport this installation sends all its mail with.\",\n transport: (backend) => `Transport: ${backend}`,\n action: \"Send a test mail to me\",\n sent: (address) => `Sent to ${address}. Check the inbox.`,\n console: \"The console transport only logs mail; nothing was sent.\",\n failed: \"Could not send. The server log has the provider’s answer.\",\n rateLimited: englishRateLimited,\n keywords: \"test mail email e-mail transport smtp resend console send deliver\",\n};\n"],"mappings":";AAIA,SAAS,0BAA0B;AAI5B,MAAM,gCAAoD;AAAA,EAC/D,OAAO;AAAA,EACP,aAAa;AAAA,EACb,WAAW,CAAC,YAAY,cAAc,OAAO;AAAA,EAC7C,QAAQ;AAAA,EACR,MAAM,CAAC,YAAY,WAAW,OAAO;AAAA,EACrC,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,UAAU;AACZ;","names":[]}
@@ -0,0 +1,75 @@
1
+ import type { ReactNode } from "react";
2
+ import { type LabelOverride } from "../i18n/kit-labels.js";
3
+ import { type SettingsSectionProps } from "../settings/settings-section.js";
4
+ import { DEFAULT_MAIL_TEST_CARD_LABELS } from "./mail-test-card-labels.js";
5
+ /** Every string {@link MailTestCard} renders — the `mailTestCard` namespace of
6
+ * `<UiKitProvider labels>`, overridable per instance through `labels`
7
+ * (docs/admin-harmonization.md §9.2.4). */
8
+ export interface MailTestCardLabels {
9
+ /** The card's title, and the app's catalogue entry's. */
10
+ title: string;
11
+ /** The sentence under it. */
12
+ description: string;
13
+ /** The transport's chip: "Transport: resend". `backend` is the server's `mail_backend`. */
14
+ transport: (backend: string) => string;
15
+ /** The button. */
16
+ action: string;
17
+ /** The answer said "sent" through a real transport. `address` is masked ("a…@example.com"). */
18
+ sent: (address: string) => string;
19
+ /** `mail_backend` is `console`: said whatever `sent` is — a console answers `true`. */
20
+ console: string;
21
+ /** Not sent, or the request failed. The provider's error stays in the server log. */
22
+ failed: string;
23
+ /** HTTP 429 — the app's limiter (server-kit's `MAIL_TEST_LIMIT`, 3 per 10 minutes);
24
+ * `seconds` from `Retry-After` when the response has one. */
25
+ rateLimited: (seconds?: number) => string;
26
+ /** For the app's catalogue entry (`keywords`); the card never reads it. */
27
+ keywords: string;
28
+ }
29
+ export { DEFAULT_MAIL_TEST_CARD_LABELS };
30
+ /** The answer of `POST /admin/mail/test` — server-kit 0.15's `MailTestResult`. */
31
+ export interface MailTestAnswer {
32
+ sent: boolean;
33
+ /** The transport that took it: `console`, `resend`, `smtp`, … */
34
+ mail_backend: string;
35
+ }
36
+ /** The card's default DOM id: its catalogue entry's `anchor`, the `?focus=` target. */
37
+ export declare const MAIL_TEST_ANCHOR = "admin-mail-test";
38
+ export interface MailTestCardProps extends Omit<SettingsSectionProps, "anchor" | "title" | "description" | "children"> {
39
+ /** The installation's transport, as the admin page has it (`mail_backend`): the chip
40
+ * before a send. After one, the answer's. Optional (0.46): without it the chip shows
41
+ * from the first answer on, and an app needs no query only for the chip. */
42
+ mailBackend?: string;
43
+ /** The app's `POST /admin/mail/test` (kastlan `/platform/mail/test`), no body: it
44
+ * awaits the app's own send and answers `{sent, mail_backend}`. A rejection is read
45
+ * for a 429 (the limiter); anything else is "could not send". */
46
+ send: () => Promise<MailTestAnswer>;
47
+ /** The signed-in admin's own address — where the mail goes. Shown masked. */
48
+ address: string;
49
+ /** Default {@link MAIL_TEST_ANCHOR}; the app's catalogue entry names the same. */
50
+ anchor?: string;
51
+ /** The app's sentence after the kit's description. */
52
+ description?: ReactNode;
53
+ labels?: LabelOverride<MailTestCardLabels>;
54
+ }
55
+ /**
56
+ * An address with its local part masked to its first character: "ada@example.com" →
57
+ * "a…@example.com". The domain stays, so the admin sees which mailbox to open.
58
+ */
59
+ export declare function maskMailAddress(address: string): string;
60
+ /**
61
+ * "Send a test mail to me" (docs/admin-harmonization.md §9.2): a `SettingsSection` with a
62
+ * sentence, the transport as a chip, the button and the result line. One mail goes to the
63
+ * signed-in admin's own address, never another, through the app's own send.
64
+ *
65
+ * **The result line reads `mail_backend` first**: with `console` it says "nothing was sent"
66
+ * whatever `sent` is, since every console path answers `true` (§9.2 Today). Otherwise
67
+ * `sent` says "Sent to a…@example.com. Check the inbox." or "Could not send." — the
68
+ * provider's error stays in the server log (§9.2.7). A 429 says the limiter's line. The
69
+ * button is busy while the request runs; the line is a live region.
70
+ *
71
+ * Not a commit: the test writes nothing, so it is not under the write lock (a demo never
72
+ * reaches the admin area, and the server refuses it anyway). The app adds the catalogue
73
+ * entry `{ id, anchor: MAIL_TEST_ANCHOR, title, keywords }` from these labels.
74
+ */
75
+ export declare function MailTestCard({ mailBackend, send, address, anchor, description, labels: labelsProp, ...rest }: MailTestCardProps): import("react").JSX.Element;
@@ -0,0 +1,82 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { Send } from "lucide-react";
5
+ import { useKitLabels } from "../i18n/kit-labels.js";
6
+ import { isRateLimited, retryAfterSeconds } from "../auth/auth-errors.js";
7
+ import { AlertBanner } from "../components/alert-banner.js";
8
+ import { Button } from "../components/button.js";
9
+ import { Chip } from "../components/chip.js";
10
+ import { SettingsSection } from "../settings/settings-section.js";
11
+ import { DEFAULT_MAIL_TEST_CARD_LABELS } from "./mail-test-card-labels.js";
12
+ const MAIL_TEST_ANCHOR = "admin-mail-test";
13
+ function maskMailAddress(address) {
14
+ const text = address.trim();
15
+ const at = text.lastIndexOf("@");
16
+ if (at <= 0) return text === "" ? "" : `${Array.from(text)[0]}\u2026`;
17
+ return `${Array.from(text.slice(0, at))[0]}\u2026${text.slice(at)}`;
18
+ }
19
+ function MailTestCard({
20
+ mailBackend,
21
+ send,
22
+ address,
23
+ anchor = MAIL_TEST_ANCHOR,
24
+ description,
25
+ labels: labelsProp,
26
+ ...rest
27
+ }) {
28
+ const labels = useKitLabels("mailTestCard", DEFAULT_MAIL_TEST_CARD_LABELS, labelsProp);
29
+ const [busy, setBusy] = useState(false);
30
+ const [outcome, setOutcome] = useState(null);
31
+ const [answered, setAnswered] = useState(void 0);
32
+ const run = async () => {
33
+ if (busy) return;
34
+ setBusy(true);
35
+ setOutcome(null);
36
+ try {
37
+ const answer = await send();
38
+ const backend2 = answer.mail_backend;
39
+ if (backend2) setAnswered(backend2);
40
+ if (backend2 === "console") setOutcome({ kind: "console", backend: backend2 });
41
+ else setOutcome(answer.sent ? { kind: "sent", backend: backend2 } : { kind: "failed", backend: backend2 });
42
+ } catch (error) {
43
+ setOutcome(isRateLimited(error) ? { kind: "limited", seconds: retryAfterSeconds(error) } : { kind: "failed" });
44
+ } finally {
45
+ setBusy(false);
46
+ }
47
+ };
48
+ const backend = answered ?? mailBackend;
49
+ return /* @__PURE__ */ jsx(
50
+ SettingsSection,
51
+ {
52
+ ...rest,
53
+ anchor,
54
+ title: labels.title,
55
+ description: description != null ? /* @__PURE__ */ jsxs(Fragment, { children: [
56
+ labels.description,
57
+ " ",
58
+ description
59
+ ] }) : labels.description,
60
+ children: /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
61
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
62
+ /* @__PURE__ */ jsxs(Button, { variant: "secondary", pending: busy, onClick: () => void run(), children: [
63
+ /* @__PURE__ */ jsx(Send, { className: "size-4", "aria-hidden": true }),
64
+ labels.action
65
+ ] }),
66
+ backend && /* @__PURE__ */ jsx(Chip, { size: "sm", "data-slot": "mail-backend", children: labels.transport(backend) })
67
+ ] }),
68
+ outcome?.kind === "sent" && /* @__PURE__ */ jsx(AlertBanner, { variant: "inline", block: true, tone: "success", children: /* @__PURE__ */ jsx("span", { "data-private": true, children: labels.sent(maskMailAddress(address)) }) }),
69
+ outcome?.kind === "console" && /* @__PURE__ */ jsx(AlertBanner, { variant: "inline", block: true, tone: "info", children: labels.console }),
70
+ outcome?.kind === "failed" && /* @__PURE__ */ jsx(AlertBanner, { variant: "inline", block: true, tone: "danger", children: labels.failed }),
71
+ outcome?.kind === "limited" && /* @__PURE__ */ jsx(AlertBanner, { variant: "inline", block: true, tone: "warning", children: labels.rateLimited(outcome.seconds) })
72
+ ] })
73
+ }
74
+ );
75
+ }
76
+ export {
77
+ DEFAULT_MAIL_TEST_CARD_LABELS,
78
+ MAIL_TEST_ANCHOR,
79
+ MailTestCard,
80
+ maskMailAddress
81
+ };
82
+ //# sourceMappingURL=mail-test-card.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/admin/mail-test-card.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Send } from \"lucide-react\";\n\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { isRateLimited, retryAfterSeconds } from \"../auth/auth-errors\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Button } from \"../components/button\";\nimport { Chip } from \"../components/chip\";\nimport { SettingsSection, type SettingsSectionProps } from \"../settings/settings-section\";\nimport { DEFAULT_MAIL_TEST_CARD_LABELS } from \"./mail-test-card-labels\";\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string {@link MailTestCard} renders — the `mailTestCard` namespace of\n * `<UiKitProvider labels>`, overridable per instance through `labels`\n * (docs/admin-harmonization.md §9.2.4). */\nexport interface MailTestCardLabels {\n /** The card's title, and the app's catalogue entry's. */\n title: string;\n /** The sentence under it. */\n description: string;\n /** The transport's chip: \"Transport: resend\". `backend` is the server's `mail_backend`. */\n transport: (backend: string) => string;\n /** The button. */\n action: string;\n /** The answer said \"sent\" through a real transport. `address` is masked (\"a…@example.com\"). */\n sent: (address: string) => string;\n /** `mail_backend` is `console`: said whatever `sent` is — a console answers `true`. */\n console: string;\n /** Not sent, or the request failed. The provider's error stays in the server log. */\n failed: string;\n /** HTTP 429 — the app's limiter (server-kit's `MAIL_TEST_LIMIT`, 3 per 10 minutes);\n * `seconds` from `Retry-After` when the response has one. */\n rateLimited: (seconds?: number) => string;\n /** For the app's catalogue entry (`keywords`); the card never reads it. */\n keywords: string;\n}\n\n// Defined in ./mail-test-card-labels, which holds no component code.\nexport { DEFAULT_MAIL_TEST_CARD_LABELS };\n\n/* ── The card ────────────────────────────────────────────────────────────── */\n\n/** The answer of `POST /admin/mail/test` — server-kit 0.15's `MailTestResult`. */\nexport interface MailTestAnswer {\n sent: boolean;\n /** The transport that took it: `console`, `resend`, `smtp`, … */\n mail_backend: string;\n}\n\n/** The card's default DOM id: its catalogue entry's `anchor`, the `?focus=` target. */\nexport const MAIL_TEST_ANCHOR = \"admin-mail-test\";\n\nexport interface MailTestCardProps\n extends Omit<SettingsSectionProps, \"anchor\" | \"title\" | \"description\" | \"children\"> {\n /** The installation's transport, as the admin page has it (`mail_backend`): the chip\n * before a send. After one, the answer's. Optional (0.46): without it the chip shows\n * from the first answer on, and an app needs no query only for the chip. */\n mailBackend?: string;\n /** The app's `POST /admin/mail/test` (kastlan `/platform/mail/test`), no body: it\n * awaits the app's own send and answers `{sent, mail_backend}`. A rejection is read\n * for a 429 (the limiter); anything else is \"could not send\". */\n send: () => Promise<MailTestAnswer>;\n /** The signed-in admin's own address — where the mail goes. Shown masked. */\n address: string;\n /** Default {@link MAIL_TEST_ANCHOR}; the app's catalogue entry names the same. */\n anchor?: string;\n /** The app's sentence after the kit's description. */\n description?: ReactNode;\n labels?: LabelOverride<MailTestCardLabels>;\n}\n\n/**\n * An address with its local part masked to its first character: \"ada@example.com\" →\n * \"a…@example.com\". The domain stays, so the admin sees which mailbox to open.\n */\nexport function maskMailAddress(address: string): string {\n const text = address.trim();\n const at = text.lastIndexOf(\"@\");\n if (at <= 0) return text === \"\" ? \"\" : `${Array.from(text)[0]}…`;\n return `${Array.from(text.slice(0, at))[0]}…${text.slice(at)}`;\n}\n\ntype Outcome =\n | { kind: \"sent\"; backend: string }\n | { kind: \"console\"; backend: string }\n | { kind: \"failed\"; backend?: string }\n | { kind: \"limited\"; seconds?: number };\n\n/**\n * \"Send a test mail to me\" (docs/admin-harmonization.md §9.2): a `SettingsSection` with a\n * sentence, the transport as a chip, the button and the result line. One mail goes to the\n * signed-in admin's own address, never another, through the app's own send.\n *\n * **The result line reads `mail_backend` first**: with `console` it says \"nothing was sent\"\n * whatever `sent` is, since every console path answers `true` (§9.2 Today). Otherwise\n * `sent` says \"Sent to a…@example.com. Check the inbox.\" or \"Could not send.\" — the\n * provider's error stays in the server log (§9.2.7). A 429 says the limiter's line. The\n * button is busy while the request runs; the line is a live region.\n *\n * Not a commit: the test writes nothing, so it is not under the write lock (a demo never\n * reaches the admin area, and the server refuses it anyway). The app adds the catalogue\n * entry `{ id, anchor: MAIL_TEST_ANCHOR, title, keywords }` from these labels.\n */\nexport function MailTestCard({\n mailBackend,\n send,\n address,\n anchor = MAIL_TEST_ANCHOR,\n description,\n labels: labelsProp,\n ...rest\n}: MailTestCardProps) {\n const labels = useKitLabels(\"mailTestCard\", DEFAULT_MAIL_TEST_CARD_LABELS, labelsProp);\n const [busy, setBusy] = useState(false);\n const [outcome, setOutcome] = useState<Outcome | null>(null);\n // The last transport an answer named: a later send with no answer (429, network) keeps it.\n const [answered, setAnswered] = useState<string | undefined>(undefined);\n\n const run = async () => {\n if (busy) return;\n setBusy(true);\n setOutcome(null);\n try {\n const answer = await send();\n const backend = answer.mail_backend;\n if (backend) setAnswered(backend);\n if (backend === \"console\") setOutcome({ kind: \"console\", backend });\n else setOutcome(answer.sent ? { kind: \"sent\", backend } : { kind: \"failed\", backend });\n } catch (error) {\n setOutcome(isRateLimited(error) ? { kind: \"limited\", seconds: retryAfterSeconds(error) } : { kind: \"failed\" });\n } finally {\n setBusy(false);\n }\n };\n\n const backend = answered ?? mailBackend;\n\n return (\n <SettingsSection\n {...rest}\n anchor={anchor}\n title={labels.title}\n description={\n description != null ? (\n <>\n {labels.description} {description}\n </>\n ) : (\n labels.description\n )\n }\n >\n <div className=\"space-y-2\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button variant=\"secondary\" pending={busy} onClick={() => void run()}>\n <Send className=\"size-4\" aria-hidden />\n {labels.action}\n </Button>\n {backend && (\n <Chip size=\"sm\" data-slot=\"mail-backend\">\n {labels.transport(backend)}\n </Chip>\n )}\n </div>\n {outcome?.kind === \"sent\" && (\n <AlertBanner variant=\"inline\" block tone=\"success\">\n <span data-private>{labels.sent(maskMailAddress(address))}</span>\n </AlertBanner>\n )}\n {outcome?.kind === \"console\" && (\n <AlertBanner variant=\"inline\" block tone=\"info\">\n {labels.console}\n </AlertBanner>\n )}\n {outcome?.kind === \"failed\" && (\n <AlertBanner variant=\"inline\" block tone=\"danger\">\n {labels.failed}\n </AlertBanner>\n )}\n {outcome?.kind === \"limited\" && (\n <AlertBanner variant=\"inline\" block tone=\"warning\">\n {labels.rateLimited(outcome.seconds)}\n </AlertBanner>\n )}\n </div>\n </SettingsSection>\n );\n}\n"],"mappings":"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","names":["backend"]}
@@ -27,6 +27,16 @@ export interface AppearanceLabels {
27
27
  * toast or the line under the setting (0.32.1, keksdose's 0.32 report).
28
28
  */
29
29
  saveFailed: string;
30
+ /** The date-format setting's name (0.46, `DateFormatSetting`). */
31
+ dateFormat: string;
32
+ /** One line under it: the format is its own choice, apart from the language. */
33
+ dateFormatHelp: string;
34
+ /** Its first option, followed by that language's sample: "Follow language — 01 Aug 2026". */
35
+ dateFormatAuto: string;
36
+ /** `AccountMenuTextSize`'s A− button (0.46, keksdose live #426): one step smaller. */
37
+ textSmaller: string;
38
+ /** Its A+ button: one step larger. */
39
+ textLarger: string;
30
40
  }
31
41
  export declare const DEFAULT_APPEARANCE_LABELS: AppearanceLabels;
32
42
  /** The `appearance` namespace, resolved — English, then the provider, then `labels`. For
@@ -7,7 +7,12 @@ const DEFAULT_APPEARANCE_LABELS = {
7
7
  contrast: "Contrast",
8
8
  contrastHelp: "More contrast darkens quiet text and lines, and thickens focus frames. System follows this device.",
9
9
  contrastModes: { system: "System", standard: "Standard", more: "More" },
10
- saveFailed: "Couldn't save this to your account. It still applies on this device."
10
+ saveFailed: "Couldn't save this to your account. It still applies on this device.",
11
+ dateFormat: "Date format",
12
+ dateFormatHelp: "How dates are written throughout the app. Independent of the interface language, so you can read the app in English and still see German dates.",
13
+ dateFormatAuto: "Follow language",
14
+ textSmaller: "Smaller text",
15
+ textLarger: "Larger text"
11
16
  };
12
17
  function useAppearanceLabels(labels) {
13
18
  return useKitLabels("appearance", DEFAULT_APPEARANCE_LABELS, labels);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/appearance-labels.ts"],"sourcesContent":["import { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport type { ContrastMode } from \"../theme/contrast\";\nimport type { TextSize } from \"../theme/text-size\";\n\n/**\n * The `appearance` namespace (0.32, docs/text-size-harmonization.md §6): the words of the\n * two new settings in the core `appearance` group — `TextSizeSetting` and\n * `ContrastSetting` — and of any quick switch an app builds from the same vocabulary (the\n * showcase's top bar). Like every kit label: prop > `<UiKitProvider labels={{ appearance\n * }}>` > English.\n */\nexport interface AppearanceLabels {\n /** The text-size setting's name. */\n textSize: string;\n /** One line under it: what changes, so \"Large\" is not mistaken for zoom. */\n textSizeHelp: string;\n /** The three steps, in {@link TEXT_SIZES} order. */\n textSizes: Record<TextSize, string>;\n /** The contrast setting's name. */\n contrast: string;\n /** One line under it. */\n contrastHelp: string;\n /** The three choices, in {@link CONTRAST_MODES} order. */\n contrastModes: Record<ContrastMode, string>;\n /**\n * The account could not keep the pick (`useAccountAppearance`'s `pickTextSize` /\n * `pickContrast` rejected): this device keeps it, the account doesn't. For the app's\n * toast or the line under the setting (0.32.1, keksdose's 0.32 report).\n */\n saveFailed: string;\n}\n\nexport const DEFAULT_APPEARANCE_LABELS: AppearanceLabels = {\n textSize: \"Text size\",\n textSizeHelp: \"Larger letters, and a layout that makes room for them.\",\n textSizes: { normal: \"Normal\", large: \"Large\", xlarge: \"Extra large\" },\n contrast: \"Contrast\",\n contrastHelp: \"More contrast darkens quiet text and lines, and thickens focus frames. System follows this device.\",\n contrastModes: { system: \"System\", standard: \"Standard\", more: \"More\" },\n saveFailed: \"Couldn't save this to your account. It still applies on this device.\",\n};\n\n/** The `appearance` namespace, resolved — English, then the provider, then `labels`. For\n * an app's catalogue entries and quick switches, without restating the defaults. */\nexport function useAppearanceLabels(labels?: LabelOverride<AppearanceLabels>): AppearanceLabels {\n return useKitLabels(\"appearance\", DEFAULT_APPEARANCE_LABELS, labels);\n}\n"],"mappings":";AAAA,SAAS,oBAAwC;AAgC1C,MAAM,4BAA8C;AAAA,EACzD,UAAU;AAAA,EACV,cAAc;AAAA,EACd,WAAW,EAAE,QAAQ,UAAU,OAAO,SAAS,QAAQ,cAAc;AAAA,EACrE,UAAU;AAAA,EACV,cAAc;AAAA,EACd,eAAe,EAAE,QAAQ,UAAU,UAAU,YAAY,MAAM,OAAO;AAAA,EACtE,YAAY;AACd;AAIO,SAAS,oBAAoB,QAA4D;AAC9F,SAAO,aAAa,cAAc,2BAA2B,MAAM;AACrE;","names":[]}
1
+ {"version":3,"sources":["../../src/components/appearance-labels.ts"],"sourcesContent":["import { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport type { ContrastMode } from \"../theme/contrast\";\nimport type { TextSize } from \"../theme/text-size\";\n\n/**\n * The `appearance` namespace (0.32, docs/text-size-harmonization.md §6): the words of the\n * two new settings in the core `appearance` group — `TextSizeSetting` and\n * `ContrastSetting` — and of any quick switch an app builds from the same vocabulary (the\n * showcase's top bar). Like every kit label: prop > `<UiKitProvider labels={{ appearance\n * }}>` > English.\n */\nexport interface AppearanceLabels {\n /** The text-size setting's name. */\n textSize: string;\n /** One line under it: what changes, so \"Large\" is not mistaken for zoom. */\n textSizeHelp: string;\n /** The three steps, in {@link TEXT_SIZES} order. */\n textSizes: Record<TextSize, string>;\n /** The contrast setting's name. */\n contrast: string;\n /** One line under it. */\n contrastHelp: string;\n /** The three choices, in {@link CONTRAST_MODES} order. */\n contrastModes: Record<ContrastMode, string>;\n /**\n * The account could not keep the pick (`useAccountAppearance`'s `pickTextSize` /\n * `pickContrast` rejected): this device keeps it, the account doesn't. For the app's\n * toast or the line under the setting (0.32.1, keksdose's 0.32 report).\n */\n saveFailed: string;\n /** The date-format setting's name (0.46, `DateFormatSetting`). */\n dateFormat: string;\n /** One line under it: the format is its own choice, apart from the language. */\n dateFormatHelp: string;\n /** Its first option, followed by that language's sample: \"Follow language — 01 Aug 2026\". */\n dateFormatAuto: string;\n /** `AccountMenuTextSize`'s A− button (0.46, keksdose live #426): one step smaller. */\n textSmaller: string;\n /** Its A+ button: one step larger. */\n textLarger: string;\n}\n\nexport const DEFAULT_APPEARANCE_LABELS: AppearanceLabels = {\n textSize: \"Text size\",\n textSizeHelp: \"Larger letters, and a layout that makes room for them.\",\n textSizes: { normal: \"Normal\", large: \"Large\", xlarge: \"Extra large\" },\n contrast: \"Contrast\",\n contrastHelp: \"More contrast darkens quiet text and lines, and thickens focus frames. System follows this device.\",\n contrastModes: { system: \"System\", standard: \"Standard\", more: \"More\" },\n saveFailed: \"Couldn't save this to your account. It still applies on this device.\",\n dateFormat: \"Date format\",\n dateFormatHelp:\n \"How dates are written throughout the app. Independent of the interface language, so you can read the app in English and still see German dates.\",\n dateFormatAuto: \"Follow language\",\n textSmaller: \"Smaller text\",\n textLarger: \"Larger text\",\n};\n\n/** The `appearance` namespace, resolved — English, then the provider, then `labels`. For\n * an app's catalogue entries and quick switches, without restating the defaults. */\nexport function useAppearanceLabels(labels?: LabelOverride<AppearanceLabels>): AppearanceLabels {\n return useKitLabels(\"appearance\", DEFAULT_APPEARANCE_LABELS, labels);\n}\n"],"mappings":";AAAA,SAAS,oBAAwC;AA0C1C,MAAM,4BAA8C;AAAA,EACzD,UAAU;AAAA,EACV,cAAc;AAAA,EACd,WAAW,EAAE,QAAQ,UAAU,OAAO,SAAS,QAAQ,cAAc;AAAA,EACrE,UAAU;AAAA,EACV,cAAc;AAAA,EACd,eAAe,EAAE,QAAQ,UAAU,UAAU,YAAY,MAAM,OAAO;AAAA,EACtE,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,gBACE;AAAA,EACF,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb,YAAY;AACd;AAIO,SAAS,oBAAoB,QAA4D;AAC9F,SAAO,aAAa,cAAc,2BAA2B,MAAM;AACrE;","names":[]}
@@ -4,7 +4,7 @@ import { useRef } from "react";
4
4
  import { X } from "lucide-react";
5
5
  import { cn } from "../lib/cn.js";
6
6
  import { Tooltip } from "./tooltip.js";
7
- import { ROW_ACTIONS_KEY } from "./data-table-row-actions.js";
7
+ import { isTableOwnColumn } from "./data-table-row-actions.js";
8
8
  import { columnLabel } from "./data-table-shared.js";
9
9
  function ColumnSettingsPanel({
10
10
  columns,
@@ -78,7 +78,7 @@ function ColumnSettingsPanel({
78
78
  }
79
79
  ),
80
80
  /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-0.5", children: columns.map((col) => {
81
- if (col.key === ROW_ACTIONS_KEY) return null;
81
+ if (isTableOwnColumn(col.key)) return null;
82
82
  const checked = !hiddenCols.has(col.key);
83
83
  const headerLabel = columnLabel(col);
84
84
  return /* @__PURE__ */ jsxs(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/data-table-column-settings.tsx"],"sourcesContent":["import { useRef } from \"react\";\nimport type { Dispatch, SetStateAction } from \"react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { Tooltip } from \"./tooltip\";\nimport type { DataTableLabels } from \"./data-table-labels\";\nimport { ROW_ACTIONS_KEY } from \"./data-table-row-actions\";\nimport { columnLabel } from \"./data-table-shared\";\nimport type { DataTableColumn } from \"./data-table\";\n\n// DataTable's column settings on a wide screen: the vertical \"Columns (n/m)\" rail and\n// the drawer it opens, with \"Auto-size\" and a show/hide box per column. Moved out of\n// data-table.tsx whole (refactor audit 2026-10, MOD-3 step 5).\n\n/** The \"Columns\" rail and its drawer (`columnSettings`). */\nexport function ColumnSettingsPanel<T>({\n columns,\n hiddenCols,\n setHiddenCols,\n showSettings,\n setShowSettings,\n columnsCountLabel,\n labels,\n autoSizeAll,\n}: {\n /** Every column, hidden ones included; the actions column is left out here. */\n columns: DataTableColumn<T>[];\n hiddenCols: Set<string>;\n setHiddenCols: Dispatch<SetStateAction<Set<string>>>;\n showSettings: boolean;\n setShowSettings: (open: boolean) => void;\n /** \"Columns (n/m)\", the rail's text and name. */\n columnsCountLabel: string;\n labels: DataTableLabels;\n autoSizeAll: () => void;\n}) {\n // The \"Columns\" rail button — where the drawer's X hands focus back (BUG-7a).\n const columnsRail = useRef<HTMLButtonElement | null>(null);\n return (\n <>\n <Tooltip label={columnsCountLabel} portal>\n <button\n ref={columnsRail}\n type=\"button\"\n onClick={() => setShowSettings(true)}\n aria-label={columnsCountLabel}\n aria-hidden={showSettings}\n tabIndex={showSettings ? -1 : 0}\n className={cn(\n \"group flex shrink-0 items-start justify-center overflow-hidden pt-3 transition-[width] duration-200 ease-out\",\n \"border-s border-subtle\",\n \"hover:bg-hover\",\n showSettings ? \"w-0 border-s-0\" : \"w-8 cursor-pointer\",\n )}\n >\n <span\n className=\"whitespace-nowrap text-xs font-medium tracking-wide text-muted group-hover:text-secondary\"\n style={{ writingMode: \"vertical-rl\", transform: \"rotate(180deg)\" }}\n >\n {columnsCountLabel}\n </span>\n </button>\n </Tooltip>\n <div\n // Closed, the drawer is only `w-0`: without `inert` its X, Auto-size and one\n // checkbox per column stayed tab stops and in the accessibility tree, and Tab\n // after the last cell walked into controls nobody could see (BUG-7a).\n inert={!showSettings || undefined}\n className={cn(\n \"shrink-0 overflow-hidden transition-[width] duration-200 ease-out\",\n showSettings ? \"w-56\" : \"w-0\",\n )}\n >\n <div className=\"flex w-56 flex-col border-s border-subtle p-2\">\n <div className=\"mb-2 flex items-center justify-between gap-1\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-muted\">\n {columnsCountLabel}\n </div>\n <Tooltip label={labels.close} portal>\n <button\n type=\"button\"\n onClick={() => {\n setShowSettings(false);\n // Back to the rail that opened it: the X is about to go inert, and\n // focus would fall to <body> with it (BUG-7a).\n columnsRail.current?.focus();\n }}\n aria-label={labels.close}\n className=\"rounded p-0.5 text-placeholder hover:bg-hover hover:text-secondary\"\n >\n <X className=\"size-4\" />\n </button>\n </Tooltip>\n </div>\n <button\n type=\"button\"\n onClick={autoSizeAll}\n className=\"mb-2 rounded border border-subtle px-2 py-1 text-xs font-medium text-secondary hover:bg-hover\"\n >\n {labels.autoSize}\n </button>\n <div className=\"flex flex-col gap-0.5\">\n {columns.map((col) => {\n if (col.key === ROW_ACTIONS_KEY) return null;\n const checked = !hiddenCols.has(col.key);\n const headerLabel = columnLabel(col);\n return (\n <label\n key={col.key}\n className=\"inline-flex cursor-pointer items-center gap-1.5 rounded px-1 py-0.5 text-xs hover:bg-hover\"\n >\n <input\n type=\"checkbox\"\n className=\"size-3.5 shrink-0\"\n checked={checked}\n onChange={(e) => {\n setHiddenCols((s) => {\n const next = new Set(s);\n if (e.target.checked) next.delete(col.key);\n else next.add(col.key);\n return next;\n });\n }}\n />\n {/* The panel is `w-56`: a long column name wraps from Large up rather\n than reading \"Accoun…\" beside its box (text size §10.17, round\n 0.34 T1). */}\n <span className=\"min-w-0 truncate-until-large\">{headerLabel}</span>\n </label>\n );\n })}\n </div>\n </div>\n </div>\n </>\n );\n}\n"],"mappings":";AAuCI,mBAgBM,KAmBA,YAnCN;AAvCJ,SAAS,cAAc;AAEvB,SAAS,SAAS;AAClB,SAAS,UAAU;AACnB,SAAS,eAAe;AAExB,SAAS,uBAAuB;AAChC,SAAS,mBAAmB;AAQrB,SAAS,oBAAuB;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAWG;AAED,QAAM,cAAc,OAAiC,IAAI;AACzD,SACE,iCACE;AAAA,wBAAC,WAAQ,OAAO,mBAAmB,QAAM,MACvC;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,SAAS,MAAM,gBAAgB,IAAI;AAAA,QACnC,cAAY;AAAA,QACZ,eAAa;AAAA,QACb,UAAU,eAAe,KAAK;AAAA,QAC9B,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,UACA,eAAe,mBAAmB;AAAA,QACpC;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,OAAO,EAAE,aAAa,eAAe,WAAW,iBAAiB;AAAA,YAEhE;AAAA;AAAA,QACH;AAAA;AAAA,IACF,GACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QAIC,OAAO,CAAC,gBAAgB;AAAA,QACxB,WAAW;AAAA,UACT;AAAA,UACA,eAAe,SAAS;AAAA,QAC1B;AAAA,QAEA,+BAAC,SAAI,WAAU,iDACb;AAAA,+BAAC,SAAI,WAAU,gDACb;AAAA,gCAAC,SAAI,WAAU,0DACZ,6BACH;AAAA,YACA,oBAAC,WAAQ,OAAO,OAAO,OAAO,QAAM,MAClC;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,SAAS,MAAM;AACb,kCAAgB,KAAK;AAGrB,8BAAY,SAAS,MAAM;AAAA,gBAC7B;AAAA,gBACA,cAAY,OAAO;AAAA,gBACnB,WAAU;AAAA,gBAEV,8BAAC,KAAE,WAAU,UAAS;AAAA;AAAA,YACxB,GACF;AAAA,aACF;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS;AAAA,cACT,WAAU;AAAA,cAET,iBAAO;AAAA;AAAA,UACV;AAAA,UACA,oBAAC,SAAI,WAAU,yBACZ,kBAAQ,IAAI,CAAC,QAAQ;AACpB,gBAAI,IAAI,QAAQ,gBAAiB,QAAO;AACxC,kBAAM,UAAU,CAAC,WAAW,IAAI,IAAI,GAAG;AACvC,kBAAM,cAAc,YAAY,GAAG;AACnC,mBACE;AAAA,cAAC;AAAA;AAAA,gBAEC,WAAU;AAAA,gBAEV;AAAA;AAAA,oBAAC;AAAA;AAAA,sBACC,MAAK;AAAA,sBACL,WAAU;AAAA,sBACV;AAAA,sBACA,UAAU,CAAC,MAAM;AACf,sCAAc,CAAC,MAAM;AACnB,gCAAM,OAAO,IAAI,IAAI,CAAC;AACtB,8BAAI,EAAE,OAAO,QAAS,MAAK,OAAO,IAAI,GAAG;AAAA,8BACpC,MAAK,IAAI,IAAI,GAAG;AACrB,iCAAO;AAAA,wBACT,CAAC;AAAA,sBACH;AAAA;AAAA,kBACF;AAAA,kBAIA,oBAAC,UAAK,WAAU,gCAAgC,uBAAY;AAAA;AAAA;AAAA,cAnBvD,IAAI;AAAA,YAoBX;AAAA,UAEJ,CAAC,GACH;AAAA,WACF;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/data-table-column-settings.tsx"],"sourcesContent":["import { useRef } from \"react\";\nimport type { Dispatch, SetStateAction } from \"react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { Tooltip } from \"./tooltip\";\nimport type { DataTableLabels } from \"./data-table-labels\";\nimport { isTableOwnColumn } from \"./data-table-row-actions\";\nimport { columnLabel } from \"./data-table-shared\";\nimport type { DataTableColumn } from \"./data-table\";\n\n// DataTable's column settings on a wide screen: the vertical \"Columns (n/m)\" rail and\n// the drawer it opens, with \"Auto-size\" and a show/hide box per column. Moved out of\n// data-table.tsx whole (refactor audit 2026-10, MOD-3 step 5).\n\n/** The \"Columns\" rail and its drawer (`columnSettings`). */\nexport function ColumnSettingsPanel<T>({\n columns,\n hiddenCols,\n setHiddenCols,\n showSettings,\n setShowSettings,\n columnsCountLabel,\n labels,\n autoSizeAll,\n}: {\n /** Every column, hidden ones included; the actions column is left out here. */\n columns: DataTableColumn<T>[];\n hiddenCols: Set<string>;\n setHiddenCols: Dispatch<SetStateAction<Set<string>>>;\n showSettings: boolean;\n setShowSettings: (open: boolean) => void;\n /** \"Columns (n/m)\", the rail's text and name. */\n columnsCountLabel: string;\n labels: DataTableLabels;\n autoSizeAll: () => void;\n}) {\n // The \"Columns\" rail button — where the drawer's X hands focus back (BUG-7a).\n const columnsRail = useRef<HTMLButtonElement | null>(null);\n return (\n <>\n <Tooltip label={columnsCountLabel} portal>\n <button\n ref={columnsRail}\n type=\"button\"\n onClick={() => setShowSettings(true)}\n aria-label={columnsCountLabel}\n aria-hidden={showSettings}\n tabIndex={showSettings ? -1 : 0}\n className={cn(\n \"group flex shrink-0 items-start justify-center overflow-hidden pt-3 transition-[width] duration-200 ease-out\",\n \"border-s border-subtle\",\n \"hover:bg-hover\",\n showSettings ? \"w-0 border-s-0\" : \"w-8 cursor-pointer\",\n )}\n >\n <span\n className=\"whitespace-nowrap text-xs font-medium tracking-wide text-muted group-hover:text-secondary\"\n style={{ writingMode: \"vertical-rl\", transform: \"rotate(180deg)\" }}\n >\n {columnsCountLabel}\n </span>\n </button>\n </Tooltip>\n <div\n // Closed, the drawer is only `w-0`: without `inert` its X, Auto-size and one\n // checkbox per column stayed tab stops and in the accessibility tree, and Tab\n // after the last cell walked into controls nobody could see (BUG-7a).\n inert={!showSettings || undefined}\n className={cn(\n \"shrink-0 overflow-hidden transition-[width] duration-200 ease-out\",\n showSettings ? \"w-56\" : \"w-0\",\n )}\n >\n <div className=\"flex w-56 flex-col border-s border-subtle p-2\">\n <div className=\"mb-2 flex items-center justify-between gap-1\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-muted\">\n {columnsCountLabel}\n </div>\n <Tooltip label={labels.close} portal>\n <button\n type=\"button\"\n onClick={() => {\n setShowSettings(false);\n // Back to the rail that opened it: the X is about to go inert, and\n // focus would fall to <body> with it (BUG-7a).\n columnsRail.current?.focus();\n }}\n aria-label={labels.close}\n className=\"rounded p-0.5 text-placeholder hover:bg-hover hover:text-secondary\"\n >\n <X className=\"size-4\" />\n </button>\n </Tooltip>\n </div>\n <button\n type=\"button\"\n onClick={autoSizeAll}\n className=\"mb-2 rounded border border-subtle px-2 py-1 text-xs font-medium text-secondary hover:bg-hover\"\n >\n {labels.autoSize}\n </button>\n <div className=\"flex flex-col gap-0.5\">\n {columns.map((col) => {\n if (isTableOwnColumn(col.key)) return null;\n const checked = !hiddenCols.has(col.key);\n const headerLabel = columnLabel(col);\n return (\n <label\n key={col.key}\n className=\"inline-flex cursor-pointer items-center gap-1.5 rounded px-1 py-0.5 text-xs hover:bg-hover\"\n >\n <input\n type=\"checkbox\"\n className=\"size-3.5 shrink-0\"\n checked={checked}\n onChange={(e) => {\n setHiddenCols((s) => {\n const next = new Set(s);\n if (e.target.checked) next.delete(col.key);\n else next.add(col.key);\n return next;\n });\n }}\n />\n {/* The panel is `w-56`: a long column name wraps from Large up rather\n than reading \"Accoun…\" beside its box (text size §10.17, round\n 0.34 T1). */}\n <span className=\"min-w-0 truncate-until-large\">{headerLabel}</span>\n </label>\n );\n })}\n </div>\n </div>\n </div>\n </>\n );\n}\n"],"mappings":";AAuCI,mBAgBM,KAmBA,YAnCN;AAvCJ,SAAS,cAAc;AAEvB,SAAS,SAAS;AAClB,SAAS,UAAU;AACnB,SAAS,eAAe;AAExB,SAAS,wBAAwB;AACjC,SAAS,mBAAmB;AAQrB,SAAS,oBAAuB;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAWG;AAED,QAAM,cAAc,OAAiC,IAAI;AACzD,SACE,iCACE;AAAA,wBAAC,WAAQ,OAAO,mBAAmB,QAAM,MACvC;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,SAAS,MAAM,gBAAgB,IAAI;AAAA,QACnC,cAAY;AAAA,QACZ,eAAa;AAAA,QACb,UAAU,eAAe,KAAK;AAAA,QAC9B,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,UACA,eAAe,mBAAmB;AAAA,QACpC;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,OAAO,EAAE,aAAa,eAAe,WAAW,iBAAiB;AAAA,YAEhE;AAAA;AAAA,QACH;AAAA;AAAA,IACF,GACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QAIC,OAAO,CAAC,gBAAgB;AAAA,QACxB,WAAW;AAAA,UACT;AAAA,UACA,eAAe,SAAS;AAAA,QAC1B;AAAA,QAEA,+BAAC,SAAI,WAAU,iDACb;AAAA,+BAAC,SAAI,WAAU,gDACb;AAAA,gCAAC,SAAI,WAAU,0DACZ,6BACH;AAAA,YACA,oBAAC,WAAQ,OAAO,OAAO,OAAO,QAAM,MAClC;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,SAAS,MAAM;AACb,kCAAgB,KAAK;AAGrB,8BAAY,SAAS,MAAM;AAAA,gBAC7B;AAAA,gBACA,cAAY,OAAO;AAAA,gBACnB,WAAU;AAAA,gBAEV,8BAAC,KAAE,WAAU,UAAS;AAAA;AAAA,YACxB,GACF;AAAA,aACF;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS;AAAA,cACT,WAAU;AAAA,cAET,iBAAO;AAAA;AAAA,UACV;AAAA,UACA,oBAAC,SAAI,WAAU,yBACZ,kBAAQ,IAAI,CAAC,QAAQ;AACpB,gBAAI,iBAAiB,IAAI,GAAG,EAAG,QAAO;AACtC,kBAAM,UAAU,CAAC,WAAW,IAAI,IAAI,GAAG;AACvC,kBAAM,cAAc,YAAY,GAAG;AACnC,mBACE;AAAA,cAAC;AAAA;AAAA,gBAEC,WAAU;AAAA,gBAEV;AAAA;AAAA,oBAAC;AAAA;AAAA,sBACC,MAAK;AAAA,sBACL,WAAU;AAAA,sBACV;AAAA,sBACA,UAAU,CAAC,MAAM;AACf,sCAAc,CAAC,MAAM;AACnB,gCAAM,OAAO,IAAI,IAAI,CAAC;AACtB,8BAAI,EAAE,OAAO,QAAS,MAAK,OAAO,IAAI,GAAG;AAAA,8BACpC,MAAK,IAAI,IAAI,GAAG;AACrB,iCAAO;AAAA,wBACT,CAAC;AAAA,sBACH;AAAA;AAAA,kBACF;AAAA,kBAIA,oBAAC,UAAK,WAAU,gCAAgC,uBAAY;AAAA;AAAA;AAAA,cAnBvD,IAAI;AAAA,YAoBX;AAAA,UAEJ,CAAC,GACH;AAAA,WACF;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;","names":[]}
@@ -7,7 +7,7 @@ import { defaultFilterState, isFilterActive, resolveFilter } from "./data-table-
7
7
  import { FilterPopover } from "./data-table-filter-popover.js";
8
8
  import { Popover } from "./popover.js";
9
9
  import { Tooltip } from "./tooltip.js";
10
- import { ROW_ACTIONS_KEY } from "./data-table-row-actions.js";
10
+ import { isTableOwnColumn } from "./data-table-row-actions.js";
11
11
  import { columnLabel, logicalAlign, splitCellProps } from "./data-table-shared.js";
12
12
  const END_ALIGN = /(^|\s)text-end(?=\s|$)/;
13
13
  function DataTableHead({
@@ -157,7 +157,7 @@ function DataTableHead({
157
157
  ]
158
158
  }
159
159
  ),
160
- resizable && col.key !== ROW_ACTIONS_KEY && /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a
160
+ resizable && !isTableOwnColumn(col.key) && /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a
161
161
  pointer-only drag handle for a visual preference (column width); it is
162
162
  deliberately not a tab stop, and no content or action depends on it. */
163
163
  /* @__PURE__ */ jsx(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/data-table-head.tsx"],"sourcesContent":["import type { Dispatch, SetStateAction } from \"react\";\nimport { ArrowDown, ArrowUp, ArrowUpDown, Filter } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { defaultFilterState, isFilterActive, resolveFilter } from \"./data-table-filters\";\nimport type { FilterValue } from \"./data-table-filters\";\nimport type { SortState } from \"./data-table-sort\";\nimport { FilterPopover } from \"./data-table-filter-popover\";\nimport { Popover } from \"./popover\";\nimport { Tooltip } from \"./tooltip\";\nimport type { DataTableLabels } from \"./data-table-labels\";\nimport { ROW_ACTIONS_KEY } from \"./data-table-row-actions\";\nimport { columnLabel, logicalAlign, splitCellProps } from \"./data-table-shared\";\nimport type { DataTableColumn, DataTableProps, FilterState } from \"./data-table\";\n\n// DataTable's `<thead>` on a wide screen: the select-all box, and per column the sort\n// button, the filter popover and the resize handle. Moved out of data-table.tsx whole\n// (refactor audit 2026-10, MOD-3 step 5); the state it shows and changes stays the\n// table's.\n\n/** Whether a column is end-aligned — the header then puts its filter button first. */\nconst END_ALIGN = /(^|\\s)text-end(?=\\s|$)/;\n\n/** The wide-screen table's header row. */\nexport function DataTableHead<T>({\n visibleColumns,\n selection,\n compact,\n labels,\n locale,\n isServer,\n hasSortCallback,\n hasFilterCallback,\n sorts,\n filters,\n selectOptionsByKey,\n multiSort,\n resizable,\n widths,\n setWidths,\n headRefs,\n toggleSort,\n setFilterValue,\n clearFilter,\n startResize,\n moveResize,\n finishResize,\n}: {\n visibleColumns: DataTableColumn<T>[];\n selection: DataTableProps<T>[\"selection\"];\n compact: boolean;\n labels: DataTableLabels;\n locale?: string;\n isServer: boolean;\n /** The caller owns the sort (`onSortsChange`), so a server table can offer it. */\n hasSortCallback: boolean;\n /** The caller owns the filters (`onFiltersChange`), likewise. */\n hasFilterCallback: boolean;\n sorts: SortState[];\n filters: FilterState;\n selectOptionsByKey: Record<string, { value: string; label: string }[]>;\n multiSort: boolean;\n resizable: boolean;\n widths: Record<string, number>;\n setWidths: Dispatch<SetStateAction<Record<string, number>>>;\n /** Filled here, one `<th>` per column — what a resize and \"Auto-size\" measure. */\n headRefs: Map<string, HTMLTableCellElement>;\n toggleSort: (key: string, shiftKey: boolean) => void;\n setFilterValue: (key: string, value: FilterValue) => void;\n clearFilter: (key: string) => void;\n startResize: (e: React.PointerEvent<HTMLElement>, key: string) => void;\n moveResize: (e: React.PointerEvent<HTMLElement>) => void;\n finishResize: (e: React.PointerEvent<HTMLElement>, commit: boolean) => void;\n}) {\n return (\n <thead className=\"bg-surface-2 text-secondary\">\n <tr className=\"text-start\">\n {selection && (\n <th\n scope=\"col\"\n className={cn(\n \"sticky top-0 z-10 bg-surface-2 align-middle backdrop-blur-sm\",\n compact ? \"w-8 px-2 py-1\" : \"w-10 px-3 py-2\",\n )}\n >\n <input\n type=\"checkbox\"\n className={cn(\"align-middle accent-indigo-600\", compact ? \"size-3.5\" : \"size-4\")}\n checked={selection.allSelected}\n ref={(el) => {\n if (el) el.indeterminate = selection.someSelected && !selection.allSelected;\n }}\n onChange={(e) => selection.onToggleAll(e.target.checked)}\n aria-label={labels.selectAllRows}\n />\n </th>\n )}\n {visibleColumns.map((col) => {\n // In server mode the utilities stay hidden unless the caller\n // takes them over and feeds them back into the server query.\n const sortable = (!isServer || hasSortCallback) && !!col.sortBy;\n const filter = !isServer || hasFilterCallback ? resolveFilter(col) : null;\n const filterState = filters[col.key] ?? (filter ? defaultFilterState(filter) : undefined);\n const sortIdx = sorts.findIndex((s) => s.key === col.key);\n const active = sortIdx !== -1;\n const Icon = active ? (sorts[sortIdx].dir === \"asc\" ? ArrowUp : ArrowDown) : ArrowUpDown;\n const filterActive = filterState && isFilterActive(filterState);\n // Logical, and a legacy `text-right` still counts — see `logicalAlign`.\n const headClass = logicalAlign(col.headClassName);\n const isEndAligned = !!headClass && END_ALIGN.test(headClass);\n const width = widths[col.key];\n const ownHead = splitCellProps(col.headProps);\n const pinnedHead = width ? { width, minWidth: width, maxWidth: width } : undefined;\n return (\n <th\n {...ownHead.attrs}\n key={col.key}\n ref={(el) => {\n if (el) headRefs.set(col.key, el);\n else headRefs.delete(col.key);\n }}\n scope=\"col\"\n // Only a header the user can actually sort says anything: an\n // `aria-sort` on a plain column is a control that is not there,\n // and a persisted or URL sort can name a column with no `sortBy`\n // (the sort chain drops it silently) — which would otherwise\n // claim a sort the table is not applying.\n //\n // \"none\" on the rest is the half that was missing. Without it a\n // sortable header is indistinguishable from a plain one until the\n // user presses it. Secondary criteria read \"none\" too: ARIA allows\n // exactly one sorted column, and the priority is carried by the\n // badge and by what the header button announces on press.\n aria-sort={\n sortable\n ? sortIdx === 0\n ? sorts[0].dir === \"asc\"\n ? \"ascending\"\n : \"descending\"\n : \"none\"\n : undefined\n }\n style={ownHead.style || pinnedHead ? { ...ownHead.style, ...pinnedHead } : undefined}\n className={cn(\n \"relative font-medium align-middle whitespace-nowrap\",\n compact ? \"px-2 py-1\" : \"px-3 py-2\",\n // Keep the header visible while the user scrolls the page.\n // Each <th> carries its own bg so the row doesn't render\n // transparent over the data rows underneath.\n \"sticky top-0 z-10 bg-surface-2 backdrop-blur-sm\",\n logicalAlign(ownHead.className),\n headClass,\n )}\n >\n <div\n className={cn(\n \"flex items-center gap-1\",\n isEndAligned && \"flex-row-reverse\",\n )}\n >\n <Tooltip label={sortable && multiSort ? labels.sortHint : undefined} portal>\n <button\n type=\"button\"\n onClick={(e) => sortable && toggleSort(col.key, e.shiftKey)}\n disabled={!sortable}\n className={cn(\n \"flex items-center gap-1 text-start\",\n sortable && \"hover:text-primary\",\n )}\n >\n <span>{col.header}</span>\n {sortable && (\n <Icon className={cn(\"size-3\", active ? \"opacity-100\" : \"opacity-40\")} />\n )}\n {/* Priority badge, only meaningful with 2+ sort keys */}\n {active && sorts.length > 1 && (\n <span className=\"text-micro font-semibold leading-none text-brand\">\n {sortIdx + 1}\n </span>\n )}\n </button>\n </Tooltip>\n {filter && (\n <Popover\n // 420 px at Normal, in rem so the calendar grows with its\n // text (§3.2); Popover caps it at the viewport.\n width={filter.type === \"date\" ? \"26.25rem\" : undefined}\n // The panel is THIS column's filter, and says so. Unnamed it was\n // announced as the kit-wide fallback, \"Popover\"; then as \"Filter\"\n // in every column alike (round 0.34 Wave 2, BUG-7c).\n labels={{ panel: labels.filterColumn(columnLabel(col, labels), false) }}\n trigger={({ open, toggle, ref }) => (\n <button\n type=\"button\"\n ref={ref}\n onClick={toggle}\n // Named for its column, and for being filtered: the tint\n // alone said that to nobody who cannot see it (BUG-7c).\n aria-label={labels.filterColumn(columnLabel(col, labels), Boolean(filterActive))}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n className={cn(\n \"rounded p-1 transition-colors\",\n filterActive\n ? \"bg-brand-soft text-brand-muted\"\n : \"text-placeholder hover:text-secondary hover:bg-hover\",\n )}\n >\n <Filter className=\"size-3\" />\n </button>\n )}\n >\n {() => (\n <FilterPopover\n column={col}\n state={filterState ?? defaultFilterState(filter)}\n onChange={(next) => setFilterValue(col.key, next)}\n onClear={() => clearFilter(col.key)}\n selectOptions={selectOptionsByKey[col.key] ?? []}\n labels={labels}\n locale={locale}\n />\n )}\n </Popover>\n )}\n </div>\n {resizable && col.key !== ROW_ACTIONS_KEY && (\n /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a\n pointer-only drag handle for a visual preference (column width); it is\n deliberately not a tab stop, and no content or action depends on it. */\n <span\n role=\"separator\"\n aria-orientation=\"vertical\"\n onPointerDown={(e) => startResize(e, col.key)}\n onPointerMove={moveResize}\n onPointerUp={(e) => finishResize(e, true)}\n onPointerCancel={(e) => finishResize(e, false)}\n onDoubleClick={(e) => {\n e.preventDefault();\n setWidths((w) => {\n const next = { ...w };\n delete next[col.key];\n return next;\n });\n }}\n // `touch-none`: without it the browser claims a finger drag for\n // scrolling before the first `pointermove` ever arrives.\n // `end-0` and a translate that flips with it: the handle sits on the column's\n // trailing edge, which is its left one in a right-to-left table.\n className=\"absolute end-0 top-0 z-10 h-full w-1.5 -translate-x-1/2 rtl:translate-x-1/2 cursor-col-resize select-none touch-none hover:bg-brand-soft-hover\"\n />\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/data-table-head.tsx"],"sourcesContent":["import type { Dispatch, SetStateAction } from \"react\";\nimport { ArrowDown, ArrowUp, ArrowUpDown, Filter } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { defaultFilterState, isFilterActive, resolveFilter } from \"./data-table-filters\";\nimport type { FilterValue } from \"./data-table-filters\";\nimport type { SortState } from \"./data-table-sort\";\nimport { FilterPopover } from \"./data-table-filter-popover\";\nimport { Popover } from \"./popover\";\nimport { Tooltip } from \"./tooltip\";\nimport type { DataTableLabels } from \"./data-table-labels\";\nimport { isTableOwnColumn } from \"./data-table-row-actions\";\nimport { columnLabel, logicalAlign, splitCellProps } from \"./data-table-shared\";\nimport type { DataTableColumn, DataTableProps, FilterState } from \"./data-table\";\n\n// DataTable's `<thead>` on a wide screen: the select-all box, and per column the sort\n// button, the filter popover and the resize handle. Moved out of data-table.tsx whole\n// (refactor audit 2026-10, MOD-3 step 5); the state it shows and changes stays the\n// table's.\n\n/** Whether a column is end-aligned — the header then puts its filter button first. */\nconst END_ALIGN = /(^|\\s)text-end(?=\\s|$)/;\n\n/** The wide-screen table's header row. */\nexport function DataTableHead<T>({\n visibleColumns,\n selection,\n compact,\n labels,\n locale,\n isServer,\n hasSortCallback,\n hasFilterCallback,\n sorts,\n filters,\n selectOptionsByKey,\n multiSort,\n resizable,\n widths,\n setWidths,\n headRefs,\n toggleSort,\n setFilterValue,\n clearFilter,\n startResize,\n moveResize,\n finishResize,\n}: {\n visibleColumns: DataTableColumn<T>[];\n selection: DataTableProps<T>[\"selection\"];\n compact: boolean;\n labels: DataTableLabels;\n locale?: string;\n isServer: boolean;\n /** The caller owns the sort (`onSortsChange`), so a server table can offer it. */\n hasSortCallback: boolean;\n /** The caller owns the filters (`onFiltersChange`), likewise. */\n hasFilterCallback: boolean;\n sorts: SortState[];\n filters: FilterState;\n selectOptionsByKey: Record<string, { value: string; label: string }[]>;\n multiSort: boolean;\n resizable: boolean;\n widths: Record<string, number>;\n setWidths: Dispatch<SetStateAction<Record<string, number>>>;\n /** Filled here, one `<th>` per column — what a resize and \"Auto-size\" measure. */\n headRefs: Map<string, HTMLTableCellElement>;\n toggleSort: (key: string, shiftKey: boolean) => void;\n setFilterValue: (key: string, value: FilterValue) => void;\n clearFilter: (key: string) => void;\n startResize: (e: React.PointerEvent<HTMLElement>, key: string) => void;\n moveResize: (e: React.PointerEvent<HTMLElement>) => void;\n finishResize: (e: React.PointerEvent<HTMLElement>, commit: boolean) => void;\n}) {\n return (\n <thead className=\"bg-surface-2 text-secondary\">\n <tr className=\"text-start\">\n {selection && (\n <th\n scope=\"col\"\n className={cn(\n \"sticky top-0 z-10 bg-surface-2 align-middle backdrop-blur-sm\",\n compact ? \"w-8 px-2 py-1\" : \"w-10 px-3 py-2\",\n )}\n >\n <input\n type=\"checkbox\"\n className={cn(\"align-middle accent-indigo-600\", compact ? \"size-3.5\" : \"size-4\")}\n checked={selection.allSelected}\n ref={(el) => {\n if (el) el.indeterminate = selection.someSelected && !selection.allSelected;\n }}\n onChange={(e) => selection.onToggleAll(e.target.checked)}\n aria-label={labels.selectAllRows}\n />\n </th>\n )}\n {visibleColumns.map((col) => {\n // In server mode the utilities stay hidden unless the caller\n // takes them over and feeds them back into the server query.\n const sortable = (!isServer || hasSortCallback) && !!col.sortBy;\n const filter = !isServer || hasFilterCallback ? resolveFilter(col) : null;\n const filterState = filters[col.key] ?? (filter ? defaultFilterState(filter) : undefined);\n const sortIdx = sorts.findIndex((s) => s.key === col.key);\n const active = sortIdx !== -1;\n const Icon = active ? (sorts[sortIdx].dir === \"asc\" ? ArrowUp : ArrowDown) : ArrowUpDown;\n const filterActive = filterState && isFilterActive(filterState);\n // Logical, and a legacy `text-right` still counts — see `logicalAlign`.\n const headClass = logicalAlign(col.headClassName);\n const isEndAligned = !!headClass && END_ALIGN.test(headClass);\n const width = widths[col.key];\n const ownHead = splitCellProps(col.headProps);\n const pinnedHead = width ? { width, minWidth: width, maxWidth: width } : undefined;\n return (\n <th\n {...ownHead.attrs}\n key={col.key}\n ref={(el) => {\n if (el) headRefs.set(col.key, el);\n else headRefs.delete(col.key);\n }}\n scope=\"col\"\n // Only a header the user can actually sort says anything: an\n // `aria-sort` on a plain column is a control that is not there,\n // and a persisted or URL sort can name a column with no `sortBy`\n // (the sort chain drops it silently) — which would otherwise\n // claim a sort the table is not applying.\n //\n // \"none\" on the rest is the half that was missing. Without it a\n // sortable header is indistinguishable from a plain one until the\n // user presses it. Secondary criteria read \"none\" too: ARIA allows\n // exactly one sorted column, and the priority is carried by the\n // badge and by what the header button announces on press.\n aria-sort={\n sortable\n ? sortIdx === 0\n ? sorts[0].dir === \"asc\"\n ? \"ascending\"\n : \"descending\"\n : \"none\"\n : undefined\n }\n style={ownHead.style || pinnedHead ? { ...ownHead.style, ...pinnedHead } : undefined}\n className={cn(\n \"relative font-medium align-middle whitespace-nowrap\",\n compact ? \"px-2 py-1\" : \"px-3 py-2\",\n // Keep the header visible while the user scrolls the page.\n // Each <th> carries its own bg so the row doesn't render\n // transparent over the data rows underneath.\n \"sticky top-0 z-10 bg-surface-2 backdrop-blur-sm\",\n logicalAlign(ownHead.className),\n headClass,\n )}\n >\n <div\n className={cn(\n \"flex items-center gap-1\",\n isEndAligned && \"flex-row-reverse\",\n )}\n >\n <Tooltip label={sortable && multiSort ? labels.sortHint : undefined} portal>\n <button\n type=\"button\"\n onClick={(e) => sortable && toggleSort(col.key, e.shiftKey)}\n disabled={!sortable}\n className={cn(\n \"flex items-center gap-1 text-start\",\n sortable && \"hover:text-primary\",\n )}\n >\n <span>{col.header}</span>\n {sortable && (\n <Icon className={cn(\"size-3\", active ? \"opacity-100\" : \"opacity-40\")} />\n )}\n {/* Priority badge, only meaningful with 2+ sort keys */}\n {active && sorts.length > 1 && (\n <span className=\"text-micro font-semibold leading-none text-brand\">\n {sortIdx + 1}\n </span>\n )}\n </button>\n </Tooltip>\n {filter && (\n <Popover\n // 420 px at Normal, in rem so the calendar grows with its\n // text (§3.2); Popover caps it at the viewport.\n width={filter.type === \"date\" ? \"26.25rem\" : undefined}\n // The panel is THIS column's filter, and says so. Unnamed it was\n // announced as the kit-wide fallback, \"Popover\"; then as \"Filter\"\n // in every column alike (round 0.34 Wave 2, BUG-7c).\n labels={{ panel: labels.filterColumn(columnLabel(col, labels), false) }}\n trigger={({ open, toggle, ref }) => (\n <button\n type=\"button\"\n ref={ref}\n onClick={toggle}\n // Named for its column, and for being filtered: the tint\n // alone said that to nobody who cannot see it (BUG-7c).\n aria-label={labels.filterColumn(columnLabel(col, labels), Boolean(filterActive))}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n className={cn(\n \"rounded p-1 transition-colors\",\n filterActive\n ? \"bg-brand-soft text-brand-muted\"\n : \"text-placeholder hover:text-secondary hover:bg-hover\",\n )}\n >\n <Filter className=\"size-3\" />\n </button>\n )}\n >\n {() => (\n <FilterPopover\n column={col}\n state={filterState ?? defaultFilterState(filter)}\n onChange={(next) => setFilterValue(col.key, next)}\n onClear={() => clearFilter(col.key)}\n selectOptions={selectOptionsByKey[col.key] ?? []}\n labels={labels}\n locale={locale}\n />\n )}\n </Popover>\n )}\n </div>\n {resizable && !isTableOwnColumn(col.key) && (\n /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a\n pointer-only drag handle for a visual preference (column width); it is\n deliberately not a tab stop, and no content or action depends on it. */\n <span\n role=\"separator\"\n aria-orientation=\"vertical\"\n onPointerDown={(e) => startResize(e, col.key)}\n onPointerMove={moveResize}\n onPointerUp={(e) => finishResize(e, true)}\n onPointerCancel={(e) => finishResize(e, false)}\n onDoubleClick={(e) => {\n e.preventDefault();\n setWidths((w) => {\n const next = { ...w };\n delete next[col.key];\n return next;\n });\n }}\n // `touch-none`: without it the browser claims a finger drag for\n // scrolling before the first `pointermove` ever arrives.\n // `end-0` and a translate that flips with it: the handle sits on the column's\n // trailing edge, which is its left one in a right-to-left table.\n className=\"absolute end-0 top-0 z-10 h-full w-1.5 -translate-x-1/2 rtl:translate-x-1/2 cursor-col-resize select-none touch-none hover:bg-brand-soft-hover\"\n />\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n );\n}\n"],"mappings":"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","names":[]}
@@ -13,7 +13,7 @@ import { SwipeableRow } from "./swipeable-row.js";
13
13
  import { DialogFrame } from "./dialog-frame.js";
14
14
  import { Checkbox } from "./checkbox.js";
15
15
  import { useLongPress } from "../hooks/use-long-press.js";
16
- import { RowLink, cleanAttrs, columnLabel, fromOutside } from "./data-table-shared.js";
16
+ import { RowLink, cleanAttrs, columnLabel, fromOutside, fromOwnControl } from "./data-table-shared.js";
17
17
  function dataAttrsOf(props) {
18
18
  if (!props) return void 0;
19
19
  const out = {};
@@ -225,7 +225,20 @@ function MobileRow({
225
225
  }
226
226
  ))
227
227
  ] });
228
- const press = select ? longPress.handlers : void 0;
228
+ const press = select ? {
229
+ ...longPress.handlers,
230
+ onPointerDown: (e) => {
231
+ if (fromOutside(e.target, e.currentTarget) || fromOwnControl(e.target, e.currentTarget)) {
232
+ longPress.consumeClick();
233
+ return;
234
+ }
235
+ longPress.handlers.onPointerDown(e);
236
+ },
237
+ onContextMenu: (e) => {
238
+ if (fromOutside(e.target, e.currentTarget) || fromOwnControl(e.target, e.currentTarget)) return;
239
+ longPress.handlers.onContextMenu(e);
240
+ }
241
+ } : void 0;
229
242
  const body = selecting ? (
230
243
  // In the mode a card is a toggle — kk's register row: `role="button"` with
231
244
  // `aria-pressed`, the box beside it only a picture of that state (the card is the
@@ -243,6 +256,7 @@ function MobileRow({
243
256
  ...press,
244
257
  onClick: (e) => {
245
258
  if (!select.selectable || fromOutside(e.target, e.currentTarget)) return;
259
+ if (fromOwnControl(e.target, e.currentTarget)) return;
246
260
  select.onToggle(!select.selected);
247
261
  },
248
262
  onKeyDown: (e) => {
@@ -302,6 +316,7 @@ function MobileRow({
302
316
  tabIndex: interactive ? 0 : void 0,
303
317
  onClick: interactive ? (e) => {
304
318
  if (fromOutside(e.target, e.currentTarget)) return;
319
+ if (fromOwnControl(e.target, e.currentTarget)) return;
305
320
  onRowClick(row);
306
321
  } : void 0,
307
322
  onKeyDown: interactive ? (e) => {