@eifi1/ui-kit 0.45.0 → 0.46.1

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 (238) hide show
  1. package/README.md +26 -14
  2. package/dist/admin/admin-area-labels.d.ts +9 -0
  3. package/dist/admin/admin-area-labels.js +5 -1
  4. package/dist/admin/admin-area-labels.js.map +1 -1
  5. package/dist/admin/admin-overview.d.ts +16 -1
  6. package/dist/admin/admin-overview.js +12 -1
  7. package/dist/admin/admin-overview.js.map +1 -1
  8. package/dist/admin/mail-test-card.d.ts +3 -2
  9. package/dist/admin/mail-test-card.js +3 -1
  10. package/dist/admin/mail-test-card.js.map +1 -1
  11. package/dist/admin/user-roster.d.ts +6 -2
  12. package/dist/admin/user-roster.js +7 -3
  13. package/dist/admin/user-roster.js.map +1 -1
  14. package/dist/components/appearance-labels.d.ts +10 -0
  15. package/dist/components/appearance-labels.js +6 -1
  16. package/dist/components/appearance-labels.js.map +1 -1
  17. package/dist/components/data-table-column-settings.js +2 -2
  18. package/dist/components/data-table-column-settings.js.map +1 -1
  19. package/dist/components/data-table-head.js +2 -2
  20. package/dist/components/data-table-head.js.map +1 -1
  21. package/dist/components/data-table-row-actions.d.ts +35 -2
  22. package/dist/components/data-table-row-actions.js +98 -5
  23. package/dist/components/data-table-row-actions.js.map +1 -1
  24. package/dist/components/data-table-shared.js +9 -2
  25. package/dist/components/data-table-shared.js.map +1 -1
  26. package/dist/components/data-table.d.ts +58 -2
  27. package/dist/components/data-table.js +11 -4
  28. package/dist/components/data-table.js.map +1 -1
  29. package/dist/components/date-format-setting.d.ts +29 -0
  30. package/dist/components/date-format-setting.js +36 -0
  31. package/dist/components/date-format-setting.js.map +1 -0
  32. package/dist/components/row-actions.d.ts +12 -5
  33. package/dist/components/row-actions.js +11 -4
  34. package/dist/components/row-actions.js.map +1 -1
  35. package/dist/components/swipeable-row.d.ts +8 -0
  36. package/dist/components/swipeable-row.js.map +1 -1
  37. package/dist/components/tabs.d.ts +3 -2
  38. package/dist/components/tabs.js +1 -1
  39. package/dist/components/tabs.js.map +1 -1
  40. package/dist/components/translation-review-editor.js +1 -0
  41. package/dist/components/translation-review-editor.js.map +1 -1
  42. package/dist/components/translation-review-labels.d.ts +5 -0
  43. package/dist/components/translation-review-labels.js +2 -0
  44. package/dist/components/translation-review-labels.js.map +1 -1
  45. package/dist/components/translation-review.d.ts +18 -8
  46. package/dist/components/translation-review.js +67 -8
  47. package/dist/components/translation-review.js.map +1 -1
  48. package/dist/components/write-lock-labels.js +2 -1
  49. package/dist/components/write-lock-labels.js.map +1 -1
  50. package/dist/components/write-lock.d.ts +33 -3
  51. package/dist/components/write-lock.js +9 -0
  52. package/dist/components/write-lock.js.map +1 -1
  53. package/dist/data-table.d.ts +2 -1
  54. package/dist/data-table.js +2 -0
  55. package/dist/data-table.js.map +1 -1
  56. package/dist/eslint/index.js +1 -1
  57. package/dist/feedback/feedback-row-detail.d.ts +4 -2
  58. package/dist/feedback/feedback-row-detail.js +17 -26
  59. package/dist/feedback/feedback-row-detail.js.map +1 -1
  60. package/dist/feedback/feedback-swipe.d.ts +18 -0
  61. package/dist/feedback/feedback-swipe.js +10 -1
  62. package/dist/feedback/feedback-swipe.js.map +1 -1
  63. package/dist/i18n/german.js +41 -5
  64. package/dist/i18n/german.js.map +1 -1
  65. package/dist/i18n/kit-labels.d.ts +12 -1
  66. package/dist/i18n/kit-labels.js +8 -2
  67. package/dist/i18n/kit-labels.js.map +1 -1
  68. package/dist/i18n/locales/en.js +40 -6
  69. package/dist/i18n/locales/en.js.map +2 -2
  70. package/dist/i18n/locales/es.js +41 -5
  71. package/dist/i18n/locales/es.js.map +1 -1
  72. package/dist/i18n/locales/fr.js +41 -5
  73. package/dist/i18n/locales/fr.js.map +1 -1
  74. package/dist/i18n/locales/hu.js +41 -5
  75. package/dist/i18n/locales/hu.js.map +1 -1
  76. package/dist/i18n/locales/it.js +41 -5
  77. package/dist/i18n/locales/it.js.map +1 -1
  78. package/dist/i18n/locales/zh.js +41 -5
  79. package/dist/i18n/locales/zh.js.map +1 -1
  80. package/dist/i18n/review.js +3 -1
  81. package/dist/i18n/review.js.map +1 -1
  82. package/dist/index.d.ts +17 -4
  83. package/dist/index.js +38 -1
  84. package/dist/index.js.map +1 -1
  85. package/dist/lib/date-format.d.ts +81 -0
  86. package/dist/lib/date-format.js +56 -0
  87. package/dist/lib/date-format.js.map +1 -0
  88. package/dist/lib/format.d.ts +3 -0
  89. package/dist/lib/format.js +5 -4
  90. package/dist/lib/format.js.map +1 -1
  91. package/dist/notifications/device-zone-report.d.ts +34 -0
  92. package/dist/notifications/device-zone-report.js +31 -0
  93. package/dist/notifications/device-zone-report.js.map +1 -0
  94. package/dist/notifications/feedback-notification-types-labels.d.ts +7 -1
  95. package/dist/notifications/feedback-notification-types-labels.js +4 -1
  96. package/dist/notifications/feedback-notification-types-labels.js.map +1 -1
  97. package/dist/notifications/feedback-notification-types.d.ts +24 -5
  98. package/dist/notifications/feedback-notification-types.js +7 -2
  99. package/dist/notifications/feedback-notification-types.js.map +1 -1
  100. package/dist/notifications/notification-bell.d.ts +33 -0
  101. package/dist/notifications/notification-bell.js +74 -0
  102. package/dist/notifications/notification-bell.js.map +1 -0
  103. package/dist/notifications/notification-inbox.d.ts +86 -0
  104. package/dist/notifications/notification-inbox.js +206 -0
  105. package/dist/notifications/notification-inbox.js.map +1 -0
  106. package/dist/notifications/notification-types-setting.d.ts +6 -4
  107. package/dist/notifications/notification-types-setting.js +103 -45
  108. package/dist/notifications/notification-types-setting.js.map +1 -1
  109. package/dist/notifications/notification-types.d.ts +19 -7
  110. package/dist/notifications/notifications-labels.d.ts +44 -8
  111. package/dist/notifications/notifications-labels.js +18 -1
  112. package/dist/notifications/notifications-labels.js.map +1 -1
  113. package/dist/notifications/push-devices-setting.d.ts +20 -3
  114. package/dist/notifications/push-devices-setting.js +93 -33
  115. package/dist/notifications/push-devices-setting.js.map +1 -1
  116. package/dist/notifications/push-worker.d.ts +108 -0
  117. package/dist/notifications/push-worker.js +62 -0
  118. package/dist/notifications/push-worker.js.map +1 -0
  119. package/dist/notifications/quiet-hours-setting.d.ts +29 -17
  120. package/dist/notifications/quiet-hours-setting.js +9 -41
  121. package/dist/notifications/quiet-hours-setting.js.map +1 -1
  122. package/dist/notifications/time-zone-select.d.ts +8 -4
  123. package/dist/notifications/time-zone-select.js +2 -7
  124. package/dist/notifications/time-zone-select.js.map +1 -1
  125. package/dist/notifications/use-push-device.d.ts +4 -17
  126. package/dist/notifications/use-push-device.js +7 -15
  127. package/dist/notifications/use-push-device.js.map +1 -1
  128. package/dist/offline/local-first.d.ts +42 -0
  129. package/dist/offline/local-first.js +53 -0
  130. package/dist/offline/local-first.js.map +1 -0
  131. package/dist/offline/query-persistence.d.ts +26 -1
  132. package/dist/offline/query-persistence.js +27 -23
  133. package/dist/offline/query-persistence.js.map +1 -1
  134. package/dist/offline/service-worker-updates.d.ts +46 -0
  135. package/dist/offline/service-worker-updates.js +39 -0
  136. package/dist/offline/service-worker-updates.js.map +1 -0
  137. package/dist/offline.d.ts +5 -2
  138. package/dist/offline.js +12 -2
  139. package/dist/offline.js.map +1 -1
  140. package/dist/push-worker.d.ts +2 -0
  141. package/dist/push-worker.js +14 -0
  142. package/dist/push-worker.js.map +1 -0
  143. package/dist/shell/account-menu-text-size.d.ts +33 -0
  144. package/dist/shell/account-menu-text-size.js +51 -0
  145. package/dist/shell/account-menu-text-size.js.map +1 -0
  146. package/dist/shell/top-bar-actions.d.ts +6 -2
  147. package/dist/shell/top-bar-actions.js +1 -0
  148. package/dist/shell/top-bar-actions.js.map +1 -1
  149. package/dist/shell.d.ts +2 -0
  150. package/dist/shell.js +2 -0
  151. package/dist/shell.js.map +1 -1
  152. package/dist/sync/db.d.ts +93 -0
  153. package/dist/sync/db.js +33 -0
  154. package/dist/sync/db.js.map +1 -0
  155. package/dist/sync/engine.d.ts +126 -0
  156. package/dist/sync/engine.js +474 -0
  157. package/dist/sync/engine.js.map +1 -0
  158. package/dist/sync/hooks.d.ts +35 -0
  159. package/dist/sync/hooks.js +67 -0
  160. package/dist/sync/hooks.js.map +1 -0
  161. package/dist/sync/testing.d.ts +28 -0
  162. package/dist/sync/testing.js +41 -0
  163. package/dist/sync/testing.js.map +1 -0
  164. package/dist/sync/wire.d.ts +50 -0
  165. package/dist/sync/wire.js +2 -0
  166. package/dist/sync/wire.js.map +1 -0
  167. package/dist/sync.d.ts +7 -0
  168. package/dist/sync.js +25 -0
  169. package/dist/sync.js.map +1 -0
  170. package/dist/tour/tour-labels.js +1 -0
  171. package/dist/tour/tour-labels.js.map +1 -1
  172. package/dist/tour/tour.d.ts +30 -0
  173. package/dist/tour/tour.js +50 -6
  174. package/dist/tour/tour.js.map +1 -1
  175. package/package.json +19 -1
  176. package/src/admin/admin-area-labels.ts +13 -0
  177. package/src/admin/admin-overview.tsx +37 -2
  178. package/src/admin/mail-test-card.tsx +7 -3
  179. package/src/admin/user-roster.tsx +12 -3
  180. package/src/components/appearance-labels.ts +16 -0
  181. package/src/components/data-table-column-settings.tsx +2 -2
  182. package/src/components/data-table-head.tsx +2 -2
  183. package/src/components/data-table-row-actions.tsx +145 -5
  184. package/src/components/data-table-shared.tsx +21 -2
  185. package/src/components/data-table.tsx +68 -6
  186. package/src/components/date-format-setting.tsx +64 -0
  187. package/src/components/row-actions.tsx +33 -9
  188. package/src/components/swipeable-row.tsx +8 -0
  189. package/src/components/tabs.tsx +4 -3
  190. package/src/components/translation-review-editor.tsx +2 -0
  191. package/src/components/translation-review-labels.ts +7 -0
  192. package/src/components/translation-review.tsx +105 -15
  193. package/src/components/write-lock-labels.ts +1 -0
  194. package/src/components/write-lock.tsx +36 -3
  195. package/src/data-table.ts +4 -0
  196. package/src/feedback/feedback-row-detail.tsx +18 -23
  197. package/src/feedback/feedback-swipe.tsx +28 -1
  198. package/src/i18n/german.ts +44 -4
  199. package/src/i18n/kit-labels.tsx +18 -1
  200. package/src/i18n/locales/en.ts +1 -0
  201. package/src/i18n/locales/es.ts +44 -4
  202. package/src/i18n/locales/fr.ts +44 -4
  203. package/src/i18n/locales/hu.ts +44 -4
  204. package/src/i18n/locales/it.ts +44 -4
  205. package/src/i18n/locales/zh.ts +44 -2
  206. package/src/i18n/review.ts +3 -1
  207. package/src/index.ts +50 -1
  208. package/src/lib/date-format.ts +151 -0
  209. package/src/lib/format.ts +10 -4
  210. package/src/notifications/device-zone-report.ts +67 -0
  211. package/src/notifications/feedback-notification-types-labels.ts +10 -1
  212. package/src/notifications/feedback-notification-types.ts +30 -5
  213. package/src/notifications/notification-bell.tsx +116 -0
  214. package/src/notifications/notification-inbox.tsx +314 -0
  215. package/src/notifications/notification-types-setting.tsx +151 -55
  216. package/src/notifications/notification-types.ts +20 -8
  217. package/src/notifications/notifications-labels.ts +72 -12
  218. package/src/notifications/push-devices-setting.tsx +123 -34
  219. package/src/notifications/push-worker.ts +160 -0
  220. package/src/notifications/quiet-hours-setting.tsx +41 -58
  221. package/src/notifications/time-zone-select.tsx +9 -10
  222. package/src/notifications/use-push-device.ts +20 -33
  223. package/src/offline/local-first.ts +100 -0
  224. package/src/offline/query-persistence.ts +57 -24
  225. package/src/offline/service-worker-updates.ts +81 -0
  226. package/src/offline.ts +7 -1
  227. package/src/push-worker.ts +25 -0
  228. package/src/shell/account-menu-text-size.tsx +77 -0
  229. package/src/shell/top-bar-actions.tsx +6 -1
  230. package/src/shell.ts +3 -0
  231. package/src/sync/db.ts +125 -0
  232. package/src/sync/engine.ts +701 -0
  233. package/src/sync/hooks.ts +98 -0
  234. package/src/sync/testing.ts +73 -0
  235. package/src/sync/wire.ts +61 -0
  236. package/src/sync.ts +29 -0
  237. package/src/tour/tour-labels.ts +1 -0
  238. package/src/tour/tour.tsx +100 -7
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/admin/user-roster.tsx"],"sourcesContent":["import { useMemo } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport type { DateInput } from \"../lib/format\";\nimport type { PersonName } from \"../lib/person-name\";\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { AccountStateChip, RoleChip, dateColumn } from \"../components/account-chips\";\nimport type { AccountState, RoleVocabulary } from \"../components/account-chips\";\nimport type { ChipTone } from \"../components/chip\";\nimport type { DataTableColumn } from \"../components/data-table\";\nimport { encodeSorts } from \"../components/data-table-sort\";\nimport type { SortState } from \"../components/data-table-sort\";\nimport { RowActions } from \"../components/row-actions\";\nimport type { RowAction, RowActionList } from \"../components/row-actions\";\nimport { UserAvatar } from \"../components/user-avatar\";\nimport { personLabel } from \"./admin-parts\";\nimport { RoleSelect } from \"./role-select\";\nimport type { RoleLock } from \"./role-select\";\nimport { DEFAULT_USER_ROSTER_LABELS } from \"./user-roster-labels\";\n\n/**\n * The admin's user list (docs/user-admin-harmonization.md §3): a column preset for the\n * kit's `DataTable`, not a table of its own.\n *\n * keksdose built its roster by hand (1150 lines, its own Badge), Kurvenschmiede on the\n * kit `DataTable` with the 0.18 chips, kastlan as a settings page. What the three share\n * is the row — name over address, a role, the account's states, created, last login, the\n * app's own columns, and a handful of actions — so that is what the kit draws, and the\n * table, its paging, its sort and its filters stay the app's `DataTable` wiring:\n *\n * ```tsx\n * const columns = useUserRosterColumns<AdminUserRow, Role>({ roles: ROLES, actions, extra });\n * <DataTable rows={page.users} columns={columns} rowKey={(r) => r.id}\n * serverPagination={{ page, pageSize, total: page.total, onPageChange }}\n * sorts={sorts} onSortsChange={setSorts} />\n * // GET /admin/users?sort=${userRosterSort(sorts)}&…\n * ```\n *\n * The column keys ARE the server's sort keys (§3.1): `name`, `email`, `role`, `created`,\n * `last_login` — so {@link userRosterSort} turns the table's sort state into the query\n * as it is. Phone cards come from the same columns: the identity is the card's headline,\n * the role, states, last login and the actions are its fields, created is left off.\n */\n\n/* ── Rows and states ────────────────────────────────────────────────────────── */\n\n/**\n * The fields of the contract's `AdminUserRow` the preset reads (§3.2, the auth\n * contract's `UserResponse` core). An app's row has more — its `extra` — and the\n * preset passes the whole row to every callback.\n */\nexport interface UserRosterRow extends AdminUserStateFields {\n id: string | number;\n email: string;\n first_name?: string | null;\n last_name?: string | null;\n /** The derived whole name — only the fallback when both parts are blank. */\n display_name?: string | null;\n /** keksdose, Kurvenschmiede: one role. */\n role?: string | null;\n /** kastlan: the account's roles in the current company. Wins over `role`. */\n roles?: readonly string[] | null;\n created_at?: DateInput;\n /** Account-wide; kastlan's comes from its session rows (§3.2). */\n last_login_at?: DateInput;\n}\n\n/** The state fields of the contract's row that {@link adminUserStates} maps (§3.2). */\nexport interface AdminUserStateFields {\n is_active?: boolean;\n email_verified?: boolean;\n password_change_required_at?: string | null;\n /** `after_days` mode: the day the account is erased (§6.4). */\n deletion_scheduled_at?: string | null;\n /** When the user asked. The only deletion field in kastlan's `operator` mode, where\n * nothing is scheduled. */\n deletion_requested_at?: string | null;\n}\n\n/** One state chip of a row: an {@link AccountState}, or one with its `date` (the\n * `deletion` day), a `tone` or the app's own words. */\nexport type RosterState =\n | AccountState\n | { state: AccountState; date?: DateInput; tone?: ChipTone; label?: ReactNode };\n\n/**\n * The contract's row as state chips (§3.2: \"map 1:1 to the kit's AccountStateChip\"):\n *\n * - marked for deletion → `deletion` with the scheduled day (none in `operator` mode),\n * IN PLACE of `inactive` — such an account is always deactivated (§2.1);\n * - else `active` / `inactive` from `is_active`;\n * - `unverified` when `email_verified` is `false` (not when it is missing);\n * - `passwordChange` while `password_change_required_at` is set.\n */\nexport function adminUserStates(row: AdminUserStateFields): RosterState[] {\n const out: RosterState[] = [];\n if (row.deletion_scheduled_at || row.deletion_requested_at) {\n out.push({ state: \"deletion\", date: row.deletion_scheduled_at ?? null });\n } else if (row.is_active !== undefined) {\n out.push(row.is_active ? \"active\" : \"inactive\");\n }\n if (row.email_verified === false) out.push(\"unverified\");\n if (row.password_change_required_at) out.push(\"passwordChange\");\n return out;\n}\n\n/** The server's sort keys, which are the preset's column keys (§3.1). */\nexport const USER_ROSTER_SORT_KEYS = [\"name\", \"email\", \"role\", \"created\", \"last_login\"] as const;\nexport type UserRosterSortKey = (typeof USER_ROSTER_SORT_KEYS)[number];\n\n/**\n * The table's sort state as the `sort` query parameter of `GET /admin/users` (§3.1), in\n * the DataTable's own encoding ({@link encodeSorts}: `\"last_login.desc\"`), which\n * server-kit 0.4 reads beside the `-key` form. Only the first criterion on one of the\n * server's keys: an app's own column (`owned`) is the client's to sort, and the query\n * takes one key — a shift-clicked second criterion is not sent. `undefined` when nothing\n * the server knows is sorted, so its default order applies.\n */\nexport function userRosterSort(sorts: readonly SortState[]): string | undefined {\n const first = sorts.find((s) => (USER_ROSTER_SORT_KEYS as readonly string[]).includes(s.key));\n return first ? (encodeSorts([first]) ?? undefined) : undefined;\n}\n\n/* ── Labels ─────────────────────────────────────────────────────────────────── */\n\n/** The `userRoster` namespace: the preset's headers and the action menu's words. */\nexport interface UserRosterLabels {\n /** The identity column — the name with the address under it. */\n name: string;\n /** The separate address column (`emailColumn`). */\n email: string;\n role: string;\n state: string;\n created: string;\n lastLogin: string;\n /** A last login that never happened. */\n never: string;\n /** The actions column's (hidden) header. */\n actions: string;\n /** The action menu's button and panel — \"Actions for Ada Example\". */\n actionsFor: (name: string) => string;\n}\n\n// Defined in ./user-roster-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_USER_ROSTER_LABELS };\n\n/* ── UserIdentityCell ───────────────────────────────────────────────────────── */\n\nexport interface UserIdentityCellProps {\n /** First and last name (auth §3.2), shown in the READER's order. */\n person?: PersonName | null;\n /** A whole name, for an API that still sends `display_name` only. */\n name?: string | null;\n email?: string | null;\n /** The initials avatar before the name. Default true. */\n avatar?: boolean;\n /** After the name, on its line — a \"You\" chip, a demo mark. */\n badge?: ReactNode;\n /** Over the provider's locale. */\n locale?: string;\n className?: string;\n}\n\n/**\n * Who an account is, in one cell (§3.3): the initials avatar, the name in the reader's\n * order ({@link formatPersonName} — \"Example Ada\" in Hungarian) and the address under it\n * — the cell both rosters drew by hand. An account with no name yet (a migrated row\n * before `CompleteNameDialog`, an invitee) shows its address once, as the name.\n *\n * Both lines carry `data-private`, so keksdose's demo blur reaches them, and both end\n * in \"…\" at Normal only: from Large up they wrap (`truncate-until-large`, 0.33,\n * docs/text-size-harmonization.md §10.17), since a cut name or address is a finding there.\n */\nexport function UserIdentityCell({\n person,\n name,\n email,\n avatar = true,\n badge,\n locale: localeProp,\n className,\n}: UserIdentityCellProps) {\n const locale = useKitLocale(localeProp);\n const named = personLabel({ first: person?.first, last: person?.last, name }, locale);\n const address = (email ?? \"\").trim();\n return (\n <div className={cn(\"flex min-w-0 items-center gap-2.5\", className)}>\n {avatar && <UserAvatar person={person} name={name} email={email} size=\"sm\" />}\n <div className=\"min-w-0\">\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <span data-private className=\"truncate-until-large font-medium text-primary\">\n {named || address}\n </span>\n {badge}\n </div>\n {named && address && (\n <div data-private className=\"truncate-until-large text-xs text-muted\">\n {address}\n </div>\n )}\n </div>\n </div>\n );\n}\n\n/* ── The row's actions ──────────────────────────────────────────────────────── */\n\n/** One entry of a row's action menu (§4) — the kit's {@link RowAction} since 0.32,\n * which generalised it for every dense row (docs/text-size-harmonization.md §10.8). */\nexport type UserRowAction = RowAction;\n\n/** What `actions(row)` may return: entries, and `false` / `null` for the ones a\n * condition left out (`row.is_active && {…}`). */\nexport type UserRowActionList = RowActionList;\n\nexport interface UserRowActionsProps {\n actions: UserRowActionList;\n /** The account's name, for the menu's name — \"Actions for Ada Example\". */\n name: string;\n labels?: LabelOverride<UserRosterLabels>;\n}\n\n/**\n * A row's actions as one \"…\" button and a menu of them: keksdose had a strip of icon\n * buttons in the expanded row, Kurvenschmiede five text buttons in a cell — neither fits\n * a phone card, and both grow with every action §2.4 adds. The menu is portalled, so a\n * table's scroller cannot clip it; Escape and an outside press close it, and focus goes\n * back to the button, which is where a confirmation dialog then returns it.\n *\n * Since 0.32 this is {@link RowActions} with `collapse=\"menu\"` — the menu at every size —\n * named by the roster's own `actionsFor`.\n */\nexport function UserRowActions({ actions, name, labels: labelsProp }: UserRowActionsProps) {\n const labels = useKitLabels(\"userRoster\", DEFAULT_USER_ROSTER_LABELS, labelsProp);\n return (\n <RowActions\n actions={actions}\n name={name}\n collapse=\"menu\"\n labels={{ actionsFor: labels.actionsFor }}\n />\n );\n}\n\n/* ── The preset ─────────────────────────────────────────────────────────────── */\n\n/** How a row's role may be changed inline: `true`, or with reasons. */\nexport interface RoleEditing<R extends string> {\n /** Why the whole select is locked for this row — `\"self\"`, `\"last_admin\"`, or words. */\n disabledReason?: RoleLock;\n /** Why one role cannot be chosen for this row (a string: it goes inside an option). */\n optionDisabledReason?: (role: R) => string | undefined;\n /** The roles that may be chosen. Default: the whole vocabulary. */\n offered?: readonly R[];\n}\n\nexport interface UserRosterColumnsOptions<T extends UserRosterRow, R extends string = string> {\n /** The app's roles, as `RoleChip` reads them. */\n roles: RoleVocabulary<R>;\n /** The row's state chips. Default {@link adminUserStates}, which reads the contract's\n * fields (`is_active`, `email_verified`, `password_change_required_at`,\n * `deletion_scheduled_at`, `deletion_requested_at`). */\n stateOf?: (row: T) => readonly RosterState[];\n /** The app's own columns — keksdose's plan and key custody, Kurvenschmiede's owned\n * counts — after the dates, before the actions. */\n extra?: readonly DataTableColumn<T>[];\n /** The row's action menu (§4). Left out: no actions column. */\n actions?: (row: T) => UserRowActionList;\n /**\n * Whether this row's role is a select rather than a chip, and with which reasons.\n * Left out, or `false`: a chip. kastlan's several roles are never a select here — they\n * are a \"Manage roles\" action that opens `RolesEditor`.\n */\n editableRole?: (row: T) => boolean | RoleEditing<R>;\n /** A role was picked in a row's select. The app confirms (`acknowledge`) and sends it. */\n onRoleChange?: (row: T, role: R) => void;\n /** The row's role(s). Default `row.roles ?? row.role`. */\n roleOf?: (row: T) => R | readonly R[] | null | undefined;\n /** A separate, sortable \"Email\" column (the server's `email` sort key) beside the\n * identity. Default false: the address is under the name already. */\n emailColumn?: boolean;\n /** The words, already resolved. Default English; {@link useUserRosterColumns} reads\n * the provider's `userRoster` namespace. */\n labels?: LabelOverride<UserRosterLabels>;\n}\n\nfunction rolesOf<T extends UserRosterRow, R extends string>(\n row: T,\n roleOf: UserRosterColumnsOptions<T, R>[\"roleOf\"],\n): R[] {\n const raw = roleOf ? roleOf(row) : ((row.roles ?? row.role) as R | readonly R[] | null | undefined);\n if (raw === null || raw === undefined || raw === \"\") return [];\n return typeof raw === \"string\" ? [raw as R] : [...(raw as readonly R[])];\n}\n\n/** The row's name in the reader's order — a component, because the order is the\n * READER's and the locale comes from the provider. */\nfunction RowName<T extends UserRosterRow>({ row, children }: { row: T; children: (name: string) => ReactNode }) {\n const locale = useKitLocale();\n const name = personLabel(\n { first: row.first_name, last: row.last_name, name: row.display_name, email: row.email },\n locale,\n );\n return <>{children(name)}</>;\n}\n\nfunction StateChips({ states }: { states: readonly RosterState[] }) {\n if (states.length === 0) return null;\n return (\n <div className=\"flex flex-wrap gap-1\">\n {states.map((entry) => {\n const spec = typeof entry === \"string\" ? { state: entry } : entry;\n return (\n <AccountStateChip key={spec.state} state={spec.state} date={spec.date} tone={spec.tone} size=\"xs\">\n {spec.label}\n </AccountStateChip>\n );\n })}\n </div>\n );\n}\n\n/**\n * The roster's columns, in the contract's order (§3.3):\n *\n * | key | cell | sort | phone |\n * |---|---|---|---|\n * | `name` | {@link UserIdentityCell} | last, first | the card's headline |\n * | `email` | the address (`emailColumn` only) | address | hidden |\n * | `role` | `RoleChip`(s), or {@link RoleSelect} where `editableRole` | role | field |\n * | `state` | `AccountStateChip`s from `stateOf` | — | field |\n * | `created` | `dateColumn` | instant | hidden |\n * | `last_login` | `dateColumn`, relative, \"Never\" | instant | field |\n * | …`extra` | the app's | the app's | the app's |\n * | `actions` | {@link UserRowActions} | — | field |\n *\n * Pure: hand it resolved words, or use {@link useUserRosterColumns}. The keys are stable,\n * so an app may drop or move a column (`columns.filter((c) => c.key !== \"created\")`).\n * Under `serverPagination` the `sortBy`s only make the headers clickable; the app sends\n * the sort ({@link userRosterSort}).\n */\nexport function userRosterColumns<T extends UserRosterRow, R extends string = string>({\n roles,\n stateOf = adminUserStates,\n extra = [],\n actions,\n editableRole,\n onRoleChange,\n roleOf,\n emailColumn = false,\n labels: labelsProp,\n}: UserRosterColumnsOptions<T, R>): DataTableColumn<T>[] {\n const labels: UserRosterLabels = { ...DEFAULT_USER_ROSTER_LABELS, ...labelsProp };\n const roleWords = (row: T) =>\n rolesOf(row, roleOf)\n .map((role) => roles[role]?.label ?? role)\n .join(\", \");\n const columns: DataTableColumn<T>[] = [\n {\n key: \"name\",\n header: labels.name,\n cell: (row) => (\n <UserIdentityCell\n person={{ first: row.first_name, last: row.last_name }}\n name={row.display_name}\n email={row.email}\n />\n ),\n // By last name, then first (auth §3.2), as the server sorts `name`.\n sortBy: (row) => `${row.last_name ?? \"\"} ${row.first_name ?? \"\"}`.trim().toLocaleLowerCase() || row.email,\n // Either part, in either order, or the address (auth §10.6).\n filterBy: (row) =>\n [row.first_name, row.last_name, row.last_name, row.first_name, row.display_name, row.email]\n .filter(Boolean)\n .join(\" \"),\n mobilePrimary: true,\n },\n ];\n if (emailColumn) {\n columns.push({\n key: \"email\",\n header: labels.email,\n className: \"whitespace-nowrap\",\n cell: (row) => (\n <span data-private className=\"text-secondary\">\n {row.email}\n </span>\n ),\n sortBy: (row) => row.email.toLowerCase(),\n filterBy: (row) => row.email,\n mobileHidden: true,\n });\n }\n columns.push(\n {\n key: \"role\",\n header: labels.role,\n cell: (row) => {\n const held = rolesOf(row, roleOf);\n const editing = held.length <= 1 ? editableRole?.(row) : false;\n if (editing && onRoleChange) {\n const rules: RoleEditing<R> = editing === true ? {} : editing;\n return (\n <RowName row={row}>\n {(name) => (\n <RoleSelect<R>\n size=\"sm\"\n name={name}\n value={held[0] ?? null}\n roles={roles}\n offered={rules.offered}\n disabledReason={rules.disabledReason}\n optionDisabledReason={rules.optionDisabledReason}\n onChange={(role) => onRoleChange(row, role)}\n />\n )}\n </RowName>\n );\n }\n if (held.length === 0) return null;\n return (\n <div className=\"flex flex-wrap gap-1\">\n {held.map((role) => (\n <RoleChip<R> key={role} value={role} roles={roles} size=\"xs\" />\n ))}\n </div>\n );\n },\n sortBy: roleWords,\n filter: {\n type: \"select\",\n getValue: (row) => rolesOf(row, roleOf)[0] ?? \"\",\n options: (Object.keys(roles) as R[]).map((value) => ({ value, label: roles[value].label })),\n },\n },\n {\n key: \"state\",\n header: labels.state,\n cell: (row) => <StateChips states={stateOf(row)} />,\n },\n dateColumn<T>({\n key: \"created\",\n header: labels.created,\n value: (row) => row.created_at ?? null,\n column: { mobileHidden: true },\n }),\n // How long ago rather than a date — the question is \"recently?\" — with the exact\n // time beside it (keksdose's last-seen column, Kurvenschmiede's; 0.32 §4: never only\n // in a tooltip).\n dateColumn<T>({\n key: \"last_login\",\n header: labels.lastLogin,\n value: (row) => row.last_login_at ?? null,\n display: \"relative\",\n empty: labels.never,\n }),\n ...extra,\n );\n if (actions) {\n columns.push({\n key: \"actions\",\n header: <span className=\"sr-only\">{labels.actions}</span>,\n headerText: labels.actions,\n // The caller's own words only: the menu reads the provider's itself, and the\n // English defaults merged in above must not shadow them.\n cell: (row) => (\n <RowName row={row}>\n {(name) => <UserRowActions actions={actions(row)} name={name} labels={labelsProp} />}\n </RowName>\n ),\n className: \"w-px whitespace-nowrap text-end\",\n headClassName: \"w-px\",\n // The menu button keeps its own click: a row link must not wrap it.\n noRowLink: true,\n });\n }\n return columns;\n}\n\n/**\n * {@link userRosterColumns} with the `userRoster` words from the provider, memoised on\n * the options — pass stable callbacks (`useCallback`) and a module-level vocabulary, or\n * the columns are rebuilt on every render.\n */\nexport function useUserRosterColumns<T extends UserRosterRow, R extends string = string>({\n roles,\n stateOf,\n extra,\n actions,\n editableRole,\n onRoleChange,\n roleOf,\n emailColumn,\n labels: labelsProp,\n}: UserRosterColumnsOptions<T, R>): DataTableColumn<T>[] {\n const labels = useKitLabels(\"userRoster\", DEFAULT_USER_ROSTER_LABELS, labelsProp);\n return useMemo(\n () =>\n userRosterColumns<T, R>({\n roles,\n stateOf,\n extra,\n actions,\n editableRole,\n onRoleChange,\n roleOf,\n emailColumn,\n labels,\n }),\n [roles, stateOf, extra, actions, editableRole, onRoleChange, roleOf, emailColumn, labels],\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/admin/user-roster.tsx"],"sourcesContent":["import { useMemo } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport type { DateInput } from \"../lib/format\";\nimport type { PersonName } from \"../lib/person-name\";\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { AccountStateChip, RoleChip, dateColumn } from \"../components/account-chips\";\nimport type { AccountState, RoleVocabulary } from \"../components/account-chips\";\nimport type { ChipTone } from \"../components/chip\";\nimport type { DataTableColumn } from \"../components/data-table\";\nimport { encodeSorts } from \"../components/data-table-sort\";\nimport type { SortState } from \"../components/data-table-sort\";\nimport { RowActions } from \"../components/row-actions\";\nimport type { RowAction, RowActionList } from \"../components/row-actions\";\nimport { UserAvatar } from \"../components/user-avatar\";\nimport { personLabel } from \"./admin-parts\";\nimport { RoleSelect } from \"./role-select\";\nimport type { RoleLock } from \"./role-select\";\nimport { DEFAULT_USER_ROSTER_LABELS } from \"./user-roster-labels\";\n\n/**\n * The admin's user list (docs/user-admin-harmonization.md §3): a column preset for the\n * kit's `DataTable`, not a table of its own.\n *\n * keksdose built its roster by hand (1150 lines, its own Badge), Kurvenschmiede on the\n * kit `DataTable` with the 0.18 chips, kastlan as a settings page. What the three share\n * is the row — name over address, a role, the account's states, created, last login, the\n * app's own columns, and a handful of actions — so that is what the kit draws, and the\n * table, its paging, its sort and its filters stay the app's `DataTable` wiring:\n *\n * ```tsx\n * const columns = useUserRosterColumns<AdminUserRow, Role>({ roles: ROLES, actions, extra });\n * <DataTable rows={page.users} columns={columns} rowKey={(r) => r.id}\n * serverPagination={{ page, pageSize, total: page.total, onPageChange }}\n * sorts={sorts} onSortsChange={setSorts} />\n * // GET /admin/users?sort=${userRosterSort(sorts)}&…\n * ```\n *\n * The column keys ARE the server's sort keys (§3.1): `name`, `email`, `role`, `created`,\n * `last_login` — so {@link userRosterSort} turns the table's sort state into the query\n * as it is. Phone cards come from the same columns: the identity is the card's headline,\n * the role, states, last login and the actions are its fields, created is left off.\n */\n\n/* ── Rows and states ────────────────────────────────────────────────────────── */\n\n/**\n * The fields of the contract's `AdminUserRow` the preset reads (§3.2, the auth\n * contract's `UserResponse` core). An app's row has more — its `extra` — and the\n * preset passes the whole row to every callback.\n */\nexport interface UserRosterRow extends AdminUserStateFields {\n id: string | number;\n email: string;\n first_name?: string | null;\n last_name?: string | null;\n /** The derived whole name — only the fallback when both parts are blank. */\n display_name?: string | null;\n /** keksdose, Kurvenschmiede: one role. */\n role?: string | null;\n /** kastlan: the account's roles in the current company. Wins over `role`. */\n roles?: readonly string[] | null;\n created_at?: DateInput;\n /** Account-wide; kastlan's comes from its session rows (§3.2). */\n last_login_at?: DateInput;\n}\n\n/** The state fields of the contract's row that {@link adminUserStates} maps (§3.2). */\nexport interface AdminUserStateFields {\n is_active?: boolean;\n email_verified?: boolean;\n password_change_required_at?: string | null;\n /** `after_days` mode: the day the account is erased (§6.4). */\n deletion_scheduled_at?: string | null;\n /** When the user asked. The only deletion field in kastlan's `operator` mode, where\n * nothing is scheduled. */\n deletion_requested_at?: string | null;\n}\n\n/** One state chip of a row: an {@link AccountState}, or one with its `date` (the\n * `deletion` day), a `tone` or the app's own words. */\nexport type RosterState =\n | AccountState\n | { state: AccountState; date?: DateInput; tone?: ChipTone; label?: ReactNode };\n\n/**\n * The contract's row as state chips (§3.2: \"map 1:1 to the kit's AccountStateChip\"):\n *\n * - marked for deletion → `deletion` with the scheduled day (none in `operator` mode),\n * IN PLACE of `inactive` — such an account is always deactivated (§2.1);\n * - else `active` / `inactive` from `is_active`;\n * - `unverified` when `email_verified` is `false` (not when it is missing);\n * - `passwordChange` while `password_change_required_at` is set.\n */\nexport function adminUserStates(row: AdminUserStateFields): RosterState[] {\n const out: RosterState[] = [];\n if (row.deletion_scheduled_at || row.deletion_requested_at) {\n out.push({ state: \"deletion\", date: row.deletion_scheduled_at ?? null });\n } else if (row.is_active !== undefined) {\n out.push(row.is_active ? \"active\" : \"inactive\");\n }\n if (row.email_verified === false) out.push(\"unverified\");\n if (row.password_change_required_at) out.push(\"passwordChange\");\n return out;\n}\n\n/** The server's sort keys, which are the preset's column keys (§3.1). */\nexport const USER_ROSTER_SORT_KEYS = [\"name\", \"email\", \"role\", \"created\", \"last_login\"] as const;\nexport type UserRosterSortKey = (typeof USER_ROSTER_SORT_KEYS)[number];\n\n/**\n * The table's sort state as the `sort` query parameter of `GET /admin/users` (§3.1), in\n * the DataTable's own encoding ({@link encodeSorts}: `\"last_login.desc\"`), which\n * server-kit 0.4 reads beside the `-key` form. Only the first criterion on one of the\n * server's keys: an app's own column (`owned`) is the client's to sort, and the query\n * takes one key — a shift-clicked second criterion is not sent. `undefined` when nothing\n * the server knows is sorted, so its default order applies.\n */\nexport function userRosterSort(sorts: readonly SortState[]): string | undefined {\n const first = sorts.find((s) => (USER_ROSTER_SORT_KEYS as readonly string[]).includes(s.key));\n return first ? (encodeSorts([first]) ?? undefined) : undefined;\n}\n\n/* ── Labels ─────────────────────────────────────────────────────────────────── */\n\n/** The `userRoster` namespace: the preset's headers and the action menu's words. */\nexport interface UserRosterLabels {\n /** The identity column — the name with the address under it. */\n name: string;\n /** The separate address column (`emailColumn`). */\n email: string;\n role: string;\n state: string;\n created: string;\n lastLogin: string;\n /** A last login that never happened. */\n never: string;\n /** The actions column's (hidden) header. */\n actions: string;\n /** The action menu's button and panel — \"Actions for Ada Example\". */\n actionsFor: (name: string) => string;\n}\n\n// Defined in ./user-roster-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_USER_ROSTER_LABELS };\n\n/* ── UserIdentityCell ───────────────────────────────────────────────────────── */\n\nexport interface UserIdentityCellProps {\n /** First and last name (auth §3.2), shown in the READER's order. */\n person?: PersonName | null;\n /** A whole name, for an API that still sends `display_name` only. */\n name?: string | null;\n email?: string | null;\n /** The initials avatar before the name. Default true. */\n avatar?: boolean;\n /** After the name, on its line — a \"You\" chip, a demo mark. */\n badge?: ReactNode;\n /** Over the provider's locale. */\n locale?: string;\n className?: string;\n}\n\n/**\n * Who an account is, in one cell (§3.3): the initials avatar, the name in the reader's\n * order ({@link formatPersonName} — \"Example Ada\" in Hungarian) and the address under it\n * — the cell both rosters drew by hand. An account with no name yet (a migrated row\n * before `CompleteNameDialog`, an invitee) shows its address once, as the name.\n *\n * Both lines carry `data-private`, so keksdose's demo blur reaches them, and both end\n * in \"…\" at Normal only: from Large up they wrap (`truncate-until-large`, 0.33,\n * docs/text-size-harmonization.md §10.17), since a cut name or address is a finding there.\n */\nexport function UserIdentityCell({\n person,\n name,\n email,\n avatar = true,\n badge,\n locale: localeProp,\n className,\n}: UserIdentityCellProps) {\n const locale = useKitLocale(localeProp);\n const named = personLabel({ first: person?.first, last: person?.last, name }, locale);\n const address = (email ?? \"\").trim();\n return (\n <div className={cn(\"flex min-w-0 items-center gap-2.5\", className)}>\n {avatar && <UserAvatar person={person} name={name} email={email} size=\"sm\" />}\n <div className=\"min-w-0\">\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <span data-private className=\"truncate-until-large font-medium text-primary\">\n {named || address}\n </span>\n {badge}\n </div>\n {named && address && (\n <div data-private className=\"truncate-until-large text-xs text-muted\">\n {address}\n </div>\n )}\n </div>\n </div>\n );\n}\n\n/* ── The row's actions ──────────────────────────────────────────────────────── */\n\n/** One entry of a row's action menu (§4) — the kit's {@link RowAction} since 0.32,\n * which generalised it for every dense row (docs/text-size-harmonization.md §10.8). */\nexport type UserRowAction = RowAction;\n\n/** What `actions(row)` may return: entries, and `false` / `null` for the ones a\n * condition left out (`row.is_active && {…}`). */\nexport type UserRowActionList = RowActionList;\n\nexport interface UserRowActionsProps {\n actions: UserRowActionList;\n /** The account's name, for the menu's name — \"Actions for Ada Example\". */\n name: string;\n labels?: LabelOverride<UserRosterLabels>;\n}\n\n/**\n * A row's actions as one \"…\" button and a menu of them: keksdose had a strip of icon\n * buttons in the expanded row, Kurvenschmiede five text buttons in a cell — neither fits\n * a phone card, and both grow with every action §2.4 adds. The menu is portalled, so a\n * table's scroller cannot clip it; Escape and an outside press close it, and focus goes\n * back to the button, which is where a confirmation dialog then returns it.\n *\n * Since 0.32 this is {@link RowActions} with `collapse=\"menu\"` — the menu at every size —\n * named by the roster's own `actionsFor`.\n */\nexport function UserRowActions({ actions, name, labels: labelsProp }: UserRowActionsProps) {\n const labels = useKitLabels(\"userRoster\", DEFAULT_USER_ROSTER_LABELS, labelsProp);\n return (\n <RowActions\n actions={actions}\n name={name}\n collapse=\"menu\"\n labels={{ actionsFor: labels.actionsFor }}\n />\n );\n}\n\n/* ── The preset ─────────────────────────────────────────────────────────────── */\n\n/** How a row's role may be changed inline: `true`, or with reasons. */\nexport interface RoleEditing<R extends string> {\n /** Why the whole select is locked for this row — `\"self\"`, `\"last_admin\"`, or words. */\n disabledReason?: RoleLock;\n /** Why one role cannot be chosen for this row (a string: it goes inside an option). */\n optionDisabledReason?: (role: R) => string | undefined;\n /** The roles that may be chosen. Default: the whole vocabulary. */\n offered?: readonly R[];\n}\n\nexport interface UserRosterColumnsOptions<T extends UserRosterRow, R extends string = string> {\n /** The app's roles, as `RoleChip` reads them. */\n roles: RoleVocabulary<R>;\n /** The row's state chips. Default {@link adminUserStates}, which reads the contract's\n * fields (`is_active`, `email_verified`, `password_change_required_at`,\n * `deletion_scheduled_at`, `deletion_requested_at`). */\n stateOf?: (row: T) => readonly RosterState[];\n /** The app's own columns — keksdose's plan and key custody, Kurvenschmiede's owned\n * counts — after the dates, before the actions. */\n extra?: readonly DataTableColumn<T>[];\n /** The row's action menu (§4). Left out: no actions column. */\n actions?: (row: T) => UserRowActionList;\n /** The actions column's heading (0.46.1, keksdose dev#598 rework — a VISIBLE \"Actions\"\n * on every table with an actions column). Default: `labels.actions`, shown. A node\n * replaces it; `false` keeps the pre-0.46.1 heading, a screen reader's name only. */\n actionsHeader?: ReactNode | false;\n /**\n * Whether this row's role is a select rather than a chip, and with which reasons.\n * Left out, or `false`: a chip. kastlan's several roles are never a select here — they\n * are a \"Manage roles\" action that opens `RolesEditor`.\n */\n editableRole?: (row: T) => boolean | RoleEditing<R>;\n /** A role was picked in a row's select. The app confirms (`acknowledge`) and sends it. */\n onRoleChange?: (row: T, role: R) => void;\n /** The row's role(s). Default `row.roles ?? row.role`. */\n roleOf?: (row: T) => R | readonly R[] | null | undefined;\n /** A separate, sortable \"Email\" column (the server's `email` sort key) beside the\n * identity. Default false: the address is under the name already. */\n emailColumn?: boolean;\n /** The words, already resolved. Default English; {@link useUserRosterColumns} reads\n * the provider's `userRoster` namespace. */\n labels?: LabelOverride<UserRosterLabels>;\n}\n\nfunction rolesOf<T extends UserRosterRow, R extends string>(\n row: T,\n roleOf: UserRosterColumnsOptions<T, R>[\"roleOf\"],\n): R[] {\n const raw = roleOf ? roleOf(row) : ((row.roles ?? row.role) as R | readonly R[] | null | undefined);\n if (raw === null || raw === undefined || raw === \"\") return [];\n return typeof raw === \"string\" ? [raw as R] : [...(raw as readonly R[])];\n}\n\n/** The row's name in the reader's order — a component, because the order is the\n * READER's and the locale comes from the provider. */\nfunction RowName<T extends UserRosterRow>({ row, children }: { row: T; children: (name: string) => ReactNode }) {\n const locale = useKitLocale();\n const name = personLabel(\n { first: row.first_name, last: row.last_name, name: row.display_name, email: row.email },\n locale,\n );\n return <>{children(name)}</>;\n}\n\nfunction StateChips({ states }: { states: readonly RosterState[] }) {\n if (states.length === 0) return null;\n return (\n <div className=\"flex flex-wrap gap-1\">\n {states.map((entry) => {\n const spec = typeof entry === \"string\" ? { state: entry } : entry;\n return (\n <AccountStateChip key={spec.state} state={spec.state} date={spec.date} tone={spec.tone} size=\"xs\">\n {spec.label}\n </AccountStateChip>\n );\n })}\n </div>\n );\n}\n\n/**\n * The roster's columns, in the contract's order (§3.3):\n *\n * | key | cell | sort | phone |\n * |---|---|---|---|\n * | `name` | {@link UserIdentityCell} | last, first | the card's headline |\n * | `email` | the address (`emailColumn` only) | address | hidden |\n * | `role` | `RoleChip`(s), or {@link RoleSelect} where `editableRole` | role | field |\n * | `state` | `AccountStateChip`s from `stateOf` | — | field |\n * | `created` | `dateColumn` | instant | hidden |\n * | `last_login` | `dateColumn`, relative, \"Never\" | instant | field |\n * | …`extra` | the app's | the app's | the app's |\n * | `actions` | {@link UserRowActions} | — | field |\n *\n * Pure: hand it resolved words, or use {@link useUserRosterColumns}. The keys are stable,\n * so an app may drop or move a column (`columns.filter((c) => c.key !== \"created\")`).\n * Under `serverPagination` the `sortBy`s only make the headers clickable; the app sends\n * the sort ({@link userRosterSort}).\n */\nexport function userRosterColumns<T extends UserRosterRow, R extends string = string>({\n roles,\n stateOf = adminUserStates,\n extra = [],\n actions,\n actionsHeader,\n editableRole,\n onRoleChange,\n roleOf,\n emailColumn = false,\n labels: labelsProp,\n}: UserRosterColumnsOptions<T, R>): DataTableColumn<T>[] {\n const labels: UserRosterLabels = { ...DEFAULT_USER_ROSTER_LABELS, ...labelsProp };\n const roleWords = (row: T) =>\n rolesOf(row, roleOf)\n .map((role) => roles[role]?.label ?? role)\n .join(\", \");\n const columns: DataTableColumn<T>[] = [\n {\n key: \"name\",\n header: labels.name,\n cell: (row) => (\n <UserIdentityCell\n person={{ first: row.first_name, last: row.last_name }}\n name={row.display_name}\n email={row.email}\n />\n ),\n // By last name, then first (auth §3.2), as the server sorts `name`.\n sortBy: (row) => `${row.last_name ?? \"\"} ${row.first_name ?? \"\"}`.trim().toLocaleLowerCase() || row.email,\n // Either part, in either order, or the address (auth §10.6).\n filterBy: (row) =>\n [row.first_name, row.last_name, row.last_name, row.first_name, row.display_name, row.email]\n .filter(Boolean)\n .join(\" \"),\n mobilePrimary: true,\n },\n ];\n if (emailColumn) {\n columns.push({\n key: \"email\",\n header: labels.email,\n className: \"whitespace-nowrap\",\n cell: (row) => (\n <span data-private className=\"text-secondary\">\n {row.email}\n </span>\n ),\n sortBy: (row) => row.email.toLowerCase(),\n filterBy: (row) => row.email,\n mobileHidden: true,\n });\n }\n columns.push(\n {\n key: \"role\",\n header: labels.role,\n cell: (row) => {\n const held = rolesOf(row, roleOf);\n const editing = held.length <= 1 ? editableRole?.(row) : false;\n if (editing && onRoleChange) {\n const rules: RoleEditing<R> = editing === true ? {} : editing;\n return (\n <RowName row={row}>\n {(name) => (\n <RoleSelect<R>\n size=\"sm\"\n name={name}\n value={held[0] ?? null}\n roles={roles}\n offered={rules.offered}\n disabledReason={rules.disabledReason}\n optionDisabledReason={rules.optionDisabledReason}\n onChange={(role) => onRoleChange(row, role)}\n />\n )}\n </RowName>\n );\n }\n if (held.length === 0) return null;\n return (\n <div className=\"flex flex-wrap gap-1\">\n {held.map((role) => (\n <RoleChip<R> key={role} value={role} roles={roles} size=\"xs\" />\n ))}\n </div>\n );\n },\n sortBy: roleWords,\n filter: {\n type: \"select\",\n getValue: (row) => rolesOf(row, roleOf)[0] ?? \"\",\n options: (Object.keys(roles) as R[]).map((value) => ({ value, label: roles[value].label })),\n },\n },\n {\n key: \"state\",\n header: labels.state,\n cell: (row) => <StateChips states={stateOf(row)} />,\n },\n dateColumn<T>({\n key: \"created\",\n header: labels.created,\n value: (row) => row.created_at ?? null,\n column: { mobileHidden: true },\n }),\n // How long ago rather than a date — the question is \"recently?\" — with the exact\n // time beside it (keksdose's last-seen column, Kurvenschmiede's; 0.32 §4: never only\n // in a tooltip).\n dateColumn<T>({\n key: \"last_login\",\n header: labels.lastLogin,\n value: (row) => row.last_login_at ?? null,\n display: \"relative\",\n empty: labels.never,\n }),\n ...extra,\n );\n if (actions) {\n columns.push({\n key: \"actions\",\n // On screen, as every actions column's heading is (0.46.1, keksdose dev#598 rework).\n header:\n actionsHeader === false ? <span className=\"sr-only\">{labels.actions}</span> : (actionsHeader ?? labels.actions),\n headerText: labels.actions,\n // The caller's own words only: the menu reads the provider's itself, and the\n // English defaults merged in above must not shadow them.\n cell: (row) => (\n <RowName row={row}>\n {(name) => <UserRowActions actions={actions(row)} name={name} labels={labelsProp} />}\n </RowName>\n ),\n className: \"w-px whitespace-nowrap text-end\",\n headClassName: \"w-px text-end\",\n // The menu button keeps its own click: a row link must not wrap it.\n noRowLink: true,\n });\n }\n return columns;\n}\n\n/**\n * {@link userRosterColumns} with the `userRoster` words from the provider, memoised on\n * the options — pass stable callbacks (`useCallback`) and a module-level vocabulary, or\n * the columns are rebuilt on every render.\n */\nexport function useUserRosterColumns<T extends UserRosterRow, R extends string = string>({\n roles,\n stateOf,\n extra,\n actions,\n actionsHeader,\n editableRole,\n onRoleChange,\n roleOf,\n emailColumn,\n labels: labelsProp,\n}: UserRosterColumnsOptions<T, R>): DataTableColumn<T>[] {\n const labels = useKitLabels(\"userRoster\", DEFAULT_USER_ROSTER_LABELS, labelsProp);\n return useMemo(\n () =>\n userRosterColumns<T, R>({\n roles,\n stateOf,\n extra,\n actions,\n actionsHeader,\n editableRole,\n onRoleChange,\n roleOf,\n emailColumn,\n labels,\n }),\n [roles, stateOf, extra, actions, actionsHeader, editableRole, onRoleChange, roleOf, emailColumn, labels],\n );\n}\n"],"mappings":"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","names":[]}
@@ -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":[]}
@@ -1,8 +1,38 @@
1
+ import type { ReactNode } from "react";
2
+ import { type RowAction } from "./row-actions.js";
1
3
  import type { DataTableLabels } from "./data-table-labels.js";
2
- import type { DataTableColumn, DataTableDensity, DataTableProps, MobileSwipeActions } from "./data-table.js";
4
+ import type { DataTableColumn, DataTableDensity, DataTableDesktopSwipeActions, DataTableProps, MobileSwipeActions } from "./data-table.js";
3
5
  /** The actions column's key. Not a column a caller declares, so it is kept out of the
4
6
  * column-settings panel. */
5
7
  export declare const ROW_ACTIONS_KEY = "__rowActions";
8
+ /** The key of the column derived from `mobileSwipeActions` (0.46, dev#598) — the
9
+ * table's own too. */
10
+ export declare const SWIPE_ACTIONS_KEY = "__swipeActions";
11
+ /** A column the TABLE adds, not the caller: out of the column settings and the count,
12
+ * never resized, never the totals row's header. */
13
+ export declare function isTableOwnColumn(key: string): boolean;
14
+ /**
15
+ * An actions column's heading (0.46.1, keksdose dev#598 rework — Marcel: a VISIBLE
16
+ * "Actions" heading on every table with an actions column): the caller's node, else the
17
+ * word on screen; `false` is the pre-0.46.1 heading, a screen reader's name only.
18
+ */
19
+ export declare function actionsHeading(header: ReactNode | false | undefined, word: string): ReactNode;
20
+ /**
21
+ * A row's swipe plan as desktop row actions (dev#598) — what DataTable's
22
+ * `desktopSwipeActions` column draws, for an app that builds its own actions column (or
23
+ * a `RowActions` outside a table) from the same `mobileSwipeActions` plan, so the two
24
+ * cannot drift (public since 0.46.1, keksdose N3).
25
+ *
26
+ * `end` then `start` — the physical `right` / `left` only where the logical side is
27
+ * missing, as the card reads them — each action once, told apart by its `id`, else by its
28
+ * label (two stages with one label would be one button twice). Each button runs the
29
+ * swipe's own `onCommit`; a swipe's `danger` / `warning` / `info` tone carries over, any
30
+ * other reads `muted`. `options.omit` leaves an action out, `options.commit` gives it a
31
+ * `CommitScope` (see {@link DataTableDesktopSwipeActions}). A `null` plan is `[]`.
32
+ *
33
+ * <RowActions actions={swipePlanRowActions(plan(row), row, { omit })} name={row.title} />
34
+ */
35
+ export declare function swipePlanRowActions<T>(plan: MobileSwipeActions | null | undefined, row: T, options?: Pick<DataTableDesktopSwipeActions<T>, "omit" | "commit">): RowAction[];
6
36
  /**
7
37
  * The caller's columns with the actions column appended, and the same actions as a
8
38
  * phone card's swipes (`swipesFromActions`; DataTable uses it unless the caller passes
@@ -12,12 +42,15 @@ export declare const ROW_ACTIONS_KEY = "__rowActions";
12
42
  * second special case beside the selection column: it then gets the cell padding,
13
43
  * the widths and the phone rules (`mobileHidden`) every other column gets.
14
44
  */
15
- export declare function useRowActionColumn<T>({ columns: columnsProp, rowActions, rowName, labels, density, }: {
45
+ export declare function useRowActionColumn<T>({ columns: columnsProp, rowActions, rowActionsHeader, rowName, labels, density, mobileSwipeActions, desktopSwipeActions, }: {
16
46
  columns: DataTableColumn<T>[];
17
47
  rowActions: DataTableProps<T>["rowActions"];
48
+ rowActionsHeader?: DataTableProps<T>["rowActionsHeader"];
18
49
  rowName: DataTableProps<T>["rowName"];
19
50
  labels: DataTableLabels;
20
51
  density: DataTableDensity;
52
+ mobileSwipeActions?: DataTableProps<T>["mobileSwipeActions"];
53
+ desktopSwipeActions?: DataTableProps<T>["desktopSwipeActions"];
21
54
  }): {
22
55
  columns: DataTableColumn<T>[];
23
56
  swipesFromActions: (row: T) => MobileSwipeActions | null;
@@ -4,6 +4,45 @@ import { isValidElement, useCallback, useMemo } from "react";
4
4
  import { Pencil, Trash2 } from "lucide-react";
5
5
  import { RowActions } from "./row-actions.js";
6
6
  const ROW_ACTIONS_KEY = "__rowActions";
7
+ const SWIPE_ACTIONS_KEY = "__swipeActions";
8
+ function isTableOwnColumn(key) {
9
+ return key === ROW_ACTIONS_KEY || key === SWIPE_ACTIONS_KEY;
10
+ }
11
+ function actionsHeading(header, word) {
12
+ if (header === false) return /* @__PURE__ */ jsx("span", { className: "sr-only", children: word });
13
+ return header ?? word;
14
+ }
15
+ function swipeTone(action) {
16
+ switch (action.tone) {
17
+ case "danger":
18
+ case "warning":
19
+ case "info":
20
+ return action.tone;
21
+ default:
22
+ return "muted";
23
+ }
24
+ }
25
+ function swipePlanRowActions(plan, row, options = {}) {
26
+ if (!plan) return [];
27
+ const out = [];
28
+ const seen = /* @__PURE__ */ new Set();
29
+ for (const action of [...plan.end ?? plan.right ?? [], ...plan.start ?? plan.left ?? []]) {
30
+ const key = action.id ?? action.label;
31
+ if (seen.has(key)) continue;
32
+ seen.add(key);
33
+ if (options.omit?.(action, row)) continue;
34
+ const commit = typeof options.commit === "function" ? options.commit(action, row) : options.commit;
35
+ out.push({
36
+ key,
37
+ label: action.label,
38
+ icon: isValidElement(action.icon) ? action.icon : action.icon != null && action.icon !== false ? /* @__PURE__ */ jsx(Fragment, { children: action.icon }) : void 0,
39
+ tone: swipeTone(action),
40
+ commit,
41
+ onSelect: action.onCommit
42
+ });
43
+ }
44
+ return out;
45
+ }
7
46
  function resolveRowAction(action, index, labels) {
8
47
  const tone = action.tone ?? (action.kind === "delete" ? "danger" : "default");
9
48
  return {
@@ -32,20 +71,66 @@ function toRowAction(a, row) {
32
71
  function useRowActionColumn({
33
72
  columns: columnsProp,
34
73
  rowActions,
74
+ rowActionsHeader,
35
75
  rowName,
36
76
  labels,
37
- density
77
+ density,
78
+ mobileSwipeActions,
79
+ desktopSwipeActions
38
80
  }) {
39
81
  const rowActionsFor = useCallback(
40
82
  (row) => rowActions ? (typeof rowActions === "function" ? rowActions(row) : rowActions).filter((a) => !a.hidden?.(row)).map((a, i) => resolveRowAction(a, i, labels)) : [],
41
83
  [rowActions, labels]
42
84
  );
43
- const columns = useMemo(
85
+ const swipeOptions = typeof desktopSwipeActions === "object" ? desktopSwipeActions : void 0;
86
+ const swipeColumnOn = !!mobileSwipeActions && (desktopSwipeActions === void 0 ? !rowActions : desktopSwipeActions !== false);
87
+ const swipeOmit = swipeOptions?.omit;
88
+ const swipeName = swipeOptions?.name;
89
+ const swipeCollapse = swipeOptions?.collapse;
90
+ const swipeCommit = swipeOptions?.commit;
91
+ const swipeHeader = swipeOptions?.header;
92
+ const swipeColumn = useMemo(
93
+ () => swipeColumnOn ? {
94
+ key: SWIPE_ACTIONS_KEY,
95
+ header: actionsHeading(swipeHeader, labels.actions),
96
+ cell: (row) => /* @__PURE__ */ jsx(
97
+ RowActions,
98
+ {
99
+ actions: swipePlanRowActions(mobileSwipeActions(row), row, { omit: swipeOmit, commit: swipeCommit }),
100
+ name: (swipeName ?? rowName)?.(row),
101
+ collapse: swipeCollapse,
102
+ size: density === "compact" ? "2xs" : "xs",
103
+ tooltipSide: "start",
104
+ labels: { actions: labels.actions },
105
+ className: "justify-end"
106
+ }
107
+ ),
108
+ className: "w-px whitespace-nowrap text-end",
109
+ // The heading over the buttons, at the row's end (0.46.1).
110
+ headClassName: "w-px text-end",
111
+ // On a phone the same actions are the card's swipes.
112
+ mobileHidden: true,
113
+ noRowLink: true
114
+ } : null,
115
+ [
116
+ swipeColumnOn,
117
+ mobileSwipeActions,
118
+ swipeOmit,
119
+ swipeCommit,
120
+ swipeName,
121
+ rowName,
122
+ swipeCollapse,
123
+ swipeHeader,
124
+ density,
125
+ labels.actions
126
+ ]
127
+ );
128
+ const actionColumns = useMemo(
44
129
  () => rowActions ? [
45
130
  ...columnsProp,
46
131
  {
47
132
  key: ROW_ACTIONS_KEY,
48
- header: /* @__PURE__ */ jsx("span", { className: "sr-only", children: labels.actions }),
133
+ header: actionsHeading(rowActionsHeader, labels.actions),
49
134
  // Through RowActions (0.32, §10.8): the icons inline at Normal, one "⋯"
50
135
  // menu at Large once there are two — every label at once does not fit a
51
136
  // row. Each control stops its own click: the row's handler already skips
@@ -62,13 +147,17 @@ function useRowActionColumn({
62
147
  }
63
148
  ),
64
149
  className: "w-px whitespace-nowrap text-end",
65
- headClassName: "w-px",
150
+ headClassName: "w-px text-end",
66
151
  // On a phone the same actions are the card's swipes.
67
152
  mobileHidden: true,
68
153
  noRowLink: true
69
154
  }
70
155
  ] : columnsProp,
71
- [columnsProp, rowActions, rowActionsFor, rowName, labels.actions, density]
156
+ [columnsProp, rowActions, rowActionsHeader, rowActionsFor, rowName, labels.actions, density]
157
+ );
158
+ const columns = useMemo(
159
+ () => swipeColumn ? [...actionColumns, swipeColumn] : actionColumns,
160
+ [actionColumns, swipeColumn]
72
161
  );
73
162
  const swipesFromActions = (row) => {
74
163
  const swipes = rowActionsFor(row).filter((a) => a.swipe && !a.action.disabledReason?.(row));
@@ -90,6 +179,10 @@ function useRowActionColumn({
90
179
  }
91
180
  export {
92
181
  ROW_ACTIONS_KEY,
182
+ SWIPE_ACTIONS_KEY,
183
+ actionsHeading,
184
+ isTableOwnColumn,
185
+ swipePlanRowActions,
93
186
  useRowActionColumn
94
187
  };
95
188
  //# sourceMappingURL=data-table-row-actions.js.map