@eifi1/ui-kit 0.48.2 → 0.50.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.
- package/README.md +34 -16
- package/dist/admin/admin-action-confirm.js +1 -1
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/invitations-panel.js +5 -1
- package/dist/admin/invitations-panel.js.map +1 -1
- package/dist/admin/user-roster.d.ts +24 -12
- package/dist/admin/user-roster.js +21 -6
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/accept-invitation.js +2 -2
- package/dist/auth/accept-invitation.js.map +1 -1
- package/dist/auth/not-found-page.js +1 -1
- package/dist/auth/not-found-page.js.map +1 -1
- package/dist/auth/reset-password-form.js +2 -2
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/verify-email.d.ts +9 -1
- package/dist/auth/verify-email.js +5 -3
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/billing/plan-card.js +2 -2
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/components/amount-input.d.ts +2 -1
- package/dist/components/amount-input.js +3 -2
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.js +1 -0
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/breadcrumbs.d.ts +19 -1
- package/dist/components/breadcrumbs.js +11 -4
- package/dist/components/breadcrumbs.js.map +1 -1
- package/dist/components/button-look.d.ts +37 -7
- package/dist/components/button-look.js +33 -6
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/button.d.ts +4 -1
- package/dist/components/button.js +2 -2
- package/dist/components/button.js.map +1 -1
- package/dist/components/calendar-heatmap.js +2 -1
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/chart-edge.d.ts +10 -0
- package/dist/components/chart-edge.js +57 -0
- package/dist/components/chart-edge.js.map +1 -0
- package/dist/components/chart.js +2 -1
- package/dist/components/chart.js.map +1 -1
- package/dist/components/chip.d.ts +8 -1
- package/dist/components/chip.js +4 -1
- package/dist/components/chip.js.map +1 -1
- package/dist/components/combobox-core.js +1 -0
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox-suggestions.js +1 -0
- package/dist/components/combobox-suggestions.js.map +1 -1
- package/dist/components/confirm-context.d.ts +5 -0
- package/dist/components/confirm-context.js +7 -0
- package/dist/components/confirm-context.js.map +1 -0
- package/dist/components/confirm-dialog.js +3 -3
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/currency-select.d.ts +28 -2
- package/dist/components/currency-select.js +56 -10
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +2 -2
- package/dist/components/danger-confirm.js +1 -1
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-fit.js +21 -3
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-mobile.d.ts +58 -3
- package/dist/components/data-table-mobile.js +147 -16
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-row-actions.d.ts +2 -1
- package/dist/components/data-table-row-actions.js +7 -0
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-row.d.ts +1 -1
- package/dist/components/data-table-row.js.map +1 -1
- package/dist/components/data-table.d.ts +34 -1
- package/dist/components/data-table.js +37 -10
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/dropdown.js +1 -0
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/error-boundary.js +2 -2
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/floating-panel.d.ts +36 -13
- package/dist/components/floating-panel.js +27 -7
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +1 -1
- package/dist/components/form-actions.js +1 -1
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/full-bleed-dialog.js +1 -0
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/icon-button.d.ts +11 -0
- package/dist/components/icon-button.js +7 -5
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/modal.js +1 -0
- package/dist/components/modal.js.map +1 -1
- package/dist/components/nav-pills.d.ts +8 -1
- package/dist/components/nav-pills.js +61 -38
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/number-input.d.ts +2 -1
- package/dist/components/number-input.js +6 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.js +2 -0
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/page-header-context.d.ts +8 -0
- package/dist/components/page-header-context.js +7 -0
- package/dist/components/page-header-context.js.map +1 -0
- package/dist/components/page-header.d.ts +14 -2
- package/dist/components/page-header.js +10 -1
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/picker-sheet.js +2 -0
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart.js +2 -1
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/popover.js +1 -0
- package/dist/components/popover.js.map +1 -1
- package/dist/components/progress-bar.js +5 -4
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/reauth-dialog.js +1 -1
- package/dist/components/reauth-dialog.js.map +1 -1
- package/dist/components/row-actions-labels.js +3 -1
- package/dist/components/row-actions-labels.js.map +1 -1
- package/dist/components/row-actions.d.ts +43 -9
- package/dist/components/row-actions.js +78 -15
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/section-nav.d.ts +37 -0
- package/dist/components/section-nav.js +33 -0
- package/dist/components/section-nav.js.map +1 -0
- package/dist/components/series-chart.js +8 -3
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/share-card.js +1 -1
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/signature-pad.js +1 -1
- package/dist/components/signature-pad.js.map +1 -1
- package/dist/components/sparkline.js +2 -1
- package/dist/components/sparkline.js.map +1 -1
- package/dist/components/stat-tile.js +14 -7
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/status-dot.d.ts +5 -1
- package/dist/components/status-dot.js +4 -1
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/tooltip.js +1 -1
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/translation-review-editor.js +2 -2
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review.js +2 -2
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/treemap.d.ts +5 -2
- package/dist/components/treemap.js +26 -6
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/use-table-state.js +37 -6
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/data-table.js.map +1 -1
- package/dist/demo/demo-banner.js +1 -1
- package/dist/demo/demo-banner.js.map +1 -1
- package/dist/demo/demo-ended.js +1 -1
- package/dist/demo/demo-ended.js.map +1 -1
- package/dist/demo/demo-start.js +1 -1
- package/dist/demo/demo-start.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-inbox.js +1 -1
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/hooks/use-dismiss.d.ts +9 -1
- package/dist/hooks/use-dismiss.js +25 -5
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/hooks/use-document-title.d.ts +14 -0
- package/dist/hooks/use-document-title.js +22 -0
- package/dist/hooks/use-document-title.js.map +1 -0
- package/dist/i18n/defaults.js +2 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +20 -3
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +34 -1
- package/dist/i18n/kit-labels.js +43 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +22 -3
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +20 -3
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +20 -3
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +20 -3
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +20 -3
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +20 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +3 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/i18n/shared-labels.js +1 -0
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/landing/landing-actions.js +2 -2
- package/dist/landing/landing-actions.js.map +1 -1
- package/dist/landing/public-header.js +2 -2
- package/dist/landing/public-header.js.map +1 -1
- package/dist/lib/clipboard-minus.d.ts +27 -0
- package/dist/lib/clipboard-minus.js +63 -0
- package/dist/lib/clipboard-minus.js.map +1 -0
- package/dist/lib/data-attributes.d.ts +11 -0
- package/dist/lib/data-attributes.js +10 -0
- package/dist/lib/data-attributes.js.map +1 -0
- package/dist/lib/document-title.d.ts +17 -0
- package/dist/lib/document-title.js +29 -0
- package/dist/lib/document-title.js.map +1 -0
- package/dist/lib/float-clearance.js +8 -2
- package/dist/lib/float-clearance.js.map +1 -1
- package/dist/lib/format.d.ts +36 -10
- package/dist/lib/format.js +57 -11
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/minus.d.ts +32 -0
- package/dist/lib/minus.js +14 -0
- package/dist/lib/minus.js.map +1 -0
- package/dist/lib/status-tone.d.ts +30 -0
- package/dist/lib/status-tone.js +17 -0
- package/dist/lib/status-tone.js.map +1 -0
- package/dist/settings/settings-layout.js +3 -2
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/shell/account-menu-language.d.ts +1 -1
- package/dist/shell/account-menu-language.js +9 -1
- package/dist/shell/account-menu-language.js.map +1 -1
- package/dist/shell/app-shell-labels.js +2 -1
- package/dist/shell/app-shell-labels.js.map +1 -1
- package/dist/shell/app-shell.d.ts +58 -16
- package/dist/shell/app-shell.js +151 -43
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/language-code.d.ts +3 -0
- package/dist/shell/language-code.js +8 -0
- package/dist/shell/language-code.js.map +1 -0
- package/dist/shell/topbar-controls.d.ts +14 -6
- package/dist/shell/topbar-controls.js +14 -17
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/testing/css-variables.generated.d.ts +1 -1
- package/dist/testing/css-variables.generated.js +1 -1
- package/dist/testing/css-variables.generated.js.map +1 -1
- package/dist/testing/react.d.ts +25 -0
- package/dist/testing/react.js +19 -0
- package/dist/testing/react.js.map +1 -0
- package/dist/theme/chart-palette.d.ts +82 -15
- package/dist/theme/chart-palette.js +51 -24
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/theme/palette-presets.d.ts +9 -0
- package/dist/theme/palette-presets.js +77 -62
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/tour/tour-dock.d.ts +42 -0
- package/dist/tour/tour-dock.js +80 -0
- package/dist/tour/tour-dock.js.map +1 -0
- package/dist/tour/tour-gates.d.ts +71 -0
- package/dist/tour/tour-gates.js +114 -0
- package/dist/tour/tour-gates.js.map +1 -0
- package/dist/tour/tour-guide-button.d.ts +27 -0
- package/dist/tour/tour-guide-button.js +45 -0
- package/dist/tour/tour-guide-button.js.map +1 -0
- package/dist/tour/tour-guide-labels.d.ts +8 -0
- package/dist/tour/tour-guide-labels.js +9 -0
- package/dist/tour/tour-guide-labels.js.map +1 -0
- package/dist/tour/tour-labels.js +9 -1
- package/dist/tour/tour-labels.js.map +1 -1
- package/dist/tour/tour.d.ts +133 -9
- package/dist/tour/tour.js +594 -70
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-url-sync.d.ts +5 -2
- package/dist/tour/use-tour-url-sync.js +7 -1
- package/dist/tour/use-tour-url-sync.js.map +1 -1
- package/dist/tour.d.ts +2 -0
- package/dist/tour.js +3 -0
- package/dist/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +1 -1
- package/dist/wizard/stepper-nav.js +18 -5
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +11 -0
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +11 -5
- package/dist/wizard/use-wizard.js.map +1 -1
- package/package.json +9 -1
- package/src/admin/admin-action-confirm.tsx +1 -1
- package/src/admin/invitations-panel.tsx +7 -3
- package/src/admin/user-roster.tsx +41 -14
- package/src/auth/accept-invitation.tsx +3 -3
- package/src/auth/not-found-page.tsx +1 -1
- package/src/auth/reset-password-form.tsx +3 -3
- package/src/auth/verify-email.tsx +13 -3
- package/src/billing/plan-card.tsx +2 -2
- package/src/components/amount-input.tsx +5 -4
- package/src/components/autocomplete.tsx +2 -0
- package/src/components/breadcrumbs.tsx +32 -4
- package/src/components/button-look.ts +86 -15
- package/src/components/button.tsx +6 -3
- package/src/components/calendar-heatmap.tsx +2 -1
- package/src/components/chart-edge.ts +73 -0
- package/src/components/chart.tsx +3 -1
- package/src/components/chip.tsx +10 -1
- package/src/components/combobox-core.tsx +2 -0
- package/src/components/combobox-suggestions.tsx +2 -0
- package/src/components/confirm-context.ts +7 -0
- package/src/components/confirm-dialog.tsx +3 -3
- package/src/components/currency-select.tsx +95 -6
- package/src/components/danger-confirm.tsx +3 -3
- package/src/components/data-table-fit.ts +41 -6
- package/src/components/data-table-mobile.tsx +259 -17
- package/src/components/data-table-row-actions.tsx +9 -1
- package/src/components/data-table-row.tsx +3 -0
- package/src/components/data-table.tsx +91 -19
- package/src/components/dropdown.tsx +2 -0
- package/src/components/error-boundary.tsx +2 -2
- package/src/components/floating-panel.tsx +67 -16
- package/src/components/form-actions.tsx +2 -2
- package/src/components/full-bleed-dialog.tsx +3 -0
- package/src/components/icon-button.tsx +21 -8
- package/src/components/modal.tsx +3 -0
- package/src/components/nav-pills.tsx +29 -2
- package/src/components/number-input.tsx +8 -3
- package/src/components/numpad-sheet.tsx +4 -0
- package/src/components/page-header-context.ts +8 -0
- package/src/components/page-header.tsx +34 -2
- package/src/components/picker-sheet.tsx +4 -0
- package/src/components/pie-chart.tsx +2 -1
- package/src/components/popover.tsx +2 -0
- package/src/components/progress-bar.tsx +6 -3
- package/src/components/reauth-dialog.tsx +1 -1
- package/src/components/row-actions-labels.ts +2 -0
- package/src/components/row-actions.tsx +154 -33
- package/src/components/section-nav.tsx +65 -0
- package/src/components/series-chart.tsx +12 -3
- package/src/components/share-card.tsx +1 -1
- package/src/components/signature-pad.tsx +1 -1
- package/src/components/sparkline.tsx +2 -1
- package/src/components/stat-tile.tsx +19 -7
- package/src/components/status-dot.tsx +7 -1
- package/src/components/tooltip.tsx +3 -1
- package/src/components/translation-review-editor.tsx +2 -2
- package/src/components/translation-review.tsx +2 -2
- package/src/components/treemap.tsx +34 -8
- package/src/components/use-table-state.ts +63 -8
- package/src/data-table.ts +3 -3
- package/src/demo/demo-banner.tsx +1 -1
- package/src/demo/demo-ended.tsx +1 -1
- package/src/demo/demo-start.tsx +1 -1
- package/src/feedback/feedback-inbox.tsx +1 -1
- package/src/hooks/use-dismiss.ts +43 -4
- package/src/hooks/use-document-title.ts +31 -0
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/german.ts +17 -0
- package/src/i18n/kit-labels.tsx +79 -1
- package/src/i18n/locales/es.ts +17 -0
- package/src/i18n/locales/fr.ts +17 -0
- package/src/i18n/locales/hu.ts +17 -0
- package/src/i18n/locales/it.ts +17 -0
- package/src/i18n/locales/zh.ts +17 -0
- package/src/i18n/review.ts +3 -0
- package/src/i18n/shared-labels.ts +1 -0
- package/src/index.ts +13 -0
- package/src/landing/landing-actions.tsx +2 -2
- package/src/landing/public-header.tsx +2 -2
- package/src/lib/clipboard-minus.ts +94 -0
- package/src/lib/data-attributes.ts +14 -0
- package/src/lib/document-title.ts +41 -0
- package/src/lib/float-clearance.ts +19 -3
- package/src/lib/format.ts +117 -30
- package/src/lib/minus.ts +50 -0
- package/src/lib/status-tone.ts +43 -0
- package/src/settings/settings-layout.tsx +3 -2
- package/src/shell/account-menu-language.tsx +14 -2
- package/src/shell/app-shell-labels.ts +1 -0
- package/src/shell/app-shell.tsx +291 -68
- package/src/shell/language-code.ts +5 -0
- package/src/shell/topbar-controls.tsx +35 -15
- package/src/testing/css-variables.generated.ts +6 -6
- package/src/testing/react.ts +59 -0
- package/src/theme/chart-palette.ts +102 -20
- package/src/theme/palette-presets.ts +121 -84
- package/src/tour/tour-dock.ts +132 -0
- package/src/tour/tour-gates.ts +195 -0
- package/src/tour/tour-guide-button.tsx +64 -0
- package/src/tour/tour-guide-labels.ts +15 -0
- package/src/tour/tour-labels.ts +8 -0
- package/src/tour/tour.tsx +949 -86
- package/src/tour/use-tour-url-sync.ts +14 -4
- package/src/tour.ts +3 -0
- package/src/wizard/stepper-nav.tsx +27 -7
- package/src/wizard/types.ts +12 -0
- package/src/wizard/use-wizard.ts +16 -9
- package/tokens.css +28 -18
package/README.md
CHANGED
|
@@ -25,6 +25,13 @@ module so consumers can still tree-shake it (see the `sideEffects` note in
|
|
|
25
25
|
entry chunk). Tailwind classes are emitted as-is; each app still compiles them
|
|
26
26
|
with its own Tailwind config.
|
|
27
27
|
|
|
28
|
+
## Compatibility with server-kit
|
|
29
|
+
|
|
30
|
+
The backend half, [`eifi1-server-kit`](https://github.com/Eifi1/server-kit), is a separate
|
|
31
|
+
package with its own version and changelog. **ui-kit 0.49 ↔ server-kit 0.19**: this release
|
|
32
|
+
is tested against server-kit 0.19.0's API export (the showcase's Server kit pages and the
|
|
33
|
+
vocabulary test, `showcase/src/__tests__/server-kit-vocabularies.test.ts`).
|
|
34
|
+
|
|
28
35
|
## Documentation
|
|
29
36
|
|
|
30
37
|
| | |
|
|
@@ -68,6 +75,7 @@ when it chooses. To work on the design system and an app together, use
|
|
|
68
75
|
| `@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
76
|
| `idb-keyval` | peer, `^6`, **optional** | The persisted cache's default IndexedDB store, in `@eifi1/ui-kit/offline` only. |
|
|
70
77
|
| `dexie` | peer, `^4`, **optional** | The sync engine's device copy, in `@eifi1/ui-kit/sync` only (0.46); `/offline` loads without it. |
|
|
78
|
+
| `@testing-library/react` | peer, `>=16`, **optional** | `rowMenuAction` in `@eifi1/ui-kit/testing/react` only (0.50), for an app's own tests; `@eifi1/ui-kit/testing` stays pure. |
|
|
71
79
|
| `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. |
|
|
72
80
|
| `flag-icons` | **nothing — you supply the stylesheet** | See below. |
|
|
73
81
|
|
|
@@ -445,7 +453,8 @@ because they answer different questions.
|
|
|
445
453
|
|---|---|---|
|
|
446
454
|
| **Semantic UI** | `--danger*` `--warning*` `--info*` `--success*`, `--status-*` | *What does this mean?* Danger is red because every other interface the reader has used made it red. Fixed meanings, so they live in `tokens.css` and are **not** part of `TokenSet` — a destructive action must not change colour because somebody picked a different preset. |
|
|
447
455
|
| **Semantic data** | `--money-income` `--money-expense` `--money-net` `--money-neutral` | *Which direction is this number?* Never red/green: that pair is exactly the axis dichromacy collapses. Always paired with a `+`/`−` or an arrow at the call site, so colour is never the only signal. |
|
|
448
|
-
| **Categorical chart** | `--chart-1` … `--chart-9` | *Which series is this?* No meaning and no order —
|
|
456
|
+
| **Categorical chart** | `--chart-1` … `--chart-8` (`--chart-9` repeats slot 1) | *Which series is this?* No meaning and no order — eight hues that must stay separable from each other: anyplot's "Imprint" set (0.49; Paul Tol's "Muted" is the `paul-tol` preset), the same in light and dark, CVD-safe across protan/deutan/tritan. `paletteFor(i)` returns the CSS var (cycling eight, series 1 always the green; `{ order: "cvd" }` for anyplot's CVD order), so a Recharts series is theme-aware for free. A hue under 3:1 on the ground gets a 1px ink edge on bars, areas and treemap tiles (`chartEdge`). |
|
|
457
|
+
| **Chart inks** | `--chart-ink` `--chart-muted` | The only chart colours that change with the theme. `CHART_ROLES`: total / baseline / outline in ink; other / rest / disabled and confidence bands in muted ink. |
|
|
449
458
|
|
|
450
459
|
Optimising a set for one of these makes it worse at the others. A UI colour needs contrast
|
|
451
460
|
against one known surface and carries a fixed meaning; a chart colour needs to stay
|
|
@@ -559,19 +568,19 @@ columns, strings and permissions (see the note at the top of `src/feedback/feedb
|
|
|
559
568
|
|
|
560
569
|
<!-- BEGIN GENERATED: exports — node scripts/gen-export-inventory.mjs -->
|
|
561
570
|
|
|
562
|
-
**
|
|
571
|
+
**2188 names from 361 modules** — 1014 values and 1174 types. _Italic_ is a type-only export.
|
|
563
572
|
|
|
564
573
|
Generated from `dist/index.d.ts` by `node scripts/gen-export-inventory.mjs`; the count
|
|
565
574
|
is pinned by `src/__tests__/public-surface.test.ts`. Do not edit between the markers.
|
|
566
575
|
|
|
567
576
|
| Entry point | Names |
|
|
568
577
|
|---|---|
|
|
569
|
-
| `@eifi1/ui-kit` |
|
|
570
|
-
| `@eifi1/ui-kit/chart` |
|
|
571
|
-
| `@eifi1/ui-kit/shell` |
|
|
578
|
+
| `@eifi1/ui-kit` | 2188 |
|
|
579
|
+
| `@eifi1/ui-kit/chart` | 100 |
|
|
580
|
+
| `@eifi1/ui-kit/shell` | 117 |
|
|
572
581
|
| `@eifi1/ui-kit/data-table` | 56 |
|
|
573
|
-
| `@eifi1/ui-kit/wizard` |
|
|
574
|
-
| `@eifi1/ui-kit/tour` |
|
|
582
|
+
| `@eifi1/ui-kit/wizard` | 25 |
|
|
583
|
+
| `@eifi1/ui-kit/tour` | 48 |
|
|
575
584
|
| `@eifi1/ui-kit/feedback` | 212 |
|
|
576
585
|
| `@eifi1/ui-kit/search` | 23 |
|
|
577
586
|
| `@eifi1/ui-kit/dates` | 22 |
|
|
@@ -579,6 +588,7 @@ is pinned by `src/__tests__/public-surface.test.ts`. Do not edit between the mar
|
|
|
579
588
|
| `@eifi1/ui-kit/currencies` | 3 |
|
|
580
589
|
| `@eifi1/ui-kit/eslint` | 1 |
|
|
581
590
|
| `@eifi1/ui-kit/testing` | 11 |
|
|
591
|
+
| `@eifi1/ui-kit/testing/react` | 2 |
|
|
582
592
|
| `@eifi1/ui-kit/rhf` | 48 |
|
|
583
593
|
| `@eifi1/ui-kit/offline` | 33 |
|
|
584
594
|
| `@eifi1/ui-kit/sync` | 38 |
|
|
@@ -595,7 +605,7 @@ re-slicing of it, never a second API.
|
|
|
595
605
|
| `lib/calc` | `commitExpression`, `evaluateExpression`, `formatResult`, `isBareAmount`, `looksLikeExpression`, `sanitizeLive`, `splitLeadingSign` |
|
|
596
606
|
| `lib/cn` | `cn` |
|
|
597
607
|
| `lib/logger` | `logger`, `setStoreLog` |
|
|
598
|
-
| `lib/format` | `currencyMinorDigits`, `EMPTY_FORMATTED_VALUE`, `formatDate`, `formatMoney`, `formatNumber`, `formatPercent`, `formatRelativeTime`, `roundToCurrency`, `toDate`, `useKitFormat`, _`DateInput`_, _`FormatDateOptions`_, _`FormatDateStyle`_, _`FormatDigits`_, _`FormatMoneyOptions`_, _`FormatNumberBaseOptions`_, _`FormatNumberOptions`_, _`FormatPercentOptions`_, _`FormatRelativeTimeOptions`_, _`KitFormat`_, _`RoundToCurrencyOptions`_ |
|
|
608
|
+
| `lib/format` | `currencyMinorDigits`, `EMPTY_FORMATTED_VALUE`, `formatDate`, `formatMoney`, `formatNumber`, `formatPercent`, `formatRelativeTime`, `roundToCurrency`, `toDate`, `useKitFormat`, _`DateInput`_, _`FormatDateOptions`_, _`FormatDateStyle`_, _`FormatDigits`_, _`FormatMoneyOptions`_, _`FormatNumberBaseOptions`_, _`FormatNumberOptions`_, _`FormatPercentOptions`_, _`FormatRelativeTimeOptions`_, _`FormattableNumber`_, _`KitFormat`_, _`RoundToCurrencyOptions`_ |
|
|
599
609
|
| `lib/safe-storage` | `readStored`, `writeStored` |
|
|
600
610
|
| `lib/download` | `filenameFromDisposition`, `saveBlob` |
|
|
601
611
|
| `lib/text-match` | `foldForSearch`, `rankMatch`, _`MatchRank`_ |
|
|
@@ -609,8 +619,12 @@ re-slicing of it, never a second API.
|
|
|
609
619
|
| `lib/isin` | `formatIsin`, `isinCheckDigit`, `isValidIsin` |
|
|
610
620
|
| `lib/phone` | `formatNationalPhone`, `formatPhone`, `isE164`, `parsePhone`, `PHONE_COUNTRIES`, `PHONE_DIAL_CODES`, _`ParsedPhone`_, _`PhoneCountry`_, _`PhoneCountryCode`_ |
|
|
611
621
|
| `lib/column-mapping` | `assignColumnRole`, `guessMapping`, `missingRoles`, `readMappedTable`, `readTextFile`, `roleOfColumn`, _`ColumnMapperResult`_, _`ColumnMapping`_, _`ColumnRole`_ |
|
|
622
|
+
| `lib/clipboard-minus` | `asciiMinusInNumbers` |
|
|
623
|
+
| `lib/document-title` | `DEFAULT_DOCUMENT_TITLE_FORMAT`, `formatDocumentTitle`, _`DocumentTitleParts`_ |
|
|
624
|
+
| `lib/status-tone` | `STATUS_KINDS`, `statusTone`, _`StatusKind`_, _`StatusTone`_ |
|
|
612
625
|
| `lib/date-format` | `DATE_FORMAT_PREFERENCES`, `dateFormatSample`, `dateFormatterFor`, `formatDateWithPreference`, `isDateFormatPreference`, `resolveDateFormat`, _`DateFormatOptions`_, _`DateFormatPreference`_, _`ResolvedDateFormat`_ |
|
|
613
626
|
| `lib/device-settings` | `deviceSettings`, `registerDeviceSetting`, `resetDeviceSettings`, _`DeviceSetting`_, _`DeviceSettingKind`_ |
|
|
627
|
+
| `lib/minus` | `formatWithMinus`, _`MinusStyle`_ |
|
|
614
628
|
| `lib/clipping` | `CLIPS_ATTRIBUTE` |
|
|
615
629
|
| `lib/chip-edge-snap` | `refreshChipEdges` |
|
|
616
630
|
|
|
@@ -629,6 +643,7 @@ re-slicing of it, never a second API.
|
|
|
629
643
|
| `hooks/use-close-transition` | `OVERLAY_EXIT_MS`, `useCloseTransition` |
|
|
630
644
|
| `hooks/use-row-swipe` | `useRowSwipe`, _`RowSwipeOptions`_, _`RowSwipeReturn`_, _`SwipeStage`_ |
|
|
631
645
|
| `hooks/use-noindex` | `useNoIndex` |
|
|
646
|
+
| `hooks/use-document-title` | `useDocumentTitle` |
|
|
632
647
|
| `hooks/use-file-drop` | `dragHasFiles`, `useFileDrop`, _`FileDropProps`_, _`UseFileDropOptions`_, _`UseFileDropReturn`_ |
|
|
633
648
|
| `hooks/use-debounce` | `useDebounce`, `useDebouncedCallback`, _`DebouncedCallbackOptions`_, _`DebouncedFunction`_ |
|
|
634
649
|
| `hooks/use-search-param-state` | `useDialogParam`, `useSearchParamsState`, `useSearchParamState`, `useTabParam`, _`DialogParam`_, _`SearchParamField`_, _`SearchParamFields`_, _`SearchParamStateOptions`_, _`SearchParamsUpdate`_ |
|
|
@@ -646,18 +661,18 @@ re-slicing of it, never a second API.
|
|
|
646
661
|
|---|---|
|
|
647
662
|
| `theme/color` | `contrast`, `deltaE`, `hexToOklch`, `luminance`, `oklchToHex`, `oklchToRgb`, `parseHex`, `rgbToOklch`, `simulateCvd`, `solveLightness`, `toHex`, _`CvdType`_, _`Oklch`_, _`Rgb`_ |
|
|
648
663
|
| `theme/palette-derive` | `auditChartRamp`, `auditPalette`, `deriveChartRamp`, `derivePalette`, `describe`, _`AnchorRole`_, _`ChartRampReport`_, _`ContrastCheck`_, _`ContrastReport`_, _`DerivedPalette`_, _`DeriveOptions`_, _`PaletteAnchors`_, _`SemanticTokens`_ |
|
|
649
|
-
| `theme/palette-presets` | `applyTokenSet`, `DEFAULT_PRESET`, `DERIVED_PRESETS`, `IMPRINT_PRESET`, `PALETTES`, `presetById`, _`PalettePreset`_, _`TokenSet`_ |
|
|
664
|
+
| `theme/palette-presets` | `applyTokenSet`, `DEFAULT_PRESET`, `DERIVED_PRESETS`, `IMPRINT_PRESET`, `PALETTES`, `PAUL_TOL_PRESET`, `presetById`, _`PalettePreset`_, _`TokenSet`_ |
|
|
650
665
|
| `theme/theme-store` | `applyPersistedTheme`, `createThemeStore`, _`ThemeMode`_, _`ThemePreference`_, _`ThemeState`_, _`ThemeStore`_ |
|
|
651
666
|
| `theme/palette-store` | `applyPersistedPalette`, `createPaletteStore`, _`PaletteStore`_ |
|
|
652
667
|
| `theme/text-size` | `applyPersistedTextSize`, `applyTextSize`, `createTextSizeStore`, `isTextSize`, `readPersistedTextSize`, `readTextSize`, `resolveTextSize`, `TEXT_SCALE`, `TEXT_SIZE_INLINE_SCRIPT`, `TEXT_SIZES`, `useTextSize`, _`ApplyTextSizeOptions`_, _`PersistedAppearanceOptions`_, _`PersistedTextSize`_, _`TextSize`_, _`TextSizeInfo`_, _`TextSizeState`_, _`TextSizeStore`_ |
|
|
653
668
|
| `theme/contrast` | `applyContrast`, `applyPersistedContrast`, `CONTRAST_MODES`, `contrastLevel`, `createContrastStore`, `isContrastMode`, `prefersMoreContrast`, `readContrastLevel`, `readPersistedContrast`, `resolveContrastMode`, _`ApplyContrastOptions`_, _`ContrastLevel`_, _`ContrastMode`_, _`ContrastState`_, _`ContrastStore`_, _`PersistedContrast`_ |
|
|
654
|
-
| `theme/chart-palette` | `CHART_COLORS`, `HEATMAP_HEX`, `lerpHex`, `PALETTE_HEX`, `paletteFor`, `textOn`, _`HeatStops`_ |
|
|
669
|
+
| `theme/chart-palette` | `CHART_COLORS`, `CHART_INK_HEX`, `CHART_MUTED_HEX`, `CHART_ROLES`, `chartEdge`, `CVD_ORDER`, `EDGE_MIN_CONTRAST`, `HEATMAP_HEX`, `lerpHex`, `PALETTE_HEX`, `PALETTE_SIZE`, `paletteFor`, `textOn`, _`HeatStops`_, _`PaletteForOptions`_ |
|
|
655
670
|
|
|
656
671
|
### i18n
|
|
657
672
|
|
|
658
673
|
| Module | Exports |
|
|
659
674
|
|---|---|
|
|
660
|
-
| `i18n/kit-labels` | `DEFAULT_APP_SHELL_LABELS`, `DEFAULT_CALCULATOR_LABELS`, `DEFAULT_COMBOBOX_LABELS`, `DEFAULT_COMMON_LABELS`, `DEFAULT_CURRENCY_LABELS`, `DEFAULT_DATE_PICKER_LABELS`, `DEFAULT_FILE_LABELS`, `DEFAULT_MULTI_SELECT_LABELS`, `DEFAULT_PICKER_SHEET_LABELS`, `DEFAULT_SWIPEABLE_ROW_LABELS`, `DEFAULT_TOP_BAR_LABELS`, `formatFileSize`, `missingKitLabels`, `UiKitProvider`, `useKitChartTooltipPlacement`, `useKitDateFormatter`, `useKitFileLabels`, `useKitLabelOverrides`, `useKitLabels`, `useKitLink`, `useKitLocale`, `useKitTableStorageKeyPrefix`, `useKitTimeZone`, `useKitWeekStart`, _`AppShellLabels`_, _`CalculatorLabels`_, _`ChartTooltipPlacement`_, _`ComboboxLabels`_, _`CommonLabels`_, _`CurrencyLabels`_, _`DatePickerLabels`_, _`FileLabels`_, _`KitDateFormatContext`_, _`KitDateFormatter`_, _`KitLinkComponent`_, _`KitLinkProps`_, _`LabelOverride`_, _`MultiSelectLabels`_, _`PickerSheetLabels`_, _`SwipeableRowLabels`_, _`TopBarLabels`_, _`UiKitLabelOverrides`_, _`UiKitLabels`_, _`UiKitProviderProps`_ |
|
|
675
|
+
| `i18n/kit-labels` | `DEFAULT_APP_SHELL_LABELS`, `DEFAULT_CALCULATOR_LABELS`, `DEFAULT_COMBOBOX_LABELS`, `DEFAULT_COMMON_LABELS`, `DEFAULT_CURRENCY_LABELS`, `DEFAULT_DATE_PICKER_LABELS`, `DEFAULT_FILE_LABELS`, `DEFAULT_MULTI_SELECT_LABELS`, `DEFAULT_PICKER_SHEET_LABELS`, `DEFAULT_SWIPEABLE_ROW_LABELS`, `DEFAULT_TOP_BAR_LABELS`, `formatFileSize`, `missingKitLabels`, `UiKitProvider`, `useKitChartTooltipPlacement`, `useKitDateFormatter`, `useKitDocumentTitle`, `useKitFileLabels`, `useKitLabelOverrides`, `useKitLabels`, `useKitLink`, `useKitLocale`, `useKitTableStorageKeyPrefix`, `useKitTimeZone`, `useKitWeekStart`, _`AppShellLabels`_, _`CalculatorLabels`_, _`ChartTooltipPlacement`_, _`ComboboxLabels`_, _`CommonLabels`_, _`CurrencyLabels`_, _`DatePickerLabels`_, _`FileLabels`_, _`KitDateFormatContext`_, _`KitDateFormatter`_, _`KitLinkComponent`_, _`KitLinkProps`_, _`LabelOverride`_, _`MultiSelectLabels`_, _`PickerSheetLabels`_, _`SwipeableRowLabels`_, _`TopBarLabels`_, _`UiKitLabelOverrides`_, _`UiKitLabels`_, _`UiKitProviderProps`_ |
|
|
661
676
|
| `i18n/defaults` | `DEFAULT_UI_KIT_LABELS` |
|
|
662
677
|
| `i18n/review` | `kitLabelStrings`, _`KitLabelStringsOptions`_ |
|
|
663
678
|
| `i18n/languages` | `formatLocaleOf`, `KIT_LANGUAGES`, `languageOptions`, `loadUiKitLabels`, `peekUiKitLabels`, `resolveLanguage`, `useUiKitLabels`, _`KitLanguage`_, _`KitLanguageCode`_ |
|
|
@@ -739,6 +754,7 @@ re-slicing of it, never a second API.
|
|
|
739
754
|
| `components/alert-banner` | `AlertBanner`, `alertFrameClass`, `toneFrameClass`, _`AlertBannerProps`_, _`AlertSize`_, _`AlertTone`_ |
|
|
740
755
|
| `components/toggle-group` | `ToggleGroup`, _`ToggleGroupBaseProps`_, _`ToggleGroupClearableProps`_, _`ToggleGroupProps`_, _`ToggleGroupRequiredProps`_, _`ToggleOption`_ |
|
|
741
756
|
| `components/nav-pills` | `NavPills`, _`NavPillItem`_, _`NavPillLinkProps`_, _`NavPillsCurrent`_, _`NavPillsProps`_ |
|
|
757
|
+
| `components/section-nav` | `SectionNav`, _`SectionNavProps`_ |
|
|
742
758
|
| `components/chip` | `Chip`, _`ChipHue`_, _`ChipLinkProps`_, _`ChipProps`_, _`ChipShape`_, _`ChipSize`_, _`ChipTone`_, _`ChipVariant`_ |
|
|
743
759
|
| `components/chip-input` | `ChipInput`, `DEFAULT_CHIP_INPUT_LABELS`, _`ChipInputLabels`_, _`ChipInputProps`_ |
|
|
744
760
|
| `components/wizard-stepper` | `WizardStepper` |
|
|
@@ -785,7 +801,7 @@ re-slicing of it, never a second API.
|
|
|
785
801
|
| `components/legal-page` | `LEGAL_SKELETON`, `LegalAcceptCheckbox`, `LegalKitSection`, `LegalPage`, _`LegalAcceptCheckboxProps`_, _`LegalDisclaimerVariant`_, _`LegalKitSectionKey`_, _`LegalKitSectionProps`_, _`LegalPageProps`_, _`LegalSectionOwner`_, _`LegalSkeletonEntry`_, _`LegalSkeletonPage`_ |
|
|
786
802
|
| `components/appearance-labels` | `DEFAULT_APPEARANCE_LABELS`, `useAppearanceLabels`, _`AppearanceLabels`_ |
|
|
787
803
|
| `components/date-format-setting` | `DateFormatSetting`, _`DateFormatSettingProps`_ |
|
|
788
|
-
| `components/row-actions` | `DEFAULT_ROW_ACTIONS_LABELS`, `ROW_ACTIONS_CELL_CLASS`, `ROW_ACTIONS_HEAD_CLASS`, `RowActions`, `rowActionsColumn`, `RowActionsHead`, _`RowAction`_, _`RowActionList`_, _`RowActionsCollapse`_, _`RowActionsColumnOptions`_, _`RowActionsHeadProps`_, _`RowActionsLabels`_, _`RowActionsProps`_, _`RowActionsSize`_, _`RowActionTone`_ |
|
|
804
|
+
| `components/row-actions` | `DEFAULT_ROW_ACTIONS_LABELS`, `ROW_ACTIONS_CELL_CLASS`, `ROW_ACTIONS_HEAD_CLASS`, `RowActions`, `rowActionsColumn`, `RowActionsHead`, _`RowAction`_, _`RowActionConfirm`_, _`RowActionList`_, _`RowActionsCollapse`_, _`RowActionsColumnOptions`_, _`RowActionsHeadProps`_, _`RowActionsLabels`_, _`RowActionsProps`_, _`RowActionsSize`_, _`RowActionTone`_ |
|
|
789
805
|
| `components/checkbox-group` | `CheckboxGroup`, _`CheckboxGroupLegendVisibility`_, _`CheckboxGroupOption`_, _`CheckboxGroupProps`_ |
|
|
790
806
|
| `components/one-time-code-input` | `OneTimeCodeInput`, _`OneTimeCodeInputProps`_ |
|
|
791
807
|
| `components/language-select` | `LanguageSelect`, _`LanguageSelectProps`_ |
|
|
@@ -823,9 +839,9 @@ re-slicing of it, never a second API.
|
|
|
823
839
|
|
|
824
840
|
| Module | Exports |
|
|
825
841
|
|---|---|
|
|
826
|
-
| `shell/topbar-controls` | `LanguageMenu`, `PaletteMenu`, `ThemeToggle`, `TOPBAR_MENU_ITEM_CLASS`, `TOPBAR_TRIGGER_CLASS`, _`LanguageOption`_ |
|
|
842
|
+
| `shell/topbar-controls` | `LanguageMenu`, `PaletteMenu`, `ThemeToggle`, `TOPBAR_MENU_ITEM_CLASS`, `TOPBAR_TRIGGER_CLASS`, _`LanguageMenuTrigger`_, _`LanguageOption`_ |
|
|
827
843
|
| `shell/top-bar` | `TopBar`, _`TopBarProps`_ |
|
|
828
|
-
| `shell/app-shell` | `AppShell`, `DEFAULT_APP_SHELL_MORE_LABELS`, `DEFAULT_MOBILE_BAR_MAX`, `splitMobileBar`, _`AppShellMoreLabels`_, _`AppShellNavItem`_, _`AppShellProps`_, _`AppShellSidebarFooterItem`_, _`AppShellSubItem`_ |
|
|
844
|
+
| `shell/app-shell` | `AppShell`, `DEFAULT_APP_SHELL_MORE_LABELS`, `DEFAULT_MOBILE_BAR_MAX`, `DEFAULT_MOBILE_MAX_ITEMS`, `pickMobileBar`, `splitMobileBar`, _`AppShellMoreLabels`_, _`AppShellNavItem`_, _`AppShellProps`_, _`AppShellSidebarFooterItem`_, _`AppShellSubItem`_ |
|
|
829
845
|
| `shell/option-switcher-menu` | `OptionSwitcherMenu`, _`OptionSwitcherOption`_ |
|
|
830
846
|
| `shell/role-switcher` | `RoleSwitcher`, _`RoleSwitcherProps`_ |
|
|
831
847
|
| `shell/topbar-action-menu` | `TopBarActionMenu`, _`TopBarMenuEntry`_, _`TopBarMenuHeader`_ |
|
|
@@ -902,7 +918,7 @@ re-slicing of it, never a second API.
|
|
|
902
918
|
|
|
903
919
|
| Module | Exports |
|
|
904
920
|
|---|---|
|
|
905
|
-
| `wizard/types` | `DEFAULT_WIZARD_LABELS`, _`FieldErrors`_, _`StepStatus`_, _`SummaryItem`_, _`SummarySection`_, _`UseWizardOptions`_, _`UseWizardReturn`_, _`ValidateResult`_, _`WizardLabels`_, _`WizardStepConfig`_, _`WizardUrlSync`_ |
|
|
921
|
+
| `wizard/types` | `DEFAULT_WIZARD_LABELS`, _`FieldErrors`_, _`StepStatus`_, _`SummaryItem`_, _`SummarySection`_, _`UseWizardOptions`_, _`UseWizardReturn`_, _`ValidateResult`_, _`WizardFinishResult`_, _`WizardLabels`_, _`WizardStepConfig`_, _`WizardUrlSync`_ |
|
|
906
922
|
| `wizard/use-wizard` | `useWizard` |
|
|
907
923
|
| `wizard/wizard-context` | `useOptionalWizardContext`, `useWizardContext`, `useWizardNextGate`, `useWizardStepValidate`, `WizardContextProvider`, _`WizardContextValue`_ |
|
|
908
924
|
| `wizard/validation` | `requiredFieldsValidator`, _`RequiredFieldSpec`_ |
|
|
@@ -914,11 +930,13 @@ re-slicing of it, never a second API.
|
|
|
914
930
|
|
|
915
931
|
| Module | Exports |
|
|
916
932
|
|---|---|
|
|
917
|
-
| `tour/tour` | `DEFAULT_TOUR_LABELS`, `TourProvider`, `useTour`, `useTourOptional`, _`TourCompletion`_, _`TourLabels`_, _`TourPlacement`_, _`TourStep`_ |
|
|
933
|
+
| `tour/tour` | `DEFAULT_TOUR_LABELS`, `TourProvider`, `useTour`, `useTourOptional`, _`TourAt`_, _`TourCompletion`_, _`TourLabels`_, _`TourLauncher`_, _`TourPlacement`_, _`TourStart`_, _`TourStep`_, _`TourStepContext`_ |
|
|
918
934
|
| `tour/tour-catalogue` | `DEFAULT_TOUR_CATALOGUE_LABELS`, `TourCatalogue`, `TourCatalogueCard`, _`TourCatalogueCardProps`_, _`TourCatalogueLabels`_, _`TourCatalogueProps`_ |
|
|
919
935
|
| `tour/tour-info` | `doneCount`, `earnedPoints`, `isTourDone`, `isTourNew`, `isTourReset`, _`TourCompletions`_, _`TourGroup`_, _`TourInfo`_, _`TourUpdate`_ |
|
|
920
936
|
| `tour/use-tour-progress` | `completeTour`, `resetTourProgress`, `signOutTourProgress`, `useTourProgress`, _`TourProgress`_, _`TourProgressAccount`_, _`TourProgressAdopt`_, _`TourProgressBody`_, _`UseTourProgressOptions`_ |
|
|
921
937
|
| `tour/use-tour-url-sync` | `useTourCatalogueUrlState`, `useTourUrlSync`, _`TourCatalogueFilter`_, _`TourStateFilter`_, _`UseTourUrlSyncOptions`_ |
|
|
938
|
+
| `tour/tour-guide-button` | `DEFAULT_TOUR_GUIDE_LABELS`, `TourGuideButton`, _`TourGuideButtonProps`_, _`TourGuideLabels`_ |
|
|
939
|
+
| `tour/tour-gates` | `useTourGate`, `useTourGateRelease`, _`TourGateRelease`_ |
|
|
922
940
|
|
|
923
941
|
### search
|
|
924
942
|
|
|
@@ -207,7 +207,7 @@ function AdminActionConfirm({
|
|
|
207
207
|
{
|
|
208
208
|
type: "submit",
|
|
209
209
|
form: formId,
|
|
210
|
-
variant: tone === "danger" ? "danger" : "
|
|
210
|
+
variant: tone === "danger" ? "danger" : "default",
|
|
211
211
|
pending: busy,
|
|
212
212
|
commit,
|
|
213
213
|
disabledReason: busy ? void 0 : guardReason,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/admin/admin-action-confirm.tsx"],"sourcesContent":["import { Children, useContext, useEffect, useId, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Checkbox } from \"../components/checkbox\";\nimport { DEFAULT_DANGER_CONFIRM_LABELS, TypedConfirmField, typedMatches } from \"../components/danger-confirm\";\nimport type { DangerConsequence } from \"../components/danger-confirm\";\nimport { DialogFrame } from \"../components/dialog-frame\";\nimport { ModalCloseContext } from \"../components/modal\";\nimport { Button } from \"../components/button\";\nimport { authErrorCode } from \"../auth/auth-errors\";\nimport type { KitErrorCode } from \"../auth/auth-errors\";\nimport { usePersonLabel } from \"./admin-parts\";\nimport { hasContent } from \"../lib/presence\";\nimport type { AdminPerson, MaybePromise } from \"./admin-parts\";\nimport { isGroupTarget } from \"./admin-action-log\";\nimport type { AdminGroupTarget } from \"./admin-action-log\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport { DEFAULT_ADMIN_ACTION_LABELS } from \"./admin-action-confirm-labels\";\n\n/**\n * The confirmation in front of an admin action, at the level the SERVER states\n * (docs/user-admin-harmonization.md §4.2):\n *\n * | level | what it asks | actions |\n * |---|---|---|\n * | `none` | nothing: it confirms at once and renders nothing | resend a mail, reactivate, the reviewer scope… |\n * | `acknowledge` | a tick under the consequence | force a password change, change a role, delete a group |\n * | `type_email` | the target's address, typed | deactivate, erase now, transfer |\n *\n * A GROUP (round 0.34, §11.8) is a target at `none` and `acknowledge` only, said by its\n * name: it has no address to type, and no level table gives a group's action\n * `type_email` (server-kit's `require_confirmation` raises for one). The props' types\n * say so. Deleting one (`group_delete`) is `acknowledge` — a tick under the app's own\n * sentence of what goes with it, Kurvenschmiede's \"3 members, 2 grants and 1 invitation\n * go with it\" (decision 5): the cascade is what nobody can see afterwards.\n *\n * keksdose decides the level per action and per account on the server\n * (`admin_user_actions_service`) and Kurvenschmiede asks for the typed address on its\n * two hard-to-undo actions; both drew the dialog by hand beside the kit's `DangerConfirm`\n * and `useConfirm`. The level is a PROP, never derived here: the server enforces it and\n * a hand-written request never sees this dialog, so a page that guessed its own level\n * could only disagree with the server. The comparison (caseless), the field and the tick\n * are `DangerConfirm`'s (`TypedConfirmField`, the `dangerConfirm` words), so the three\n * confirmations in the kit cannot disagree about what counts as the address.\n *\n * WHY TYPE THE TARGET'S ADDRESS and not the admin's password: the admin proved who they\n * are at the admin gate. What is unproven is which ROW they are on (keksdose's\n * `TYPE_EMAIL` rule, Kurvenschmiede's `TypedConfirm`).\n *\n * THE REFUSALS (§4.1, §9.6) are read by code — `authErrorCode`, which knows the account\n * codes beside the sign-in ones — and stay in the dialog, which stays open so the admin\n * can read them: `last_admin`, `self_action`, `confirmation_mismatch`, and kastlan's\n * `other_companies` — with \"Remove from company\" offered in its place when the app passes\n * `onRemoveFromCompany`. Anything else is `describeError`'s words, else \"That didn't work\".\n * At level `none` nothing is drawn while the action runs; a refusal opens a dialog of its\n * own to say it.\n *\n * It never sends a request: `onConfirm` does, with what the guards were answered with.\n */\n\n/** How much confirmation an action needs — the server's `confirmation_level(action)`. */\nexport type AdminConfirmLevel = \"none\" | \"acknowledge\" | \"type_email\";\n\n/** What {@link AdminActionConfirmProps.onConfirm} receives — the guards' answers, for\n * the request body (the server re-checks both). */\nexport interface AdminActionConfirmValues {\n /** `type_email`: the address as typed — trimmed, its case the admin's; the body's\n * `confirm_email`, which the server compares with the account's (server-kit's\n * `confirm_email_matches`). */\n confirmEmail?: string;\n /** `acknowledge` and `type_email`: the action was confirmed past a guard — the body's\n * `acknowledged`, which every admin action carries (server-kit 0.4). */\n acknowledged?: true;\n}\n\n/** The account the action is about. A group is an {@link AdminGroupTarget}. */\nexport type AdminActionTarget = AdminPerson & { email: string };\n\n/** The `adminAction` namespace. The tick, the typed field and their held-button reasons\n * are the `dangerConfirm` namespace's, shared with `DangerConfirm`. */\nexport interface AdminActionLabels {\n /** The confirm button when the action names none. */\n confirm: string;\n cancel: string;\n /** The refusal dialog's only button (level `none`). */\n close: string;\n /** `409 last_admin`. */\n lastAdmin: string;\n /** `409 self_action`: the admin's own account. */\n self: string;\n /** `confirmation_mismatch`: the typed address is not this account's — the page and\n * the server disagree about which row this is. */\n confirmationMismatch: string;\n /** kastlan's `409 other_companies` (§9.6). */\n otherCompanies: string;\n /** The alternative offered with it. */\n removeFromCompany: string;\n /** Any other failure, unless `describeError` says better. */\n failed: string;\n}\n\n// Defined in ./admin-action-confirm-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_ADMIN_ACTION_LABELS };\n\n/** What the action is about, and the level the server states for it. A union, so a group\n * can't be given `type_email` (§11.8). */\ntype AdminActionConfirmTarget =\n | {\n /** The level the server states for this action on this account (§4.2). */\n level: AdminConfirmLevel;\n /** Whom it is about: the address (typed at `type_email`) and the name parts. */\n target: AdminActionTarget;\n }\n | {\n /** A group's action: `none`, or `acknowledge` for `group_delete` (decision 5). */\n level: Exclude<AdminConfirmLevel, \"type_email\">;\n /** The group it is about, said by its name (0.34, §11.8). */\n target: AdminGroupTarget;\n };\n\n/** The props whatever the target. */\ninterface AdminActionConfirmCommonProps {\n /** The confirm button — \"Deactivate\". Also the title when no `title` is given. */\n confirmLabel?: ReactNode;\n /** The dialog's heading — \"Deactivate Ada Example?\". Default `confirmLabel`. */\n title?: ReactNode;\n /**\n * What the action does to THIS account, said before it happens: a sentence, or lines\n * (`DangerConsequence`, a `severe` one in the danger colour). Derive them from the\n * row's facts rather than reciting the worst case (keksdose dev#488).\n */\n consequence?: ReactNode | readonly (string | DangerConsequence)[];\n /** The tick's words at `acknowledge`. Default `dangerConfirm.acknowledge` — \"I have\n * read what this does and want to continue.\" */\n acknowledgeLabel?: ReactNode;\n /** `danger` (default at `type_email`): a red confirm. `warning` (default at\n * `acknowledge`) and `neutral`: the primary confirm. */\n tone?: \"danger\" | \"warning\" | \"neutral\";\n /**\n * Runs the action. Return a promise and the dialog manages itself: busy until it\n * settles, closed (`onClose(true)`) when it resolves, open with the refusal when it\n * rejects. Return nothing and the app owns the rest — `busy`, `error`, closing it.\n */\n onConfirm: (values: AdminActionConfirmValues) => MaybePromise;\n /** The dialog has closed: `true` after the action (or the alternative) went through,\n * `false` for Cancel, Escape, the backdrop or Back. */\n onClose: (done: boolean) => void;\n /** kastlan: offered with an `other_companies` refusal — `DELETE …/membership`,\n * logged as `membership_remove` (§4.1). Promise handling as `onConfirm`. */\n onRemoveFromCompany?: () => MaybePromise;\n /** The app's words for a failure the kit has no code for. Asked first. */\n describeError?: (error: unknown) => ReactNode | undefined;\n /** A refusal the app caught itself, when `onConfirm` returns nothing. */\n error?: unknown;\n /** The action is running, for an app that tracks it itself. */\n busy?: boolean;\n /**\n * Fields of the action's own, above the guard — the transfer's recipient, kastlan's\n * roles (`RolesEditor`). Their values are the app's; hold the confirm with\n * `confirmDisabledReason` until they are complete.\n */\n children?: ReactNode;\n /** Why the confirm is held for a reason of the app's (\"Choose who receives it\").\n * Named before the guard's own, since the fields are above it. */\n confirmDisabledReason?: ReactNode;\n /** Kept-mounted mode, as on `DialogFrame`; left out, open while mounted. */\n open?: boolean;\n /** The confirm COMMITS: under a locked `WriteLockProvider` it is held with the lock's\n * reason. Off by default — an admin action is not a write to the open record. */\n commit?: CommitScope;\n labels?: LabelOverride<AdminActionLabels>;\n}\n\n/** A type since 0.34 (an interface before): a person at any level, or a group at `none`\n * and `acknowledge`. */\nexport type AdminActionConfirmProps = AdminActionConfirmCommonProps & AdminActionConfirmTarget;\n\n/** Whether a callback answered a promise. */\nfunction isThenable(value: unknown): value is PromiseLike<unknown> {\n return typeof value === \"object\" && value !== null && typeof (value as PromiseLike<unknown>).then === \"function\";\n}\n\n/** The refusal's words and code. */\nfunction refusalOf(\n error: unknown,\n present: boolean,\n describeError: AdminActionConfirmProps[\"describeError\"],\n labels: AdminActionLabels,\n): { text: ReactNode; code: KitErrorCode | undefined } | null {\n if (!present) return null;\n const code = authErrorCode(error);\n const own = describeError?.(error);\n if (hasContent(own)) return { text: own, code };\n const text =\n code === \"last_admin\"\n ? labels.lastAdmin\n : code === \"self_action\"\n ? labels.self\n : code === \"confirmation_mismatch\"\n ? labels.confirmationMismatch\n : code === \"other_companies\"\n ? labels.otherCompanies\n : labels.failed;\n return { text, code };\n}\n\n/** The selector for the first field among the caller's `children` — what opening focuses. */\nconst FIELD = 'input:not([type=\"hidden\"]):not(:disabled), select:not(:disabled), textarea:not(:disabled), [role=\"combobox\"]';\n\nexport function AdminActionConfirm({\n level,\n target,\n confirmLabel,\n title,\n consequence,\n acknowledgeLabel,\n tone: toneProp,\n onConfirm,\n onClose,\n onRemoveFromCompany,\n describeError,\n error: errorProp,\n busy: busyProp,\n children,\n confirmDisabledReason,\n open,\n commit,\n labels: labelsProp,\n}: AdminActionConfirmProps) {\n const labels = useKitLabels(\"adminAction\", DEFAULT_ADMIN_ACTION_LABELS, labelsProp);\n const guardLabels = useKitLabels(\"dangerConfirm\", DEFAULT_DANGER_CONFIRM_LABELS);\n const nameOf = usePersonLabel();\n const isOpen = open ?? true;\n const [typed, setTyped] = useState(\"\");\n const [ticked, setTicked] = useState(false);\n const [pending, setPending] = useState(false);\n // The last rejection, boxed so a rejection with `undefined` still counts as one.\n const [failure, setFailure] = useState<{ error: unknown } | null>(null);\n // Resolved: the actions row lowers the panel, and `onClose` answers true.\n const [done, setDone] = useState(false);\n const confirmed = useRef(false);\n const mounted = useRef(true);\n const started = useRef(false);\n const formId = useId();\n const slotRef = useRef<HTMLDivElement>(null);\n const guardRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n // A close (kept-mounted) wipes the guards and the refusal: a reopened dialog starts\n // over. During render, like ReauthDialog's, so no frame shows the old answers.\n const [wasOpen, setWasOpen] = useState(isOpen);\n if (wasOpen !== isOpen) {\n setWasOpen(isOpen);\n if (!isOpen) {\n setTyped(\"\");\n setTicked(false);\n setFailure(null);\n setDone(false);\n }\n }\n\n const busy = Boolean(busyProp) || pending;\n\n /** Runs `call`, managing busy, the refusal and the close — or not, when it answers\n * no promise (the app owns it then). */\n const perform = (call: () => MaybePromise) => {\n let result: MaybePromise;\n try {\n result = call();\n } catch (caught) {\n setFailure({ error: caught });\n return;\n }\n if (!isThenable(result)) return;\n setFailure(null);\n setPending(true);\n result.then(\n () => {\n if (!mounted.current) return;\n setPending(false);\n confirmed.current = true;\n setDone(true);\n },\n (caught: unknown) => {\n if (!mounted.current) return;\n setPending(false);\n setFailure({ error: caught });\n },\n );\n };\n\n // Level `none`: confirmed the moment it opens, once per opening. Nothing is drawn\n // while it runs; the promise's answer comes back asynchronously.\n useEffect(() => {\n if (level !== \"none\") return;\n if (!isOpen) {\n started.current = false;\n return;\n }\n if (started.current) return;\n started.current = true;\n confirmed.current = false;\n let result: unknown;\n try {\n result = onConfirm({});\n } catch (caught) {\n result = Promise.reject(caught);\n }\n // No promise: the app owns the rest (it closes the part, or passes `error`).\n if (!isThenable(result)) return;\n result.then(\n () => {\n if (mounted.current) onClose(true);\n },\n (caught: unknown) => {\n if (mounted.current) setFailure({ error: caught });\n },\n );\n }, [level, isOpen, onConfirm, onClose]);\n\n // Focus the first thing the dialog waits for, after Modal's own effect has recorded\n // the trigger to return to (a parent's effects run after its children's).\n useEffect(() => {\n if (!isOpen || level === \"none\") return;\n const field = slotRef.current?.querySelector<HTMLElement>(FIELD) ?? guardRef.current;\n field?.focus();\n }, [isOpen, level]);\n\n const hasRefusal = failure !== null || errorProp !== undefined;\n const refusal = refusalOf(failure ? failure.error : errorProp, hasRefusal, describeError, labels);\n const offerRemoval = refusal?.code === \"other_companies\" && onRemoveFromCompany !== undefined;\n\n // A group has no address: it is said by its name, and nothing is typed for it — not\n // even when a caller outside the types passes `type_email` (the server refuses that).\n const group = isGroupTarget(target);\n const email = group ? \"\" : target.email;\n const name = group ? (target.name ?? \"\").trim() : nameOf(target);\n const said = !name || name === email ? email : email ? `${name} · ${email}` : name;\n const identity = <span data-private>{said}</span>;\n const heading = title ?? confirmLabel ?? labels.confirm;\n\n const refusalBanner = refusal && (\n <AlertBanner\n tone=\"danger\"\n size=\"sm\"\n action={\n offerRemoval ? (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"secondary\"\n disabled={busy}\n onClick={() => perform(() => onRemoveFromCompany!())}\n >\n {labels.removeFromCompany}\n </Button>\n ) : undefined\n }\n >\n {refusal.text}\n </AlertBanner>\n );\n\n if (level === \"none\") {\n // Nothing to ask: only a refusal is worth a dialog.\n if (!isOpen || !refusal) return null;\n return (\n <DialogFrame\n role=\"alertdialog\"\n title={heading}\n description={identity}\n onClose={() => onClose(confirmed.current)}\n actions={(close) => (\n <CloseOnDone done={done}>\n <Button type=\"button\" variant=\"secondary\" disabled={busy} onClick={close}>\n {labels.close}\n </Button>\n </CloseOnDone>\n )}\n >\n {refusalBanner}\n </DialogFrame>\n );\n }\n\n const tone = toneProp ?? (level === \"type_email\" ? \"danger\" : \"warning\");\n const askTyped = level === \"type_email\" && !group;\n const typedOk = !askTyped || typedMatches(typed, email, \"caseless\");\n const tickOk = askTyped || ticked;\n const guardReason: ReactNode = hasContent(confirmDisabledReason)\n ? confirmDisabledReason\n : !tickOk\n ? guardLabels.needsAcknowledge\n : !typedOk\n ? typeof guardLabels.needsPhrase === \"function\"\n ? guardLabels.needsPhrase(email)\n : guardLabels.needsPhrase\n : undefined;\n\n const submit = () => {\n if (busy || guardReason !== undefined) return;\n perform(() =>\n onConfirm(askTyped ? { confirmEmail: typed.trim(), acknowledged: true } : { acknowledged: true }),\n );\n };\n\n const lines = Array.isArray(consequence)\n ? (consequence as readonly (string | DangerConsequence)[]).map((item, index) =>\n typeof item === \"string\"\n ? { key: item, text: item as ReactNode, severe: false }\n : { key: item.key ?? String(index), text: item.text, severe: Boolean(item.severe) },\n )\n : null;\n\n return (\n <DialogFrame\n open={open}\n role=\"alertdialog\"\n title={heading}\n description={identity}\n onClose={() => onClose(confirmed.current)}\n // A request that has left cannot be called back by closing the dialog.\n onKeyDown={(event: KeyboardEvent<HTMLDivElement>) => {\n if (busy && event.key === \"Escape\") event.preventDefault();\n }}\n aria-busy={busy || undefined}\n actions={(close) => (\n <CloseOnDone done={done}>\n <Button type=\"button\" variant=\"ghost\" disabled={busy} onClick={close}>\n {labels.cancel}\n </Button>\n <Button\n type=\"submit\"\n form={formId}\n variant={tone === \"danger\" ? \"danger\" : \"primary\"}\n pending={busy}\n commit={commit}\n disabledReason={busy ? undefined : guardReason}\n >\n {confirmLabel ?? labels.confirm}\n </Button>\n </CloseOnDone>\n )}\n >\n <form\n id={formId}\n noValidate\n className=\"space-y-3\"\n onSubmit={(event) => {\n event.preventDefault();\n submit();\n }}\n >\n {lines ? (\n <ul className=\"list-disc space-y-1 ps-5 text-sm text-secondary\">\n {lines.map((line) => (\n <li key={line.key} className={line.severe ? \"text-danger\" : undefined}>\n {line.text}\n </li>\n ))}\n </ul>\n ) : (\n hasContent(consequence as ReactNode) && (\n <p className=\"text-sm text-secondary\">{consequence as ReactNode}</p>\n )\n )}\n {Children.toArray(children).some(hasContent) && (\n <div ref={slotRef} className=\"space-y-3\">\n {children}\n </div>\n )}\n {askTyped ? (\n <TypedConfirmField\n ref={guardRef}\n target={email}\n match=\"caseless\"\n value={typed}\n onValueChange={setTyped}\n busy={busy}\n data-private\n />\n ) : (\n <Checkbox\n ref={guardRef}\n label={acknowledgeLabel ?? guardLabels.acknowledge}\n checked={ticked}\n // Not `disabled` while busy: the box may hold the focus.\n onCheckedChange={(next) => {\n if (!busy) setTicked(next);\n }}\n />\n )}\n {refusalBanner}\n </form>\n </DialogFrame>\n );\n}\n\n/**\n * The actions row's wrapper: it renders INSIDE the Modal, where the panel's animated\n * close is in context, so a resolved action lowers the panel the way Cancel does and\n * Modal then calls `onClose` (ReauthDialog's pattern).\n */\nfunction CloseOnDone({ done, children }: { done: boolean; children: ReactNode }) {\n const close = useContext(ModalCloseContext);\n useEffect(() => {\n if (done) close?.();\n }, [done, close]);\n return <>{children}</>;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/admin/admin-action-confirm.tsx"],"sourcesContent":["import { Children, useContext, useEffect, useId, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Checkbox } from \"../components/checkbox\";\nimport { DEFAULT_DANGER_CONFIRM_LABELS, TypedConfirmField, typedMatches } from \"../components/danger-confirm\";\nimport type { DangerConsequence } from \"../components/danger-confirm\";\nimport { DialogFrame } from \"../components/dialog-frame\";\nimport { ModalCloseContext } from \"../components/modal\";\nimport { Button } from \"../components/button\";\nimport { authErrorCode } from \"../auth/auth-errors\";\nimport type { KitErrorCode } from \"../auth/auth-errors\";\nimport { usePersonLabel } from \"./admin-parts\";\nimport { hasContent } from \"../lib/presence\";\nimport type { AdminPerson, MaybePromise } from \"./admin-parts\";\nimport { isGroupTarget } from \"./admin-action-log\";\nimport type { AdminGroupTarget } from \"./admin-action-log\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport { DEFAULT_ADMIN_ACTION_LABELS } from \"./admin-action-confirm-labels\";\n\n/**\n * The confirmation in front of an admin action, at the level the SERVER states\n * (docs/user-admin-harmonization.md §4.2):\n *\n * | level | what it asks | actions |\n * |---|---|---|\n * | `none` | nothing: it confirms at once and renders nothing | resend a mail, reactivate, the reviewer scope… |\n * | `acknowledge` | a tick under the consequence | force a password change, change a role, delete a group |\n * | `type_email` | the target's address, typed | deactivate, erase now, transfer |\n *\n * A GROUP (round 0.34, §11.8) is a target at `none` and `acknowledge` only, said by its\n * name: it has no address to type, and no level table gives a group's action\n * `type_email` (server-kit's `require_confirmation` raises for one). The props' types\n * say so. Deleting one (`group_delete`) is `acknowledge` — a tick under the app's own\n * sentence of what goes with it, Kurvenschmiede's \"3 members, 2 grants and 1 invitation\n * go with it\" (decision 5): the cascade is what nobody can see afterwards.\n *\n * keksdose decides the level per action and per account on the server\n * (`admin_user_actions_service`) and Kurvenschmiede asks for the typed address on its\n * two hard-to-undo actions; both drew the dialog by hand beside the kit's `DangerConfirm`\n * and `useConfirm`. The level is a PROP, never derived here: the server enforces it and\n * a hand-written request never sees this dialog, so a page that guessed its own level\n * could only disagree with the server. The comparison (caseless), the field and the tick\n * are `DangerConfirm`'s (`TypedConfirmField`, the `dangerConfirm` words), so the three\n * confirmations in the kit cannot disagree about what counts as the address.\n *\n * WHY TYPE THE TARGET'S ADDRESS and not the admin's password: the admin proved who they\n * are at the admin gate. What is unproven is which ROW they are on (keksdose's\n * `TYPE_EMAIL` rule, Kurvenschmiede's `TypedConfirm`).\n *\n * THE REFUSALS (§4.1, §9.6) are read by code — `authErrorCode`, which knows the account\n * codes beside the sign-in ones — and stay in the dialog, which stays open so the admin\n * can read them: `last_admin`, `self_action`, `confirmation_mismatch`, and kastlan's\n * `other_companies` — with \"Remove from company\" offered in its place when the app passes\n * `onRemoveFromCompany`. Anything else is `describeError`'s words, else \"That didn't work\".\n * At level `none` nothing is drawn while the action runs; a refusal opens a dialog of its\n * own to say it.\n *\n * It never sends a request: `onConfirm` does, with what the guards were answered with.\n */\n\n/** How much confirmation an action needs — the server's `confirmation_level(action)`. */\nexport type AdminConfirmLevel = \"none\" | \"acknowledge\" | \"type_email\";\n\n/** What {@link AdminActionConfirmProps.onConfirm} receives — the guards' answers, for\n * the request body (the server re-checks both). */\nexport interface AdminActionConfirmValues {\n /** `type_email`: the address as typed — trimmed, its case the admin's; the body's\n * `confirm_email`, which the server compares with the account's (server-kit's\n * `confirm_email_matches`). */\n confirmEmail?: string;\n /** `acknowledge` and `type_email`: the action was confirmed past a guard — the body's\n * `acknowledged`, which every admin action carries (server-kit 0.4). */\n acknowledged?: true;\n}\n\n/** The account the action is about. A group is an {@link AdminGroupTarget}. */\nexport type AdminActionTarget = AdminPerson & { email: string };\n\n/** The `adminAction` namespace. The tick, the typed field and their held-button reasons\n * are the `dangerConfirm` namespace's, shared with `DangerConfirm`. */\nexport interface AdminActionLabels {\n /** The confirm button when the action names none. */\n confirm: string;\n cancel: string;\n /** The refusal dialog's only button (level `none`). */\n close: string;\n /** `409 last_admin`. */\n lastAdmin: string;\n /** `409 self_action`: the admin's own account. */\n self: string;\n /** `confirmation_mismatch`: the typed address is not this account's — the page and\n * the server disagree about which row this is. */\n confirmationMismatch: string;\n /** kastlan's `409 other_companies` (§9.6). */\n otherCompanies: string;\n /** The alternative offered with it. */\n removeFromCompany: string;\n /** Any other failure, unless `describeError` says better. */\n failed: string;\n}\n\n// Defined in ./admin-action-confirm-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_ADMIN_ACTION_LABELS };\n\n/** What the action is about, and the level the server states for it. A union, so a group\n * can't be given `type_email` (§11.8). */\ntype AdminActionConfirmTarget =\n | {\n /** The level the server states for this action on this account (§4.2). */\n level: AdminConfirmLevel;\n /** Whom it is about: the address (typed at `type_email`) and the name parts. */\n target: AdminActionTarget;\n }\n | {\n /** A group's action: `none`, or `acknowledge` for `group_delete` (decision 5). */\n level: Exclude<AdminConfirmLevel, \"type_email\">;\n /** The group it is about, said by its name (0.34, §11.8). */\n target: AdminGroupTarget;\n };\n\n/** The props whatever the target. */\ninterface AdminActionConfirmCommonProps {\n /** The confirm button — \"Deactivate\". Also the title when no `title` is given. */\n confirmLabel?: ReactNode;\n /** The dialog's heading — \"Deactivate Ada Example?\". Default `confirmLabel`. */\n title?: ReactNode;\n /**\n * What the action does to THIS account, said before it happens: a sentence, or lines\n * (`DangerConsequence`, a `severe` one in the danger colour). Derive them from the\n * row's facts rather than reciting the worst case (keksdose dev#488).\n */\n consequence?: ReactNode | readonly (string | DangerConsequence)[];\n /** The tick's words at `acknowledge`. Default `dangerConfirm.acknowledge` — \"I have\n * read what this does and want to continue.\" */\n acknowledgeLabel?: ReactNode;\n /** `danger` (default at `type_email`): a red confirm. `warning` (default at\n * `acknowledge`) and `neutral`: the primary confirm. */\n tone?: \"danger\" | \"warning\" | \"neutral\";\n /**\n * Runs the action. Return a promise and the dialog manages itself: busy until it\n * settles, closed (`onClose(true)`) when it resolves, open with the refusal when it\n * rejects. Return nothing and the app owns the rest — `busy`, `error`, closing it.\n */\n onConfirm: (values: AdminActionConfirmValues) => MaybePromise;\n /** The dialog has closed: `true` after the action (or the alternative) went through,\n * `false` for Cancel, Escape, the backdrop or Back. */\n onClose: (done: boolean) => void;\n /** kastlan: offered with an `other_companies` refusal — `DELETE …/membership`,\n * logged as `membership_remove` (§4.1). Promise handling as `onConfirm`. */\n onRemoveFromCompany?: () => MaybePromise;\n /** The app's words for a failure the kit has no code for. Asked first. */\n describeError?: (error: unknown) => ReactNode | undefined;\n /** A refusal the app caught itself, when `onConfirm` returns nothing. */\n error?: unknown;\n /** The action is running, for an app that tracks it itself. */\n busy?: boolean;\n /**\n * Fields of the action's own, above the guard — the transfer's recipient, kastlan's\n * roles (`RolesEditor`). Their values are the app's; hold the confirm with\n * `confirmDisabledReason` until they are complete.\n */\n children?: ReactNode;\n /** Why the confirm is held for a reason of the app's (\"Choose who receives it\").\n * Named before the guard's own, since the fields are above it. */\n confirmDisabledReason?: ReactNode;\n /** Kept-mounted mode, as on `DialogFrame`; left out, open while mounted. */\n open?: boolean;\n /** The confirm COMMITS: under a locked `WriteLockProvider` it is held with the lock's\n * reason. Off by default — an admin action is not a write to the open record. */\n commit?: CommitScope;\n labels?: LabelOverride<AdminActionLabels>;\n}\n\n/** A type since 0.34 (an interface before): a person at any level, or a group at `none`\n * and `acknowledge`. */\nexport type AdminActionConfirmProps = AdminActionConfirmCommonProps & AdminActionConfirmTarget;\n\n/** Whether a callback answered a promise. */\nfunction isThenable(value: unknown): value is PromiseLike<unknown> {\n return typeof value === \"object\" && value !== null && typeof (value as PromiseLike<unknown>).then === \"function\";\n}\n\n/** The refusal's words and code. */\nfunction refusalOf(\n error: unknown,\n present: boolean,\n describeError: AdminActionConfirmProps[\"describeError\"],\n labels: AdminActionLabels,\n): { text: ReactNode; code: KitErrorCode | undefined } | null {\n if (!present) return null;\n const code = authErrorCode(error);\n const own = describeError?.(error);\n if (hasContent(own)) return { text: own, code };\n const text =\n code === \"last_admin\"\n ? labels.lastAdmin\n : code === \"self_action\"\n ? labels.self\n : code === \"confirmation_mismatch\"\n ? labels.confirmationMismatch\n : code === \"other_companies\"\n ? labels.otherCompanies\n : labels.failed;\n return { text, code };\n}\n\n/** The selector for the first field among the caller's `children` — what opening focuses. */\nconst FIELD = 'input:not([type=\"hidden\"]):not(:disabled), select:not(:disabled), textarea:not(:disabled), [role=\"combobox\"]';\n\nexport function AdminActionConfirm({\n level,\n target,\n confirmLabel,\n title,\n consequence,\n acknowledgeLabel,\n tone: toneProp,\n onConfirm,\n onClose,\n onRemoveFromCompany,\n describeError,\n error: errorProp,\n busy: busyProp,\n children,\n confirmDisabledReason,\n open,\n commit,\n labels: labelsProp,\n}: AdminActionConfirmProps) {\n const labels = useKitLabels(\"adminAction\", DEFAULT_ADMIN_ACTION_LABELS, labelsProp);\n const guardLabels = useKitLabels(\"dangerConfirm\", DEFAULT_DANGER_CONFIRM_LABELS);\n const nameOf = usePersonLabel();\n const isOpen = open ?? true;\n const [typed, setTyped] = useState(\"\");\n const [ticked, setTicked] = useState(false);\n const [pending, setPending] = useState(false);\n // The last rejection, boxed so a rejection with `undefined` still counts as one.\n const [failure, setFailure] = useState<{ error: unknown } | null>(null);\n // Resolved: the actions row lowers the panel, and `onClose` answers true.\n const [done, setDone] = useState(false);\n const confirmed = useRef(false);\n const mounted = useRef(true);\n const started = useRef(false);\n const formId = useId();\n const slotRef = useRef<HTMLDivElement>(null);\n const guardRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n // A close (kept-mounted) wipes the guards and the refusal: a reopened dialog starts\n // over. During render, like ReauthDialog's, so no frame shows the old answers.\n const [wasOpen, setWasOpen] = useState(isOpen);\n if (wasOpen !== isOpen) {\n setWasOpen(isOpen);\n if (!isOpen) {\n setTyped(\"\");\n setTicked(false);\n setFailure(null);\n setDone(false);\n }\n }\n\n const busy = Boolean(busyProp) || pending;\n\n /** Runs `call`, managing busy, the refusal and the close — or not, when it answers\n * no promise (the app owns it then). */\n const perform = (call: () => MaybePromise) => {\n let result: MaybePromise;\n try {\n result = call();\n } catch (caught) {\n setFailure({ error: caught });\n return;\n }\n if (!isThenable(result)) return;\n setFailure(null);\n setPending(true);\n result.then(\n () => {\n if (!mounted.current) return;\n setPending(false);\n confirmed.current = true;\n setDone(true);\n },\n (caught: unknown) => {\n if (!mounted.current) return;\n setPending(false);\n setFailure({ error: caught });\n },\n );\n };\n\n // Level `none`: confirmed the moment it opens, once per opening. Nothing is drawn\n // while it runs; the promise's answer comes back asynchronously.\n useEffect(() => {\n if (level !== \"none\") return;\n if (!isOpen) {\n started.current = false;\n return;\n }\n if (started.current) return;\n started.current = true;\n confirmed.current = false;\n let result: unknown;\n try {\n result = onConfirm({});\n } catch (caught) {\n result = Promise.reject(caught);\n }\n // No promise: the app owns the rest (it closes the part, or passes `error`).\n if (!isThenable(result)) return;\n result.then(\n () => {\n if (mounted.current) onClose(true);\n },\n (caught: unknown) => {\n if (mounted.current) setFailure({ error: caught });\n },\n );\n }, [level, isOpen, onConfirm, onClose]);\n\n // Focus the first thing the dialog waits for, after Modal's own effect has recorded\n // the trigger to return to (a parent's effects run after its children's).\n useEffect(() => {\n if (!isOpen || level === \"none\") return;\n const field = slotRef.current?.querySelector<HTMLElement>(FIELD) ?? guardRef.current;\n field?.focus();\n }, [isOpen, level]);\n\n const hasRefusal = failure !== null || errorProp !== undefined;\n const refusal = refusalOf(failure ? failure.error : errorProp, hasRefusal, describeError, labels);\n const offerRemoval = refusal?.code === \"other_companies\" && onRemoveFromCompany !== undefined;\n\n // A group has no address: it is said by its name, and nothing is typed for it — not\n // even when a caller outside the types passes `type_email` (the server refuses that).\n const group = isGroupTarget(target);\n const email = group ? \"\" : target.email;\n const name = group ? (target.name ?? \"\").trim() : nameOf(target);\n const said = !name || name === email ? email : email ? `${name} · ${email}` : name;\n const identity = <span data-private>{said}</span>;\n const heading = title ?? confirmLabel ?? labels.confirm;\n\n const refusalBanner = refusal && (\n <AlertBanner\n tone=\"danger\"\n size=\"sm\"\n action={\n offerRemoval ? (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"secondary\"\n disabled={busy}\n onClick={() => perform(() => onRemoveFromCompany!())}\n >\n {labels.removeFromCompany}\n </Button>\n ) : undefined\n }\n >\n {refusal.text}\n </AlertBanner>\n );\n\n if (level === \"none\") {\n // Nothing to ask: only a refusal is worth a dialog.\n if (!isOpen || !refusal) return null;\n return (\n <DialogFrame\n role=\"alertdialog\"\n title={heading}\n description={identity}\n onClose={() => onClose(confirmed.current)}\n actions={(close) => (\n <CloseOnDone done={done}>\n <Button type=\"button\" variant=\"secondary\" disabled={busy} onClick={close}>\n {labels.close}\n </Button>\n </CloseOnDone>\n )}\n >\n {refusalBanner}\n </DialogFrame>\n );\n }\n\n const tone = toneProp ?? (level === \"type_email\" ? \"danger\" : \"warning\");\n const askTyped = level === \"type_email\" && !group;\n const typedOk = !askTyped || typedMatches(typed, email, \"caseless\");\n const tickOk = askTyped || ticked;\n const guardReason: ReactNode = hasContent(confirmDisabledReason)\n ? confirmDisabledReason\n : !tickOk\n ? guardLabels.needsAcknowledge\n : !typedOk\n ? typeof guardLabels.needsPhrase === \"function\"\n ? guardLabels.needsPhrase(email)\n : guardLabels.needsPhrase\n : undefined;\n\n const submit = () => {\n if (busy || guardReason !== undefined) return;\n perform(() =>\n onConfirm(askTyped ? { confirmEmail: typed.trim(), acknowledged: true } : { acknowledged: true }),\n );\n };\n\n const lines = Array.isArray(consequence)\n ? (consequence as readonly (string | DangerConsequence)[]).map((item, index) =>\n typeof item === \"string\"\n ? { key: item, text: item as ReactNode, severe: false }\n : { key: item.key ?? String(index), text: item.text, severe: Boolean(item.severe) },\n )\n : null;\n\n return (\n <DialogFrame\n open={open}\n role=\"alertdialog\"\n title={heading}\n description={identity}\n onClose={() => onClose(confirmed.current)}\n // A request that has left cannot be called back by closing the dialog.\n onKeyDown={(event: KeyboardEvent<HTMLDivElement>) => {\n if (busy && event.key === \"Escape\") event.preventDefault();\n }}\n aria-busy={busy || undefined}\n actions={(close) => (\n <CloseOnDone done={done}>\n <Button type=\"button\" variant=\"ghost\" disabled={busy} onClick={close}>\n {labels.cancel}\n </Button>\n <Button\n type=\"submit\"\n form={formId}\n variant={tone === \"danger\" ? \"danger\" : \"default\"}\n pending={busy}\n commit={commit}\n disabledReason={busy ? undefined : guardReason}\n >\n {confirmLabel ?? labels.confirm}\n </Button>\n </CloseOnDone>\n )}\n >\n <form\n id={formId}\n noValidate\n className=\"space-y-3\"\n onSubmit={(event) => {\n event.preventDefault();\n submit();\n }}\n >\n {lines ? (\n <ul className=\"list-disc space-y-1 ps-5 text-sm text-secondary\">\n {lines.map((line) => (\n <li key={line.key} className={line.severe ? \"text-danger\" : undefined}>\n {line.text}\n </li>\n ))}\n </ul>\n ) : (\n hasContent(consequence as ReactNode) && (\n <p className=\"text-sm text-secondary\">{consequence as ReactNode}</p>\n )\n )}\n {Children.toArray(children).some(hasContent) && (\n <div ref={slotRef} className=\"space-y-3\">\n {children}\n </div>\n )}\n {askTyped ? (\n <TypedConfirmField\n ref={guardRef}\n target={email}\n match=\"caseless\"\n value={typed}\n onValueChange={setTyped}\n busy={busy}\n data-private\n />\n ) : (\n <Checkbox\n ref={guardRef}\n label={acknowledgeLabel ?? guardLabels.acknowledge}\n checked={ticked}\n // Not `disabled` while busy: the box may hold the focus.\n onCheckedChange={(next) => {\n if (!busy) setTicked(next);\n }}\n />\n )}\n {refusalBanner}\n </form>\n </DialogFrame>\n );\n}\n\n/**\n * The actions row's wrapper: it renders INSIDE the Modal, where the panel's animated\n * close is in context, so a resolved action lowers the panel the way Cancel does and\n * Modal then calls `onClose` (ReauthDialog's pattern).\n */\nfunction CloseOnDone({ done, children }: { done: boolean; children: ReactNode }) {\n const close = useContext(ModalCloseContext);\n useEffect(() => {\n if (done) close?.();\n }, [done, close]);\n return <>{children}</>;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -180,7 +180,7 @@ function InvitationsPanel({
|
|
|
180
180
|
Button,
|
|
181
181
|
{
|
|
182
182
|
type: "submit",
|
|
183
|
-
variant: "
|
|
183
|
+
variant: "accent",
|
|
184
184
|
commit: true,
|
|
185
185
|
pending: busy === "invite",
|
|
186
186
|
disabled: busy !== null || address === "" || scopeRequired && scopes !== void 0 && scope === "",
|
|
@@ -253,6 +253,10 @@ function InvitationsPanel({
|
|
|
253
253
|
label: labels.revoke(invitation.email),
|
|
254
254
|
icon: Trash2,
|
|
255
255
|
tone: "danger",
|
|
256
|
+
// Level `none` (user admin §4.2): re-inviting undoes it, so
|
|
257
|
+
// the kit's KIT-12 question stays out; an app may ask in
|
|
258
|
+
// `onRevoke` (keksdose does).
|
|
259
|
+
confirm: false,
|
|
256
260
|
// Removing access stays open under a lapsed plan (§12.36).
|
|
257
261
|
commit: COMMIT_EXCEPT_BILLING,
|
|
258
262
|
pending: busy === `revoke:${key}`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/admin/invitations-panel.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { FormEvent, ReactNode } from \"react\";\nimport { RefreshCw, Trash2, UserPlus } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport type { DateInput } from \"../lib/format\";\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { KIT_LANGUAGES, resolveLanguage } from \"../i18n/languages\";\nimport type { KitLanguageCode } from \"../i18n/languages\";\nimport { RoleChip } from \"../components/account-chips\";\nimport type { RoleVocabulary } from \"../components/account-chips\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Chip } from \"../components/chip\";\nimport type { ChipTone } from \"../components/chip\";\nimport { CopyButton } from \"../components/copy-button\";\nimport { LanguageSelect } from \"../components/language-select\";\nimport { SectionLabel, Caption } from \"../components/text\";\nimport { RowActions } from \"../components/row-actions\";\nimport { Button } from \"../components/button\";\nimport { Input } from \"../components/input\";\nimport { Select } from \"../components/select\";\nimport { Spinner } from \"../components/spinner\";\nimport { COMMIT_EXCEPT_BILLING, useWriteLock } from \"../components/write-lock\";\nimport { settle, useDayText, usePersonLabel } from \"./admin-parts\";\nimport { hasContent } from \"../lib/presence\";\nimport type { AdminPerson, MaybePromise } from \"./admin-parts\";\nimport { RoleSelect } from \"./role-select\";\nimport { DEFAULT_INVITATIONS_LABELS } from \"./invitations-panel-labels\";\n\n/**\n * Who is invited, and the form that invites (docs/user-admin-harmonization.md §5; auth\n * §4.4: a hashed one-time token, 14 days, an address with a role and a scope).\n *\n * Kurvenschmiede's `invitations-panel.tsx` with its parts made props: a form (the address,\n * a role from the app's vocabulary, an optional scope — a team, a company — the language\n * the invitation is written in, a note), then one row per invitation with its status, when\n * it was sent and until when it works, and resend / copy link / revoke.\n *\n * - **It assumes nothing about who invites.** Admins do, team managers into their team,\n * kastlan's company admins into their company — and in keksdose ANY budget owner, from\n * the budget's sharing (Marcel, 2026-10-07). So no word here says \"admin\"; the roles,\n * the scopes and the callbacks are the app's, and so is the frame around it.\n * - **The address is someone else's.** No `+app` tag is offered (auth §4.5): a tag is\n * the owner's own choice, made when they sign up.\n * - **The link is shown once.** The token is stored hashed, so the server can show a link\n * only in the answer that minted it — on creation and on resend, and only while mail\n * goes to the console (dev, a deploy without mail). The panel keeps such a link for as\n * long as it is mounted and offers to copy it on its row; reloading the page loses it,\n * and \"resend\" mints a new one. With the console backend the panel says so up front.\n * - **Revoked is optional.** An app that deletes the row instead (kastlan) never sends\n * `revoked`; the status chip simply never says it (§5).\n *\n * It never sends a request: `onInvite`, `onResend` and `onRevoke` do.\n *\n * Under a write lock, inviting and resending are refused; a lapsed plan's lock (`kind:\n * \"billing\"`) still lets an invitation be revoked, since removing access is what a\n * lapsed plan allows (docs/billing-harmonization.md §12.13, §12.36). A demo lock refuses\n * all three.\n */\n\nexport type InvitationStatus = \"open\" | \"accepted\" | \"expired\" | \"revoked\";\n\n/** One row of `GET /admin/invitations` (§5), in the wire's own field names. */\nexport interface InvitationRow<R extends string = string> {\n id: string | number;\n email: string;\n role?: R | null;\n /** The scope's value — a team's or a company's id, matched against `scopes`. */\n scope?: string | number | null;\n note?: string | null;\n /** The language it was written in. */\n locale?: string | null;\n /** Who sent it: a name, or the person. */\n invited_by?: string | AdminPerson | null;\n created_at: DateInput;\n expires_at?: DateInput;\n status: InvitationStatus;\n /** The one-time link, while mail goes to the console — in the answer that minted it. */\n link?: string | null;\n}\n\n/** What the form sends — `POST /admin/invitations {email, role, scope?, note?, locale}`. */\nexport interface InvitationDraft<R extends string = string> {\n email: string;\n /** Absent when the app offers no role choice (`roles` left out). */\n role?: R;\n scope?: string;\n note?: string;\n locale: KitLanguageCode;\n}\n\n/** What `onInvite` / `onResend` may answer, so the panel can keep a shown-once link. */\nexport interface InvitationSentAnswer {\n /** The invitation the answer is about — `onInvite` needs it for the link to find its\n * row; `onResend` knows it already. */\n id?: string | number;\n /** The one-time link, while mail goes to the console. */\n link?: string | null;\n /** `false`: the invitation exists but the mail failed — say so, resend it. */\n sent?: boolean;\n}\n\n/** A scope an invitation can carry. */\nexport interface InvitationScope {\n value: string;\n label: string;\n}\n\n/** The `invitations` namespace. */\nexport interface InvitationsLabels {\n /** The form's address field. */\n email: string;\n /** Under it while what was typed is not an address. */\n invalidEmail: string;\n role: string;\n /** The scope select's label — the app's word (\"Team\", \"Company\") goes over it. */\n scope: string;\n /** The scope select's empty choice. */\n scopeNone: string;\n /** The language select's label. */\n language: string;\n note: string;\n /** The form's button. */\n invite: string;\n /** Heading over the rows. */\n listTitle: string;\n empty: string;\n loading: string;\n /** The status chip, per status. */\n status: Record<InvitationStatus, string>;\n /** \"Sent 6 Oct 2026\". */\n sent: (date: string) => string;\n /** \"Expires 20 Oct 2026\" — on an open invitation. */\n expires: (date: string) => string;\n /** \"by Ada Example\". */\n invitedBy: (name: string) => string;\n /** The resend button — \"Send ben@example.com a new link\". */\n resend: (email: string) => string;\n /** The copy button. */\n copyLink: string;\n /** The revoke button — \"Revoke the invitation for ben@example.com\". */\n revoke: (email: string) => string;\n /** Over everything while mail goes to the console. */\n consoleHint: string;\n /** Beside a link the answer carried with no row to put it on. */\n linkReady: (email: string) => string;\n /** The answer said the mail was not sent. */\n notSent: (email: string) => string;\n /** A rejected callback, unless `describeError` says better. */\n failed: string;\n}\n\n// Defined in ./invitations-panel-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_INVITATIONS_LABELS };\n\n/** The tone of each status. */\nconst STATUS_TONES: Readonly<Record<InvitationStatus, ChipTone>> = {\n open: \"info\",\n accepted: \"success\",\n expired: \"warning\",\n revoked: \"neutral\",\n};\n\nexport interface InvitationsPanelProps<R extends string = string> {\n invitations: readonly InvitationRow<R>[];\n /** The app's roles. Left out: no role choice, and no role chip on a row. */\n roles?: RoleVocabulary<R>;\n /** The roles an invitation may carry — Kurvenschmiede's member or customer. Default:\n * the whole vocabulary. One role: no choice is drawn, and it is sent. */\n invitableRoles?: readonly R[];\n /** The role the form starts with. Default: the first invitable one. */\n defaultRole?: R;\n /** The scopes an invitation may carry — teams, companies. Left out: no scope field. */\n scopes?: readonly InvitationScope[];\n /** A scope must be chosen (no \"None\"). */\n scopeRequired?: boolean;\n /** The languages offered. Default: all seven of the kit's. */\n languages?: readonly KitLanguageCode[];\n /** The language the form starts with. Default: the page's, resolved to `languages`. */\n defaultLocale?: string;\n /** The server's `mail_backend`. `\"console\"` draws the hint. */\n mailBackend?: string | null;\n /** Creates one (resolve to clear the form; reject to keep it and say why). Left out:\n * no form — a list someone may look at but not add to. */\n onInvite?: (draft: InvitationDraft<R>) => MaybePromise<InvitationSentAnswer | void>;\n /** Mints a new link and mails it. Left out: no resend button. */\n onResend?: (invitation: InvitationRow<R>) => MaybePromise<InvitationSentAnswer | void>;\n /** Revokes an open or an expired invitation (level `none`, §4.2: re-inviting undoes\n * it). An expired row can be resent, and since 0.31 also removed: in an app that\n * keeps the rows it would otherwise stay listed for good (Kurvenschmiede's 0.30\n * adoption). Left out: no revoke button. */\n onRevoke?: (invitation: InvitationRow<R>) => MaybePromise;\n /** The list is being fetched. */\n loading?: boolean;\n /** The app's words for a rejected callback. */\n describeError?: (error: unknown) => ReactNode | undefined;\n /** Over the form: who can be invited and what happens, in the app's words. */\n intro?: ReactNode;\n /** The list's visible heading. Default: the `listTitle` label. `null` leaves it out,\n * for a page whose card is already headed \"Invitations\" (Kurvenschmiede's 0.30\n * adoption); the list keeps the label as its accessible name either way. */\n listTitle?: ReactNode | null;\n className?: string;\n labels?: LabelOverride<InvitationsLabels>;\n}\n\n// Shape only — whether the address may be invited is the server's question.\nconst EMAIL_SHAPE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\nconst ALL_CODES: readonly KitLanguageCode[] = KIT_LANGUAGES.map((language) => language.code);\n\n/** A failed run, told apart from an answer of `undefined`. */\nconst FAILED = Symbol(\"failed\");\n\nexport function InvitationsPanel<R extends string = string>({\n invitations,\n roles,\n invitableRoles,\n defaultRole,\n scopes,\n scopeRequired = false,\n languages = ALL_CODES,\n defaultLocale,\n mailBackend,\n onInvite,\n onResend,\n onRevoke,\n loading = false,\n describeError,\n intro,\n listTitle,\n className,\n labels: labelsProp,\n}: InvitationsPanelProps<R>) {\n const labels = useKitLabels(\"invitations\", DEFAULT_INVITATIONS_LABELS, labelsProp);\n const kitLocale = useKitLocale();\n const dayText = useDayText();\n const nameOf = usePersonLabel();\n const lock = useWriteLock();\n const offered: readonly R[] = invitableRoles ?? (roles ? (Object.keys(roles) as R[]) : []);\n const [email, setEmail] = useState(\"\");\n const [invalid, setInvalid] = useState(false);\n const [role, setRole] = useState<R | undefined>(defaultRole ?? offered[0]);\n const [scope, setScope] = useState(\"\");\n const [locale, setLocale] = useState<KitLanguageCode>(() =>\n resolveLanguage([defaultLocale, kitLocale], languages),\n );\n const [note, setNote] = useState(\"\");\n // One request at a time, for the whole panel; the key says which control spins.\n const [busy, setBusy] = useState<string | null>(null);\n const [failure, setFailure] = useState<ReactNode>(null);\n const [notice, setNotice] = useState<ReactNode>(null);\n // Links the answers carried, by invitation — shown once, kept while mounted.\n const [links, setLinks] = useState<Record<string, string>>({});\n // A link whose answer named no invitation: shown above the list instead.\n const [looseLink, setLooseLink] = useState<{ email: string; link: string } | null>(null);\n\n const run = async <T,>(key: string, call: () => MaybePromise<T>): Promise<T | void | typeof FAILED> => {\n setBusy(key);\n setFailure(null);\n setNotice(null);\n try {\n return await settle(call);\n } catch (error) {\n const own = describeError?.(error);\n setFailure(hasContent(own) ? own : labels.failed);\n return FAILED;\n } finally {\n setBusy(null);\n }\n };\n\n /** Keep what an answer carried: its link, and whether the mail went. */\n const took = (answer: InvitationSentAnswer | void, address: string, id?: string | number) => {\n if (!answer) return;\n const key = answer.id ?? id;\n if (answer.link) {\n if (key !== undefined) setLinks((known) => ({ ...known, [String(key)]: answer.link as string }));\n else setLooseLink({ email: address, link: answer.link });\n }\n if (answer.sent === false) setNotice(labels.notSent(address));\n };\n\n const address = email.trim();\n const choosesRole = roles !== undefined && offered.length > 1;\n const canInvite = onInvite !== undefined;\n\n const submit = async (event: FormEvent) => {\n event.preventDefault();\n // Enter in a field is a commit too — the button's lock does not see it.\n if (!onInvite || busy || lock.locked || address === \"\") return;\n if (!EMAIL_SHAPE.test(address)) {\n setInvalid(true);\n return;\n }\n if (scopes && scopeRequired && scope === \"\") return;\n const draft: InvitationDraft<R> = {\n email: address,\n locale,\n ...(role !== undefined && roles !== undefined ? { role } : {}),\n ...(scope !== \"\" ? { scope } : {}),\n ...(note.trim() !== \"\" ? { note: note.trim() } : {}),\n };\n const answer = await run(\"invite\", () => onInvite(draft));\n if (answer === FAILED) return;\n took(answer, address);\n setEmail(\"\");\n setNote(\"\");\n };\n\n const scopeLabel = (value: InvitationRow[\"scope\"]) =>\n value === null || value === undefined || value === \"\"\n ? null\n : (scopes?.find((s) => s.value === String(value))?.label ?? String(value));\n\n return (\n <div className={cn(\"space-y-3\", className)}>\n {intro && <div className=\"text-sm text-secondary\">{intro}</div>}\n {mailBackend === \"console\" && (\n <AlertBanner tone=\"info\" size=\"sm\">\n {labels.consoleHint}\n </AlertBanner>\n )}\n\n {canInvite && (\n <form noValidate onSubmit={submit} className=\"@container space-y-2\">\n <div className=\"grid grid-cols-1 gap-2 @md:grid-cols-2\">\n <Input\n label={labels.email}\n type=\"email\"\n inputMode=\"email\"\n autoComplete=\"off\"\n autoCapitalize=\"none\"\n spellCheck={false}\n required\n value={email}\n error={invalid ? labels.invalidEmail : undefined}\n data-private\n onChange={(event) => {\n setInvalid(false);\n setEmail(event.target.value);\n }}\n />\n {choosesRole && (\n <RoleSelect<R>\n label={labels.role}\n value={role}\n roles={roles}\n offered={offered}\n onChange={setRole}\n />\n )}\n {scopes && scopes.length > 0 && (\n <Select\n label={labels.scope}\n value={scope}\n required={scopeRequired}\n onChange={(event) => setScope(event.target.value)}\n >\n <option value=\"\" disabled={scopeRequired}>\n {labels.scopeNone}\n </option>\n {scopes.map((s) => (\n <option key={s.value} value={s.value}>\n {s.label}\n </option>\n ))}\n </Select>\n )}\n {languages.length > 1 && (\n <LanguageSelect label={labels.language} codes={languages} value={locale} onChange={setLocale} />\n )}\n <Input\n label={labels.note}\n value={note}\n maxLength={255}\n onChange={(event) => setNote(event.target.value)}\n className=\"@md:col-span-2\"\n />\n </div>\n <div className=\"flex justify-end\">\n <Button\n type=\"submit\"\n variant=\"brand\"\n commit\n pending={busy === \"invite\"}\n disabled={busy !== null || address === \"\" || (scopeRequired && scopes !== undefined && scope === \"\")}\n >\n <UserPlus className=\"size-4\" aria-hidden />\n {labels.invite}\n </Button>\n </div>\n </form>\n )}\n\n {failure && (\n <AlertBanner tone=\"danger\" size=\"sm\">\n {failure}\n </AlertBanner>\n )}\n {notice && (\n <AlertBanner tone=\"warning\" size=\"sm\" onDismiss={() => setNotice(null)}>\n {notice}\n </AlertBanner>\n )}\n {looseLink && (\n <AlertBanner tone=\"info\" size=\"sm\" onDismiss={() => setLooseLink(null)}>\n <div className=\"flex min-w-0 flex-wrap items-center gap-2\">\n <span data-private>{labels.linkReady(looseLink.email)}</span>\n <code className=\"min-w-0 break-all text-xs\">{looseLink.link}</code>\n <CopyButton size=\"xs\" text={looseLink.link} label={labels.copyLink} />\n </div>\n </AlertBanner>\n )}\n\n <div className=\"space-y-2\">\n {listTitle !== null && <SectionLabel as=\"h4\">{listTitle ?? labels.listTitle}</SectionLabel>}\n {loading && invitations.length === 0 ? (\n <div className=\"flex justify-center py-3\">\n <Spinner label={labels.loading} />\n </div>\n ) : invitations.length === 0 ? (\n <Caption>{labels.empty}</Caption>\n ) : (\n <ul aria-label={labels.listTitle} className=\"divide-y divide-subtle\">\n {invitations.map((invitation) => {\n const key = String(invitation.id);\n const link = invitation.link ?? links[key];\n const open = invitation.status === \"open\";\n const resendable = onResend && (open || invitation.status === \"expired\");\n const revocable = onRevoke && (open || invitation.status === \"expired\");\n const scopeText = scopeLabel(invitation.scope);\n const by =\n typeof invitation.invited_by === \"string\" ? invitation.invited_by : nameOf(invitation.invited_by);\n const facts = [\n dayText(invitation.created_at) && labels.sent(dayText(invitation.created_at)),\n open && dayText(invitation.expires_at ?? null) && labels.expires(dayText(invitation.expires_at ?? null)),\n by && labels.invitedBy(by),\n ].filter(Boolean);\n return (\n <li key={key} data-status={invitation.status} className=\"py-2 first:pt-0 last:pb-0\">\n <div className=\"flex flex-wrap items-center justify-between gap-x-3 gap-y-1.5\">\n <div className=\"min-w-0 flex-1 basis-48 space-y-1\">\n <div data-private className=\"truncate-until-large text-sm font-medium text-primary\">\n {invitation.email}\n </div>\n <div className=\"flex flex-wrap items-center gap-1\">\n <Chip size=\"xs\" shape=\"square\" caps tone={STATUS_TONES[invitation.status]}>\n {labels.status[invitation.status]}\n </Chip>\n {roles && invitation.role && <RoleChip<R> value={invitation.role} roles={roles} size=\"xs\" />}\n {scopeText && (\n <Chip size=\"xs\" shape=\"square\">\n {scopeText}\n </Chip>\n )}\n </div>\n {(facts.length > 0 || invitation.note) && (\n <div className=\"text-xs text-muted\">\n {facts.join(\" · \")}\n {invitation.note && (\n <>\n {facts.length > 0 && \" · \"}\n <span data-private>{invitation.note}</span>\n </>\n )}\n </div>\n )}\n </div>\n {(link || resendable || revocable) && (\n <div className=\"ms-auto flex shrink-0 items-center gap-1\">\n {/* The copy stays its own icon: it says \"Copied\" where it was\n pressed, which a menu entry that closes cannot. */}\n {link && open && <CopyButton size=\"xs\" tone=\"muted\" text={link} label={labels.copyLink} />}\n {/* Resend and revoke are the row's actions (docs/text-size-\n harmonization.md §10.8): two icons at Normal, one \"⋯\" menu at\n Large, where both names as text (\"Send … a new link\") would\n not fit a phone's row beside the copy icon. */}\n <RowActions\n name={invitation.email}\n actions={[\n resendable && {\n key: \"resend\",\n label: labels.resend(invitation.email),\n icon: RefreshCw,\n commit: true,\n pending: busy === `resend:${key}`,\n disabled: busy !== null,\n onSelect: () =>\n void (async () => {\n const answer = await run(`resend:${key}`, () => onResend(invitation));\n if (answer !== FAILED) took(answer, invitation.email, invitation.id);\n })(),\n },\n revocable && {\n key: \"revoke\",\n label: labels.revoke(invitation.email),\n icon: Trash2,\n tone: \"danger\",\n // Removing access stays open under a lapsed plan (§12.36).\n commit: COMMIT_EXCEPT_BILLING,\n pending: busy === `revoke:${key}`,\n disabled: busy !== null,\n onSelect: () => void run(`revoke:${key}`, () => onRevoke(invitation)),\n },\n ]}\n />\n </div>\n )}\n </div>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["address"]}
|
|
1
|
+
{"version":3,"sources":["../../src/admin/invitations-panel.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { FormEvent, ReactNode } from \"react\";\nimport { RefreshCw, Trash2, UserPlus } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport type { DateInput } from \"../lib/format\";\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { KIT_LANGUAGES, resolveLanguage } from \"../i18n/languages\";\nimport type { KitLanguageCode } from \"../i18n/languages\";\nimport { RoleChip } from \"../components/account-chips\";\nimport type { RoleVocabulary } from \"../components/account-chips\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Chip } from \"../components/chip\";\nimport type { ChipTone } from \"../components/chip\";\nimport { CopyButton } from \"../components/copy-button\";\nimport { LanguageSelect } from \"../components/language-select\";\nimport { SectionLabel, Caption } from \"../components/text\";\nimport { RowActions } from \"../components/row-actions\";\nimport { Button } from \"../components/button\";\nimport { Input } from \"../components/input\";\nimport { Select } from \"../components/select\";\nimport { Spinner } from \"../components/spinner\";\nimport { COMMIT_EXCEPT_BILLING, useWriteLock } from \"../components/write-lock\";\nimport { settle, useDayText, usePersonLabel } from \"./admin-parts\";\nimport { hasContent } from \"../lib/presence\";\nimport type { AdminPerson, MaybePromise } from \"./admin-parts\";\nimport { RoleSelect } from \"./role-select\";\nimport { DEFAULT_INVITATIONS_LABELS } from \"./invitations-panel-labels\";\n\n/**\n * Who is invited, and the form that invites (docs/user-admin-harmonization.md §5; auth\n * §4.4: a hashed one-time token, 14 days, an address with a role and a scope).\n *\n * Kurvenschmiede's `invitations-panel.tsx` with its parts made props: a form (the address,\n * a role from the app's vocabulary, an optional scope — a team, a company — the language\n * the invitation is written in, a note), then one row per invitation with its status, when\n * it was sent and until when it works, and resend / copy link / revoke.\n *\n * - **It assumes nothing about who invites.** Admins do, team managers into their team,\n * kastlan's company admins into their company — and in keksdose ANY budget owner, from\n * the budget's sharing (Marcel, 2026-10-07). So no word here says \"admin\"; the roles,\n * the scopes and the callbacks are the app's, and so is the frame around it.\n * - **The address is someone else's.** No `+app` tag is offered (auth §4.5): a tag is\n * the owner's own choice, made when they sign up.\n * - **The link is shown once.** The token is stored hashed, so the server can show a link\n * only in the answer that minted it — on creation and on resend, and only while mail\n * goes to the console (dev, a deploy without mail). The panel keeps such a link for as\n * long as it is mounted and offers to copy it on its row; reloading the page loses it,\n * and \"resend\" mints a new one. With the console backend the panel says so up front.\n * - **Revoked is optional.** An app that deletes the row instead (kastlan) never sends\n * `revoked`; the status chip simply never says it (§5).\n *\n * It never sends a request: `onInvite`, `onResend` and `onRevoke` do.\n *\n * Under a write lock, inviting and resending are refused; a lapsed plan's lock (`kind:\n * \"billing\"`) still lets an invitation be revoked, since removing access is what a\n * lapsed plan allows (docs/billing-harmonization.md §12.13, §12.36). A demo lock refuses\n * all three.\n */\n\nexport type InvitationStatus = \"open\" | \"accepted\" | \"expired\" | \"revoked\";\n\n/** One row of `GET /admin/invitations` (§5), in the wire's own field names. */\nexport interface InvitationRow<R extends string = string> {\n id: string | number;\n email: string;\n role?: R | null;\n /** The scope's value — a team's or a company's id, matched against `scopes`. */\n scope?: string | number | null;\n note?: string | null;\n /** The language it was written in. */\n locale?: string | null;\n /** Who sent it: a name, or the person. */\n invited_by?: string | AdminPerson | null;\n created_at: DateInput;\n expires_at?: DateInput;\n status: InvitationStatus;\n /** The one-time link, while mail goes to the console — in the answer that minted it. */\n link?: string | null;\n}\n\n/** What the form sends — `POST /admin/invitations {email, role, scope?, note?, locale}`. */\nexport interface InvitationDraft<R extends string = string> {\n email: string;\n /** Absent when the app offers no role choice (`roles` left out). */\n role?: R;\n scope?: string;\n note?: string;\n locale: KitLanguageCode;\n}\n\n/** What `onInvite` / `onResend` may answer, so the panel can keep a shown-once link. */\nexport interface InvitationSentAnswer {\n /** The invitation the answer is about — `onInvite` needs it for the link to find its\n * row; `onResend` knows it already. */\n id?: string | number;\n /** The one-time link, while mail goes to the console. */\n link?: string | null;\n /** `false`: the invitation exists but the mail failed — say so, resend it. */\n sent?: boolean;\n}\n\n/** A scope an invitation can carry. */\nexport interface InvitationScope {\n value: string;\n label: string;\n}\n\n/** The `invitations` namespace. */\nexport interface InvitationsLabels {\n /** The form's address field. */\n email: string;\n /** Under it while what was typed is not an address. */\n invalidEmail: string;\n role: string;\n /** The scope select's label — the app's word (\"Team\", \"Company\") goes over it. */\n scope: string;\n /** The scope select's empty choice. */\n scopeNone: string;\n /** The language select's label. */\n language: string;\n note: string;\n /** The form's button. */\n invite: string;\n /** Heading over the rows. */\n listTitle: string;\n empty: string;\n loading: string;\n /** The status chip, per status. */\n status: Record<InvitationStatus, string>;\n /** \"Sent 6 Oct 2026\". */\n sent: (date: string) => string;\n /** \"Expires 20 Oct 2026\" — on an open invitation. */\n expires: (date: string) => string;\n /** \"by Ada Example\". */\n invitedBy: (name: string) => string;\n /** The resend button — \"Send ben@example.com a new link\". */\n resend: (email: string) => string;\n /** The copy button. */\n copyLink: string;\n /** The revoke button — \"Revoke the invitation for ben@example.com\". */\n revoke: (email: string) => string;\n /** Over everything while mail goes to the console. */\n consoleHint: string;\n /** Beside a link the answer carried with no row to put it on. */\n linkReady: (email: string) => string;\n /** The answer said the mail was not sent. */\n notSent: (email: string) => string;\n /** A rejected callback, unless `describeError` says better. */\n failed: string;\n}\n\n// Defined in ./invitations-panel-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_INVITATIONS_LABELS };\n\n/** The tone of each status. */\nconst STATUS_TONES: Readonly<Record<InvitationStatus, ChipTone>> = {\n open: \"info\",\n accepted: \"success\",\n expired: \"warning\",\n revoked: \"neutral\",\n};\n\nexport interface InvitationsPanelProps<R extends string = string> {\n invitations: readonly InvitationRow<R>[];\n /** The app's roles. Left out: no role choice, and no role chip on a row. */\n roles?: RoleVocabulary<R>;\n /** The roles an invitation may carry — Kurvenschmiede's member or customer. Default:\n * the whole vocabulary. One role: no choice is drawn, and it is sent. */\n invitableRoles?: readonly R[];\n /** The role the form starts with. Default: the first invitable one. */\n defaultRole?: R;\n /** The scopes an invitation may carry — teams, companies. Left out: no scope field. */\n scopes?: readonly InvitationScope[];\n /** A scope must be chosen (no \"None\"). */\n scopeRequired?: boolean;\n /** The languages offered. Default: all seven of the kit's. */\n languages?: readonly KitLanguageCode[];\n /** The language the form starts with. Default: the page's, resolved to `languages`. */\n defaultLocale?: string;\n /** The server's `mail_backend`. `\"console\"` draws the hint. */\n mailBackend?: string | null;\n /** Creates one (resolve to clear the form; reject to keep it and say why). Left out:\n * no form — a list someone may look at but not add to. */\n onInvite?: (draft: InvitationDraft<R>) => MaybePromise<InvitationSentAnswer | void>;\n /** Mints a new link and mails it. Left out: no resend button. */\n onResend?: (invitation: InvitationRow<R>) => MaybePromise<InvitationSentAnswer | void>;\n /** Revokes an open or an expired invitation (level `none`, §4.2: re-inviting undoes\n * it). An expired row can be resent, and since 0.31 also removed: in an app that\n * keeps the rows it would otherwise stay listed for good (Kurvenschmiede's 0.30\n * adoption). Left out: no revoke button. */\n onRevoke?: (invitation: InvitationRow<R>) => MaybePromise;\n /** The list is being fetched. */\n loading?: boolean;\n /** The app's words for a rejected callback. */\n describeError?: (error: unknown) => ReactNode | undefined;\n /** Over the form: who can be invited and what happens, in the app's words. */\n intro?: ReactNode;\n /** The list's visible heading. Default: the `listTitle` label. `null` leaves it out,\n * for a page whose card is already headed \"Invitations\" (Kurvenschmiede's 0.30\n * adoption); the list keeps the label as its accessible name either way. */\n listTitle?: ReactNode | null;\n className?: string;\n labels?: LabelOverride<InvitationsLabels>;\n}\n\n// Shape only — whether the address may be invited is the server's question.\nconst EMAIL_SHAPE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\nconst ALL_CODES: readonly KitLanguageCode[] = KIT_LANGUAGES.map((language) => language.code);\n\n/** A failed run, told apart from an answer of `undefined`. */\nconst FAILED = Symbol(\"failed\");\n\nexport function InvitationsPanel<R extends string = string>({\n invitations,\n roles,\n invitableRoles,\n defaultRole,\n scopes,\n scopeRequired = false,\n languages = ALL_CODES,\n defaultLocale,\n mailBackend,\n onInvite,\n onResend,\n onRevoke,\n loading = false,\n describeError,\n intro,\n listTitle,\n className,\n labels: labelsProp,\n}: InvitationsPanelProps<R>) {\n const labels = useKitLabels(\"invitations\", DEFAULT_INVITATIONS_LABELS, labelsProp);\n const kitLocale = useKitLocale();\n const dayText = useDayText();\n const nameOf = usePersonLabel();\n const lock = useWriteLock();\n const offered: readonly R[] = invitableRoles ?? (roles ? (Object.keys(roles) as R[]) : []);\n const [email, setEmail] = useState(\"\");\n const [invalid, setInvalid] = useState(false);\n const [role, setRole] = useState<R | undefined>(defaultRole ?? offered[0]);\n const [scope, setScope] = useState(\"\");\n const [locale, setLocale] = useState<KitLanguageCode>(() =>\n resolveLanguage([defaultLocale, kitLocale], languages),\n );\n const [note, setNote] = useState(\"\");\n // One request at a time, for the whole panel; the key says which control spins.\n const [busy, setBusy] = useState<string | null>(null);\n const [failure, setFailure] = useState<ReactNode>(null);\n const [notice, setNotice] = useState<ReactNode>(null);\n // Links the answers carried, by invitation — shown once, kept while mounted.\n const [links, setLinks] = useState<Record<string, string>>({});\n // A link whose answer named no invitation: shown above the list instead.\n const [looseLink, setLooseLink] = useState<{ email: string; link: string } | null>(null);\n\n const run = async <T,>(key: string, call: () => MaybePromise<T>): Promise<T | void | typeof FAILED> => {\n setBusy(key);\n setFailure(null);\n setNotice(null);\n try {\n return await settle(call);\n } catch (error) {\n const own = describeError?.(error);\n setFailure(hasContent(own) ? own : labels.failed);\n return FAILED;\n } finally {\n setBusy(null);\n }\n };\n\n /** Keep what an answer carried: its link, and whether the mail went. */\n const took = (answer: InvitationSentAnswer | void, address: string, id?: string | number) => {\n if (!answer) return;\n const key = answer.id ?? id;\n if (answer.link) {\n if (key !== undefined) setLinks((known) => ({ ...known, [String(key)]: answer.link as string }));\n else setLooseLink({ email: address, link: answer.link });\n }\n if (answer.sent === false) setNotice(labels.notSent(address));\n };\n\n const address = email.trim();\n const choosesRole = roles !== undefined && offered.length > 1;\n const canInvite = onInvite !== undefined;\n\n const submit = async (event: FormEvent) => {\n event.preventDefault();\n // Enter in a field is a commit too — the button's lock does not see it.\n if (!onInvite || busy || lock.locked || address === \"\") return;\n if (!EMAIL_SHAPE.test(address)) {\n setInvalid(true);\n return;\n }\n if (scopes && scopeRequired && scope === \"\") return;\n const draft: InvitationDraft<R> = {\n email: address,\n locale,\n ...(role !== undefined && roles !== undefined ? { role } : {}),\n ...(scope !== \"\" ? { scope } : {}),\n ...(note.trim() !== \"\" ? { note: note.trim() } : {}),\n };\n const answer = await run(\"invite\", () => onInvite(draft));\n if (answer === FAILED) return;\n took(answer, address);\n setEmail(\"\");\n setNote(\"\");\n };\n\n const scopeLabel = (value: InvitationRow[\"scope\"]) =>\n value === null || value === undefined || value === \"\"\n ? null\n : (scopes?.find((s) => s.value === String(value))?.label ?? String(value));\n\n return (\n <div className={cn(\"space-y-3\", className)}>\n {intro && <div className=\"text-sm text-secondary\">{intro}</div>}\n {mailBackend === \"console\" && (\n <AlertBanner tone=\"info\" size=\"sm\">\n {labels.consoleHint}\n </AlertBanner>\n )}\n\n {canInvite && (\n <form noValidate onSubmit={submit} className=\"@container space-y-2\">\n <div className=\"grid grid-cols-1 gap-2 @md:grid-cols-2\">\n <Input\n label={labels.email}\n type=\"email\"\n inputMode=\"email\"\n autoComplete=\"off\"\n autoCapitalize=\"none\"\n spellCheck={false}\n required\n value={email}\n error={invalid ? labels.invalidEmail : undefined}\n data-private\n onChange={(event) => {\n setInvalid(false);\n setEmail(event.target.value);\n }}\n />\n {choosesRole && (\n <RoleSelect<R>\n label={labels.role}\n value={role}\n roles={roles}\n offered={offered}\n onChange={setRole}\n />\n )}\n {scopes && scopes.length > 0 && (\n <Select\n label={labels.scope}\n value={scope}\n required={scopeRequired}\n onChange={(event) => setScope(event.target.value)}\n >\n <option value=\"\" disabled={scopeRequired}>\n {labels.scopeNone}\n </option>\n {scopes.map((s) => (\n <option key={s.value} value={s.value}>\n {s.label}\n </option>\n ))}\n </Select>\n )}\n {languages.length > 1 && (\n <LanguageSelect label={labels.language} codes={languages} value={locale} onChange={setLocale} />\n )}\n <Input\n label={labels.note}\n value={note}\n maxLength={255}\n onChange={(event) => setNote(event.target.value)}\n className=\"@md:col-span-2\"\n />\n </div>\n <div className=\"flex justify-end\">\n <Button\n type=\"submit\"\n variant=\"accent\"\n commit\n pending={busy === \"invite\"}\n disabled={busy !== null || address === \"\" || (scopeRequired && scopes !== undefined && scope === \"\")}\n >\n <UserPlus className=\"size-4\" aria-hidden />\n {labels.invite}\n </Button>\n </div>\n </form>\n )}\n\n {failure && (\n <AlertBanner tone=\"danger\" size=\"sm\">\n {failure}\n </AlertBanner>\n )}\n {notice && (\n <AlertBanner tone=\"warning\" size=\"sm\" onDismiss={() => setNotice(null)}>\n {notice}\n </AlertBanner>\n )}\n {looseLink && (\n <AlertBanner tone=\"info\" size=\"sm\" onDismiss={() => setLooseLink(null)}>\n <div className=\"flex min-w-0 flex-wrap items-center gap-2\">\n <span data-private>{labels.linkReady(looseLink.email)}</span>\n <code className=\"min-w-0 break-all text-xs\">{looseLink.link}</code>\n <CopyButton size=\"xs\" text={looseLink.link} label={labels.copyLink} />\n </div>\n </AlertBanner>\n )}\n\n <div className=\"space-y-2\">\n {listTitle !== null && <SectionLabel as=\"h4\">{listTitle ?? labels.listTitle}</SectionLabel>}\n {loading && invitations.length === 0 ? (\n <div className=\"flex justify-center py-3\">\n <Spinner label={labels.loading} />\n </div>\n ) : invitations.length === 0 ? (\n <Caption>{labels.empty}</Caption>\n ) : (\n <ul aria-label={labels.listTitle} className=\"divide-y divide-subtle\">\n {invitations.map((invitation) => {\n const key = String(invitation.id);\n const link = invitation.link ?? links[key];\n const open = invitation.status === \"open\";\n const resendable = onResend && (open || invitation.status === \"expired\");\n const revocable = onRevoke && (open || invitation.status === \"expired\");\n const scopeText = scopeLabel(invitation.scope);\n const by =\n typeof invitation.invited_by === \"string\" ? invitation.invited_by : nameOf(invitation.invited_by);\n const facts = [\n dayText(invitation.created_at) && labels.sent(dayText(invitation.created_at)),\n open && dayText(invitation.expires_at ?? null) && labels.expires(dayText(invitation.expires_at ?? null)),\n by && labels.invitedBy(by),\n ].filter(Boolean);\n return (\n <li key={key} data-status={invitation.status} className=\"py-2 first:pt-0 last:pb-0\">\n <div className=\"flex flex-wrap items-center justify-between gap-x-3 gap-y-1.5\">\n <div className=\"min-w-0 flex-1 basis-48 space-y-1\">\n <div data-private className=\"truncate-until-large text-sm font-medium text-primary\">\n {invitation.email}\n </div>\n <div className=\"flex flex-wrap items-center gap-1\">\n <Chip size=\"xs\" shape=\"square\" caps tone={STATUS_TONES[invitation.status]}>\n {labels.status[invitation.status]}\n </Chip>\n {roles && invitation.role && <RoleChip<R> value={invitation.role} roles={roles} size=\"xs\" />}\n {scopeText && (\n <Chip size=\"xs\" shape=\"square\">\n {scopeText}\n </Chip>\n )}\n </div>\n {(facts.length > 0 || invitation.note) && (\n <div className=\"text-xs text-muted\">\n {facts.join(\" · \")}\n {invitation.note && (\n <>\n {facts.length > 0 && \" · \"}\n <span data-private>{invitation.note}</span>\n </>\n )}\n </div>\n )}\n </div>\n {(link || resendable || revocable) && (\n <div className=\"ms-auto flex shrink-0 items-center gap-1\">\n {/* The copy stays its own icon: it says \"Copied\" where it was\n pressed, which a menu entry that closes cannot. */}\n {link && open && <CopyButton size=\"xs\" tone=\"muted\" text={link} label={labels.copyLink} />}\n {/* Resend and revoke are the row's actions (docs/text-size-\n harmonization.md §10.8): resend an icon at Normal, revoke in\n the \"⋯\" menu (0.49 KIT-12); both in the menu at Large, where both names as text (\"Send … a new link\") would\n not fit a phone's row beside the copy icon. */}\n <RowActions\n name={invitation.email}\n actions={[\n resendable && {\n key: \"resend\",\n label: labels.resend(invitation.email),\n icon: RefreshCw,\n commit: true,\n pending: busy === `resend:${key}`,\n disabled: busy !== null,\n onSelect: () =>\n void (async () => {\n const answer = await run(`resend:${key}`, () => onResend(invitation));\n if (answer !== FAILED) took(answer, invitation.email, invitation.id);\n })(),\n },\n revocable && {\n key: \"revoke\",\n label: labels.revoke(invitation.email),\n icon: Trash2,\n tone: \"danger\",\n // Level `none` (user admin §4.2): re-inviting undoes it, so\n // the kit's KIT-12 question stays out; an app may ask in\n // `onRevoke` (keksdose does).\n confirm: false,\n // Removing access stays open under a lapsed plan (§12.36).\n commit: COMMIT_EXCEPT_BILLING,\n pending: busy === `revoke:${key}`,\n disabled: busy !== null,\n onSelect: () => void run(`revoke:${key}`, () => onRevoke(invitation)),\n },\n ]}\n />\n </div>\n )}\n </div>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["address"]}
|
|
@@ -29,8 +29,9 @@ import { DEFAULT_USER_ROSTER_LABELS } from "./user-roster-labels.js";
|
|
|
29
29
|
*
|
|
30
30
|
* The column keys ARE the server's sort keys (§3.1): `name`, `email`, `role`, `created`,
|
|
31
31
|
* `last_login` — so {@link userRosterSort} turns the table's sort state into the query
|
|
32
|
-
* as it is. Phone cards come from the same columns: the identity is the card's headline
|
|
33
|
-
* the
|
|
32
|
+
* as it is. Phone cards come from the same columns: the identity is the card's headline
|
|
33
|
+
* with the account's states as chips beside it, the role, last login and the actions are
|
|
34
|
+
* its three fields, created is left off.
|
|
34
35
|
*/
|
|
35
36
|
/**
|
|
36
37
|
* The fields of the contract's `AdminUserRow` the preset reads (§3.2, the auth
|
|
@@ -204,16 +205,27 @@ export interface UserRosterColumnsOptions<T extends UserRosterRow, R extends str
|
|
|
204
205
|
/**
|
|
205
206
|
* The roster's columns, in the contract's order (§3.3):
|
|
206
207
|
*
|
|
207
|
-
* | key | cell | sort | phone |
|
|
208
|
-
*
|
|
209
|
-
* | `name` | {@link UserIdentityCell} | last, first | the card's headline |
|
|
210
|
-
* | `email` | the address (`emailColumn` only) | address | hidden |
|
|
211
|
-
* | `role` | `RoleChip`(s), or {@link RoleSelect} where `editableRole` | role | field |
|
|
212
|
-
* | `state` | `AccountStateChip`s from `stateOf` | — |
|
|
213
|
-
* | `created` | `dateColumn` | instant | hidden |
|
|
214
|
-
* | `last_login` | `dateColumn`, relative, "Never" | instant | field |
|
|
215
|
-
* | …`extra` | the app's | the app's | the app's |
|
|
216
|
-
* | `actions` | {@link UserRowActions} | — |
|
|
208
|
+
* | key | cell | sort | phone | fit (0.50) |
|
|
209
|
+
* |---|---|---|---|---|
|
|
210
|
+
* | `name` | {@link UserIdentityCell} | last, first | the card's headline | `grow` |
|
|
211
|
+
* | `email` | the address (`emailColumn` only) | address | hidden | — |
|
|
212
|
+
* | `role` | `RoleChip`(s), or {@link RoleSelect} where `editableRole` | role | field | — |
|
|
213
|
+
* | `state` | `AccountStateChip`s from `stateOf` | — | chips beside the headline | — |
|
|
214
|
+
* | `created` | `dateColumn` | instant | hidden | `fitPriority: 1` |
|
|
215
|
+
* | `last_login` | `dateColumn`, relative, "Never" | instant | field | `fitPriority: 2` |
|
|
216
|
+
* | …`extra` | the app's | the app's | off the card unless marked `mobile: "secondary"` | the app's |
|
|
217
|
+
* | `actions` | {@link UserRowActions} | — | the card's actions row (`role: "actions"`) | — |
|
|
218
|
+
*
|
|
219
|
+
* The fit (0.50, kastlan K2): Name takes the rest of the row, and a table narrower than
|
|
220
|
+
* its columns leaves out Created, then Last login (DataTable's `grow` / `fitPriority`).
|
|
221
|
+
* The role and state chips still wrap in their cells (0.48), and the other cells keep one
|
|
222
|
+
* line at Normal. `{ ...col, grow: undefined, fitPriority: undefined }` restores the 0.49
|
|
223
|
+
* layout; an `extra` column takes a `fitPriority` of its own to leave too.
|
|
224
|
+
*
|
|
225
|
+
* The card holds three fields — `role`, `last_login` and one more: an `extra` column marked
|
|
226
|
+
* `mobile: "secondary"` takes that place. The actions are `role: "actions"` (0.49), drawn
|
|
227
|
+
* after the fields and never one of the three. Drop them from the card with
|
|
228
|
+
* `mobile: "hidden"`, or draw a `mobileCard`.
|
|
217
229
|
*
|
|
218
230
|
* Pure: hand it resolved words, or use {@link useUserRosterColumns}. The keys are stable,
|
|
219
231
|
* so an app may drop or move a column (`columns.filter((c) => c.key !== "created")`).
|