@eifi1/ui-kit 0.44.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +10 -6
  2. package/dist/admin/admin-action-log-labels.js +9 -0
  3. package/dist/admin/admin-action-log-labels.js.map +1 -1
  4. package/dist/admin/admin-action-log.d.ts +18 -1
  5. package/dist/admin/admin-action-log.js +25 -16
  6. package/dist/admin/admin-action-log.js.map +1 -1
  7. package/dist/admin/admin-area-labels.d.ts +6 -0
  8. package/dist/admin/admin-area-labels.js +1 -0
  9. package/dist/admin/admin-area-labels.js.map +1 -1
  10. package/dist/admin/mail-test-card-labels.d.ts +3 -0
  11. package/dist/admin/mail-test-card-labels.js +17 -0
  12. package/dist/admin/mail-test-card-labels.js.map +1 -0
  13. package/dist/admin/mail-test-card.d.ts +74 -0
  14. package/dist/admin/mail-test-card.js +80 -0
  15. package/dist/admin/mail-test-card.js.map +1 -0
  16. package/dist/components/data-table-mobile.js +17 -2
  17. package/dist/components/data-table-mobile.js.map +1 -1
  18. package/dist/components/data-table-shared.d.ts +2 -0
  19. package/dist/components/data-table-shared.js +8 -0
  20. package/dist/components/data-table-shared.js.map +1 -1
  21. package/dist/components/data-table.js +1 -6
  22. package/dist/components/data-table.js.map +1 -1
  23. package/dist/eslint/index.js +1 -1
  24. package/dist/i18n/defaults.js +9 -1
  25. package/dist/i18n/defaults.js.map +1 -1
  26. package/dist/i18n/german.js +40 -0
  27. package/dist/i18n/german.js.map +1 -1
  28. package/dist/i18n/kit-labels.d.ts +13 -0
  29. package/dist/i18n/kit-labels.js.map +1 -1
  30. package/dist/i18n/locales/en.js +57 -1
  31. package/dist/i18n/locales/en.js.map +3 -3
  32. package/dist/i18n/locales/es.js +40 -0
  33. package/dist/i18n/locales/es.js.map +1 -1
  34. package/dist/i18n/locales/fr.js +40 -0
  35. package/dist/i18n/locales/fr.js.map +1 -1
  36. package/dist/i18n/locales/hu.js +40 -0
  37. package/dist/i18n/locales/hu.js.map +1 -1
  38. package/dist/i18n/locales/it.js +40 -0
  39. package/dist/i18n/locales/it.js.map +1 -1
  40. package/dist/i18n/locales/zh.js +40 -0
  41. package/dist/i18n/locales/zh.js.map +1 -1
  42. package/dist/i18n/review.js +13 -1
  43. package/dist/i18n/review.js.map +1 -1
  44. package/dist/index.d.ts +2 -0
  45. package/dist/index.js +5 -0
  46. package/dist/index.js.map +1 -1
  47. package/dist/lib/format.d.ts +2 -1
  48. package/dist/lib/format.js +6 -1
  49. package/dist/lib/format.js.map +1 -1
  50. package/dist/offline/pwa-update-prompt-labels.d.ts +2 -0
  51. package/dist/offline/pwa-update-prompt-labels.js +9 -0
  52. package/dist/offline/pwa-update-prompt-labels.js.map +1 -0
  53. package/dist/offline/pwa-update-prompt.d.ts +32 -0
  54. package/dist/offline/pwa-update-prompt.js +36 -0
  55. package/dist/offline/pwa-update-prompt.js.map +1 -0
  56. package/dist/offline/query-persistence.d.ts +100 -0
  57. package/dist/offline/query-persistence.js +179 -0
  58. package/dist/offline/query-persistence.js.map +1 -0
  59. package/dist/offline/shared-computer-labels.d.ts +2 -0
  60. package/dist/offline/shared-computer-labels.js +9 -0
  61. package/dist/offline/shared-computer-labels.js.map +1 -0
  62. package/dist/offline/shared-computer.d.ts +44 -0
  63. package/dist/offline/shared-computer.js +51 -0
  64. package/dist/offline/shared-computer.js.map +1 -0
  65. package/dist/offline/sign-out-pending-dialog-labels.d.ts +2 -0
  66. package/dist/offline/sign-out-pending-dialog-labels.js +14 -0
  67. package/dist/offline/sign-out-pending-dialog-labels.js.map +1 -0
  68. package/dist/offline/sign-out-pending-dialog.d.ts +58 -0
  69. package/dist/offline/sign-out-pending-dialog.js +66 -0
  70. package/dist/offline/sign-out-pending-dialog.js.map +1 -0
  71. package/dist/offline.d.ts +8 -0
  72. package/dist/offline.js +24 -0
  73. package/dist/offline.js.map +1 -0
  74. package/dist/shell/admin-menu.d.ts +24 -0
  75. package/dist/shell/admin-menu.js +12 -3
  76. package/dist/shell/admin-menu.js.map +1 -1
  77. package/dist/shell.d.ts +2 -0
  78. package/dist/shell.js +2 -0
  79. package/dist/shell.js.map +1 -1
  80. package/dist/tour/tour-catalogue.d.ts +7 -1
  81. package/dist/tour/tour-catalogue.js.map +1 -1
  82. package/dist/tour/tour.js +4 -2
  83. package/dist/tour/tour.js.map +1 -1
  84. package/dist/tour/use-tour-progress.d.ts +99 -1
  85. package/dist/tour/use-tour-progress.js +228 -12
  86. package/dist/tour/use-tour-progress.js.map +1 -1
  87. package/dist/tour.d.ts +2 -0
  88. package/dist/tour.js +2 -0
  89. package/dist/tour.js.map +1 -1
  90. package/package.json +25 -1
  91. package/src/admin/admin-action-log-labels.ts +9 -0
  92. package/src/admin/admin-action-log.tsx +41 -5
  93. package/src/admin/admin-area-labels.ts +7 -0
  94. package/src/admin/mail-test-card-labels.ts +19 -0
  95. package/src/admin/mail-test-card.tsx +186 -0
  96. package/src/components/data-table-mobile.tsx +26 -3
  97. package/src/components/data-table-shared.tsx +18 -1
  98. package/src/components/data-table.tsx +1 -15
  99. package/src/i18n/defaults.ts +8 -0
  100. package/src/i18n/german.ts +46 -0
  101. package/src/i18n/kit-labels.tsx +13 -0
  102. package/src/i18n/locales/en.ts +12 -0
  103. package/src/i18n/locales/es.ts +46 -0
  104. package/src/i18n/locales/fr.ts +46 -0
  105. package/src/i18n/locales/hu.ts +42 -0
  106. package/src/i18n/locales/it.ts +46 -0
  107. package/src/i18n/locales/zh.ts +40 -0
  108. package/src/i18n/review.ts +12 -0
  109. package/src/index.ts +3 -0
  110. package/src/lib/format.ts +13 -2
  111. package/src/offline/pwa-update-prompt-labels.ts +9 -0
  112. package/src/offline/pwa-update-prompt.tsx +72 -0
  113. package/src/offline/query-persistence.ts +323 -0
  114. package/src/offline/shared-computer-labels.ts +8 -0
  115. package/src/offline/shared-computer.tsx +87 -0
  116. package/src/offline/sign-out-pending-dialog-labels.ts +21 -0
  117. package/src/offline/sign-out-pending-dialog.tsx +135 -0
  118. package/src/offline.ts +26 -0
  119. package/src/shell/admin-menu.tsx +40 -3
  120. package/src/shell.ts +4 -0
  121. package/src/tour/tour-catalogue.tsx +7 -1
  122. package/src/tour/tour.tsx +5 -2
  123. package/src/tour/use-tour-progress.ts +378 -12
  124. package/src/tour.ts +3 -0
package/README.md CHANGED
@@ -65,6 +65,8 @@ when it chooses. To work on the design system and an app together, use
65
65
  | `sonner` | peer, `>=2`, **optional** | Under the kit's `toast` and `<Toaster>` (and through them `FileDropzone` and the wizard's "fill in the required fields" toast), reached only through `await import()`. Pass `onValidationFailed` to route the wizard's elsewhere. |
66
66
  | `react-hook-form` | peer, `^7.55.0`, **optional** | Only `@eifi1/ui-kit/rhf` imports it; the main barrel does not (see "Forms / refs" below). |
67
67
  | `modern-screenshot` | peer, `^4.7.0`, **optional** | The feedback dialog's "Capture screenshot" (`captureAppScreenshot`), reached only through `await import()`. Without it the capture rejects and the dialog toasts `feedbackToast.captureFailed`. |
68
+ | `@tanstack/react-query`, `@tanstack/react-query-persist-client`, `@tanstack/query-async-storage-persister` | peer, `^5`, **optional** | Only `@eifi1/ui-kit/offline` imports them (0.45, the persisted query cache, `createQueryPersistence`); the main barrel does not. |
69
+ | `idb-keyval` | peer, `^6`, **optional** | The persisted cache's default IndexedDB store, in `@eifi1/ui-kit/offline` only. |
68
70
  | `clsx`, `tailwind-merge`, `zustand` | dependency | Small, imported directly, and the kit owns the version. Add `zustand` to Vite's `dedupe` (below) — the appearance stores are singletons. |
69
71
  | `flag-icons` | **nothing — you supply the stylesheet** | See below. |
70
72
 
@@ -556,19 +558,19 @@ columns, strings and permissions (see the note at the top of `src/feedback/feedb
556
558
 
557
559
  <!-- BEGIN GENERATED: exports — node scripts/gen-export-inventory.mjs -->
558
560
 
559
- **2081 names from 341 modules** — 959 values and 1122 types. _Italic_ is a type-only export.
561
+ **2094 names from 342 modules** — 965 values and 1129 types. _Italic_ is a type-only export.
560
562
 
561
563
  Generated from `dist/index.d.ts` by `node scripts/gen-export-inventory.mjs`; the count
562
564
  is pinned by `src/__tests__/public-surface.test.ts`. Do not edit between the markers.
563
565
 
564
566
  | Entry point | Names |
565
567
  |---|---|
566
- | `@eifi1/ui-kit` | 2081 |
568
+ | `@eifi1/ui-kit` | 2094 |
567
569
  | `@eifi1/ui-kit/chart` | 92 |
568
- | `@eifi1/ui-kit/shell` | 110 |
570
+ | `@eifi1/ui-kit/shell` | 112 |
569
571
  | `@eifi1/ui-kit/data-table` | 51 |
570
572
  | `@eifi1/ui-kit/wizard` | 24 |
571
- | `@eifi1/ui-kit/tour` | 33 |
573
+ | `@eifi1/ui-kit/tour` | 37 |
572
574
  | `@eifi1/ui-kit/feedback` | 205 |
573
575
  | `@eifi1/ui-kit/search` | 23 |
574
576
  | `@eifi1/ui-kit/dates` | 22 |
@@ -577,6 +579,7 @@ is pinned by `src/__tests__/public-surface.test.ts`. Do not edit between the mar
577
579
  | `@eifi1/ui-kit/eslint` | 1 |
578
580
  | `@eifi1/ui-kit/testing` | 11 |
579
581
  | `@eifi1/ui-kit/rhf` | 48 |
582
+ | `@eifi1/ui-kit/offline` | 22 |
580
583
 
581
584
  Everything below is reachable from the main `@eifi1/ui-kit` barrel. The subpaths are a
582
585
  re-slicing of it, never a second API.
@@ -819,7 +822,7 @@ re-slicing of it, never a second API.
819
822
  | `shell/top-bar-brand` | `TopBarBrand`, _`TopBarBrandProps`_ |
820
823
  | `shell/auth-layout` | `AuthLayout`, _`AuthLayoutProps`_ |
821
824
  | `shell/fill-height` | `FILL_HEIGHT_CLASS`, `FillHeight`, _`FillHeightProps`_ |
822
- | `shell/admin-menu` | `adminMenuEntry`, `AdminMenuRow`, `useAdminMenuEntry`, _`AdminMenuEntryOptions`_, _`AdminMenuRowProps`_, _`UseAdminMenuEntryOptions`_ |
825
+ | `shell/admin-menu` | `adminMenuEntry`, `AdminMenuRow`, `AdminWaitingBadge`, `useAdminMenuEntry`, _`AdminMenuEntryOptions`_, _`AdminMenuRowProps`_, _`AdminWaitingBadgeProps`_, _`UseAdminMenuEntryOptions`_ |
823
826
  | `shell/top-bar-actions` | `TOP_BAR_ACTION_ORDER`, `TopBarActions`, _`TopBarActionsProps`_ |
824
827
  | `shell/account-menu-language` | `AccountMenuLanguage`, _`AccountMenuLanguageOption`_, _`AccountMenuLanguageProps`_ |
825
828
  | `shell/tour-menu` | `TourMenu`, _`TourMenuProps`_, _`TourMenuTour`_ |
@@ -902,7 +905,7 @@ re-slicing of it, never a second API.
902
905
  | `tour/tour` | `DEFAULT_TOUR_LABELS`, `TourProvider`, `useTour`, `useTourOptional`, _`TourCompletion`_, _`TourLabels`_, _`TourPlacement`_, _`TourStep`_ |
903
906
  | `tour/tour-catalogue` | `DEFAULT_TOUR_CATALOGUE_LABELS`, `TourCatalogue`, `TourCatalogueCard`, _`TourCatalogueCardProps`_, _`TourCatalogueLabels`_, _`TourCatalogueProps`_ |
904
907
  | `tour/tour-info` | `doneCount`, `earnedPoints`, `isTourDone`, `isTourNew`, `isTourReset`, _`TourCompletions`_, _`TourGroup`_, _`TourInfo`_, _`TourUpdate`_ |
905
- | `tour/use-tour-progress` | `completeTour`, `resetTourProgress`, `useTourProgress`, _`TourProgress`_, _`UseTourProgressOptions`_ |
908
+ | `tour/use-tour-progress` | `completeTour`, `resetTourProgress`, `signOutTourProgress`, `useTourProgress`, _`TourProgress`_, _`TourProgressAccount`_, _`TourProgressAdopt`_, _`TourProgressBody`_, _`UseTourProgressOptions`_ |
906
909
  | `tour/use-tour-url-sync` | `useTourCatalogueUrlState`, `useTourUrlSync`, _`TourCatalogueFilter`_, _`TourStateFilter`_, _`UseTourUrlSyncOptions`_ |
907
910
 
908
911
  ### search
@@ -959,6 +962,7 @@ re-slicing of it, never a second API.
959
962
  | `admin/admin-forbidden` | `AdminForbidden`, _`AdminForbiddenProps`_ |
960
963
  | `admin/admin-overview` | `AdminOverview`, `adminWaitingCount`, `AttentionTile`, `DEFAULT_ADMIN_ATTENTION_HREFS`, _`AdminAttention`_, _`AdminAttentionTile`_, _`AdminExtraTile`_, _`AdminOverviewProps`_, _`AttentionTileProps`_ |
961
964
  | `admin/admin-tool-links` | `AdminToolLinks`, _`AdminToolLink`_, _`AdminToolLinksProps`_ |
965
+ | `admin/mail-test-card` | `DEFAULT_MAIL_TEST_CARD_LABELS`, `MAIL_TEST_ANCHOR`, `MailTestCard`, `maskMailAddress`, _`MailTestAnswer`_, _`MailTestCardLabels`_, _`MailTestCardProps`_ |
962
966
  | `admin/plan-change` | `DEFAULT_PLAN_CHANGE_LABELS`, `PlanChangeConfirm`, `planColumn`, `usePlanChangeResult`, `usePlanColumn`, _`PlanChange`_, _`PlanChangeConfirmProps`_, _`PlanChangeLabels`_, _`PlanChangeLine`_, _`PlanChangeOutcome`_, _`PlanChangeResultOptions`_, _`PlanChangeTone`_, _`PlanColumnOptions`_, _`PlanColumnUsage`_ |
963
967
 
964
968
  ### billing
@@ -28,11 +28,14 @@ const DEFAULT_ADMIN_ACTION_LOG_LABELS = {
28
28
  erase: "Account erased",
29
29
  plan: "Plan changed"
30
30
  },
31
+ renamedFromTo: (from, to) => `from ${from} to ${to}`,
31
32
  groupActions: {
32
33
  group: {
33
34
  group_create: "Group created",
34
35
  group_rename: "Group renamed",
35
36
  group_delete: "Group deleted",
37
+ group_deactivate: "Group deactivated",
38
+ group_reactivate: "Group reactivated",
36
39
  membership_add: "Added to the group",
37
40
  membership_role: "Role in the group changed",
38
41
  membership_remove: "Removed from the group",
@@ -42,6 +45,8 @@ const DEFAULT_ADMIN_ACTION_LOG_LABELS = {
42
45
  group_create: "Team created",
43
46
  group_rename: "Team renamed",
44
47
  group_delete: "Team deleted",
48
+ group_deactivate: "Team deactivated",
49
+ group_reactivate: "Team reactivated",
45
50
  membership_add: "Added to the team",
46
51
  membership_role: "Role in the team changed",
47
52
  membership_remove: "Removed from the team",
@@ -51,6 +56,8 @@ const DEFAULT_ADMIN_ACTION_LOG_LABELS = {
51
56
  group_create: "Company created",
52
57
  group_rename: "Company renamed",
53
58
  group_delete: "Company deleted",
59
+ group_deactivate: "Company deactivated",
60
+ group_reactivate: "Company reactivated",
54
61
  membership_add: "Added to the company",
55
62
  membership_role: "Role in the company changed",
56
63
  membership_remove: "Removed from the company",
@@ -60,6 +67,8 @@ const DEFAULT_ADMIN_ACTION_LOG_LABELS = {
60
67
  group_create: "Budget created",
61
68
  group_rename: "Budget renamed",
62
69
  group_delete: "Budget deleted",
70
+ group_deactivate: "Budget deactivated",
71
+ group_reactivate: "Budget reactivated",
63
72
  membership_add: "Added to the budget",
64
73
  membership_role: "Role in the budget changed",
65
74
  membership_remove: "Removed from the budget",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/admin/admin-action-log-labels.ts"],"sourcesContent":["// The English words of ./admin-action-log, in a module with no component code: the English\n// catalogue (src/i18n/defaults.ts) imports them without the component, so `en` loads as one\n// chunk like the other languages (0.34, KS U8). ./admin-action-log re-exports them.\n\nimport type { AdminActionLogLabels } from \"./admin-action-log\";\n\nexport const DEFAULT_ADMIN_ACTION_LOG_LABELS: AdminActionLogLabels = {\n title: \"Admin actions\",\n empty: \"No admin actions yet.\",\n loading: \"Loading…\",\n by: (actor) => `by ${actor}`,\n bySelf: \"by the user themselves\",\n automatic: \"automatically\",\n erased: \"an erased account\",\n filteredTo: (target) => `Only ${target}`,\n showAll: \"Show all actions\",\n filterBy: (target) => `Show only the actions on ${target}`,\n actions: {\n deactivate: \"Deactivated\",\n reactivate: \"Reactivated\",\n role: \"Role changed\",\n password_change_require: \"New password required\",\n password_change_withdraw: \"Password requirement withdrawn\",\n mail_verification: \"Verification mail sent\",\n mail_reset: \"Password reset mail sent\",\n reviewer: \"Reviewer scope changed\",\n invite: \"Invited\",\n invite_resend: \"Invitation resent\",\n invite_revoke: \"Invitation revoked\",\n transfer: \"Work transferred\",\n deletion_request: \"Deletion requested\",\n deletion_cancel: \"Deletion cancelled\",\n erase: \"Account erased\",\n plan: \"Plan changed\",\n },\n groupActions: {\n group: {\n group_create: \"Group created\",\n group_rename: \"Group renamed\",\n group_delete: \"Group deleted\",\n membership_add: \"Added to the group\",\n membership_role: \"Role in the group changed\",\n membership_remove: \"Removed from the group\",\n deleted: \"a deleted group\",\n },\n team: {\n group_create: \"Team created\",\n group_rename: \"Team renamed\",\n group_delete: \"Team deleted\",\n membership_add: \"Added to the team\",\n membership_role: \"Role in the team changed\",\n membership_remove: \"Removed from the team\",\n deleted: \"a deleted team\",\n },\n company: {\n group_create: \"Company created\",\n group_rename: \"Company renamed\",\n group_delete: \"Company deleted\",\n membership_add: \"Added to the company\",\n membership_role: \"Role in the company changed\",\n membership_remove: \"Removed from the company\",\n deleted: \"a deleted company\",\n },\n budget: {\n group_create: \"Budget created\",\n group_rename: \"Budget renamed\",\n group_delete: \"Budget deleted\",\n membership_add: \"Added to the budget\",\n membership_role: \"Role in the budget changed\",\n membership_remove: \"Removed from the budget\",\n deleted: \"a deleted budget\",\n },\n },\n};\n"],"mappings":";AAMO,MAAM,kCAAwD;AAAA,EACnE,OAAO;AAAA,EACP,OAAO;AAAA,EACP,SAAS;AAAA,EACT,IAAI,CAAC,UAAU,MAAM,KAAK;AAAA,EAC1B,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,YAAY,CAAC,WAAW,QAAQ,MAAM;AAAA,EACtC,SAAS;AAAA,EACT,UAAU,CAAC,WAAW,4BAA4B,MAAM;AAAA,EACxD,SAAS;AAAA,IACP,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ,MAAM;AAAA,IACN,yBAAyB;AAAA,IACzB,0BAA0B;AAAA,IAC1B,mBAAmB;AAAA,IACnB,YAAY;AAAA,IACZ,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,eAAe;AAAA,IACf,eAAe;AAAA,IACf,UAAU;AAAA,IACV,kBAAkB;AAAA,IAClB,iBAAiB;AAAA,IACjB,OAAO;AAAA,IACP,MAAM;AAAA,EACR;AAAA,EACA,cAAc;AAAA,IACZ,OAAO;AAAA,MACL,cAAc;AAAA,MACd,cAAc;AAAA,MACd,cAAc;AAAA,MACd,gBAAgB;AAAA,MAChB,iBAAiB;AAAA,MACjB,mBAAmB;AAAA,MACnB,SAAS;AAAA,IACX;AAAA,IACA,MAAM;AAAA,MACJ,cAAc;AAAA,MACd,cAAc;AAAA,MACd,cAAc;AAAA,MACd,gBAAgB;AAAA,MAChB,iBAAiB;AAAA,MACjB,mBAAmB;AAAA,MACnB,SAAS;AAAA,IACX;AAAA,IACA,SAAS;AAAA,MACP,cAAc;AAAA,MACd,cAAc;AAAA,MACd,cAAc;AAAA,MACd,gBAAgB;AAAA,MAChB,iBAAiB;AAAA,MACjB,mBAAmB;AAAA,MACnB,SAAS;AAAA,IACX;AAAA,IACA,QAAQ;AAAA,MACN,cAAc;AAAA,MACd,cAAc;AAAA,MACd,cAAc;AAAA,MACd,gBAAgB;AAAA,MAChB,iBAAiB;AAAA,MACjB,mBAAmB;AAAA,MACnB,SAAS;AAAA,IACX;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/admin/admin-action-log-labels.ts"],"sourcesContent":["// The English words of ./admin-action-log, in a module with no component code: the English\n// catalogue (src/i18n/defaults.ts) imports them without the component, so `en` loads as one\n// chunk like the other languages (0.34, KS U8). ./admin-action-log re-exports them.\n\nimport type { AdminActionLogLabels } from \"./admin-action-log\";\n\nexport const DEFAULT_ADMIN_ACTION_LOG_LABELS: AdminActionLogLabels = {\n title: \"Admin actions\",\n empty: \"No admin actions yet.\",\n loading: \"Loading…\",\n by: (actor) => `by ${actor}`,\n bySelf: \"by the user themselves\",\n automatic: \"automatically\",\n erased: \"an erased account\",\n filteredTo: (target) => `Only ${target}`,\n showAll: \"Show all actions\",\n filterBy: (target) => `Show only the actions on ${target}`,\n actions: {\n deactivate: \"Deactivated\",\n reactivate: \"Reactivated\",\n role: \"Role changed\",\n password_change_require: \"New password required\",\n password_change_withdraw: \"Password requirement withdrawn\",\n mail_verification: \"Verification mail sent\",\n mail_reset: \"Password reset mail sent\",\n reviewer: \"Reviewer scope changed\",\n invite: \"Invited\",\n invite_resend: \"Invitation resent\",\n invite_revoke: \"Invitation revoked\",\n transfer: \"Work transferred\",\n deletion_request: \"Deletion requested\",\n deletion_cancel: \"Deletion cancelled\",\n erase: \"Account erased\",\n plan: \"Plan changed\",\n },\n renamedFromTo: (from, to) => `from ${from} to ${to}`,\n groupActions: {\n group: {\n group_create: \"Group created\",\n group_rename: \"Group renamed\",\n group_delete: \"Group deleted\",\n group_deactivate: \"Group deactivated\",\n group_reactivate: \"Group reactivated\",\n membership_add: \"Added to the group\",\n membership_role: \"Role in the group changed\",\n membership_remove: \"Removed from the group\",\n deleted: \"a deleted group\",\n },\n team: {\n group_create: \"Team created\",\n group_rename: \"Team renamed\",\n group_delete: \"Team deleted\",\n group_deactivate: \"Team deactivated\",\n group_reactivate: \"Team reactivated\",\n membership_add: \"Added to the team\",\n membership_role: \"Role in the team changed\",\n membership_remove: \"Removed from the team\",\n deleted: \"a deleted team\",\n },\n company: {\n group_create: \"Company created\",\n group_rename: \"Company renamed\",\n group_delete: \"Company deleted\",\n group_deactivate: \"Company deactivated\",\n group_reactivate: \"Company reactivated\",\n membership_add: \"Added to the company\",\n membership_role: \"Role in the company changed\",\n membership_remove: \"Removed from the company\",\n deleted: \"a deleted company\",\n },\n budget: {\n group_create: \"Budget created\",\n group_rename: \"Budget renamed\",\n group_delete: \"Budget deleted\",\n group_deactivate: \"Budget deactivated\",\n group_reactivate: \"Budget reactivated\",\n membership_add: \"Added to the budget\",\n membership_role: \"Role in the budget changed\",\n membership_remove: \"Removed from the budget\",\n deleted: \"a deleted budget\",\n },\n },\n};\n"],"mappings":";AAMO,MAAM,kCAAwD;AAAA,EACnE,OAAO;AAAA,EACP,OAAO;AAAA,EACP,SAAS;AAAA,EACT,IAAI,CAAC,UAAU,MAAM,KAAK;AAAA,EAC1B,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,YAAY,CAAC,WAAW,QAAQ,MAAM;AAAA,EACtC,SAAS;AAAA,EACT,UAAU,CAAC,WAAW,4BAA4B,MAAM;AAAA,EACxD,SAAS;AAAA,IACP,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ,MAAM;AAAA,IACN,yBAAyB;AAAA,IACzB,0BAA0B;AAAA,IAC1B,mBAAmB;AAAA,IACnB,YAAY;AAAA,IACZ,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,eAAe;AAAA,IACf,eAAe;AAAA,IACf,UAAU;AAAA,IACV,kBAAkB;AAAA,IAClB,iBAAiB;AAAA,IACjB,OAAO;AAAA,IACP,MAAM;AAAA,EACR;AAAA,EACA,eAAe,CAAC,MAAM,OAAO,QAAQ,IAAI,OAAO,EAAE;AAAA,EAClD,cAAc;AAAA,IACZ,OAAO;AAAA,MACL,cAAc;AAAA,MACd,cAAc;AAAA,MACd,cAAc;AAAA,MACd,kBAAkB;AAAA,MAClB,kBAAkB;AAAA,MAClB,gBAAgB;AAAA,MAChB,iBAAiB;AAAA,MACjB,mBAAmB;AAAA,MACnB,SAAS;AAAA,IACX;AAAA,IACA,MAAM;AAAA,MACJ,cAAc;AAAA,MACd,cAAc;AAAA,MACd,cAAc;AAAA,MACd,kBAAkB;AAAA,MAClB,kBAAkB;AAAA,MAClB,gBAAgB;AAAA,MAChB,iBAAiB;AAAA,MACjB,mBAAmB;AAAA,MACnB,SAAS;AAAA,IACX;AAAA,IACA,SAAS;AAAA,MACP,cAAc;AAAA,MACd,cAAc;AAAA,MACd,cAAc;AAAA,MACd,kBAAkB;AAAA,MAClB,kBAAkB;AAAA,MAClB,gBAAgB;AAAA,MAChB,iBAAiB;AAAA,MACjB,mBAAmB;AAAA,MACnB,SAAS;AAAA,IACX;AAAA,IACA,QAAQ;AAAA,MACN,cAAc;AAAA,MACd,cAAc;AAAA,MACd,cAAc;AAAA,MACd,kBAAkB;AAAA,MAClB,kBAAkB;AAAA,MAClB,gBAAgB;AAAA,MAChB,iBAAiB;AAAA,MACjB,mBAAmB;AAAA,MACnB,SAAS;AAAA,IACX;AAAA,EACF;AACF;","names":[]}
@@ -35,7 +35,7 @@ import { DEFAULT_ADMIN_ACTION_LOG_LABELS } from "./admin-action-log-labels.js";
35
35
  * It never fetches: the page of rows and the paging are the app's.
36
36
  */
37
37
  /** The shared vocabulary of `admin_actions.action` (§4.3). */
38
- export type AdminActionKind = "deactivate" | "reactivate" | "role" | "membership_remove" | "password_change_require" | "password_change_withdraw" | "mail_verification" | "mail_reset" | "reviewer" | "invite" | "invite_resend" | "invite_revoke" | "transfer" | "deletion_request" | "deletion_cancel" | "erase" | "plan" | "group_create" | "group_rename" | "group_delete" | "membership_add" | "membership_role";
38
+ export type AdminActionKind = "deactivate" | "reactivate" | "role" | "membership_remove" | "password_change_require" | "password_change_withdraw" | "mail_verification" | "mail_reset" | "reviewer" | "invite" | "invite_resend" | "invite_revoke" | "transfer" | "deletion_request" | "deletion_cancel" | "erase" | "plan" | "group_create" | "group_rename" | "group_delete" | "membership_add" | "membership_role" | "group_deactivate" | "group_reactivate";
39
39
  /** Every {@link AdminActionKind}, in the contract's order. */
40
40
  export declare const ADMIN_ACTION_KINDS: readonly AdminActionKind[];
41
41
  /** The app's word for its groups (§11.5): Kurvenschmiede's teams, kastlan's companies,
@@ -94,6 +94,16 @@ export interface AdminActionLogEntry {
94
94
  group?: AdminGroupTarget | null;
95
95
  /** The `detail` in the app's words — "Member → Admin", "12 items to Ben Example". */
96
96
  detail?: ReactNode;
97
+ /**
98
+ * 0.45 (admin §9.4, Marcel 2026-10-19): a `group_rename` row's `detail.{from, to}` —
99
+ * kastlan's company rename logs both names. With both, the row reads "Company renamed
100
+ * from A to B" (`renamedFromTo`) in place of the target. A rename that logs `{}` (KS's
101
+ * team rename: never a name) leaves it out and reads "Team renamed" and the group.
102
+ */
103
+ renamed?: {
104
+ from?: string | null;
105
+ to?: string | null;
106
+ } | null;
97
107
  }
98
108
  /** The `adminActionLog` namespace. */
99
109
  export interface AdminActionLogLabels {
@@ -115,6 +125,8 @@ export interface AdminActionLogLabels {
115
125
  showAll: string;
116
126
  /** A target name as a filter button — "Show only the actions on Ada Example". */
117
127
  filterBy: (target: string) => string;
128
+ /** 0.45: after "Company renamed" when the row carries both names — "from A to B". */
129
+ renamedFromTo: (from: string, to: string) => string;
118
130
  /** What each action is called, past tense, keyed by the vocabulary. An app adds its
119
131
  * own here (`labels={{ actions: { plan: "Plan changed" } }}`). A word given here
120
132
  * outranks the noun's in `groupActions` — so no catalogue holds the group kinds
@@ -135,6 +147,11 @@ interface AdminGroupActionLabels {
135
147
  group_rename: string;
136
148
  /** "Team deleted". */
137
149
  group_delete: string;
150
+ /** "Company deactivated" (0.45, admin §9.1.3): the group switched off — its people
151
+ * locked out — not a person's `deactivate`. */
152
+ group_deactivate: string;
153
+ /** "Company reactivated" (0.45). */
154
+ group_reactivate: string;
138
155
  /** "Added to the team" — said of the person, so it agrees with the account where a
139
156
  * language agrees (es "Añadida", fr "Ajouté"). */
140
157
  membership_add: string;
@@ -32,12 +32,16 @@ const ADMIN_ACTION_KINDS = [
32
32
  "group_rename",
33
33
  "group_delete",
34
34
  "membership_add",
35
- "membership_role"
35
+ "membership_role",
36
+ "group_deactivate",
37
+ "group_reactivate"
36
38
  ];
37
39
  const GROUP_ACTION_KINDS = /* @__PURE__ */ new Set([
38
40
  "group_create",
39
41
  "group_rename",
40
42
  "group_delete",
43
+ "group_deactivate",
44
+ "group_reactivate",
41
45
  "membership_add",
42
46
  "membership_role",
43
47
  "membership_remove"
@@ -117,23 +121,28 @@ function AdminActionLog({
117
121
  "aria-label": labels.title,
118
122
  "aria-busy": loading || void 0,
119
123
  className: cn("divide-y divide-subtle", loading && "opacity-60"),
120
- children: entries.map((entry) => /* @__PURE__ */ jsxs("li", { "data-action": entry.action, className: "space-y-0.5 py-2 first:pt-0 last:pb-0", children: [
121
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5", children: [
122
- /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5 text-sm", children: [
123
- /* @__PURE__ */ jsx("span", { className: "font-medium text-primary", children: actionText(entry.action) }),
124
- entry.target !== void 0 && /* @__PURE__ */ jsx("span", { className: "min-w-0 break-words", children: targetNode(entry) }),
125
- entry.group && /* @__PURE__ */ jsx("span", { "data-group": true, className: "min-w-0 break-words text-xs text-muted", children: groupText(entry.group) })
124
+ children: entries.map((entry) => {
125
+ const from = (entry.renamed?.from ?? "").trim();
126
+ const to = (entry.renamed?.to ?? "").trim();
127
+ const renamed = entry.action === "group_rename" && from !== "" && to !== "";
128
+ return /* @__PURE__ */ jsxs("li", { "data-action": entry.action, className: "space-y-0.5 py-2 first:pt-0 last:pb-0", children: [
129
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5", children: [
130
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5 text-sm", children: [
131
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-primary", children: actionText(entry.action) }),
132
+ renamed ? /* @__PURE__ */ jsx("span", { "data-private": true, "data-renamed": true, className: "min-w-0 break-words text-primary", children: labels.renamedFromTo(from, to) }) : entry.target !== void 0 && /* @__PURE__ */ jsx("span", { className: "min-w-0 break-words", children: targetNode(entry) }),
133
+ entry.group && /* @__PURE__ */ jsx("span", { "data-group": true, className: "min-w-0 break-words text-xs text-muted", children: groupText(entry.group) })
134
+ ] }),
135
+ /* @__PURE__ */ jsx(DateMark, { value: entry.at, display: "relative", className: "text-xs text-muted" })
126
136
  ] }),
127
- /* @__PURE__ */ jsx(DateMark, { value: entry.at, display: "relative", className: "text-xs text-muted" })
128
- ] }),
129
- /* @__PURE__ */ jsxs("div", { className: "text-xs text-muted", children: [
130
- /* @__PURE__ */ jsx("span", { "data-private": entry.actor !== null && entry.actor !== "self" ? true : void 0, children: actorText(entry) }),
131
- entry.detail !== void 0 && entry.detail !== null && entry.detail !== "" && /* @__PURE__ */ jsxs(Fragment, { children: [
132
- " \xB7 ",
133
- /* @__PURE__ */ jsx("span", { className: "text-secondary", children: entry.detail })
137
+ /* @__PURE__ */ jsxs("div", { className: "text-xs text-muted", children: [
138
+ /* @__PURE__ */ jsx("span", { "data-private": entry.actor !== null && entry.actor !== "self" ? true : void 0, children: actorText(entry) }),
139
+ entry.detail !== void 0 && entry.detail !== null && entry.detail !== "" && /* @__PURE__ */ jsxs(Fragment, { children: [
140
+ " \xB7 ",
141
+ /* @__PURE__ */ jsx("span", { className: "text-secondary", children: entry.detail })
142
+ ] })
134
143
  ] })
135
- ] })
136
- ] }, entry.id))
144
+ ] }, entry.id);
145
+ })
137
146
  }
138
147
  ),
139
148
  paging && paging.total > 0 && /* @__PURE__ */ jsx(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/admin/admin-action-log.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport type { DateInput } from \"../lib/format\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { DateMark } from \"../components/account-chips\";\nimport { Chip } from \"../components/chip\";\nimport { Pagination } from \"../components/data-table-pagination\";\nimport { Caption } from \"../components/text\";\nimport { Button } from \"../components/button\";\nimport { Spinner } from \"../components/spinner\";\nimport { usePersonLabel } from \"./admin-parts\";\nimport type { AdminPerson } from \"./admin-parts\";\nimport { DEFAULT_ADMIN_ACTION_LOG_LABELS } from \"./admin-action-log-labels\";\n\n/**\n * The admin actions, as the audit table records them (docs/user-admin-harmonization.md\n * §2.3, §4.3): `GET /admin/actions?limit=&offset=&target=`, newest first, on the admin\n * page and filtered to one account from its row.\n *\n * Every app writes the same small `admin_actions` row in the transaction of the action\n * (keksdose and Kurvenschmiede had log lines; kastlan has an HTTP log with no named\n * action), so the vocabulary is shared — {@link AdminActionKind} — and an app's own\n * actions join it through `labels.actions`.\n *\n * What a row says, and what it never does: who, what, to whom, when, and a summary of the\n * `detail` the APP writes (ids, roles, counts — never content, §4.3). Three kinds of\n * actor: an admin; **the user themselves** (`deletion_request`: the actor is the target);\n * and **nobody** (the scheduled erasure, the hand-over at erasure, §2.7), said as\n * \"automatically\". After an erasure the target's name and address are scrubbed and the\n * row stays: it reads \"an erased account\".\n *\n * GROUPS (round 0.34, §11): Kurvenschmiede's teams, kastlan's companies and keksdose's\n * budgets are groups of accounts, and the vocabulary has words for what is done to one\n * (`group_create`, `group_rename`, `group_delete`) and to who is in it (`membership_add`,\n * `membership_role`, `membership_remove`). A row about the group itself has the group as\n * its `target`; a row about a person in it has the person as its target and the group as\n * its `group`, drawn after the person by its name alone, muted — no preposition, so no\n * language has to inflect the name. The kinds and codes say \"group\"; the labels say the\n * app's own noun, picked with `groupNoun` — whole phrases per noun, since a noun dropped\n * into one phrase breaks the participle's agreement in fr and it and the suffix in hu\n * (decision 4). A group with no name left reads as the noun's \"a deleted team\"; only a\n * missing PERSON is \"an erased account\".\n *\n * It never fetches: the page of rows and the paging are the app's.\n */\n\n/** The shared vocabulary of `admin_actions.action` (§4.3). */\nexport type AdminActionKind =\n | \"deactivate\"\n | \"reactivate\"\n | \"role\"\n | \"membership_remove\"\n | \"password_change_require\"\n | \"password_change_withdraw\"\n | \"mail_verification\"\n | \"mail_reset\"\n | \"reviewer\"\n | \"invite\"\n | \"invite_resend\"\n | \"invite_revoke\"\n | \"transfer\"\n | \"deletion_request\"\n | \"deletion_cancel\"\n | \"erase\"\n // 0.32.1 (keksdose's 0.32 report): server-kit 0.6's `AdminAction.PLAN`, an operator's\n // plan change or grant (docs/billing-harmonization.md §6).\n | \"plan\"\n // 0.34 (§11.3): what is done to a group of accounts, and to who is in it — beside\n // `membership_remove`. server-kit 0.8's `AdminAction` has the same five.\n | \"group_create\"\n | \"group_rename\"\n | \"group_delete\"\n | \"membership_add\"\n | \"membership_role\";\n\n/** Every {@link AdminActionKind}, in the contract's order. */\nexport const ADMIN_ACTION_KINDS: readonly AdminActionKind[] = [\n \"deactivate\",\n \"reactivate\",\n \"role\",\n \"membership_remove\",\n \"password_change_require\",\n \"password_change_withdraw\",\n \"mail_verification\",\n \"mail_reset\",\n \"reviewer\",\n \"invite\",\n \"invite_resend\",\n \"invite_revoke\",\n \"transfer\",\n \"deletion_request\",\n \"deletion_cancel\",\n \"erase\",\n \"plan\",\n \"group_create\",\n \"group_rename\",\n \"group_delete\",\n \"membership_add\",\n \"membership_role\",\n];\n\n/** The app's word for its groups (§11.5): Kurvenschmiede's teams, kastlan's companies,\n * keksdose's budgets; `group` is the neutral word. Picks the set of phrases the group\n * actions are said in. */\nexport type AdminGroupNoun = \"group\" | \"team\" | \"company\" | \"budget\";\n\n/** The kinds said in the noun's words: the five of 0.34, and `membership_remove`. */\ntype AdminGroupActionKind =\n | \"group_create\"\n | \"group_rename\"\n | \"group_delete\"\n | \"membership_add\"\n | \"membership_role\"\n | \"membership_remove\";\n\nconst GROUP_ACTION_KINDS: ReadonlySet<string> = new Set<AdminGroupActionKind>([\n \"group_create\",\n \"group_rename\",\n \"group_delete\",\n \"membership_add\",\n \"membership_role\",\n \"membership_remove\",\n]);\n\n/**\n * A group as a log target (§11.4): a row about the group itself (`group_*`, kastlan's\n * `plan`), or the `group` a person was added to, moved in or taken out of. The app joins\n * its `name` when the log is read — from the live group, else from its `group_delete`\n * row's stamp (`target_group_name`, decision 6) — as it joins a person's. With no name\n * left it reads as the noun's \"a deleted team\".\n */\nexport interface AdminGroupTarget {\n kind: \"group\";\n id?: string | number | null;\n name?: string | null;\n}\n\n/** A target that is neither a person nor a group (§11.4): an app's own action on a demo\n * dataset or a setting, said by its `label`. */\nexport interface AdminOtherTarget {\n kind: \"other\";\n /** What it was done to, in the app's words, as it is drawn. */\n label: string;\n /** The app's id for it, when it has one. Beyond §11.4's shape so that code reading\n * `entry.target.id` on any target — keksdose's filter, the showcase's — still\n * type-checks against the wider union. */\n id?: string | number | null;\n}\n\n/** Whether a target is a group. A person has no `kind`; an object whose `kind` is\n * anything else is read as a person too. */\nexport function isGroupTarget(target: unknown): target is AdminGroupTarget {\n return typeof target === \"object\" && target !== null && (target as { kind?: unknown }).kind === \"group\";\n}\n\nfunction isOtherTarget(target: unknown): target is AdminOtherTarget {\n return typeof target === \"object\" && target !== null && (target as { kind?: unknown }).kind === \"other\";\n}\n\n/** One logged action — the app's `admin_actions` row with the people joined in. */\nexport interface AdminActionLogEntry {\n id: string | number;\n /** When, as the row's `at`. */\n at: DateInput;\n /**\n * Who did it: a person; `\"self\"` — the user themselves (a deletion request); or `null`\n * — nobody, the scheduled job. A person whose `id` (or address) is the target's is\n * read as `\"self\"` too.\n */\n actor: AdminPerson | \"self\" | null;\n /** A {@link AdminActionKind}, or one of the app's own (named in `labels.actions`). */\n action: AdminActionKind | (string & {});\n /**\n * What it was done to (§11.4): a person (no `kind`) — `null`, or a person with nothing\n * left in it, is \"an erased account\", and an invitation's person is its address\n * (`target_email`); a group (`kind: \"group\"`), for a row about the group itself; or\n * something else (`kind: \"other\"`), said by its label. Left out: nothing is drawn\n * (until 0.34 it read as erased).\n */\n target?: AdminPerson | AdminGroupTarget | AdminOtherTarget | null;\n /** The group a `membership_*` or invitation row happened in, drawn after the person\n * by its name (§11.4). */\n group?: AdminGroupTarget | null;\n /** The `detail` in the app's words — \"Member → Admin\", \"12 items to Ben Example\". */\n detail?: ReactNode;\n}\n\n/** The `adminActionLog` namespace. */\nexport interface AdminActionLogLabels {\n /** The list's accessible name. */\n title: string;\n empty: string;\n loading: string;\n /** \"by Ada Example\". */\n by: (actor: string) => string;\n /** The user acted on their own account. */\n bySelf: string;\n /** No actor: a scheduled job. */\n automatic: string;\n /** A target scrubbed by an erasure. */\n erased: string;\n /** The filter chip: only one account's actions are shown. */\n filteredTo: (target: string) => string;\n /** The chip's ×. */\n showAll: string;\n /** A target name as a filter button — \"Show only the actions on Ada Example\". */\n filterBy: (target: string) => string;\n /** What each action is called, past tense, keyed by the vocabulary. An app adds its\n * own here (`labels={{ actions: { plan: \"Plan changed\" } }}`). A word given here\n * outranks the noun's in `groupActions` — so no catalogue holds the group kinds\n * here, `membership_remove` included (0.34). */\n actions: { [K in AdminActionKind]?: string } & { [action: string]: string | undefined };\n /** The group actions in each noun's words (§11.5), picked with `groupNoun`. */\n groupActions: Record<AdminGroupNoun, AdminGroupActionLabels>;\n}\n\n/** One noun's phrases (§11.5): whole phrases, never a noun dropped into one. */\ninterface AdminGroupActionLabels {\n /** \"Team created\". */\n group_create: string;\n /** \"Team renamed\". */\n group_rename: string;\n /** \"Team deleted\". */\n group_delete: string;\n /** \"Added to the team\" — said of the person, so it agrees with the account where a\n * language agrees (es \"Añadida\", fr \"Ajouté\"). */\n membership_add: string;\n /** \"Role in the team changed\" — the role in the group, not the account's (`role`). */\n membership_role: string;\n /** \"Removed from the team\". The `company` set's is the string `actions` held until\n * 0.34, in every language. */\n membership_remove: string;\n /** A group target with no name left: \"a deleted team\". */\n deleted: string;\n}\n\n// Defined in ./admin-action-log-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_ADMIN_ACTION_LOG_LABELS };\n\n/** The paging of `GET /admin/actions` — DataTable's `ServerPagination`, which the\n * footer is. */\nexport interface AdminActionLogPaging {\n /** 0-based. */\n page: number;\n pageSize: number;\n total: number;\n onPageChange: (page: number) => void;\n onPageSizeChange?: (pageSize: number) => void;\n}\n\nexport interface AdminActionLogProps {\n /** One page, newest first, as the server sends it. */\n entries: readonly AdminActionLogEntry[];\n /** Left out: no footer (a short list on a row). */\n paging?: AdminActionLogPaging;\n /** The page is being fetched. */\n loading?: boolean;\n /** The filter in force: only this account's actions are shown (`?target=`). With\n * `onTargetFilter`, a chip says so and its × clears it. */\n target?: AdminPerson | null;\n /** Filter by an account — or clear it with `null`. Given, every person target's name\n * is a button that narrows the list to it; a group or other target never is (0.34:\n * `?target=` stays a person's). Left out, the list is not filterable here. */\n onTargetFilter?: (target: AdminPerson | null) => void;\n /** The app's word for its groups (§11.5): picks the phrases the group actions and \"a\n * deleted …\" are said in. Default `group`; `labels.actions[kind]` still wins. */\n groupNoun?: AdminGroupNoun;\n className?: string;\n /** Any part, at any depth: `{ groupActions: { team: { group_create: \"…\" } } }`. */\n labels?: LabelOverride<AdminActionLogLabels>;\n}\n\n/** Whether a person has nothing left that names them — an erased target. */\nfunction scrubbed(person: AdminPerson | null | undefined): boolean {\n return !person || ![person.first, person.last, person.name, person.email].some((part) => (part ?? \"\").trim());\n}\n\n/** Whether two people are the same account. A group or other target is never a person\n * (§11.4), so it never reads \"by the user themselves\". */\nfunction samePerson(a: AdminPerson, b: AdminActionLogEntry[\"target\"]): boolean {\n if (!b || isGroupTarget(b) || isOtherTarget(b)) return false;\n if (a.id !== undefined && a.id !== null && b.id !== undefined && b.id !== null) return a.id === b.id;\n const ea = (a.email ?? \"\").trim().toLowerCase();\n return ea !== \"\" && ea === (b.email ?? \"\").trim().toLowerCase();\n}\n\nexport function AdminActionLog({\n entries,\n paging,\n loading = false,\n target,\n onTargetFilter,\n groupNoun = \"group\",\n className,\n labels: labelsProp,\n}: AdminActionLogProps) {\n // Merged key by key at every depth, so a deep override is what `useKitLabels` takes.\n const labels = useKitLabels(\"adminActionLog\", DEFAULT_ADMIN_ACTION_LOG_LABELS, labelsProp);\n const nameOf = usePersonLabel();\n const filtered = target !== undefined && target !== null && !scrubbed(target);\n // A noun the type doesn't know (an app in plain JS) says the neutral word.\n const groupWords = labels.groupActions[groupNoun] ?? labels.groupActions.group;\n\n /** An app's own word first, then the noun's phrase for a group kind, else the key. */\n const actionText = (action: string): string =>\n labels.actions[action] ??\n (GROUP_ACTION_KINDS.has(action) ? groupWords[action as AdminGroupActionKind] : undefined) ??\n action;\n\n /** A group by its name, or — muted, as an erased person is — the noun's \"a deleted\n * team\" when none is left. */\n const groupText = (group: AdminGroupTarget, className?: string): ReactNode => {\n const name = (group.name ?? \"\").trim();\n return name ? (\n <span data-private className={className}>\n {name}\n </span>\n ) : (\n <span className=\"italic text-muted\">{groupWords.deleted}</span>\n );\n };\n\n const actorText = (entry: AdminActionLogEntry): string => {\n const { actor } = entry;\n if (actor === null) return labels.automatic;\n if (actor === \"self\" || samePerson(actor, entry.target)) return labels.bySelf;\n return labels.by(nameOf(actor) || labels.erased);\n };\n\n const targetNode = (entry: AdminActionLogEntry): ReactNode => {\n const subject = entry.target;\n // Left out: nothing (§11.4). `null` is a person the erasure took.\n if (subject === undefined) return null;\n if (isGroupTarget(subject)) return groupText(subject, \"font-medium text-primary\");\n if (isOtherTarget(subject)) return <span className=\"font-medium text-primary\">{subject.label}</span>;\n if (scrubbed(subject)) return <span className=\"italic text-muted\">{labels.erased}</span>;\n const person = subject!;\n const name = nameOf(person);\n const address = (person.email ?? \"\").trim();\n const text = (\n <>\n <span data-private className=\"font-medium text-primary\">\n {name}\n </span>\n {address && address !== name && (\n <span data-private className=\"ms-1 text-xs text-muted\">\n {address}\n </span>\n )}\n </>\n );\n if (!onTargetFilter || (filtered && samePerson(person, target))) return text;\n return (\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n className=\"h-auto min-h-0 p-0 text-start\"\n aria-label={labels.filterBy(name)}\n onClick={() => onTargetFilter(person)}\n >\n {text}\n </Button>\n );\n };\n\n return (\n <div className={cn(\"space-y-3\", className)}>\n {filtered && onTargetFilter && (\n <Chip size=\"sm\" tone=\"brand\" onRemove={() => onTargetFilter(null)} removeLabel={labels.showAll}>\n <span data-private>{labels.filteredTo(nameOf(target))}</span>\n </Chip>\n )}\n {loading && entries.length === 0 ? (\n <div className=\"flex justify-center py-4\">\n <Spinner label={labels.loading} />\n </div>\n ) : entries.length === 0 ? (\n <Caption>{labels.empty}</Caption>\n ) : (\n <ol\n aria-label={labels.title}\n aria-busy={loading || undefined}\n className={cn(\"divide-y divide-subtle\", loading && \"opacity-60\")}\n >\n {entries.map((entry) => (\n <li key={entry.id} data-action={entry.action} className=\"space-y-0.5 py-2 first:pt-0 last:pb-0\">\n <div className=\"flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5\">\n <div className=\"flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5 text-sm\">\n <span className=\"font-medium text-primary\">{actionText(entry.action)}</span>\n {entry.target !== undefined && <span className=\"min-w-0 break-words\">{targetNode(entry)}</span>}\n {entry.group && (\n <span data-group className=\"min-w-0 break-words text-xs text-muted\">\n {groupText(entry.group)}\n </span>\n )}\n </div>\n <DateMark value={entry.at} display=\"relative\" className=\"text-xs text-muted\" />\n </div>\n <div className=\"text-xs text-muted\">\n <span data-private={entry.actor !== null && entry.actor !== \"self\" ? true : undefined}>\n {actorText(entry)}\n </span>\n {entry.detail !== undefined && entry.detail !== null && entry.detail !== \"\" && (\n <>\n {\" · \"}\n <span className=\"text-secondary\">{entry.detail}</span>\n </>\n )}\n </div>\n </li>\n ))}\n </ol>\n )}\n {paging && paging.total > 0 && (\n <Pagination\n page={paging.page}\n pageSize={paging.pageSize}\n total={paging.total}\n totalPages={Math.max(1, Math.ceil(paging.total / Math.max(1, paging.pageSize)))}\n onPage={paging.onPageChange}\n onPageSize={paging.onPageSizeChange}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["className"]}
1
+ {"version":3,"sources":["../../src/admin/admin-action-log.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport type { DateInput } from \"../lib/format\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { DateMark } from \"../components/account-chips\";\nimport { Chip } from \"../components/chip\";\nimport { Pagination } from \"../components/data-table-pagination\";\nimport { Caption } from \"../components/text\";\nimport { Button } from \"../components/button\";\nimport { Spinner } from \"../components/spinner\";\nimport { usePersonLabel } from \"./admin-parts\";\nimport type { AdminPerson } from \"./admin-parts\";\nimport { DEFAULT_ADMIN_ACTION_LOG_LABELS } from \"./admin-action-log-labels\";\n\n/**\n * The admin actions, as the audit table records them (docs/user-admin-harmonization.md\n * §2.3, §4.3): `GET /admin/actions?limit=&offset=&target=`, newest first, on the admin\n * page and filtered to one account from its row.\n *\n * Every app writes the same small `admin_actions` row in the transaction of the action\n * (keksdose and Kurvenschmiede had log lines; kastlan has an HTTP log with no named\n * action), so the vocabulary is shared — {@link AdminActionKind} — and an app's own\n * actions join it through `labels.actions`.\n *\n * What a row says, and what it never does: who, what, to whom, when, and a summary of the\n * `detail` the APP writes (ids, roles, counts — never content, §4.3). Three kinds of\n * actor: an admin; **the user themselves** (`deletion_request`: the actor is the target);\n * and **nobody** (the scheduled erasure, the hand-over at erasure, §2.7), said as\n * \"automatically\". After an erasure the target's name and address are scrubbed and the\n * row stays: it reads \"an erased account\".\n *\n * GROUPS (round 0.34, §11): Kurvenschmiede's teams, kastlan's companies and keksdose's\n * budgets are groups of accounts, and the vocabulary has words for what is done to one\n * (`group_create`, `group_rename`, `group_delete`) and to who is in it (`membership_add`,\n * `membership_role`, `membership_remove`). A row about the group itself has the group as\n * its `target`; a row about a person in it has the person as its target and the group as\n * its `group`, drawn after the person by its name alone, muted — no preposition, so no\n * language has to inflect the name. The kinds and codes say \"group\"; the labels say the\n * app's own noun, picked with `groupNoun` — whole phrases per noun, since a noun dropped\n * into one phrase breaks the participle's agreement in fr and it and the suffix in hu\n * (decision 4). A group with no name left reads as the noun's \"a deleted team\"; only a\n * missing PERSON is \"an erased account\".\n *\n * It never fetches: the page of rows and the paging are the app's.\n */\n\n/** The shared vocabulary of `admin_actions.action` (§4.3). */\nexport type AdminActionKind =\n | \"deactivate\"\n | \"reactivate\"\n | \"role\"\n | \"membership_remove\"\n | \"password_change_require\"\n | \"password_change_withdraw\"\n | \"mail_verification\"\n | \"mail_reset\"\n | \"reviewer\"\n | \"invite\"\n | \"invite_resend\"\n | \"invite_revoke\"\n | \"transfer\"\n | \"deletion_request\"\n | \"deletion_cancel\"\n | \"erase\"\n // 0.32.1 (keksdose's 0.32 report): server-kit 0.6's `AdminAction.PLAN`, an operator's\n // plan change or grant (docs/billing-harmonization.md §6).\n | \"plan\"\n // 0.34 (§11.3): what is done to a group of accounts, and to who is in it — beside\n // `membership_remove`. server-kit 0.8's `AdminAction` has the same five.\n | \"group_create\"\n | \"group_rename\"\n | \"group_delete\"\n | \"membership_add\"\n | \"membership_role\"\n // 0.45 (admin §9.1.3): a group switched off or on again — kastlan's company PATCH\n // (`is_active`). server-kit 0.15's `AdminAction.GROUP_DEACTIVATE` / `GROUP_REACTIVATE`.\n | \"group_deactivate\"\n | \"group_reactivate\";\n\n/** Every {@link AdminActionKind}, in the contract's order. */\nexport const ADMIN_ACTION_KINDS: readonly AdminActionKind[] = [\n \"deactivate\",\n \"reactivate\",\n \"role\",\n \"membership_remove\",\n \"password_change_require\",\n \"password_change_withdraw\",\n \"mail_verification\",\n \"mail_reset\",\n \"reviewer\",\n \"invite\",\n \"invite_resend\",\n \"invite_revoke\",\n \"transfer\",\n \"deletion_request\",\n \"deletion_cancel\",\n \"erase\",\n \"plan\",\n \"group_create\",\n \"group_rename\",\n \"group_delete\",\n \"membership_add\",\n \"membership_role\",\n \"group_deactivate\",\n \"group_reactivate\",\n];\n\n/** The app's word for its groups (§11.5): Kurvenschmiede's teams, kastlan's companies,\n * keksdose's budgets; `group` is the neutral word. Picks the set of phrases the group\n * actions are said in. */\nexport type AdminGroupNoun = \"group\" | \"team\" | \"company\" | \"budget\";\n\n/** The kinds said in the noun's words: the five of 0.34, `membership_remove`, and 0.45's\n * `group_deactivate` / `group_reactivate`. */\ntype AdminGroupActionKind =\n | \"group_create\"\n | \"group_rename\"\n | \"group_delete\"\n | \"group_deactivate\"\n | \"group_reactivate\"\n | \"membership_add\"\n | \"membership_role\"\n | \"membership_remove\";\n\nconst GROUP_ACTION_KINDS: ReadonlySet<string> = new Set<AdminGroupActionKind>([\n \"group_create\",\n \"group_rename\",\n \"group_delete\",\n \"group_deactivate\",\n \"group_reactivate\",\n \"membership_add\",\n \"membership_role\",\n \"membership_remove\",\n]);\n\n/**\n * A group as a log target (§11.4): a row about the group itself (`group_*`, kastlan's\n * `plan`), or the `group` a person was added to, moved in or taken out of. The app joins\n * its `name` when the log is read — from the live group, else from its `group_delete`\n * row's stamp (`target_group_name`, decision 6) — as it joins a person's. With no name\n * left it reads as the noun's \"a deleted team\".\n */\nexport interface AdminGroupTarget {\n kind: \"group\";\n id?: string | number | null;\n name?: string | null;\n}\n\n/** A target that is neither a person nor a group (§11.4): an app's own action on a demo\n * dataset or a setting, said by its `label`. */\nexport interface AdminOtherTarget {\n kind: \"other\";\n /** What it was done to, in the app's words, as it is drawn. */\n label: string;\n /** The app's id for it, when it has one. Beyond §11.4's shape so that code reading\n * `entry.target.id` on any target — keksdose's filter, the showcase's — still\n * type-checks against the wider union. */\n id?: string | number | null;\n}\n\n/** Whether a target is a group. A person has no `kind`; an object whose `kind` is\n * anything else is read as a person too. */\nexport function isGroupTarget(target: unknown): target is AdminGroupTarget {\n return typeof target === \"object\" && target !== null && (target as { kind?: unknown }).kind === \"group\";\n}\n\nfunction isOtherTarget(target: unknown): target is AdminOtherTarget {\n return typeof target === \"object\" && target !== null && (target as { kind?: unknown }).kind === \"other\";\n}\n\n/** One logged action — the app's `admin_actions` row with the people joined in. */\nexport interface AdminActionLogEntry {\n id: string | number;\n /** When, as the row's `at`. */\n at: DateInput;\n /**\n * Who did it: a person; `\"self\"` — the user themselves (a deletion request); or `null`\n * — nobody, the scheduled job. A person whose `id` (or address) is the target's is\n * read as `\"self\"` too.\n */\n actor: AdminPerson | \"self\" | null;\n /** A {@link AdminActionKind}, or one of the app's own (named in `labels.actions`). */\n action: AdminActionKind | (string & {});\n /**\n * What it was done to (§11.4): a person (no `kind`) — `null`, or a person with nothing\n * left in it, is \"an erased account\", and an invitation's person is its address\n * (`target_email`); a group (`kind: \"group\"`), for a row about the group itself; or\n * something else (`kind: \"other\"`), said by its label. Left out: nothing is drawn\n * (until 0.34 it read as erased).\n */\n target?: AdminPerson | AdminGroupTarget | AdminOtherTarget | null;\n /** The group a `membership_*` or invitation row happened in, drawn after the person\n * by its name (§11.4). */\n group?: AdminGroupTarget | null;\n /** The `detail` in the app's words — \"Member → Admin\", \"12 items to Ben Example\". */\n detail?: ReactNode;\n /**\n * 0.45 (admin §9.4, Marcel 2026-10-19): a `group_rename` row's `detail.{from, to}` —\n * kastlan's company rename logs both names. With both, the row reads \"Company renamed\n * from A to B\" (`renamedFromTo`) in place of the target. A rename that logs `{}` (KS's\n * team rename: never a name) leaves it out and reads \"Team renamed\" and the group.\n */\n renamed?: { from?: string | null; to?: string | null } | null;\n}\n\n/** The `adminActionLog` namespace. */\nexport interface AdminActionLogLabels {\n /** The list's accessible name. */\n title: string;\n empty: string;\n loading: string;\n /** \"by Ada Example\". */\n by: (actor: string) => string;\n /** The user acted on their own account. */\n bySelf: string;\n /** No actor: a scheduled job. */\n automatic: string;\n /** A target scrubbed by an erasure. */\n erased: string;\n /** The filter chip: only one account's actions are shown. */\n filteredTo: (target: string) => string;\n /** The chip's ×. */\n showAll: string;\n /** A target name as a filter button — \"Show only the actions on Ada Example\". */\n filterBy: (target: string) => string;\n /** 0.45: after \"Company renamed\" when the row carries both names — \"from A to B\". */\n renamedFromTo: (from: string, to: string) => string;\n /** What each action is called, past tense, keyed by the vocabulary. An app adds its\n * own here (`labels={{ actions: { plan: \"Plan changed\" } }}`). A word given here\n * outranks the noun's in `groupActions` — so no catalogue holds the group kinds\n * here, `membership_remove` included (0.34). */\n actions: { [K in AdminActionKind]?: string } & { [action: string]: string | undefined };\n /** The group actions in each noun's words (§11.5), picked with `groupNoun`. */\n groupActions: Record<AdminGroupNoun, AdminGroupActionLabels>;\n}\n\n/** One noun's phrases (§11.5): whole phrases, never a noun dropped into one. */\ninterface AdminGroupActionLabels {\n /** \"Team created\". */\n group_create: string;\n /** \"Team renamed\". */\n group_rename: string;\n /** \"Team deleted\". */\n group_delete: string;\n /** \"Company deactivated\" (0.45, admin §9.1.3): the group switched off — its people\n * locked out — not a person's `deactivate`. */\n group_deactivate: string;\n /** \"Company reactivated\" (0.45). */\n group_reactivate: string;\n /** \"Added to the team\" — said of the person, so it agrees with the account where a\n * language agrees (es \"Añadida\", fr \"Ajouté\"). */\n membership_add: string;\n /** \"Role in the team changed\" — the role in the group, not the account's (`role`). */\n membership_role: string;\n /** \"Removed from the team\". The `company` set's is the string `actions` held until\n * 0.34, in every language. */\n membership_remove: string;\n /** A group target with no name left: \"a deleted team\". */\n deleted: string;\n}\n\n// Defined in ./admin-action-log-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_ADMIN_ACTION_LOG_LABELS };\n\n/** The paging of `GET /admin/actions` — DataTable's `ServerPagination`, which the\n * footer is. */\nexport interface AdminActionLogPaging {\n /** 0-based. */\n page: number;\n pageSize: number;\n total: number;\n onPageChange: (page: number) => void;\n onPageSizeChange?: (pageSize: number) => void;\n}\n\nexport interface AdminActionLogProps {\n /** One page, newest first, as the server sends it. */\n entries: readonly AdminActionLogEntry[];\n /** Left out: no footer (a short list on a row). */\n paging?: AdminActionLogPaging;\n /** The page is being fetched. */\n loading?: boolean;\n /** The filter in force: only this account's actions are shown (`?target=`). With\n * `onTargetFilter`, a chip says so and its × clears it. */\n target?: AdminPerson | null;\n /** Filter by an account — or clear it with `null`. Given, every person target's name\n * is a button that narrows the list to it; a group or other target never is (0.34:\n * `?target=` stays a person's). Left out, the list is not filterable here. */\n onTargetFilter?: (target: AdminPerson | null) => void;\n /** The app's word for its groups (§11.5): picks the phrases the group actions and \"a\n * deleted …\" are said in. Default `group`; `labels.actions[kind]` still wins. */\n groupNoun?: AdminGroupNoun;\n className?: string;\n /** Any part, at any depth: `{ groupActions: { team: { group_create: \"…\" } } }`. */\n labels?: LabelOverride<AdminActionLogLabels>;\n}\n\n/** Whether a person has nothing left that names them — an erased target. */\nfunction scrubbed(person: AdminPerson | null | undefined): boolean {\n return !person || ![person.first, person.last, person.name, person.email].some((part) => (part ?? \"\").trim());\n}\n\n/** Whether two people are the same account. A group or other target is never a person\n * (§11.4), so it never reads \"by the user themselves\". */\nfunction samePerson(a: AdminPerson, b: AdminActionLogEntry[\"target\"]): boolean {\n if (!b || isGroupTarget(b) || isOtherTarget(b)) return false;\n if (a.id !== undefined && a.id !== null && b.id !== undefined && b.id !== null) return a.id === b.id;\n const ea = (a.email ?? \"\").trim().toLowerCase();\n return ea !== \"\" && ea === (b.email ?? \"\").trim().toLowerCase();\n}\n\nexport function AdminActionLog({\n entries,\n paging,\n loading = false,\n target,\n onTargetFilter,\n groupNoun = \"group\",\n className,\n labels: labelsProp,\n}: AdminActionLogProps) {\n // Merged key by key at every depth, so a deep override is what `useKitLabels` takes.\n const labels = useKitLabels(\"adminActionLog\", DEFAULT_ADMIN_ACTION_LOG_LABELS, labelsProp);\n const nameOf = usePersonLabel();\n const filtered = target !== undefined && target !== null && !scrubbed(target);\n // A noun the type doesn't know (an app in plain JS) says the neutral word.\n const groupWords = labels.groupActions[groupNoun] ?? labels.groupActions.group;\n\n /** An app's own word first, then the noun's phrase for a group kind, else the key. */\n const actionText = (action: string): string =>\n labels.actions[action] ??\n (GROUP_ACTION_KINDS.has(action) ? groupWords[action as AdminGroupActionKind] : undefined) ??\n action;\n\n /** A group by its name, or — muted, as an erased person is — the noun's \"a deleted\n * team\" when none is left. */\n const groupText = (group: AdminGroupTarget, className?: string): ReactNode => {\n const name = (group.name ?? \"\").trim();\n return name ? (\n <span data-private className={className}>\n {name}\n </span>\n ) : (\n <span className=\"italic text-muted\">{groupWords.deleted}</span>\n );\n };\n\n const actorText = (entry: AdminActionLogEntry): string => {\n const { actor } = entry;\n if (actor === null) return labels.automatic;\n if (actor === \"self\" || samePerson(actor, entry.target)) return labels.bySelf;\n return labels.by(nameOf(actor) || labels.erased);\n };\n\n const targetNode = (entry: AdminActionLogEntry): ReactNode => {\n const subject = entry.target;\n // Left out: nothing (§11.4). `null` is a person the erasure took.\n if (subject === undefined) return null;\n if (isGroupTarget(subject)) return groupText(subject, \"font-medium text-primary\");\n if (isOtherTarget(subject)) return <span className=\"font-medium text-primary\">{subject.label}</span>;\n if (scrubbed(subject)) return <span className=\"italic text-muted\">{labels.erased}</span>;\n const person = subject!;\n const name = nameOf(person);\n const address = (person.email ?? \"\").trim();\n const text = (\n <>\n <span data-private className=\"font-medium text-primary\">\n {name}\n </span>\n {address && address !== name && (\n <span data-private className=\"ms-1 text-xs text-muted\">\n {address}\n </span>\n )}\n </>\n );\n if (!onTargetFilter || (filtered && samePerson(person, target))) return text;\n return (\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n className=\"h-auto min-h-0 p-0 text-start\"\n aria-label={labels.filterBy(name)}\n onClick={() => onTargetFilter(person)}\n >\n {text}\n </Button>\n );\n };\n\n return (\n <div className={cn(\"space-y-3\", className)}>\n {filtered && onTargetFilter && (\n <Chip size=\"sm\" tone=\"brand\" onRemove={() => onTargetFilter(null)} removeLabel={labels.showAll}>\n <span data-private>{labels.filteredTo(nameOf(target))}</span>\n </Chip>\n )}\n {loading && entries.length === 0 ? (\n <div className=\"flex justify-center py-4\">\n <Spinner label={labels.loading} />\n </div>\n ) : entries.length === 0 ? (\n <Caption>{labels.empty}</Caption>\n ) : (\n <ol\n aria-label={labels.title}\n aria-busy={loading || undefined}\n className={cn(\"divide-y divide-subtle\", loading && \"opacity-60\")}\n >\n {entries.map((entry) => {\n const from = (entry.renamed?.from ?? \"\").trim();\n const to = (entry.renamed?.to ?? \"\").trim();\n const renamed = entry.action === \"group_rename\" && from !== \"\" && to !== \"\";\n return (\n <li key={entry.id} data-action={entry.action} className=\"space-y-0.5 py-2 first:pt-0 last:pb-0\">\n <div className=\"flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5\">\n <div className=\"flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5 text-sm\">\n <span className=\"font-medium text-primary\">{actionText(entry.action)}</span>\n {renamed ? (\n <span data-private data-renamed className=\"min-w-0 break-words text-primary\">\n {labels.renamedFromTo(from, to)}\n </span>\n ) : (\n entry.target !== undefined && <span className=\"min-w-0 break-words\">{targetNode(entry)}</span>\n )}\n {entry.group && (\n <span data-group className=\"min-w-0 break-words text-xs text-muted\">\n {groupText(entry.group)}\n </span>\n )}\n </div>\n <DateMark value={entry.at} display=\"relative\" className=\"text-xs text-muted\" />\n </div>\n <div className=\"text-xs text-muted\">\n <span data-private={entry.actor !== null && entry.actor !== \"self\" ? true : undefined}>\n {actorText(entry)}\n </span>\n {entry.detail !== undefined && entry.detail !== null && entry.detail !== \"\" && (\n <>\n {\" · \"}\n <span className=\"text-secondary\">{entry.detail}</span>\n </>\n )}\n </div>\n </li>\n );\n })}\n </ol>\n )}\n {paging && paging.total > 0 && (\n <Pagination\n page={paging.page}\n pageSize={paging.pageSize}\n total={paging.total}\n totalPages={Math.max(1, Math.ceil(paging.total / Math.max(1, paging.pageSize)))}\n onPage={paging.onPageChange}\n onPageSize={paging.onPageSizeChange}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["className"]}
@@ -28,6 +28,12 @@ export interface AdminAreaLabels {
28
28
  /** The row's waiting count, for a reader ("Administration, 3 waiting"); the eye sees
29
29
  * the number in the badge. */
30
30
  waiting: (count: number) => string;
31
+ /**
32
+ * 0.45 (admin §9.3.4): a button's whole name with its count — "Platform, 2 waiting" —
33
+ * for an `IconButton` whose `badge` is an `AdminWaitingBadge`: the badge is decorative
34
+ * there, so the name carries the count. `label` is the button's own name.
35
+ */
36
+ waitingLabel: (label: string, count: number) => string;
31
37
  /** The 403 page's heading (§2.2 decision 8). */
32
38
  forbiddenTitle: string;
33
39
  /**
@@ -3,6 +3,7 @@ import { useKitLabels } from "../i18n/kit-labels.js";
3
3
  const DEFAULT_ADMIN_AREA_LABELS = {
4
4
  title: "Administration",
5
5
  waiting: (count) => `${count} waiting`,
6
+ waitingLabel: (label, count) => `${label}, ${count} waiting`,
6
7
  forbiddenTitle: "Only for administrators",
7
8
  forbiddenBody: (appName) => `This is where administrators run ${appName}. Your account cannot open this page.`,
8
9
  forbiddenHome: "Go to the start page",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/admin/admin-area-labels.ts"],"sourcesContent":["import { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport type { SettingsGroupLabels } from \"../settings/settings-labels\";\n\n/**\n * The `adminArea` namespace (0.42, docs/admin-harmonization.md §3.1): the kit's words of\n * the admin area every app shares — the account menu's \"Administration\" row, the 403\n * page, the shared sections' names and help lines, the overview's tiles and the Tools\n * list. A module of words with no component code, like every `*-labels.ts` (0.34, KS U8).\n * An app's own sections and tiles bring their own words.\n */\n\n/** The sections every app's admin area shares, in the template's order (§2.3). */\nexport type AdminSharedSection = \"overview\" | \"users\" | \"invitations\" | \"billing\" | \"activity\";\n\n/** The overview's shared tiles (§2.4, §6.1). */\nexport interface AdminTileLabels {\n /** `feedback_waiting`: OPEN reports, crash reports included (§6.1). */\n feedbackWaiting: string;\n /** `deletion_requests`. */\n deletionRequests: string;\n /** `invitations_pending`. */\n invitationsPending: string;\n /** `users_total`. */\n users: string;\n /** `users_new_7d`, the line under the users tile. */\n usersNew: (count: number) => string;\n}\n\nexport interface AdminAreaLabels {\n /** \"Administration\": the account menu's row, and the admin page's heading. */\n title: string;\n /** The row's waiting count, for a reader (\"Administration, 3 waiting\"); the eye sees\n * the number in the badge. */\n waiting: (count: number) => string;\n /** The 403 page's heading (§2.2 decision 8). */\n forbiddenTitle: string;\n /**\n * Under it. Says only that THIS area is closed to the account — never that the person\n * administers nothing anywhere (§6.7: a kastlan superuser who isn't ADMIN in the acting\n * company gets this page).\n */\n forbiddenBody: (appName: string) => string;\n /** The button back to the app's start, unless the app names its own (`homeLabel`). */\n forbiddenHome: string;\n /** A reviewer's second way out, to the translations review. */\n forbiddenReviewer: string;\n /** The shared sections' names and help lines (§2.3 decision 11). */\n sections: Record<AdminSharedSection, SettingsGroupLabels>;\n /** The overview's tiles. */\n tiles: AdminTileLabels;\n /** 0.43: `AdminOverview`'s `error` — the counts could not be loaded. */\n overviewError: string;\n /** 0.43: its button, which calls `onRetry`. */\n retry: string;\n /** The Tools list's heading and name (§2.5 decision 16). */\n tools: string;\n}\n\nexport const DEFAULT_ADMIN_AREA_LABELS: AdminAreaLabels = {\n title: \"Administration\",\n waiting: (count) => `${count} waiting`,\n forbiddenTitle: \"Only for administrators\",\n forbiddenBody: (appName) => `This is where administrators run ${appName}. Your account cannot open this page.`,\n forbiddenHome: \"Go to the start page\",\n forbiddenReviewer: \"Review translations\",\n sections: {\n overview: { title: \"Overview\", help: \"What is waiting, at a glance.\" },\n users: { title: \"Users\", help: \"Who has an account, and what each may do.\" },\n invitations: { title: \"Invitations\", help: \"Invite people by e-mail in their language; resend or revoke what’s still open.\" },\n billing: { title: \"Billing\", help: \"The plan, the payment and the invoices.\" },\n activity: { title: \"Activity\", help: \"What the admins did, newest first.\" },\n },\n tiles: {\n feedbackWaiting: \"To triage\",\n deletionRequests: \"Deletion requests\",\n invitationsPending: \"Pending invitations\",\n users: \"Users\",\n usersNew: (count) => `${count} new in 7 days`,\n },\n overviewError: \"The overview couldn’t be loaded.\",\n retry: \"Try again\",\n tools: \"Tools\",\n};\n\n/**\n * The `adminArea` namespace, resolved — English, then the provider, then `labels`. What\n * an app reads to hand {@link adminSections} its words from inside a component.\n */\nexport function useAdminAreaLabels(labels?: LabelOverride<AdminAreaLabels>): AdminAreaLabels {\n return useKitLabels(\"adminArea\", DEFAULT_ADMIN_AREA_LABELS, labels);\n}\n"],"mappings":";AAAA,SAAS,oBAAwC;AA0D1C,MAAM,4BAA6C;AAAA,EACxD,OAAO;AAAA,EACP,SAAS,CAAC,UAAU,GAAG,KAAK;AAAA,EAC5B,gBAAgB;AAAA,EAChB,eAAe,CAAC,YAAY,oCAAoC,OAAO;AAAA,EACvE,eAAe;AAAA,EACf,mBAAmB;AAAA,EACnB,UAAU;AAAA,IACR,UAAU,EAAE,OAAO,YAAY,MAAM,gCAAgC;AAAA,IACrE,OAAO,EAAE,OAAO,SAAS,MAAM,4CAA4C;AAAA,IAC3E,aAAa,EAAE,OAAO,eAAe,MAAM,sFAAiF;AAAA,IAC5H,SAAS,EAAE,OAAO,WAAW,MAAM,0CAA0C;AAAA,IAC7E,UAAU,EAAE,OAAO,YAAY,MAAM,qCAAqC;AAAA,EAC5E;AAAA,EACA,OAAO;AAAA,IACL,iBAAiB;AAAA,IACjB,kBAAkB;AAAA,IAClB,oBAAoB;AAAA,IACpB,OAAO;AAAA,IACP,UAAU,CAAC,UAAU,GAAG,KAAK;AAAA,EAC/B;AAAA,EACA,eAAe;AAAA,EACf,OAAO;AAAA,EACP,OAAO;AACT;AAMO,SAAS,mBAAmB,QAA0D;AAC3F,SAAO,aAAa,aAAa,2BAA2B,MAAM;AACpE;","names":[]}
1
+ {"version":3,"sources":["../../src/admin/admin-area-labels.ts"],"sourcesContent":["import { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport type { SettingsGroupLabels } from \"../settings/settings-labels\";\n\n/**\n * The `adminArea` namespace (0.42, docs/admin-harmonization.md §3.1): the kit's words of\n * the admin area every app shares — the account menu's \"Administration\" row, the 403\n * page, the shared sections' names and help lines, the overview's tiles and the Tools\n * list. A module of words with no component code, like every `*-labels.ts` (0.34, KS U8).\n * An app's own sections and tiles bring their own words.\n */\n\n/** The sections every app's admin area shares, in the template's order (§2.3). */\nexport type AdminSharedSection = \"overview\" | \"users\" | \"invitations\" | \"billing\" | \"activity\";\n\n/** The overview's shared tiles (§2.4, §6.1). */\nexport interface AdminTileLabels {\n /** `feedback_waiting`: OPEN reports, crash reports included (§6.1). */\n feedbackWaiting: string;\n /** `deletion_requests`. */\n deletionRequests: string;\n /** `invitations_pending`. */\n invitationsPending: string;\n /** `users_total`. */\n users: string;\n /** `users_new_7d`, the line under the users tile. */\n usersNew: (count: number) => string;\n}\n\nexport interface AdminAreaLabels {\n /** \"Administration\": the account menu's row, and the admin page's heading. */\n title: string;\n /** The row's waiting count, for a reader (\"Administration, 3 waiting\"); the eye sees\n * the number in the badge. */\n waiting: (count: number) => string;\n /**\n * 0.45 (admin §9.3.4): a button's whole name with its count — \"Platform, 2 waiting\" —\n * for an `IconButton` whose `badge` is an `AdminWaitingBadge`: the badge is decorative\n * there, so the name carries the count. `label` is the button's own name.\n */\n waitingLabel: (label: string, count: number) => string;\n /** The 403 page's heading (§2.2 decision 8). */\n forbiddenTitle: string;\n /**\n * Under it. Says only that THIS area is closed to the account — never that the person\n * administers nothing anywhere (§6.7: a kastlan superuser who isn't ADMIN in the acting\n * company gets this page).\n */\n forbiddenBody: (appName: string) => string;\n /** The button back to the app's start, unless the app names its own (`homeLabel`). */\n forbiddenHome: string;\n /** A reviewer's second way out, to the translations review. */\n forbiddenReviewer: string;\n /** The shared sections' names and help lines (§2.3 decision 11). */\n sections: Record<AdminSharedSection, SettingsGroupLabels>;\n /** The overview's tiles. */\n tiles: AdminTileLabels;\n /** 0.43: `AdminOverview`'s `error` — the counts could not be loaded. */\n overviewError: string;\n /** 0.43: its button, which calls `onRetry`. */\n retry: string;\n /** The Tools list's heading and name (§2.5 decision 16). */\n tools: string;\n}\n\nexport const DEFAULT_ADMIN_AREA_LABELS: AdminAreaLabels = {\n title: \"Administration\",\n waiting: (count) => `${count} waiting`,\n waitingLabel: (label, count) => `${label}, ${count} waiting`,\n forbiddenTitle: \"Only for administrators\",\n forbiddenBody: (appName) => `This is where administrators run ${appName}. Your account cannot open this page.`,\n forbiddenHome: \"Go to the start page\",\n forbiddenReviewer: \"Review translations\",\n sections: {\n overview: { title: \"Overview\", help: \"What is waiting, at a glance.\" },\n users: { title: \"Users\", help: \"Who has an account, and what each may do.\" },\n invitations: { title: \"Invitations\", help: \"Invite people by e-mail in their language; resend or revoke what’s still open.\" },\n billing: { title: \"Billing\", help: \"The plan, the payment and the invoices.\" },\n activity: { title: \"Activity\", help: \"What the admins did, newest first.\" },\n },\n tiles: {\n feedbackWaiting: \"To triage\",\n deletionRequests: \"Deletion requests\",\n invitationsPending: \"Pending invitations\",\n users: \"Users\",\n usersNew: (count) => `${count} new in 7 days`,\n },\n overviewError: \"The overview couldn’t be loaded.\",\n retry: \"Try again\",\n tools: \"Tools\",\n};\n\n/**\n * The `adminArea` namespace, resolved — English, then the provider, then `labels`. What\n * an app reads to hand {@link adminSections} its words from inside a component.\n */\nexport function useAdminAreaLabels(labels?: LabelOverride<AdminAreaLabels>): AdminAreaLabels {\n return useKitLabels(\"adminArea\", DEFAULT_ADMIN_AREA_LABELS, labels);\n}\n"],"mappings":";AAAA,SAAS,oBAAwC;AAgE1C,MAAM,4BAA6C;AAAA,EACxD,OAAO;AAAA,EACP,SAAS,CAAC,UAAU,GAAG,KAAK;AAAA,EAC5B,cAAc,CAAC,OAAO,UAAU,GAAG,KAAK,KAAK,KAAK;AAAA,EAClD,gBAAgB;AAAA,EAChB,eAAe,CAAC,YAAY,oCAAoC,OAAO;AAAA,EACvE,eAAe;AAAA,EACf,mBAAmB;AAAA,EACnB,UAAU;AAAA,IACR,UAAU,EAAE,OAAO,YAAY,MAAM,gCAAgC;AAAA,IACrE,OAAO,EAAE,OAAO,SAAS,MAAM,4CAA4C;AAAA,IAC3E,aAAa,EAAE,OAAO,eAAe,MAAM,sFAAiF;AAAA,IAC5H,SAAS,EAAE,OAAO,WAAW,MAAM,0CAA0C;AAAA,IAC7E,UAAU,EAAE,OAAO,YAAY,MAAM,qCAAqC;AAAA,EAC5E;AAAA,EACA,OAAO;AAAA,IACL,iBAAiB;AAAA,IACjB,kBAAkB;AAAA,IAClB,oBAAoB;AAAA,IACpB,OAAO;AAAA,IACP,UAAU,CAAC,UAAU,GAAG,KAAK;AAAA,EAC/B;AAAA,EACA,eAAe;AAAA,EACf,OAAO;AAAA,EACP,OAAO;AACT;AAMO,SAAS,mBAAmB,QAA0D;AAC3F,SAAO,aAAa,aAAa,2BAA2B,MAAM;AACpE;","names":[]}
@@ -0,0 +1,3 @@
1
+ import type { MailTestCardLabels } from "./mail-test-card.js";
2
+ /** docs/admin-harmonization.md §9.2.4, word for word. */
3
+ export declare const DEFAULT_MAIL_TEST_CARD_LABELS: MailTestCardLabels;
@@ -0,0 +1,17 @@
1
+ "use client";
2
+ import { englishRateLimited } from "../auth/auth-errors.js";
3
+ const DEFAULT_MAIL_TEST_CARD_LABELS = {
4
+ title: "Test mail",
5
+ description: "Sends one mail to your own address, through the transport this installation sends all its mail with.",
6
+ transport: (backend) => `Transport: ${backend}`,
7
+ action: "Send a test mail to me",
8
+ sent: (address) => `Sent to ${address}. Check the inbox.`,
9
+ console: "The console transport only logs mail; nothing was sent.",
10
+ failed: "Could not send. The server log has the provider\u2019s answer.",
11
+ rateLimited: englishRateLimited,
12
+ keywords: "test mail email e-mail transport smtp resend console send deliver"
13
+ };
14
+ export {
15
+ DEFAULT_MAIL_TEST_CARD_LABELS
16
+ };
17
+ //# sourceMappingURL=mail-test-card-labels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/admin/mail-test-card-labels.ts"],"sourcesContent":["// The English words of ./mail-test-card, in a module with no component code: the English\n// catalogue (src/i18n/defaults.ts) imports them without the component, so `en` loads as one\n// chunk like the other languages (0.34, KS U8). ./mail-test-card re-exports them.\n\nimport { englishRateLimited } from \"../auth/auth-errors\";\nimport type { MailTestCardLabels } from \"./mail-test-card\";\n\n/** docs/admin-harmonization.md §9.2.4, word for word. */\nexport const DEFAULT_MAIL_TEST_CARD_LABELS: MailTestCardLabels = {\n title: \"Test mail\",\n description: \"Sends one mail to your own address, through the transport this installation sends all its mail with.\",\n transport: (backend) => `Transport: ${backend}`,\n action: \"Send a test mail to me\",\n sent: (address) => `Sent to ${address}. Check the inbox.`,\n console: \"The console transport only logs mail; nothing was sent.\",\n failed: \"Could not send. The server log has the provider’s answer.\",\n rateLimited: englishRateLimited,\n keywords: \"test mail email e-mail transport smtp resend console send deliver\",\n};\n"],"mappings":";AAIA,SAAS,0BAA0B;AAI5B,MAAM,gCAAoD;AAAA,EAC/D,OAAO;AAAA,EACP,aAAa;AAAA,EACb,WAAW,CAAC,YAAY,cAAc,OAAO;AAAA,EAC7C,QAAQ;AAAA,EACR,MAAM,CAAC,YAAY,WAAW,OAAO;AAAA,EACrC,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,UAAU;AACZ;","names":[]}
@@ -0,0 +1,74 @@
1
+ import type { ReactNode } from "react";
2
+ import { type LabelOverride } from "../i18n/kit-labels.js";
3
+ import { type SettingsSectionProps } from "../settings/settings-section.js";
4
+ import { DEFAULT_MAIL_TEST_CARD_LABELS } from "./mail-test-card-labels.js";
5
+ /** Every string {@link MailTestCard} renders — the `mailTestCard` namespace of
6
+ * `<UiKitProvider labels>`, overridable per instance through `labels`
7
+ * (docs/admin-harmonization.md §9.2.4). */
8
+ export interface MailTestCardLabels {
9
+ /** The card's title, and the app's catalogue entry's. */
10
+ title: string;
11
+ /** The sentence under it. */
12
+ description: string;
13
+ /** The transport's chip: "Transport: resend". `backend` is the server's `mail_backend`. */
14
+ transport: (backend: string) => string;
15
+ /** The button. */
16
+ action: string;
17
+ /** The answer said "sent" through a real transport. `address` is masked ("a…@example.com"). */
18
+ sent: (address: string) => string;
19
+ /** `mail_backend` is `console`: said whatever `sent` is — a console answers `true`. */
20
+ console: string;
21
+ /** Not sent, or the request failed. The provider's error stays in the server log. */
22
+ failed: string;
23
+ /** HTTP 429 — the app's limiter (server-kit's `MAIL_TEST_LIMIT`, 3 per 10 minutes);
24
+ * `seconds` from `Retry-After` when the response has one. */
25
+ rateLimited: (seconds?: number) => string;
26
+ /** For the app's catalogue entry (`keywords`); the card never reads it. */
27
+ keywords: string;
28
+ }
29
+ export { DEFAULT_MAIL_TEST_CARD_LABELS };
30
+ /** The answer of `POST /admin/mail/test` — server-kit 0.15's `MailTestResult`. */
31
+ export interface MailTestAnswer {
32
+ sent: boolean;
33
+ /** The transport that took it: `console`, `resend`, `smtp`, … */
34
+ mail_backend: string;
35
+ }
36
+ /** The card's default DOM id: its catalogue entry's `anchor`, the `?focus=` target. */
37
+ export declare const MAIL_TEST_ANCHOR = "admin-mail-test";
38
+ export interface MailTestCardProps extends Omit<SettingsSectionProps, "anchor" | "title" | "description" | "children"> {
39
+ /** The installation's transport, as the admin page has it (`mail_backend`): the chip
40
+ * before a send. After one, the answer's. */
41
+ mailBackend: string;
42
+ /** The app's `POST /admin/mail/test` (kastlan `/platform/mail/test`), no body: it
43
+ * awaits the app's own send and answers `{sent, mail_backend}`. A rejection is read
44
+ * for a 429 (the limiter); anything else is "could not send". */
45
+ send: () => Promise<MailTestAnswer>;
46
+ /** The signed-in admin's own address — where the mail goes. Shown masked. */
47
+ address: string;
48
+ /** Default {@link MAIL_TEST_ANCHOR}; the app's catalogue entry names the same. */
49
+ anchor?: string;
50
+ /** The app's sentence after the kit's description. */
51
+ description?: ReactNode;
52
+ labels?: LabelOverride<MailTestCardLabels>;
53
+ }
54
+ /**
55
+ * An address with its local part masked to its first character: "ada@example.com" →
56
+ * "a…@example.com". The domain stays, so the admin sees which mailbox to open.
57
+ */
58
+ export declare function maskMailAddress(address: string): string;
59
+ /**
60
+ * "Send a test mail to me" (docs/admin-harmonization.md §9.2): a `SettingsSection` with a
61
+ * sentence, the transport as a chip, the button and the result line. One mail goes to the
62
+ * signed-in admin's own address, never another, through the app's own send.
63
+ *
64
+ * **The result line reads `mail_backend` first**: with `console` it says "nothing was sent"
65
+ * whatever `sent` is, since every console path answers `true` (§9.2 Today). Otherwise
66
+ * `sent` says "Sent to a…@example.com. Check the inbox." or "Could not send." — the
67
+ * provider's error stays in the server log (§9.2.7). A 429 says the limiter's line. The
68
+ * button is busy while the request runs; the line is a live region.
69
+ *
70
+ * Not a commit: the test writes nothing, so it is not under the write lock (a demo never
71
+ * reaches the admin area, and the server refuses it anyway). The app adds the catalogue
72
+ * entry `{ id, anchor: MAIL_TEST_ANCHOR, title, keywords }` from these labels.
73
+ */
74
+ export declare function MailTestCard({ mailBackend, send, address, anchor, description, labels: labelsProp, ...rest }: MailTestCardProps): import("react").JSX.Element;
@@ -0,0 +1,80 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { Send } from "lucide-react";
5
+ import { useKitLabels } from "../i18n/kit-labels.js";
6
+ import { isRateLimited, retryAfterSeconds } from "../auth/auth-errors.js";
7
+ import { AlertBanner } from "../components/alert-banner.js";
8
+ import { Button } from "../components/button.js";
9
+ import { Chip } from "../components/chip.js";
10
+ import { SettingsSection } from "../settings/settings-section.js";
11
+ import { DEFAULT_MAIL_TEST_CARD_LABELS } from "./mail-test-card-labels.js";
12
+ const MAIL_TEST_ANCHOR = "admin-mail-test";
13
+ function maskMailAddress(address) {
14
+ const text = address.trim();
15
+ const at = text.lastIndexOf("@");
16
+ if (at <= 0) return text === "" ? "" : `${Array.from(text)[0]}\u2026`;
17
+ return `${Array.from(text.slice(0, at))[0]}\u2026${text.slice(at)}`;
18
+ }
19
+ function MailTestCard({
20
+ mailBackend,
21
+ send,
22
+ address,
23
+ anchor = MAIL_TEST_ANCHOR,
24
+ description,
25
+ labels: labelsProp,
26
+ ...rest
27
+ }) {
28
+ const labels = useKitLabels("mailTestCard", DEFAULT_MAIL_TEST_CARD_LABELS, labelsProp);
29
+ const [busy, setBusy] = useState(false);
30
+ const [outcome, setOutcome] = useState(null);
31
+ const run = async () => {
32
+ if (busy) return;
33
+ setBusy(true);
34
+ setOutcome(null);
35
+ try {
36
+ const answer = await send();
37
+ const backend2 = answer.mail_backend;
38
+ if (backend2 === "console") setOutcome({ kind: "console", backend: backend2 });
39
+ else setOutcome(answer.sent ? { kind: "sent", backend: backend2 } : { kind: "failed", backend: backend2 });
40
+ } catch (error) {
41
+ setOutcome(isRateLimited(error) ? { kind: "limited", seconds: retryAfterSeconds(error) } : { kind: "failed" });
42
+ } finally {
43
+ setBusy(false);
44
+ }
45
+ };
46
+ const backend = outcome && "backend" in outcome && outcome.backend ? outcome.backend : mailBackend;
47
+ return /* @__PURE__ */ jsx(
48
+ SettingsSection,
49
+ {
50
+ ...rest,
51
+ anchor,
52
+ title: labels.title,
53
+ description: description != null ? /* @__PURE__ */ jsxs(Fragment, { children: [
54
+ labels.description,
55
+ " ",
56
+ description
57
+ ] }) : labels.description,
58
+ children: /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
59
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
60
+ /* @__PURE__ */ jsxs(Button, { variant: "secondary", pending: busy, onClick: () => void run(), children: [
61
+ /* @__PURE__ */ jsx(Send, { className: "size-4", "aria-hidden": true }),
62
+ labels.action
63
+ ] }),
64
+ backend && /* @__PURE__ */ jsx(Chip, { size: "sm", "data-slot": "mail-backend", children: labels.transport(backend) })
65
+ ] }),
66
+ outcome?.kind === "sent" && /* @__PURE__ */ jsx(AlertBanner, { variant: "inline", block: true, tone: "success", children: /* @__PURE__ */ jsx("span", { "data-private": true, children: labels.sent(maskMailAddress(address)) }) }),
67
+ outcome?.kind === "console" && /* @__PURE__ */ jsx(AlertBanner, { variant: "inline", block: true, tone: "info", children: labels.console }),
68
+ outcome?.kind === "failed" && /* @__PURE__ */ jsx(AlertBanner, { variant: "inline", block: true, tone: "danger", children: labels.failed }),
69
+ outcome?.kind === "limited" && /* @__PURE__ */ jsx(AlertBanner, { variant: "inline", block: true, tone: "warning", children: labels.rateLimited(outcome.seconds) })
70
+ ] })
71
+ }
72
+ );
73
+ }
74
+ export {
75
+ DEFAULT_MAIL_TEST_CARD_LABELS,
76
+ MAIL_TEST_ANCHOR,
77
+ MailTestCard,
78
+ maskMailAddress
79
+ };
80
+ //# sourceMappingURL=mail-test-card.js.map