@eifi1/ui-kit 0.44.0 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -14
- package/dist/admin/admin-action-log-labels.js +9 -0
- package/dist/admin/admin-action-log-labels.js.map +1 -1
- package/dist/admin/admin-action-log.d.ts +18 -1
- package/dist/admin/admin-action-log.js +25 -16
- package/dist/admin/admin-action-log.js.map +1 -1
- package/dist/admin/admin-area-labels.d.ts +15 -0
- package/dist/admin/admin-area-labels.js +6 -1
- package/dist/admin/admin-area-labels.js.map +1 -1
- package/dist/admin/admin-overview.d.ts +16 -1
- package/dist/admin/admin-overview.js +12 -1
- package/dist/admin/admin-overview.js.map +1 -1
- package/dist/admin/mail-test-card-labels.d.ts +3 -0
- package/dist/admin/mail-test-card-labels.js +17 -0
- package/dist/admin/mail-test-card-labels.js.map +1 -0
- package/dist/admin/mail-test-card.d.ts +75 -0
- package/dist/admin/mail-test-card.js +82 -0
- package/dist/admin/mail-test-card.js.map +1 -0
- package/dist/components/appearance-labels.d.ts +10 -0
- package/dist/components/appearance-labels.js +6 -1
- package/dist/components/appearance-labels.js.map +1 -1
- package/dist/components/data-table-column-settings.js +2 -2
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-head.js +2 -2
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-mobile.js +17 -2
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-row-actions.d.ts +18 -2
- package/dist/components/data-table-row-actions.js +86 -2
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-shared.d.ts +2 -0
- package/dist/components/data-table-shared.js +17 -2
- package/dist/components/data-table-shared.js.map +1 -1
- package/dist/components/data-table.d.ts +43 -2
- package/dist/components/data-table.js +10 -10
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-format-setting.d.ts +29 -0
- package/dist/components/date-format-setting.js +36 -0
- package/dist/components/date-format-setting.js.map +1 -0
- package/dist/components/swipeable-row.d.ts +8 -0
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/tabs.d.ts +3 -2
- package/dist/components/tabs.js +1 -1
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/translation-review-editor.js +1 -0
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review-labels.d.ts +5 -0
- package/dist/components/translation-review-labels.js +2 -0
- package/dist/components/translation-review-labels.js.map +1 -1
- package/dist/components/translation-review.d.ts +18 -8
- package/dist/components/translation-review.js +64 -7
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/write-lock-labels.js +2 -1
- package/dist/components/write-lock-labels.js.map +1 -1
- package/dist/components/write-lock.d.ts +33 -3
- package/dist/components/write-lock.js +9 -0
- package/dist/components/write-lock.js.map +1 -1
- package/dist/data-table.d.ts +1 -1
- package/dist/data-table.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +4 -2
- package/dist/feedback/feedback-row-detail.js +17 -26
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-swipe.d.ts +18 -0
- package/dist/feedback/feedback-swipe.js +10 -1
- package/dist/feedback/feedback-swipe.js.map +1 -1
- package/dist/i18n/defaults.js +9 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +81 -5
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +25 -1
- package/dist/i18n/kit-labels.js +8 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +97 -7
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +81 -5
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +81 -5
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +81 -5
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +81 -5
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +81 -5
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +16 -2
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +17 -2
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/date-format.d.ts +81 -0
- package/dist/lib/date-format.js +56 -0
- package/dist/lib/date-format.js.map +1 -0
- package/dist/lib/format.d.ts +5 -1
- package/dist/lib/format.js +11 -5
- package/dist/lib/format.js.map +1 -1
- package/dist/notifications/device-zone-report.d.ts +27 -0
- package/dist/notifications/device-zone-report.js +24 -0
- package/dist/notifications/device-zone-report.js.map +1 -0
- package/dist/notifications/feedback-notification-types-labels.d.ts +7 -1
- package/dist/notifications/feedback-notification-types-labels.js +4 -1
- package/dist/notifications/feedback-notification-types-labels.js.map +1 -1
- package/dist/notifications/feedback-notification-types.d.ts +11 -5
- package/dist/notifications/feedback-notification-types.js +2 -1
- package/dist/notifications/feedback-notification-types.js.map +1 -1
- package/dist/notifications/notification-bell.d.ts +33 -0
- package/dist/notifications/notification-bell.js +74 -0
- package/dist/notifications/notification-bell.js.map +1 -0
- package/dist/notifications/notification-inbox.d.ts +77 -0
- package/dist/notifications/notification-inbox.js +206 -0
- package/dist/notifications/notification-inbox.js.map +1 -0
- package/dist/notifications/notification-types-setting.d.ts +6 -4
- package/dist/notifications/notification-types-setting.js +103 -45
- package/dist/notifications/notification-types-setting.js.map +1 -1
- package/dist/notifications/notification-types.d.ts +19 -7
- package/dist/notifications/notifications-labels.d.ts +41 -7
- package/dist/notifications/notifications-labels.js +18 -1
- package/dist/notifications/notifications-labels.js.map +1 -1
- package/dist/notifications/push-devices-setting.d.ts +20 -3
- package/dist/notifications/push-devices-setting.js +93 -33
- package/dist/notifications/push-devices-setting.js.map +1 -1
- package/dist/notifications/push-worker.d.ts +108 -0
- package/dist/notifications/push-worker.js +62 -0
- package/dist/notifications/push-worker.js.map +1 -0
- package/dist/notifications/quiet-hours-setting.d.ts +29 -17
- package/dist/notifications/quiet-hours-setting.js +9 -41
- package/dist/notifications/quiet-hours-setting.js.map +1 -1
- package/dist/notifications/time-zone-select.d.ts +5 -2
- package/dist/notifications/time-zone-select.js.map +1 -1
- package/dist/notifications/use-push-device.d.ts +4 -17
- package/dist/notifications/use-push-device.js +7 -15
- package/dist/notifications/use-push-device.js.map +1 -1
- package/dist/offline/local-first.d.ts +33 -0
- package/dist/offline/local-first.js +39 -0
- package/dist/offline/local-first.js.map +1 -0
- package/dist/offline/pwa-update-prompt-labels.d.ts +2 -0
- package/dist/offline/pwa-update-prompt-labels.js +9 -0
- package/dist/offline/pwa-update-prompt-labels.js.map +1 -0
- package/dist/offline/pwa-update-prompt.d.ts +32 -0
- package/dist/offline/pwa-update-prompt.js +36 -0
- package/dist/offline/pwa-update-prompt.js.map +1 -0
- package/dist/offline/query-persistence.d.ts +125 -0
- package/dist/offline/query-persistence.js +183 -0
- package/dist/offline/query-persistence.js.map +1 -0
- package/dist/offline/service-worker-updates.d.ts +46 -0
- package/dist/offline/service-worker-updates.js +39 -0
- package/dist/offline/service-worker-updates.js.map +1 -0
- package/dist/offline/shared-computer-labels.d.ts +2 -0
- package/dist/offline/shared-computer-labels.js +9 -0
- package/dist/offline/shared-computer-labels.js.map +1 -0
- package/dist/offline/shared-computer.d.ts +44 -0
- package/dist/offline/shared-computer.js +51 -0
- package/dist/offline/shared-computer.js.map +1 -0
- package/dist/offline/sign-out-pending-dialog-labels.d.ts +2 -0
- package/dist/offline/sign-out-pending-dialog-labels.js +14 -0
- package/dist/offline/sign-out-pending-dialog-labels.js.map +1 -0
- package/dist/offline/sign-out-pending-dialog.d.ts +58 -0
- package/dist/offline/sign-out-pending-dialog.js +66 -0
- package/dist/offline/sign-out-pending-dialog.js.map +1 -0
- package/dist/offline.d.ts +11 -0
- package/dist/offline.js +33 -0
- package/dist/offline.js.map +1 -0
- package/dist/push-worker.d.ts +2 -0
- package/dist/push-worker.js +14 -0
- package/dist/push-worker.js.map +1 -0
- package/dist/shell/account-menu-text-size.d.ts +33 -0
- package/dist/shell/account-menu-text-size.js +51 -0
- package/dist/shell/account-menu-text-size.js.map +1 -0
- package/dist/shell/admin-menu.d.ts +24 -0
- package/dist/shell/admin-menu.js +12 -3
- package/dist/shell/admin-menu.js.map +1 -1
- package/dist/shell/top-bar-actions.d.ts +6 -2
- package/dist/shell/top-bar-actions.js +1 -0
- package/dist/shell/top-bar-actions.js.map +1 -1
- package/dist/shell.d.ts +4 -0
- package/dist/shell.js +4 -0
- package/dist/shell.js.map +1 -1
- package/dist/sync/db.d.ts +76 -0
- package/dist/sync/db.js +24 -0
- package/dist/sync/db.js.map +1 -0
- package/dist/sync/engine.d.ts +124 -0
- package/dist/sync/engine.js +474 -0
- package/dist/sync/engine.js.map +1 -0
- package/dist/sync/hooks.d.ts +30 -0
- package/dist/sync/hooks.js +67 -0
- package/dist/sync/hooks.js.map +1 -0
- package/dist/sync/testing.d.ts +28 -0
- package/dist/sync/testing.js +41 -0
- package/dist/sync/testing.js.map +1 -0
- package/dist/sync/wire.d.ts +50 -0
- package/dist/sync/wire.js +2 -0
- package/dist/sync/wire.js.map +1 -0
- package/dist/sync.d.ts +7 -0
- package/dist/sync.js +22 -0
- package/dist/sync.js.map +1 -0
- package/dist/tour/tour-catalogue.d.ts +7 -1
- package/dist/tour/tour-catalogue.js.map +1 -1
- package/dist/tour/tour-labels.js +1 -0
- package/dist/tour/tour-labels.js.map +1 -1
- package/dist/tour/tour.d.ts +30 -0
- package/dist/tour/tour.js +54 -8
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-progress.d.ts +99 -1
- package/dist/tour/use-tour-progress.js +228 -12
- package/dist/tour/use-tour-progress.js.map +1 -1
- package/dist/tour.d.ts +2 -0
- package/dist/tour.js +2 -0
- package/dist/tour.js.map +1 -1
- package/package.json +43 -1
- package/src/admin/admin-action-log-labels.ts +9 -0
- package/src/admin/admin-action-log.tsx +41 -5
- package/src/admin/admin-area-labels.ts +20 -0
- package/src/admin/admin-overview.tsx +37 -2
- package/src/admin/mail-test-card-labels.ts +19 -0
- package/src/admin/mail-test-card.tsx +190 -0
- package/src/components/appearance-labels.ts +16 -0
- package/src/components/data-table-column-settings.tsx +2 -2
- package/src/components/data-table-head.tsx +2 -2
- package/src/components/data-table-mobile.tsx +26 -3
- package/src/components/data-table-row-actions.tsx +118 -2
- package/src/components/data-table-shared.tsx +39 -3
- package/src/components/data-table.tsx +52 -21
- package/src/components/date-format-setting.tsx +64 -0
- package/src/components/swipeable-row.tsx +8 -0
- package/src/components/tabs.tsx +4 -3
- package/src/components/translation-review-editor.tsx +2 -0
- package/src/components/translation-review-labels.ts +7 -0
- package/src/components/translation-review.tsx +102 -14
- package/src/components/write-lock-labels.ts +1 -0
- package/src/components/write-lock.tsx +36 -3
- package/src/data-table.ts +1 -0
- package/src/feedback/feedback-row-detail.tsx +18 -23
- package/src/feedback/feedback-swipe.tsx +28 -1
- package/src/i18n/defaults.ts +8 -0
- package/src/i18n/german.ts +90 -4
- package/src/i18n/kit-labels.tsx +31 -1
- package/src/i18n/locales/en.ts +13 -0
- package/src/i18n/locales/es.ts +90 -4
- package/src/i18n/locales/fr.ts +90 -4
- package/src/i18n/locales/hu.ts +86 -4
- package/src/i18n/locales/it.ts +90 -4
- package/src/i18n/locales/zh.ts +84 -2
- package/src/i18n/review.ts +15 -1
- package/src/index.ts +45 -0
- package/src/lib/date-format.ts +151 -0
- package/src/lib/format.ts +23 -6
- package/src/notifications/device-zone-report.ts +54 -0
- package/src/notifications/feedback-notification-types-labels.ts +10 -1
- package/src/notifications/feedback-notification-types.ts +12 -5
- package/src/notifications/notification-bell.tsx +116 -0
- package/src/notifications/notification-inbox.tsx +304 -0
- package/src/notifications/notification-types-setting.tsx +151 -55
- package/src/notifications/notification-types.ts +20 -8
- package/src/notifications/notifications-labels.ts +69 -11
- package/src/notifications/push-devices-setting.tsx +123 -34
- package/src/notifications/push-worker.ts +160 -0
- package/src/notifications/quiet-hours-setting.tsx +41 -58
- package/src/notifications/time-zone-select.tsx +5 -2
- package/src/notifications/use-push-device.ts +20 -33
- package/src/offline/local-first.ts +76 -0
- package/src/offline/pwa-update-prompt-labels.ts +9 -0
- package/src/offline/pwa-update-prompt.tsx +72 -0
- package/src/offline/query-persistence.ts +356 -0
- package/src/offline/service-worker-updates.ts +81 -0
- package/src/offline/shared-computer-labels.ts +8 -0
- package/src/offline/shared-computer.tsx +87 -0
- package/src/offline/sign-out-pending-dialog-labels.ts +21 -0
- package/src/offline/sign-out-pending-dialog.tsx +135 -0
- package/src/offline.ts +32 -0
- package/src/push-worker.ts +25 -0
- package/src/shell/account-menu-text-size.tsx +77 -0
- package/src/shell/admin-menu.tsx +40 -3
- package/src/shell/top-bar-actions.tsx +6 -1
- package/src/shell.ts +7 -0
- package/src/sync/db.ts +103 -0
- package/src/sync/engine.ts +699 -0
- package/src/sync/hooks.ts +93 -0
- package/src/sync/testing.ts +73 -0
- package/src/sync/wire.ts +61 -0
- package/src/sync.ts +29 -0
- package/src/tour/tour-catalogue.tsx +7 -1
- package/src/tour/tour-labels.ts +1 -0
- package/src/tour/tour.tsx +105 -9
- package/src/tour/use-tour-progress.ts +378 -12
- package/src/tour.ts +3 -0
|
@@ -72,7 +72,11 @@ export type AdminActionKind =
|
|
|
72
72
|
| "group_rename"
|
|
73
73
|
| "group_delete"
|
|
74
74
|
| "membership_add"
|
|
75
|
-
| "membership_role"
|
|
75
|
+
| "membership_role"
|
|
76
|
+
// 0.45 (admin §9.1.3): a group switched off or on again — kastlan's company PATCH
|
|
77
|
+
// (`is_active`). server-kit 0.15's `AdminAction.GROUP_DEACTIVATE` / `GROUP_REACTIVATE`.
|
|
78
|
+
| "group_deactivate"
|
|
79
|
+
| "group_reactivate";
|
|
76
80
|
|
|
77
81
|
/** Every {@link AdminActionKind}, in the contract's order. */
|
|
78
82
|
export const ADMIN_ACTION_KINDS: readonly AdminActionKind[] = [
|
|
@@ -98,6 +102,8 @@ export const ADMIN_ACTION_KINDS: readonly AdminActionKind[] = [
|
|
|
98
102
|
"group_delete",
|
|
99
103
|
"membership_add",
|
|
100
104
|
"membership_role",
|
|
105
|
+
"group_deactivate",
|
|
106
|
+
"group_reactivate",
|
|
101
107
|
];
|
|
102
108
|
|
|
103
109
|
/** The app's word for its groups (§11.5): Kurvenschmiede's teams, kastlan's companies,
|
|
@@ -105,11 +111,14 @@ export const ADMIN_ACTION_KINDS: readonly AdminActionKind[] = [
|
|
|
105
111
|
* actions are said in. */
|
|
106
112
|
export type AdminGroupNoun = "group" | "team" | "company" | "budget";
|
|
107
113
|
|
|
108
|
-
/** The kinds said in the noun's words: the five of 0.34,
|
|
114
|
+
/** The kinds said in the noun's words: the five of 0.34, `membership_remove`, and 0.45's
|
|
115
|
+
* `group_deactivate` / `group_reactivate`. */
|
|
109
116
|
type AdminGroupActionKind =
|
|
110
117
|
| "group_create"
|
|
111
118
|
| "group_rename"
|
|
112
119
|
| "group_delete"
|
|
120
|
+
| "group_deactivate"
|
|
121
|
+
| "group_reactivate"
|
|
113
122
|
| "membership_add"
|
|
114
123
|
| "membership_role"
|
|
115
124
|
| "membership_remove";
|
|
@@ -118,6 +127,8 @@ const GROUP_ACTION_KINDS: ReadonlySet<string> = new Set<AdminGroupActionKind>([
|
|
|
118
127
|
"group_create",
|
|
119
128
|
"group_rename",
|
|
120
129
|
"group_delete",
|
|
130
|
+
"group_deactivate",
|
|
131
|
+
"group_reactivate",
|
|
121
132
|
"membership_add",
|
|
122
133
|
"membership_role",
|
|
123
134
|
"membership_remove",
|
|
@@ -184,6 +195,13 @@ export interface AdminActionLogEntry {
|
|
|
184
195
|
group?: AdminGroupTarget | null;
|
|
185
196
|
/** The `detail` in the app's words — "Member → Admin", "12 items to Ben Example". */
|
|
186
197
|
detail?: ReactNode;
|
|
198
|
+
/**
|
|
199
|
+
* 0.45 (admin §9.4, Marcel 2026-10-19): a `group_rename` row's `detail.{from, to}` —
|
|
200
|
+
* kastlan's company rename logs both names. With both, the row reads "Company renamed
|
|
201
|
+
* from A to B" (`renamedFromTo`) in place of the target. A rename that logs `{}` (KS's
|
|
202
|
+
* team rename: never a name) leaves it out and reads "Team renamed" and the group.
|
|
203
|
+
*/
|
|
204
|
+
renamed?: { from?: string | null; to?: string | null } | null;
|
|
187
205
|
}
|
|
188
206
|
|
|
189
207
|
/** The `adminActionLog` namespace. */
|
|
@@ -206,6 +224,8 @@ export interface AdminActionLogLabels {
|
|
|
206
224
|
showAll: string;
|
|
207
225
|
/** A target name as a filter button — "Show only the actions on Ada Example". */
|
|
208
226
|
filterBy: (target: string) => string;
|
|
227
|
+
/** 0.45: after "Company renamed" when the row carries both names — "from A to B". */
|
|
228
|
+
renamedFromTo: (from: string, to: string) => string;
|
|
209
229
|
/** What each action is called, past tense, keyed by the vocabulary. An app adds its
|
|
210
230
|
* own here (`labels={{ actions: { plan: "Plan changed" } }}`). A word given here
|
|
211
231
|
* outranks the noun's in `groupActions` — so no catalogue holds the group kinds
|
|
@@ -223,6 +243,11 @@ interface AdminGroupActionLabels {
|
|
|
223
243
|
group_rename: string;
|
|
224
244
|
/** "Team deleted". */
|
|
225
245
|
group_delete: string;
|
|
246
|
+
/** "Company deactivated" (0.45, admin §9.1.3): the group switched off — its people
|
|
247
|
+
* locked out — not a person's `deactivate`. */
|
|
248
|
+
group_deactivate: string;
|
|
249
|
+
/** "Company reactivated" (0.45). */
|
|
250
|
+
group_reactivate: string;
|
|
226
251
|
/** "Added to the team" — said of the person, so it agrees with the account where a
|
|
227
252
|
* language agrees (es "Añadida", fr "Ajouté"). */
|
|
228
253
|
membership_add: string;
|
|
@@ -384,12 +409,22 @@ export function AdminActionLog({
|
|
|
384
409
|
aria-busy={loading || undefined}
|
|
385
410
|
className={cn("divide-y divide-subtle", loading && "opacity-60")}
|
|
386
411
|
>
|
|
387
|
-
{entries.map((entry) =>
|
|
412
|
+
{entries.map((entry) => {
|
|
413
|
+
const from = (entry.renamed?.from ?? "").trim();
|
|
414
|
+
const to = (entry.renamed?.to ?? "").trim();
|
|
415
|
+
const renamed = entry.action === "group_rename" && from !== "" && to !== "";
|
|
416
|
+
return (
|
|
388
417
|
<li key={entry.id} data-action={entry.action} className="space-y-0.5 py-2 first:pt-0 last:pb-0">
|
|
389
418
|
<div className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5">
|
|
390
419
|
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5 text-sm">
|
|
391
420
|
<span className="font-medium text-primary">{actionText(entry.action)}</span>
|
|
392
|
-
{
|
|
421
|
+
{renamed ? (
|
|
422
|
+
<span data-private data-renamed className="min-w-0 break-words text-primary">
|
|
423
|
+
{labels.renamedFromTo(from, to)}
|
|
424
|
+
</span>
|
|
425
|
+
) : (
|
|
426
|
+
entry.target !== undefined && <span className="min-w-0 break-words">{targetNode(entry)}</span>
|
|
427
|
+
)}
|
|
393
428
|
{entry.group && (
|
|
394
429
|
<span data-group className="min-w-0 break-words text-xs text-muted">
|
|
395
430
|
{groupText(entry.group)}
|
|
@@ -410,7 +445,8 @@ export function AdminActionLog({
|
|
|
410
445
|
)}
|
|
411
446
|
</div>
|
|
412
447
|
</li>
|
|
413
|
-
|
|
448
|
+
);
|
|
449
|
+
})}
|
|
414
450
|
</ol>
|
|
415
451
|
)}
|
|
416
452
|
{paging && paging.total > 0 && (
|
|
@@ -24,6 +24,15 @@ export interface AdminTileLabels {
|
|
|
24
24
|
users: string;
|
|
25
25
|
/** `users_new_7d`, the line under the users tile. */
|
|
26
26
|
usersNew: (count: number) => string;
|
|
27
|
+
/** 0.46 (Kurvenschmiede 1, admin §9.5): server-kit's `awaiting_verdict` as an `extra`
|
|
28
|
+
* tile — answered reports that wait on a verdict (IN_EVALUATION, NEEDS_LIVE_TEST). */
|
|
29
|
+
awaitingVerdict: string;
|
|
30
|
+
/** Its line under the count. */
|
|
31
|
+
awaitingVerdictHint: string;
|
|
32
|
+
/** 0.46: server-kit's `released_for_work` — the reports released for the work queue. */
|
|
33
|
+
releasedForWork: string;
|
|
34
|
+
/** Its line under the count. */
|
|
35
|
+
releasedForWorkHint: string;
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
export interface AdminAreaLabels {
|
|
@@ -32,6 +41,12 @@ export interface AdminAreaLabels {
|
|
|
32
41
|
/** The row's waiting count, for a reader ("Administration, 3 waiting"); the eye sees
|
|
33
42
|
* the number in the badge. */
|
|
34
43
|
waiting: (count: number) => string;
|
|
44
|
+
/**
|
|
45
|
+
* 0.45 (admin §9.3.4): a button's whole name with its count — "Platform, 2 waiting" —
|
|
46
|
+
* for an `IconButton` whose `badge` is an `AdminWaitingBadge`: the badge is decorative
|
|
47
|
+
* there, so the name carries the count. `label` is the button's own name.
|
|
48
|
+
*/
|
|
49
|
+
waitingLabel: (label: string, count: number) => string;
|
|
35
50
|
/** The 403 page's heading (§2.2 decision 8). */
|
|
36
51
|
forbiddenTitle: string;
|
|
37
52
|
/**
|
|
@@ -59,6 +74,7 @@ export interface AdminAreaLabels {
|
|
|
59
74
|
export const DEFAULT_ADMIN_AREA_LABELS: AdminAreaLabels = {
|
|
60
75
|
title: "Administration",
|
|
61
76
|
waiting: (count) => `${count} waiting`,
|
|
77
|
+
waitingLabel: (label, count) => `${label}, ${count} waiting`,
|
|
62
78
|
forbiddenTitle: "Only for administrators",
|
|
63
79
|
forbiddenBody: (appName) => `This is where administrators run ${appName}. Your account cannot open this page.`,
|
|
64
80
|
forbiddenHome: "Go to the start page",
|
|
@@ -76,6 +92,10 @@ export const DEFAULT_ADMIN_AREA_LABELS: AdminAreaLabels = {
|
|
|
76
92
|
invitationsPending: "Pending invitations",
|
|
77
93
|
users: "Users",
|
|
78
94
|
usersNew: (count) => `${count} new in 7 days`,
|
|
95
|
+
awaitingVerdict: "Awaiting a verdict",
|
|
96
|
+
awaitingVerdictHint: "Answered, and waiting for you or the reporter to confirm the fix",
|
|
97
|
+
releasedForWork: "Released for work",
|
|
98
|
+
releasedForWorkHint: "Ready, rework requested or in progress",
|
|
79
99
|
},
|
|
80
100
|
overviewError: "The overview couldn’t be loaded.",
|
|
81
101
|
retry: "Try again",
|
|
@@ -86,8 +86,18 @@ export function AttentionTile({ count, href, ...tile }: AttentionTileProps) {
|
|
|
86
86
|
return <StatTile data-slot="attention-tile" {...tile} value={count ?? null} href={href} />;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* 0.46 (Kurvenschmiede 1, admin §9.5): the `extra` keys server-kit's
|
|
91
|
+
* `feedback_attention` fills, whose tiles have the kit's words
|
|
92
|
+
* (`adminArea.tiles.awaitingVerdict`, `releasedForWork`, with a line each).
|
|
93
|
+
*/
|
|
94
|
+
export type AdminFeedbackTileKey = "awaiting_verdict" | "released_for_work";
|
|
95
|
+
|
|
89
96
|
/** One of the app's own tiles (§2.4 decision 13: keksdose's "Support waiting"). */
|
|
90
|
-
export
|
|
97
|
+
export type AdminExtraTile = AdminOwnTile | AdminFeedbackTile;
|
|
98
|
+
|
|
99
|
+
/** A tile of the app's own: it brings its words. */
|
|
100
|
+
export interface AdminOwnTile {
|
|
91
101
|
/** Unique among the tiles; usually the `extra` key it shows. */
|
|
92
102
|
key: string;
|
|
93
103
|
label: ReactNode;
|
|
@@ -100,6 +110,14 @@ export interface AdminExtraTile {
|
|
|
100
110
|
description?: ReactNode;
|
|
101
111
|
}
|
|
102
112
|
|
|
113
|
+
/** 0.46: a tile of server-kit's feedback counts — `label` and `description` default to
|
|
114
|
+
* the kit's words; an app's own still win. The kit's `description` fills in only when
|
|
115
|
+
* `label` is left out too: an app that names the tile gets no hint line it did not ask for. */
|
|
116
|
+
export interface AdminFeedbackTile extends Omit<AdminOwnTile, "key" | "label"> {
|
|
117
|
+
key: AdminFeedbackTileKey;
|
|
118
|
+
label?: ReactNode;
|
|
119
|
+
}
|
|
120
|
+
|
|
103
121
|
export interface AdminOverviewProps {
|
|
104
122
|
/** The server's answer; `undefined` while it loads (the grid shows its skeletons). */
|
|
105
123
|
attention: AdminAttention | null | undefined;
|
|
@@ -186,9 +204,26 @@ export function AdminOverview({
|
|
|
186
204
|
attention && typeof attention.users_new_7d === "number" ? labels.tiles.usersNew(attention.users_new_7d) : undefined,
|
|
187
205
|
},
|
|
188
206
|
];
|
|
207
|
+
const feedbackWords: Record<AdminFeedbackTileKey, { label: string; description: string }> = {
|
|
208
|
+
awaiting_verdict: { label: labels.tiles.awaitingVerdict, description: labels.tiles.awaitingVerdictHint },
|
|
209
|
+
released_for_work: { label: labels.tiles.releasedForWork, description: labels.tiles.releasedForWorkHint },
|
|
210
|
+
};
|
|
189
211
|
const tiles = [
|
|
190
212
|
...shared.map((t) => ({ ...t, count: attention ? attention[t.key] : undefined, href: href[t.key] })),
|
|
191
|
-
...extraTiles
|
|
213
|
+
...extraTiles.map((t) => {
|
|
214
|
+
const words = Object.prototype.hasOwnProperty.call(feedbackWords, t.key)
|
|
215
|
+
? feedbackWords[t.key as AdminFeedbackTileKey]
|
|
216
|
+
: undefined;
|
|
217
|
+
// The kit's hint comes with the kit's label only: an app that names the tile
|
|
218
|
+
// itself gets no line it did not ask for (keksdose S5, round 0.46).
|
|
219
|
+
return words
|
|
220
|
+
? {
|
|
221
|
+
...t,
|
|
222
|
+
label: t.label ?? words.label,
|
|
223
|
+
description: t.description ?? (t.label === undefined ? words.description : undefined),
|
|
224
|
+
}
|
|
225
|
+
: t;
|
|
226
|
+
}),
|
|
192
227
|
].filter((t) => loading || (t.count !== null && t.count !== undefined));
|
|
193
228
|
if (!loading && tiles.length === 0) return error ? errorBox(true) : null;
|
|
194
229
|
const grid = (
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// The English words of ./mail-test-card, in a module with no component code: the English
|
|
2
|
+
// catalogue (src/i18n/defaults.ts) imports them without the component, so `en` loads as one
|
|
3
|
+
// chunk like the other languages (0.34, KS U8). ./mail-test-card re-exports them.
|
|
4
|
+
|
|
5
|
+
import { englishRateLimited } from "../auth/auth-errors";
|
|
6
|
+
import type { MailTestCardLabels } from "./mail-test-card";
|
|
7
|
+
|
|
8
|
+
/** docs/admin-harmonization.md §9.2.4, word for word. */
|
|
9
|
+
export const DEFAULT_MAIL_TEST_CARD_LABELS: MailTestCardLabels = {
|
|
10
|
+
title: "Test mail",
|
|
11
|
+
description: "Sends one mail to your own address, through the transport this installation sends all its mail with.",
|
|
12
|
+
transport: (backend) => `Transport: ${backend}`,
|
|
13
|
+
action: "Send a test mail to me",
|
|
14
|
+
sent: (address) => `Sent to ${address}. Check the inbox.`,
|
|
15
|
+
console: "The console transport only logs mail; nothing was sent.",
|
|
16
|
+
failed: "Could not send. The server log has the provider’s answer.",
|
|
17
|
+
rateLimited: englishRateLimited,
|
|
18
|
+
keywords: "test mail email e-mail transport smtp resend console send deliver",
|
|
19
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { Send } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import { useKitLabels, type LabelOverride } from "../i18n/kit-labels";
|
|
6
|
+
import { isRateLimited, retryAfterSeconds } from "../auth/auth-errors";
|
|
7
|
+
import { AlertBanner } from "../components/alert-banner";
|
|
8
|
+
import { Button } from "../components/button";
|
|
9
|
+
import { Chip } from "../components/chip";
|
|
10
|
+
import { SettingsSection, type SettingsSectionProps } from "../settings/settings-section";
|
|
11
|
+
import { DEFAULT_MAIL_TEST_CARD_LABELS } from "./mail-test-card-labels";
|
|
12
|
+
|
|
13
|
+
/* ── Labels ──────────────────────────────────────────────────────────────── */
|
|
14
|
+
|
|
15
|
+
/** Every string {@link MailTestCard} renders — the `mailTestCard` namespace of
|
|
16
|
+
* `<UiKitProvider labels>`, overridable per instance through `labels`
|
|
17
|
+
* (docs/admin-harmonization.md §9.2.4). */
|
|
18
|
+
export interface MailTestCardLabels {
|
|
19
|
+
/** The card's title, and the app's catalogue entry's. */
|
|
20
|
+
title: string;
|
|
21
|
+
/** The sentence under it. */
|
|
22
|
+
description: string;
|
|
23
|
+
/** The transport's chip: "Transport: resend". `backend` is the server's `mail_backend`. */
|
|
24
|
+
transport: (backend: string) => string;
|
|
25
|
+
/** The button. */
|
|
26
|
+
action: string;
|
|
27
|
+
/** The answer said "sent" through a real transport. `address` is masked ("a…@example.com"). */
|
|
28
|
+
sent: (address: string) => string;
|
|
29
|
+
/** `mail_backend` is `console`: said whatever `sent` is — a console answers `true`. */
|
|
30
|
+
console: string;
|
|
31
|
+
/** Not sent, or the request failed. The provider's error stays in the server log. */
|
|
32
|
+
failed: string;
|
|
33
|
+
/** HTTP 429 — the app's limiter (server-kit's `MAIL_TEST_LIMIT`, 3 per 10 minutes);
|
|
34
|
+
* `seconds` from `Retry-After` when the response has one. */
|
|
35
|
+
rateLimited: (seconds?: number) => string;
|
|
36
|
+
/** For the app's catalogue entry (`keywords`); the card never reads it. */
|
|
37
|
+
keywords: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Defined in ./mail-test-card-labels, which holds no component code.
|
|
41
|
+
export { DEFAULT_MAIL_TEST_CARD_LABELS };
|
|
42
|
+
|
|
43
|
+
/* ── The card ────────────────────────────────────────────────────────────── */
|
|
44
|
+
|
|
45
|
+
/** The answer of `POST /admin/mail/test` — server-kit 0.15's `MailTestResult`. */
|
|
46
|
+
export interface MailTestAnswer {
|
|
47
|
+
sent: boolean;
|
|
48
|
+
/** The transport that took it: `console`, `resend`, `smtp`, … */
|
|
49
|
+
mail_backend: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The card's default DOM id: its catalogue entry's `anchor`, the `?focus=` target. */
|
|
53
|
+
export const MAIL_TEST_ANCHOR = "admin-mail-test";
|
|
54
|
+
|
|
55
|
+
export interface MailTestCardProps
|
|
56
|
+
extends Omit<SettingsSectionProps, "anchor" | "title" | "description" | "children"> {
|
|
57
|
+
/** The installation's transport, as the admin page has it (`mail_backend`): the chip
|
|
58
|
+
* before a send. After one, the answer's. Optional (0.46): without it the chip shows
|
|
59
|
+
* from the first answer on, and an app needs no query only for the chip. */
|
|
60
|
+
mailBackend?: string;
|
|
61
|
+
/** The app's `POST /admin/mail/test` (kastlan `/platform/mail/test`), no body: it
|
|
62
|
+
* awaits the app's own send and answers `{sent, mail_backend}`. A rejection is read
|
|
63
|
+
* for a 429 (the limiter); anything else is "could not send". */
|
|
64
|
+
send: () => Promise<MailTestAnswer>;
|
|
65
|
+
/** The signed-in admin's own address — where the mail goes. Shown masked. */
|
|
66
|
+
address: string;
|
|
67
|
+
/** Default {@link MAIL_TEST_ANCHOR}; the app's catalogue entry names the same. */
|
|
68
|
+
anchor?: string;
|
|
69
|
+
/** The app's sentence after the kit's description. */
|
|
70
|
+
description?: ReactNode;
|
|
71
|
+
labels?: LabelOverride<MailTestCardLabels>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* An address with its local part masked to its first character: "ada@example.com" →
|
|
76
|
+
* "a…@example.com". The domain stays, so the admin sees which mailbox to open.
|
|
77
|
+
*/
|
|
78
|
+
export function maskMailAddress(address: string): string {
|
|
79
|
+
const text = address.trim();
|
|
80
|
+
const at = text.lastIndexOf("@");
|
|
81
|
+
if (at <= 0) return text === "" ? "" : `${Array.from(text)[0]}…`;
|
|
82
|
+
return `${Array.from(text.slice(0, at))[0]}…${text.slice(at)}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type Outcome =
|
|
86
|
+
| { kind: "sent"; backend: string }
|
|
87
|
+
| { kind: "console"; backend: string }
|
|
88
|
+
| { kind: "failed"; backend?: string }
|
|
89
|
+
| { kind: "limited"; seconds?: number };
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* "Send a test mail to me" (docs/admin-harmonization.md §9.2): a `SettingsSection` with a
|
|
93
|
+
* sentence, the transport as a chip, the button and the result line. One mail goes to the
|
|
94
|
+
* signed-in admin's own address, never another, through the app's own send.
|
|
95
|
+
*
|
|
96
|
+
* **The result line reads `mail_backend` first**: with `console` it says "nothing was sent"
|
|
97
|
+
* whatever `sent` is, since every console path answers `true` (§9.2 Today). Otherwise
|
|
98
|
+
* `sent` says "Sent to a…@example.com. Check the inbox." or "Could not send." — the
|
|
99
|
+
* provider's error stays in the server log (§9.2.7). A 429 says the limiter's line. The
|
|
100
|
+
* button is busy while the request runs; the line is a live region.
|
|
101
|
+
*
|
|
102
|
+
* Not a commit: the test writes nothing, so it is not under the write lock (a demo never
|
|
103
|
+
* reaches the admin area, and the server refuses it anyway). The app adds the catalogue
|
|
104
|
+
* entry `{ id, anchor: MAIL_TEST_ANCHOR, title, keywords }` from these labels.
|
|
105
|
+
*/
|
|
106
|
+
export function MailTestCard({
|
|
107
|
+
mailBackend,
|
|
108
|
+
send,
|
|
109
|
+
address,
|
|
110
|
+
anchor = MAIL_TEST_ANCHOR,
|
|
111
|
+
description,
|
|
112
|
+
labels: labelsProp,
|
|
113
|
+
...rest
|
|
114
|
+
}: MailTestCardProps) {
|
|
115
|
+
const labels = useKitLabels("mailTestCard", DEFAULT_MAIL_TEST_CARD_LABELS, labelsProp);
|
|
116
|
+
const [busy, setBusy] = useState(false);
|
|
117
|
+
const [outcome, setOutcome] = useState<Outcome | null>(null);
|
|
118
|
+
// The last transport an answer named: a later send with no answer (429, network) keeps it.
|
|
119
|
+
const [answered, setAnswered] = useState<string | undefined>(undefined);
|
|
120
|
+
|
|
121
|
+
const run = async () => {
|
|
122
|
+
if (busy) return;
|
|
123
|
+
setBusy(true);
|
|
124
|
+
setOutcome(null);
|
|
125
|
+
try {
|
|
126
|
+
const answer = await send();
|
|
127
|
+
const backend = answer.mail_backend;
|
|
128
|
+
if (backend) setAnswered(backend);
|
|
129
|
+
if (backend === "console") setOutcome({ kind: "console", backend });
|
|
130
|
+
else setOutcome(answer.sent ? { kind: "sent", backend } : { kind: "failed", backend });
|
|
131
|
+
} catch (error) {
|
|
132
|
+
setOutcome(isRateLimited(error) ? { kind: "limited", seconds: retryAfterSeconds(error) } : { kind: "failed" });
|
|
133
|
+
} finally {
|
|
134
|
+
setBusy(false);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const backend = answered ?? mailBackend;
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<SettingsSection
|
|
142
|
+
{...rest}
|
|
143
|
+
anchor={anchor}
|
|
144
|
+
title={labels.title}
|
|
145
|
+
description={
|
|
146
|
+
description != null ? (
|
|
147
|
+
<>
|
|
148
|
+
{labels.description} {description}
|
|
149
|
+
</>
|
|
150
|
+
) : (
|
|
151
|
+
labels.description
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
>
|
|
155
|
+
<div className="space-y-2">
|
|
156
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
157
|
+
<Button variant="secondary" pending={busy} onClick={() => void run()}>
|
|
158
|
+
<Send className="size-4" aria-hidden />
|
|
159
|
+
{labels.action}
|
|
160
|
+
</Button>
|
|
161
|
+
{backend && (
|
|
162
|
+
<Chip size="sm" data-slot="mail-backend">
|
|
163
|
+
{labels.transport(backend)}
|
|
164
|
+
</Chip>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
{outcome?.kind === "sent" && (
|
|
168
|
+
<AlertBanner variant="inline" block tone="success">
|
|
169
|
+
<span data-private>{labels.sent(maskMailAddress(address))}</span>
|
|
170
|
+
</AlertBanner>
|
|
171
|
+
)}
|
|
172
|
+
{outcome?.kind === "console" && (
|
|
173
|
+
<AlertBanner variant="inline" block tone="info">
|
|
174
|
+
{labels.console}
|
|
175
|
+
</AlertBanner>
|
|
176
|
+
)}
|
|
177
|
+
{outcome?.kind === "failed" && (
|
|
178
|
+
<AlertBanner variant="inline" block tone="danger">
|
|
179
|
+
{labels.failed}
|
|
180
|
+
</AlertBanner>
|
|
181
|
+
)}
|
|
182
|
+
{outcome?.kind === "limited" && (
|
|
183
|
+
<AlertBanner variant="inline" block tone="warning">
|
|
184
|
+
{labels.rateLimited(outcome.seconds)}
|
|
185
|
+
</AlertBanner>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
</SettingsSection>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -28,6 +28,16 @@ export interface AppearanceLabels {
|
|
|
28
28
|
* toast or the line under the setting (0.32.1, keksdose's 0.32 report).
|
|
29
29
|
*/
|
|
30
30
|
saveFailed: string;
|
|
31
|
+
/** The date-format setting's name (0.46, `DateFormatSetting`). */
|
|
32
|
+
dateFormat: string;
|
|
33
|
+
/** One line under it: the format is its own choice, apart from the language. */
|
|
34
|
+
dateFormatHelp: string;
|
|
35
|
+
/** Its first option, followed by that language's sample: "Follow language — 01 Aug 2026". */
|
|
36
|
+
dateFormatAuto: string;
|
|
37
|
+
/** `AccountMenuTextSize`'s A− button (0.46, keksdose live #426): one step smaller. */
|
|
38
|
+
textSmaller: string;
|
|
39
|
+
/** Its A+ button: one step larger. */
|
|
40
|
+
textLarger: string;
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
export const DEFAULT_APPEARANCE_LABELS: AppearanceLabels = {
|
|
@@ -38,6 +48,12 @@ export const DEFAULT_APPEARANCE_LABELS: AppearanceLabels = {
|
|
|
38
48
|
contrastHelp: "More contrast darkens quiet text and lines, and thickens focus frames. System follows this device.",
|
|
39
49
|
contrastModes: { system: "System", standard: "Standard", more: "More" },
|
|
40
50
|
saveFailed: "Couldn't save this to your account. It still applies on this device.",
|
|
51
|
+
dateFormat: "Date format",
|
|
52
|
+
dateFormatHelp:
|
|
53
|
+
"How dates are written throughout the app. Independent of the interface language, so you can read the app in English and still see German dates.",
|
|
54
|
+
dateFormatAuto: "Follow language",
|
|
55
|
+
textSmaller: "Smaller text",
|
|
56
|
+
textLarger: "Larger text",
|
|
41
57
|
};
|
|
42
58
|
|
|
43
59
|
/** The `appearance` namespace, resolved — English, then the provider, then `labels`. For
|
|
@@ -4,7 +4,7 @@ import { X } from "lucide-react";
|
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
import { Tooltip } from "./tooltip";
|
|
6
6
|
import type { DataTableLabels } from "./data-table-labels";
|
|
7
|
-
import {
|
|
7
|
+
import { isTableOwnColumn } from "./data-table-row-actions";
|
|
8
8
|
import { columnLabel } from "./data-table-shared";
|
|
9
9
|
import type { DataTableColumn } from "./data-table";
|
|
10
10
|
|
|
@@ -101,7 +101,7 @@ export function ColumnSettingsPanel<T>({
|
|
|
101
101
|
</button>
|
|
102
102
|
<div className="flex flex-col gap-0.5">
|
|
103
103
|
{columns.map((col) => {
|
|
104
|
-
if (col.key
|
|
104
|
+
if (isTableOwnColumn(col.key)) return null;
|
|
105
105
|
const checked = !hiddenCols.has(col.key);
|
|
106
106
|
const headerLabel = columnLabel(col);
|
|
107
107
|
return (
|
|
@@ -8,7 +8,7 @@ import { FilterPopover } from "./data-table-filter-popover";
|
|
|
8
8
|
import { Popover } from "./popover";
|
|
9
9
|
import { Tooltip } from "./tooltip";
|
|
10
10
|
import type { DataTableLabels } from "./data-table-labels";
|
|
11
|
-
import {
|
|
11
|
+
import { isTableOwnColumn } from "./data-table-row-actions";
|
|
12
12
|
import { columnLabel, logicalAlign, splitCellProps } from "./data-table-shared";
|
|
13
13
|
import type { DataTableColumn, DataTableProps, FilterState } from "./data-table";
|
|
14
14
|
|
|
@@ -223,7 +223,7 @@ export function DataTableHead<T>({
|
|
|
223
223
|
</Popover>
|
|
224
224
|
)}
|
|
225
225
|
</div>
|
|
226
|
-
{resizable && col.key
|
|
226
|
+
{resizable && !isTableOwnColumn(col.key) && (
|
|
227
227
|
/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a
|
|
228
228
|
pointer-only drag handle for a visual preference (column width); it is
|
|
229
229
|
deliberately not a tab stop, and no content or action depends on it. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
2
|
-
import type { ReactNode } from "react";
|
|
2
|
+
import type { MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent, ReactNode } from "react";
|
|
3
3
|
import { ArrowDownWideNarrow, ArrowUpNarrowWide, ChevronDown, ChevronRight, Filter } from "lucide-react";
|
|
4
4
|
import { FOCUS_RING } from "./focus-ring";
|
|
5
5
|
import { IconButton } from "./icon-button";
|
|
@@ -15,7 +15,7 @@ import { DialogFrame } from "./dialog-frame";
|
|
|
15
15
|
import { Checkbox } from "./checkbox";
|
|
16
16
|
import { useLongPress } from "../hooks/use-long-press";
|
|
17
17
|
import type { DataTableLabels } from "./data-table-labels";
|
|
18
|
-
import { RowLink, cleanAttrs, columnLabel, fromOutside } from "./data-table-shared";
|
|
18
|
+
import { RowLink, cleanAttrs, columnLabel, fromOutside, fromOwnControl } from "./data-table-shared";
|
|
19
19
|
import type {
|
|
20
20
|
DataTableCellProps,
|
|
21
21
|
DataTableColumn,
|
|
@@ -383,7 +383,25 @@ export function MobileRow<T>({
|
|
|
383
383
|
// Attached whenever the table selects, the hook's `enabled` deciding whether a press
|
|
384
384
|
// arms: its pointerdown also clears a stale swallow latch, which must happen in the
|
|
385
385
|
// mode too, or a long press whose click never came would eat the first toggle.
|
|
386
|
-
|
|
386
|
+
//
|
|
387
|
+
// A press that starts on a control in a cell (or in a portal one opened) is that
|
|
388
|
+
// control's, not a way into the mode (keksdose live #428): it only clears the latch.
|
|
389
|
+
const press = select
|
|
390
|
+
? {
|
|
391
|
+
...longPress.handlers,
|
|
392
|
+
onPointerDown: (e: ReactPointerEvent<HTMLElement>) => {
|
|
393
|
+
if (fromOutside(e.target, e.currentTarget) || fromOwnControl(e.target, e.currentTarget)) {
|
|
394
|
+
longPress.consumeClick();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
longPress.handlers.onPointerDown(e);
|
|
398
|
+
},
|
|
399
|
+
onContextMenu: (e: ReactMouseEvent<HTMLElement>) => {
|
|
400
|
+
if (fromOutside(e.target, e.currentTarget) || fromOwnControl(e.target, e.currentTarget)) return;
|
|
401
|
+
longPress.handlers.onContextMenu(e);
|
|
402
|
+
},
|
|
403
|
+
}
|
|
404
|
+
: undefined;
|
|
387
405
|
const body = selecting ? (
|
|
388
406
|
// In the mode a card is a toggle — kk's register row: `role="button"` with
|
|
389
407
|
// `aria-pressed`, the box beside it only a picture of that state (the card is the
|
|
@@ -399,6 +417,8 @@ export function MobileRow<T>({
|
|
|
399
417
|
{...press}
|
|
400
418
|
onClick={(e) => {
|
|
401
419
|
if (!select!.selectable || fromOutside(e.target, e.currentTarget)) return;
|
|
420
|
+
// A tap on a control in a cell is that control's (keksdose live #428).
|
|
421
|
+
if (fromOwnControl(e.target, e.currentTarget)) return;
|
|
402
422
|
select!.onToggle(!select!.selected);
|
|
403
423
|
}}
|
|
404
424
|
onKeyDown={(e) => {
|
|
@@ -455,6 +475,9 @@ export function MobileRow<T>({
|
|
|
455
475
|
interactive
|
|
456
476
|
? (e) => {
|
|
457
477
|
if (fromOutside(e.target, e.currentTarget)) return;
|
|
478
|
+
// A tap on a control in a cell (a select, a button) is that
|
|
479
|
+
// control's, as on the desktop `<tr>` (keksdose live #428).
|
|
480
|
+
if (fromOwnControl(e.target, e.currentTarget)) return;
|
|
458
481
|
onRowClick!(row);
|
|
459
482
|
}
|
|
460
483
|
: undefined
|