@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
|
@@ -10,6 +10,7 @@ import { Skeleton } from "../components/skeleton";
|
|
|
10
10
|
import { Switch } from "../components/switch";
|
|
11
11
|
import { SectionLabel } from "../components/text";
|
|
12
12
|
import { COMMIT_EXCEPT_BILLING } from "../components/write-lock";
|
|
13
|
+
import { usePhoneLayout } from "../hooks/use-breakpoint";
|
|
13
14
|
import { CARD_DESCRIPTION_CLASS } from "../settings/settings-card-classes";
|
|
14
15
|
import { SettingsCardTitle } from "../settings/settings-heading";
|
|
15
16
|
import { settle, useMounted } from "../account/account-parts";
|
|
@@ -18,6 +19,7 @@ import type { WriteOverlay } from "./notification-parts";
|
|
|
18
19
|
import type { NotificationsLabels } from "./notifications-labels";
|
|
19
20
|
import type {
|
|
20
21
|
DigestCadence,
|
|
22
|
+
NotificationChannel,
|
|
21
23
|
NotificationSettingsPatch,
|
|
22
24
|
NotificationSwitches,
|
|
23
25
|
NotificationTypeEntry,
|
|
@@ -73,15 +75,33 @@ interface Row<Id extends string> {
|
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
/** A switch's key in the write overlay. */
|
|
76
|
-
const switchKey = (id: string, channel:
|
|
78
|
+
const switchKey = (id: string, channel: NotificationChannel) => `${id}.${channel}`;
|
|
77
79
|
const CADENCE_KEY = "digest_cadence";
|
|
78
80
|
|
|
81
|
+
/** The grid of a several-channel row on a wide screen: the type's words, then one narrow
|
|
82
|
+
* column per channel drawn. Literal classes, so Tailwind's scan finds them. */
|
|
83
|
+
const COLUMN_GRID: Record<number, string> = {
|
|
84
|
+
1: "grid-cols-[minmax(0,1fr)_4.5rem]",
|
|
85
|
+
2: "grid-cols-[minmax(0,1fr)_repeat(2,4.5rem)]",
|
|
86
|
+
3: "grid-cols-[minmax(0,1fr)_repeat(3,4.5rem)]",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** The channels in the card's column order (§10.2). */
|
|
90
|
+
const CHANNELS: readonly NotificationChannel[] = ["app", "push", "email"];
|
|
91
|
+
|
|
92
|
+
/** The channels a type offers: every one the server did not answer `null` (or leave out,
|
|
93
|
+
* as `push` before server-kit 0.16). */
|
|
94
|
+
const channelsOf = (switches: NotificationSwitches): NotificationChannel[] =>
|
|
95
|
+
CHANNELS.filter((channel) => switches[channel] !== null && switches[channel] !== undefined);
|
|
96
|
+
|
|
79
97
|
/**
|
|
80
98
|
* What the person hears about, and how (docs/notifications-harmonization.md §2.1–§2.3,
|
|
81
|
-
* §4.2): one row per type the SERVER answered, the app's words on it, a switch
|
|
82
|
-
* the
|
|
83
|
-
*
|
|
84
|
-
*
|
|
99
|
+
* §4.2, §10.2): one row per type the SERVER answered, the app's words on it, and a switch
|
|
100
|
+
* per channel the type offers — "In the app", "Push", "E-mail". On a wide screen they are
|
|
101
|
+
* narrow columns under those headings; on a phone they stack under the type's label. A
|
|
102
|
+
* channel the server answers `null` is not drawn (kastlan and KS offer no push; KS no
|
|
103
|
+
* "in the app"), and a row with one channel keeps a single switch named by the type
|
|
104
|
+
* (Kurvenschmiede's e-mail-only rows).
|
|
85
105
|
*
|
|
86
106
|
* **The server's list is the only list.** The card keeps no defaults and no list of
|
|
87
107
|
* types: it draws what `value` holds, in the order of `groups`, else of `types`. A type
|
|
@@ -140,23 +160,24 @@ export function NotificationTypesSetting<Id extends string = string>({
|
|
|
140
160
|
}
|
|
141
161
|
}
|
|
142
162
|
|
|
163
|
+
const phone = usePhoneLayout();
|
|
143
164
|
const rowOf = (id: Id): Row<Id> | null => {
|
|
144
165
|
const switches = value?.[id];
|
|
145
166
|
const entry = entries.get(id);
|
|
146
|
-
if (!switches || !entry || (switches.
|
|
167
|
+
if (!switches || !entry || channelsOf(switches).length === 0) return null;
|
|
147
168
|
return { entry, switches };
|
|
148
169
|
};
|
|
149
170
|
const rowsOf = (ids: readonly Id[]) => ids.map(rowOf).filter((row): row is Row<Id> => row !== null);
|
|
150
171
|
|
|
151
|
-
const shown = (id: string, channel:
|
|
172
|
+
const shown = (id: string, channel: NotificationChannel, server: boolean): boolean => {
|
|
152
173
|
const key = switchKey(id, channel);
|
|
153
174
|
return overlay.get(key) ?? server;
|
|
154
175
|
};
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
176
|
+
const channelOn = ({ entry, switches }: Row<Id>, channel: NotificationChannel) =>
|
|
177
|
+
shown(entry.id, channel, switches[channel] === true);
|
|
178
|
+
/** The type reaches the person at all: some channel is on (the cadence and the app's
|
|
179
|
+
* extra show then). */
|
|
180
|
+
const rowOn = (row: Row<Id>) => channelsOf(row.switches).some((channel) => channelOn(row, channel));
|
|
160
181
|
|
|
161
182
|
const save = <V,>(key: string, patch: NotificationSettingsPatch<Id>, which: WriteOverlay<V>, next: V) => {
|
|
162
183
|
if (which.pending(key)) return;
|
|
@@ -174,7 +195,7 @@ export function NotificationTypesSetting<Id extends string = string>({
|
|
|
174
195
|
);
|
|
175
196
|
};
|
|
176
197
|
|
|
177
|
-
const flip = (id: Id, channel:
|
|
198
|
+
const flip = (id: Id, channel: NotificationChannel, on: boolean) =>
|
|
178
199
|
save(
|
|
179
200
|
switchKey(id, channel),
|
|
180
201
|
{ types: { [id]: { [channel]: on } } as NotificationSettingsPatch<Id>["types"] },
|
|
@@ -190,11 +211,33 @@ export function NotificationTypesSetting<Id extends string = string>({
|
|
|
190
211
|
</span>
|
|
191
212
|
) : null;
|
|
192
213
|
|
|
214
|
+
const channelLabel: Record<NotificationChannel, string> = {
|
|
215
|
+
app: labels.channelApp,
|
|
216
|
+
push: labels.channelPush,
|
|
217
|
+
email: labels.channelEmail,
|
|
218
|
+
};
|
|
219
|
+
/** The columns on a wide screen: every channel some several-channel row offers. */
|
|
220
|
+
const drawnRows = value && !loading ? rowsOf(groups ? groups.flatMap((group) => group.types) : types.map((entry) => entry.id)) : [];
|
|
221
|
+
const multi = drawnRows.filter((row) => channelsOf(row.switches).length > 1);
|
|
222
|
+
const columns = CHANNELS.filter((channel) => multi.some((row) => channelsOf(row.switches).includes(channel)));
|
|
223
|
+
const columnHeadings =
|
|
224
|
+
!phone && columns.length > 0 ? (
|
|
225
|
+
// The switches carry the channel's name themselves; the headings are for the eye.
|
|
226
|
+
<div aria-hidden className={cn("grid gap-x-2", COLUMN_GRID[columns.length])} data-channel-headings="">
|
|
227
|
+
<span />
|
|
228
|
+
{columns.map((channel) => (
|
|
229
|
+
<span key={channel} className="text-center text-caption font-medium text-muted">
|
|
230
|
+
{channelLabel[channel]}
|
|
231
|
+
</span>
|
|
232
|
+
))}
|
|
233
|
+
</div>
|
|
234
|
+
) : null;
|
|
235
|
+
|
|
193
236
|
const renderRow = (row: Row<Id>) => {
|
|
194
237
|
const { entry, switches } = row;
|
|
195
238
|
const on = rowOn(row);
|
|
196
|
-
const
|
|
197
|
-
const
|
|
239
|
+
const offered = channelsOf(switches);
|
|
240
|
+
const single = offered.length === 1 ? offered[0] : null;
|
|
198
241
|
const cadenceSelect =
|
|
199
242
|
on && digest?.type === entry.id && cadence ? (
|
|
200
243
|
<Select
|
|
@@ -213,52 +256,99 @@ export function NotificationTypesSetting<Id extends string = string>({
|
|
|
213
256
|
</Select>
|
|
214
257
|
) : null;
|
|
215
258
|
const extra = on ? renderExtra?.(entry.id) : null;
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
{
|
|
220
|
-
{
|
|
221
|
-
|
|
222
|
-
) :
|
|
223
|
-
|
|
259
|
+
const below =
|
|
260
|
+
cadenceSelect || hasContent(extra) ? (
|
|
261
|
+
<div className="ps-4 space-y-2">
|
|
262
|
+
{cadenceSelect}
|
|
263
|
+
{extra}
|
|
264
|
+
</div>
|
|
265
|
+
) : null;
|
|
266
|
+
|
|
267
|
+
// One channel: today's single switch, named by the type (KS's e-mail-only rows).
|
|
268
|
+
if (single) {
|
|
269
|
+
const description =
|
|
270
|
+
single === "email" && unverifiedLine ? (
|
|
271
|
+
<>
|
|
272
|
+
{entry.description !== undefined && <span className="block">{entry.description}</span>}
|
|
273
|
+
{unverifiedLine}
|
|
274
|
+
</>
|
|
275
|
+
) : (
|
|
276
|
+
entry.description
|
|
277
|
+
);
|
|
278
|
+
return (
|
|
279
|
+
<div key={entry.id} data-notification-type={entry.id} data-channels={single} className="space-y-2">
|
|
280
|
+
<Switch
|
|
281
|
+
label={entry.label}
|
|
282
|
+
description={description}
|
|
283
|
+
checked={channelOn(row, single)}
|
|
284
|
+
disabled={disabled}
|
|
285
|
+
commit={COMMIT_EXCEPT_BILLING}
|
|
286
|
+
onCheckedChange={(next) => flip(entry.id, single, next)}
|
|
287
|
+
/>
|
|
288
|
+
{below}
|
|
289
|
+
</div>
|
|
224
290
|
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Several channels: a group named by its type, a switch per channel — columns on a
|
|
294
|
+
// wide screen (the headings above the list name them), stacked on a phone.
|
|
295
|
+
const labelId = `${groupId}-type-${entry.id}`;
|
|
296
|
+
const unverifiedId = `${groupId}-unverified-${entry.id}`;
|
|
297
|
+
const channelSwitch = (channel: NotificationChannel) => (
|
|
298
|
+
<Switch
|
|
299
|
+
key={channel}
|
|
300
|
+
size="sm"
|
|
301
|
+
label={phone ? channelLabel[channel] : undefined}
|
|
302
|
+
aria-label={phone ? undefined : channelLabel[channel]}
|
|
303
|
+
description={phone && channel === "email" ? (unverifiedLine ?? undefined) : undefined}
|
|
304
|
+
aria-describedby={!phone && channel === "email" && unverifiedLine ? unverifiedId : undefined}
|
|
305
|
+
checked={channelOn(row, channel)}
|
|
306
|
+
disabled={disabled}
|
|
307
|
+
commit={COMMIT_EXCEPT_BILLING}
|
|
308
|
+
onCheckedChange={(next) => flip(entry.id, channel, next)}
|
|
309
|
+
data-channel={channel}
|
|
310
|
+
/>
|
|
311
|
+
);
|
|
312
|
+
const text = (
|
|
313
|
+
<div className="min-w-0">
|
|
314
|
+
<div id={labelId} className="text-sm font-medium text-primary">
|
|
315
|
+
{entry.label}
|
|
316
|
+
</div>
|
|
317
|
+
{(entry.description !== undefined || (!phone && offered.includes("email") && unverifiedLine)) && (
|
|
318
|
+
<div className={CARD_DESCRIPTION_CLASS}>
|
|
319
|
+
{entry.description !== undefined && <span className="block">{entry.description}</span>}
|
|
320
|
+
{!phone && offered.includes("email") && <span id={unverifiedId}>{unverifiedLine}</span>}
|
|
321
|
+
</div>
|
|
322
|
+
)}
|
|
323
|
+
</div>
|
|
324
|
+
);
|
|
225
325
|
return (
|
|
226
326
|
<div
|
|
227
327
|
key={entry.id}
|
|
228
328
|
data-notification-type={entry.id}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
aria-label={both ? entry.label : undefined}
|
|
329
|
+
data-channels={offered.join(" ")}
|
|
330
|
+
role="group"
|
|
331
|
+
aria-labelledby={labelId}
|
|
233
332
|
className="space-y-2"
|
|
234
333
|
>
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
disabled={disabled}
|
|
251
|
-
commit={COMMIT_EXCEPT_BILLING}
|
|
252
|
-
onCheckedChange={(next) => flip(entry.id, "email", next)}
|
|
253
|
-
/>
|
|
254
|
-
</div>
|
|
255
|
-
)}
|
|
256
|
-
{(cadenceSelect || hasContent(extra)) && (
|
|
257
|
-
<div className="ps-4 space-y-2">
|
|
258
|
-
{cadenceSelect}
|
|
259
|
-
{extra}
|
|
334
|
+
{phone ? (
|
|
335
|
+
<>
|
|
336
|
+
{text}
|
|
337
|
+
<div className="ps-4 space-y-2" data-stacked="">
|
|
338
|
+
{offered.map(channelSwitch)}
|
|
339
|
+
</div>
|
|
340
|
+
</>
|
|
341
|
+
) : (
|
|
342
|
+
<div className={cn("grid items-start gap-x-2", COLUMN_GRID[columns.length])}>
|
|
343
|
+
{text}
|
|
344
|
+
{columns.map((channel) => (
|
|
345
|
+
<div key={channel} className="flex justify-center pt-0.5">
|
|
346
|
+
{offered.includes(channel) ? channelSwitch(channel) : null}
|
|
347
|
+
</div>
|
|
348
|
+
))}
|
|
260
349
|
</div>
|
|
261
350
|
)}
|
|
351
|
+
{below}
|
|
262
352
|
</div>
|
|
263
353
|
);
|
|
264
354
|
};
|
|
@@ -274,7 +364,9 @@ export function NotificationTypesSetting<Id extends string = string>({
|
|
|
274
364
|
drawn.length === 0 ? (
|
|
275
365
|
<p className={CARD_DESCRIPTION_CLASS}>{labels.typesEmpty}</p>
|
|
276
366
|
) : (
|
|
277
|
-
|
|
367
|
+
<>
|
|
368
|
+
{columnHeadings}
|
|
369
|
+
{drawn.map(({ group, rows }) => {
|
|
278
370
|
const headingId = `${groupId}-${group.id}`;
|
|
279
371
|
return (
|
|
280
372
|
<div key={group.id} role="group" aria-labelledby={headingId} className="space-y-3">
|
|
@@ -284,7 +376,8 @@ export function NotificationTypesSetting<Id extends string = string>({
|
|
|
284
376
|
{rows.map(renderRow)}
|
|
285
377
|
</div>
|
|
286
378
|
);
|
|
287
|
-
})
|
|
379
|
+
})}
|
|
380
|
+
</>
|
|
288
381
|
);
|
|
289
382
|
} else {
|
|
290
383
|
const rows = rowsOf(types.map((entry) => entry.id));
|
|
@@ -292,7 +385,10 @@ export function NotificationTypesSetting<Id extends string = string>({
|
|
|
292
385
|
rows.length === 0 ? (
|
|
293
386
|
<p className={CARD_DESCRIPTION_CLASS}>{labels.typesEmpty}</p>
|
|
294
387
|
) : (
|
|
295
|
-
<div className="space-y-3">
|
|
388
|
+
<div className="space-y-3">
|
|
389
|
+
{columnHeadings}
|
|
390
|
+
{rows.map(renderRow)}
|
|
391
|
+
</div>
|
|
296
392
|
);
|
|
297
393
|
}
|
|
298
394
|
|
|
@@ -13,16 +13,23 @@ import type { ReactNode } from "react";
|
|
|
13
13
|
export type DigestCadence = "weekly" | "monthly";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* One type's
|
|
17
|
-
* that channel: `email` for a type with no mail, `app` in an app with no
|
|
18
|
-
* channel (Kurvenschmiede: `{app: null, email: true}`)
|
|
19
|
-
*
|
|
16
|
+
* One type's switches in the `GET` (§3.2; §10.2: one per channel). `null` where the type
|
|
17
|
+
* does not offer that channel: `email` for a type with no mail, `app` in an app with no
|
|
18
|
+
* "in the app" channel (Kurvenschmiede: `{app: null, push: null, email: true}`), `push` in
|
|
19
|
+
* an app with no push (server-kit `NotificationTypes(..., push_channel=False)`: kastlan,
|
|
20
|
+
* KS). An unset switch already reads as its default — the server fills it in.
|
|
20
21
|
*/
|
|
21
22
|
export interface NotificationSwitches {
|
|
22
23
|
app: boolean | null;
|
|
24
|
+
/** 0.46 (§10.2): push, independent of `app`. Left out by a server before
|
|
25
|
+
* server-kit 0.16, which reads as `null` (not offered). */
|
|
26
|
+
push?: boolean | null;
|
|
23
27
|
email: boolean | null;
|
|
24
28
|
}
|
|
25
29
|
|
|
30
|
+
/** A channel a type may offer (§10.2), in the order the types card draws them. */
|
|
31
|
+
export type NotificationChannel = "app" | "push" | "email";
|
|
32
|
+
|
|
26
33
|
/** The `GET`'s `types`: every type the account can receive, in the registry's order. */
|
|
27
34
|
export type NotificationTypesValue = Readonly<Record<string, NotificationSwitches>>;
|
|
28
35
|
|
|
@@ -51,16 +58,18 @@ export interface NotificationTypeGroup<Id extends string = string> {
|
|
|
51
58
|
|
|
52
59
|
/**
|
|
53
60
|
* The parts of `PATCH /notifications/settings` (§3.2) the cards send. Omitted keeps; a
|
|
54
|
-
* type's `null` puts
|
|
61
|
+
* type's `null` puts its switches back to their defaults; `quiet_hours_*: null`
|
|
55
62
|
* clears a time. One switch per call from the types card (`{types: {digest: {app:
|
|
56
|
-
* true}}}`), or `{digest_cadence}`; quiet hours send a time
|
|
57
|
-
*
|
|
63
|
+
* true}}}`), or `{digest_cadence}`; quiet hours send a time. No zone: it is the
|
|
64
|
+
* account's, automatic only (§10.3, amended 2026-10-20).
|
|
58
65
|
*/
|
|
59
66
|
export interface NotificationSettingsPatch<Id extends string = string> {
|
|
60
|
-
types?: Partial<Record<Id, { app?: boolean; email?: boolean } | null>>;
|
|
67
|
+
types?: Partial<Record<Id, { app?: boolean; push?: boolean; email?: boolean } | null>>;
|
|
61
68
|
digest_cadence?: DigestCadence;
|
|
62
69
|
quiet_hours_start?: string | null;
|
|
63
70
|
quiet_hours_end?: string | null;
|
|
71
|
+
/** @deprecated 0.46 — the zone is the account's, automatic only (§10.3), and server-kit 0.16 refuses it here;
|
|
72
|
+
* removed in 0.47. No kit card sends it. */
|
|
64
73
|
timezone?: string;
|
|
65
74
|
}
|
|
66
75
|
|
|
@@ -77,4 +86,7 @@ export interface PushDeviceItem<Id extends string | number = string> {
|
|
|
77
86
|
last_success_at: string | null;
|
|
78
87
|
/** Failed deliveries in a row; the server removes the device at 10. */
|
|
79
88
|
failure_count: number;
|
|
89
|
+
/** 0.46 (§10.2): the type ids this device mutes — a device can mute types, never add
|
|
90
|
+
* them. Left out by a server before server-kit 0.16 (none muted). */
|
|
91
|
+
muted_types?: string[];
|
|
80
92
|
}
|
|
@@ -30,8 +30,13 @@ export interface NotificationsLabels {
|
|
|
30
30
|
typesTitle: string;
|
|
31
31
|
/** The line under it. */
|
|
32
32
|
typesDescription: string;
|
|
33
|
-
/**
|
|
33
|
+
/** @deprecated 0.46 — the card's channels are columns now (`channelEmail`); removed in 0.47. Unused,
|
|
34
|
+
* kept so an app's override still type-checks. */
|
|
34
35
|
byEmail: string;
|
|
36
|
+
/** 0.46 (§10.2): the channel columns' headings, and each switch's name. */
|
|
37
|
+
channelApp: string;
|
|
38
|
+
channelPush: string;
|
|
39
|
+
channelEmail: string;
|
|
35
40
|
/** Under every e-mail switch while the account's address is unverified (decision 16):
|
|
36
41
|
* no notification mail goes to an unverified address. */
|
|
37
42
|
emailUnverified: string;
|
|
@@ -47,8 +52,12 @@ export interface NotificationsLabels {
|
|
|
47
52
|
/* ── Quiet hours ────────────────────────────────────────────────────── */
|
|
48
53
|
quietTitle: string;
|
|
49
54
|
/** The line under the title; `inbox`: the app keeps an inbox, which a held push still
|
|
50
|
-
* reaches.
|
|
51
|
-
|
|
55
|
+
* reaches; `held` (0.46, §10.4): the app queues a held push and sends it when the
|
|
56
|
+
* window ends, instead of dropping it. */
|
|
57
|
+
quietDescription: (inbox: boolean, held?: boolean) => string;
|
|
58
|
+
/** 0.46 (§10.3, automatic only): the card names the account's zone, read-only, given
|
|
59
|
+
* the zone as `zoneName` writes it ("Europe / Zurich"); it follows the device last used. */
|
|
60
|
+
quietZone: (zone: string) => string;
|
|
52
61
|
/** The start time's label. */
|
|
53
62
|
quietFrom: string;
|
|
54
63
|
/** The end time's label. */
|
|
@@ -57,12 +66,13 @@ export interface NotificationsLabels {
|
|
|
57
66
|
quietIncomplete: string;
|
|
58
67
|
/** The zone picker's label. */
|
|
59
68
|
timeZone: string;
|
|
60
|
-
/**
|
|
61
|
-
*
|
|
69
|
+
/** @deprecated 0.46 — the zone is automatic (§10.3), the card offers no device zone; removed in 0.47.
|
|
70
|
+
* Unused, kept so an app's override still type-checks. */
|
|
62
71
|
deviceZone: (zone: string) => string;
|
|
63
|
-
/**
|
|
72
|
+
/** @deprecated 0.46 — the zone is automatic (§10.3), nothing to take; removed in 0.47. Unused, kept
|
|
73
|
+
* so an app's override still type-checks. */
|
|
64
74
|
useDeviceZone: string;
|
|
65
|
-
/** The zone picker's
|
|
75
|
+
/** The zone's region headings, in the picker and in a zone's name ("Europe / Zurich"). */
|
|
66
76
|
regions: NotificationRegionLabels;
|
|
67
77
|
|
|
68
78
|
/* ── This device ────────────────────────────────────────────────────── */
|
|
@@ -123,6 +133,32 @@ export interface NotificationsLabels {
|
|
|
123
133
|
removeItem: (device: string) => string;
|
|
124
134
|
/** The question the row asks before it removes. */
|
|
125
135
|
removePrompt: string;
|
|
136
|
+
/** 0.46 (§10.2): the disclosure on a device's row listing the types with push on. */
|
|
137
|
+
deviceTypes: string;
|
|
138
|
+
/** The line inside it. */
|
|
139
|
+
deviceTypesDescription: string;
|
|
140
|
+
/** No type has push on for the account. */
|
|
141
|
+
deviceTypesEmpty: string;
|
|
142
|
+
|
|
143
|
+
/* ── The inbox and the bell (0.46, §10.1) ───────────────────────────── */
|
|
144
|
+
/** The inbox's heading (keksdose's "Inbox"). */
|
|
145
|
+
inboxTitle: string;
|
|
146
|
+
inboxLoading: string;
|
|
147
|
+
/** Nothing in the inbox. */
|
|
148
|
+
inboxEmpty: string;
|
|
149
|
+
markAllRead: string;
|
|
150
|
+
/** The header's clear button, and the confirm's answer. */
|
|
151
|
+
clearAll: string;
|
|
152
|
+
/** The question asked before the inbox is cleared. */
|
|
153
|
+
clearPrompt: string;
|
|
154
|
+
/** The row that loads the next page. */
|
|
155
|
+
showMore: string;
|
|
156
|
+
/** The bell's name. */
|
|
157
|
+
bellLabel: string;
|
|
158
|
+
/** Read after the bell's name, given the unread count (≥ 1). */
|
|
159
|
+
bellUnread: (count: number) => string;
|
|
160
|
+
/** The bell panel's link to the app's inbox. */
|
|
161
|
+
allNotifications: string;
|
|
126
162
|
|
|
127
163
|
/* ── The unsubscribe page ───────────────────────────────────────────── */
|
|
128
164
|
unsubscribeTitle: string;
|
|
@@ -150,6 +186,9 @@ export const DEFAULT_NOTIFICATIONS_LABELS: NotificationsLabels = {
|
|
|
150
186
|
typesTitle: "Notify me about",
|
|
151
187
|
typesDescription: "Choose what you hear about, and how.",
|
|
152
188
|
byEmail: "By e-mail",
|
|
189
|
+
channelApp: "In the app",
|
|
190
|
+
channelPush: "Push",
|
|
191
|
+
channelEmail: "E-mail",
|
|
153
192
|
emailUnverified: "Confirm your address to receive these.",
|
|
154
193
|
digestCadence: "Frequency",
|
|
155
194
|
digestWeekly: "Weekly",
|
|
@@ -158,10 +197,15 @@ export const DEFAULT_NOTIFICATIONS_LABELS: NotificationsLabels = {
|
|
|
158
197
|
typesEmpty: "There are no notifications for this account.",
|
|
159
198
|
|
|
160
199
|
quietTitle: "Quiet hours",
|
|
161
|
-
quietDescription: (inbox) =>
|
|
162
|
-
|
|
163
|
-
?
|
|
164
|
-
|
|
200
|
+
quietDescription: (inbox, held) =>
|
|
201
|
+
held
|
|
202
|
+
? inbox
|
|
203
|
+
? "Push notifications wait during this window and arrive when it ends. They reach your inbox at once."
|
|
204
|
+
: "Push notifications wait during this window and arrive when it ends."
|
|
205
|
+
: inbox
|
|
206
|
+
? "No push notifications during this window. They still reach your inbox."
|
|
207
|
+
: "No push notifications during this window.",
|
|
208
|
+
quietZone: (zone) => `In your time zone (${zone}), which follows the device you last used.`,
|
|
165
209
|
quietFrom: "From",
|
|
166
210
|
quietUntil: "Until",
|
|
167
211
|
quietIncomplete: "Set both a start and an end time. Quiet hours take effect once both are set.",
|
|
@@ -221,6 +265,20 @@ export const DEFAULT_NOTIFICATIONS_LABELS: NotificationsLabels = {
|
|
|
221
265
|
remove: "Remove",
|
|
222
266
|
removeItem: (device) => `Remove ${device}`,
|
|
223
267
|
removePrompt: "Stop sending notifications to this device?",
|
|
268
|
+
deviceTypes: "Types on this device",
|
|
269
|
+
deviceTypesDescription: "Turn a type off to mute it on this device only.",
|
|
270
|
+
deviceTypesEmpty: "No type has push notifications turned on.",
|
|
271
|
+
|
|
272
|
+
inboxTitle: "Inbox",
|
|
273
|
+
inboxLoading: "Loading notifications…",
|
|
274
|
+
inboxEmpty: "You’re all caught up.",
|
|
275
|
+
markAllRead: "Mark all as read",
|
|
276
|
+
clearAll: "Clear all",
|
|
277
|
+
clearPrompt: "Delete all notifications from your inbox?",
|
|
278
|
+
showMore: "Show more",
|
|
279
|
+
bellLabel: "Notifications",
|
|
280
|
+
bellUnread: (count) => `${count} unread`,
|
|
281
|
+
allNotifications: "All notifications",
|
|
224
282
|
|
|
225
283
|
unsubscribeTitle: "E-mail notifications",
|
|
226
284
|
unsubscribeAbout: (type) => `You get e-mails about: ${type}.`,
|