@eifi1/ui-kit 0.41.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/README.md +26 -7
  2. package/dist/admin/admin-area-labels.d.ts +59 -0
  3. package/dist/admin/admin-area-labels.js +35 -0
  4. package/dist/admin/admin-area-labels.js.map +1 -0
  5. package/dist/admin/admin-forbidden.d.ts +35 -0
  6. package/dist/admin/admin-forbidden.js +36 -0
  7. package/dist/admin/admin-forbidden.js.map +1 -0
  8. package/dist/admin/admin-overview.d.ts +110 -0
  9. package/dist/admin/admin-overview.js +92 -0
  10. package/dist/admin/admin-overview.js.map +1 -0
  11. package/dist/admin/admin-sections.d.ts +94 -0
  12. package/dist/admin/admin-sections.js +81 -0
  13. package/dist/admin/admin-sections.js.map +1 -0
  14. package/dist/admin/admin-tool-links.d.ts +48 -0
  15. package/dist/admin/admin-tool-links.js +31 -0
  16. package/dist/admin/admin-tool-links.js.map +1 -0
  17. package/dist/billing/billing-banner.d.ts +33 -2
  18. package/dist/billing/billing-banner.js +29 -3
  19. package/dist/billing/billing-banner.js.map +1 -1
  20. package/dist/billing/billing-labels.d.ts +36 -0
  21. package/dist/billing/billing-labels.js +10 -0
  22. package/dist/billing/billing-labels.js.map +1 -1
  23. package/dist/billing/billing-overview.d.ts +33 -0
  24. package/dist/billing/billing-overview.js +6 -0
  25. package/dist/billing/billing-overview.js.map +1 -1
  26. package/dist/billing/billing-standing.d.ts +8 -0
  27. package/dist/billing/billing-standing.js.map +1 -1
  28. package/dist/billing/cancel-request.d.ts +53 -0
  29. package/dist/billing/cancel-request.js +21 -0
  30. package/dist/billing/cancel-request.js.map +1 -0
  31. package/dist/billing/cancel-subscription.d.ts +46 -0
  32. package/dist/billing/cancel-subscription.js +57 -0
  33. package/dist/billing/cancel-subscription.js.map +1 -0
  34. package/dist/billing/plan-card.d.ts +45 -1
  35. package/dist/billing/plan-card.js +53 -2
  36. package/dist/billing/plan-card.js.map +1 -1
  37. package/dist/billing/plan-switch.d.ts +10 -1
  38. package/dist/billing/plan-switch.js +22 -13
  39. package/dist/billing/plan-switch.js.map +1 -1
  40. package/dist/billing/provider-page.d.ts +40 -0
  41. package/dist/billing/provider-page.js +33 -0
  42. package/dist/billing/provider-page.js.map +1 -0
  43. package/dist/billing/subscription-actions.d.ts +68 -4
  44. package/dist/billing/subscription-actions.js +65 -5
  45. package/dist/billing/subscription-actions.js.map +1 -1
  46. package/dist/components/account-chips.d.ts +50 -2
  47. package/dist/components/account-chips.js +33 -0
  48. package/dist/components/account-chips.js.map +1 -1
  49. package/dist/components/empty-state.d.ts +2 -1
  50. package/dist/components/empty-state.js.map +1 -1
  51. package/dist/eslint/index.js +1 -1
  52. package/dist/feedback/feedback-menu.d.ts +7 -4
  53. package/dist/feedback/feedback-menu.js +4 -2
  54. package/dist/feedback/feedback-menu.js.map +1 -1
  55. package/dist/hooks/use-search-param-state.js +4 -1
  56. package/dist/hooks/use-search-param-state.js.map +1 -1
  57. package/dist/i18n/defaults.js +7 -1
  58. package/dist/i18n/defaults.js.map +1 -1
  59. package/dist/i18n/german.js +113 -0
  60. package/dist/i18n/german.js.map +1 -1
  61. package/dist/i18n/kit-labels.d.ts +12 -0
  62. package/dist/i18n/kit-labels.js.map +1 -1
  63. package/dist/i18n/locales/en.js +142 -1
  64. package/dist/i18n/locales/en.js.map +3 -3
  65. package/dist/i18n/locales/es.js +110 -0
  66. package/dist/i18n/locales/es.js.map +1 -1
  67. package/dist/i18n/locales/fr.js +109 -0
  68. package/dist/i18n/locales/fr.js.map +1 -1
  69. package/dist/i18n/locales/hu.js +110 -0
  70. package/dist/i18n/locales/hu.js.map +1 -1
  71. package/dist/i18n/locales/it.js +110 -0
  72. package/dist/i18n/locales/it.js.map +1 -1
  73. package/dist/i18n/locales/zh.js +109 -0
  74. package/dist/i18n/locales/zh.js.map +1 -1
  75. package/dist/i18n/review.js +34 -1
  76. package/dist/i18n/review.js.map +1 -1
  77. package/dist/index.d.ts +24 -2
  78. package/dist/index.js +29 -1
  79. package/dist/index.js.map +1 -1
  80. package/dist/search/global-search.d.ts +3 -0
  81. package/dist/search/global-search.js +3 -1
  82. package/dist/search/global-search.js.map +1 -1
  83. package/dist/settings/settings-catalogue.d.ts +9 -1
  84. package/dist/settings/settings-catalogue.js +1 -1
  85. package/dist/settings/settings-catalogue.js.map +1 -1
  86. package/dist/settings/settings-layout.d.ts +7 -1
  87. package/dist/settings/settings-layout.js +35 -27
  88. package/dist/settings/settings-layout.js.map +1 -1
  89. package/dist/shell/account-menu-language.d.ts +46 -0
  90. package/dist/shell/account-menu-language.js +64 -0
  91. package/dist/shell/account-menu-language.js.map +1 -0
  92. package/dist/shell/admin-menu.d.ts +69 -0
  93. package/dist/shell/admin-menu.js +64 -0
  94. package/dist/shell/admin-menu.js.map +1 -0
  95. package/dist/shell/app-shell.d.ts +10 -0
  96. package/dist/shell/app-shell.js +22 -4
  97. package/dist/shell/app-shell.js.map +1 -1
  98. package/dist/shell/dev-switchers.d.ts +53 -0
  99. package/dist/shell/dev-switchers.js +107 -0
  100. package/dist/shell/dev-switchers.js.map +1 -0
  101. package/dist/shell/sync-status-indicator.d.ts +86 -0
  102. package/dist/shell/sync-status-indicator.js +170 -0
  103. package/dist/shell/sync-status-indicator.js.map +1 -0
  104. package/dist/shell/top-bar-actions.d.ts +49 -0
  105. package/dist/shell/top-bar-actions.js +31 -0
  106. package/dist/shell/top-bar-actions.js.map +1 -0
  107. package/dist/shell/top-bar-parts-labels.d.ts +90 -0
  108. package/dist/shell/top-bar-parts-labels.js +51 -0
  109. package/dist/shell/top-bar-parts-labels.js.map +1 -0
  110. package/dist/shell/topbar-action-menu.d.ts +13 -1
  111. package/dist/shell/topbar-action-menu.js +5 -2
  112. package/dist/shell/topbar-action-menu.js.map +1 -1
  113. package/dist/shell/topbar-controls.d.ts +16 -3
  114. package/dist/shell/topbar-controls.js +5 -1
  115. package/dist/shell/topbar-controls.js.map +1 -1
  116. package/dist/shell/tour-menu.d.ts +67 -0
  117. package/dist/shell/tour-menu.js +87 -0
  118. package/dist/shell/tour-menu.js.map +1 -0
  119. package/dist/shell.d.ts +16 -0
  120. package/dist/shell.js +21 -0
  121. package/dist/shell.js.map +1 -1
  122. package/dist/tour/tour-catalogue-labels.d.ts +44 -0
  123. package/dist/tour/tour-catalogue-labels.js +32 -0
  124. package/dist/tour/tour-catalogue-labels.js.map +1 -0
  125. package/dist/tour/tour-catalogue.d.ts +68 -0
  126. package/dist/tour/tour-catalogue.js +189 -0
  127. package/dist/tour/tour-catalogue.js.map +1 -0
  128. package/dist/tour/tour-info.d.ts +112 -0
  129. package/dist/tour/tour-info.js +36 -0
  130. package/dist/tour/tour-info.js.map +1 -0
  131. package/dist/tour/use-tour-progress.d.ts +61 -0
  132. package/dist/tour/use-tour-progress.js +116 -0
  133. package/dist/tour/use-tour-progress.js.map +1 -0
  134. package/dist/tour/use-tour-url-sync.d.ts +58 -0
  135. package/dist/tour/use-tour-url-sync.js +110 -0
  136. package/dist/tour/use-tour-url-sync.js.map +1 -0
  137. package/dist/tour.d.ts +8 -0
  138. package/dist/tour.js +18 -0
  139. package/dist/tour.js.map +1 -1
  140. package/package.json +1 -1
  141. package/src/admin/admin-area-labels.ts +91 -0
  142. package/src/admin/admin-forbidden.tsx +75 -0
  143. package/src/admin/admin-overview.tsx +217 -0
  144. package/src/admin/admin-sections.tsx +198 -0
  145. package/src/admin/admin-tool-links.tsx +84 -0
  146. package/src/billing/billing-banner.tsx +57 -6
  147. package/src/billing/billing-labels.ts +59 -0
  148. package/src/billing/billing-overview.ts +37 -0
  149. package/src/billing/billing-standing.ts +8 -0
  150. package/src/billing/cancel-request.ts +75 -0
  151. package/src/billing/cancel-subscription.tsx +102 -0
  152. package/src/billing/plan-card.tsx +97 -1
  153. package/src/billing/plan-switch.tsx +16 -0
  154. package/src/billing/provider-page.ts +75 -0
  155. package/src/billing/subscription-actions.tsx +155 -20
  156. package/src/components/account-chips.tsx +89 -2
  157. package/src/components/empty-state.tsx +2 -1
  158. package/src/feedback/feedback-menu.tsx +8 -3
  159. package/src/hooks/use-search-param-state.ts +6 -1
  160. package/src/i18n/defaults.ts +6 -0
  161. package/src/i18n/german.ts +127 -0
  162. package/src/i18n/kit-labels.tsx +12 -0
  163. package/src/i18n/locales/en.ts +32 -0
  164. package/src/i18n/locales/es.ts +121 -0
  165. package/src/i18n/locales/fr.ts +121 -0
  166. package/src/i18n/locales/hu.ts +119 -0
  167. package/src/i18n/locales/it.ts +122 -0
  168. package/src/i18n/locales/zh.ts +114 -0
  169. package/src/i18n/review.ts +33 -0
  170. package/src/index.ts +38 -2
  171. package/src/search/global-search.tsx +5 -0
  172. package/src/settings/settings-catalogue.ts +10 -2
  173. package/src/settings/settings-layout.tsx +58 -34
  174. package/src/shell/account-menu-language.tsx +116 -0
  175. package/src/shell/admin-menu.tsx +146 -0
  176. package/src/shell/app-shell.tsx +44 -3
  177. package/src/shell/dev-switchers.tsx +179 -0
  178. package/src/shell/sync-status-indicator.tsx +301 -0
  179. package/src/shell/top-bar-actions.tsx +79 -0
  180. package/src/shell/top-bar-parts-labels.ts +142 -0
  181. package/src/shell/topbar-action-menu.tsx +15 -2
  182. package/src/shell/topbar-controls.tsx +20 -3
  183. package/src/shell/tour-menu.tsx +179 -0
  184. package/src/shell.ts +27 -0
  185. package/src/tour/tour-catalogue-labels.ts +75 -0
  186. package/src/tour/tour-catalogue.tsx +316 -0
  187. package/src/tour/tour-info.ts +144 -0
  188. package/src/tour/use-tour-progress.ts +198 -0
  189. package/src/tour/use-tour-url-sync.ts +221 -0
  190. package/src/tour.ts +11 -0
@@ -1,11 +1,30 @@
1
- import type { ComponentPropsWithRef } from "react";
1
+ import type { ComponentPropsWithRef, ReactNode } from "react";
2
+ import { ExternalLink } from "lucide-react";
2
3
 
4
+ import { DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels";
3
5
  import type { LabelOverride } from "../i18n/kit-labels";
4
6
  import { cn } from "../lib/cn";
5
7
  import { Button } from "../components/button";
6
8
  import { useBillingLabels } from "./billing-labels";
7
9
  import type { BillingLabels } from "./billing-labels";
8
- import type { PortalTarget } from "./billing-overview";
10
+ import type { BillingPortalAccess, PortalTarget } from "./billing-overview";
11
+ import type { CancelRequested } from "./cancel-request";
12
+
13
+ /** The portal action's mark (§17.3): the external-link glyph after the words, and "(opens
14
+ * in a new tab)" for a reader — on a link, `Button`'s `external` says the latter. */
15
+ export function ExternalMark({ reader }: { reader?: string }) {
16
+ return (
17
+ <>
18
+ <ExternalLink aria-hidden data-external="" className="size-4 shrink-0 rtl:-scale-x-100" />
19
+ {reader !== undefined && (
20
+ <>
21
+ {" "}
22
+ <span className="sr-only">({reader})</span>
23
+ </>
24
+ )}
25
+ </>
26
+ );
27
+ }
9
28
 
10
29
  export interface SubscriptionActionsProps extends Omit<ComponentPropsWithRef<"div">, "children"> {
11
30
  /** "Payment and invoices", as a link — or {@link onManage}. Without either, none. */
@@ -35,8 +54,54 @@ export interface SubscriptionActionsProps extends Omit<ComponentPropsWithRef<"di
35
54
  * (§12.26). Default true (0.32's behaviour).
36
55
  */
37
56
  atProvider?: boolean;
38
- /** Which of the two is under way: its spinner, no second press. */
39
- pending?: "manage" | "cancel" | null;
57
+ /**
58
+ * 0.42 (§16.2, server-kit 0.14): the overview's `portal`. `"buyer_only"` — the row has
59
+ * a buyer and the viewer isn't them, so the portal would answer `403
60
+ * billing_portal_buyer_only`: "Payment and invoices" becomes a caption naming the buyer
61
+ * (`buyerHolds`, no button), and "Cancel subscription" calls {@link onCancelInApp}
62
+ * (no portal at all — `cancelHref`, `onCancel` and `onPortal` are not used). `"open"`
63
+ * (default): as before.
64
+ */
65
+ portal?: BillingPortalAccess;
66
+ /** 0.42: the overview's `buyer_name`, for the caption under `"buyer_only"`. Empty: "…with
67
+ * the admin who bought the subscription". */
68
+ buyerName?: string | null;
69
+ /**
70
+ * 0.42 (§16.2 decision 2): "Cancel subscription" in the app — open
71
+ * `CancelSubscriptionConfirm`, whose Confirm is `POST /billing/cancel`. The cancel under
72
+ * `"buyer_only"`. Under `"open"` it is the last resort, used only when no portal cancel
73
+ * (`cancelHref`, `onCancel`, `onPortal`) is given — a person-payer app that mounts the
74
+ * route and not the portal's cancel.
75
+ */
76
+ onCancelInApp?: () => void;
77
+ /** 0.42 (§16.7 C): the overview's `cancel_at_period_end` — a cancel is scheduled. With
78
+ * {@link onResume}, "Keep subscription" takes the cancel's place. */
79
+ cancelAtPeriodEnd?: boolean;
80
+ /** 0.42 (§16.7 C): "Keep subscription", the app's `POST /billing/resume` — shown in
81
+ * the cancel's place while {@link cancelAtPeriodEnd} is true. Without it, the cancel
82
+ * stays as before. */
83
+ onResume?: () => void;
84
+ /**
85
+ * 0.43 (kastlan `18bf4c0`): what was asked in the app and the overview does not show
86
+ * yet — `useCancelRequest().view(row).requested`. `"cancel"`: shown as scheduled ("Keep
87
+ * subscription" with {@link onResume}; without it, no second "Cancel subscription").
88
+ * `"resume"`: shown as not scheduled. `null` (default): {@link cancelAtPeriodEnd} says.
89
+ */
90
+ cancelRequested?: CancelRequested;
91
+ /**
92
+ * 0.43 (§17): the portal opens in a NEW TAB — the app's handler calls
93
+ * `openProviderPage` — so its actions carry the external-link mark, a link opens with
94
+ * `target="_blank"`, and the `portalOpensNewTab` note shows under them. Default true;
95
+ * `false` for an app whose portal still opens in the same tab.
96
+ */
97
+ portalNewTab?: boolean;
98
+ /** 0.43: the provider's name in the note. Default `"Paddle"` (§2 decision 16). */
99
+ providerName?: string;
100
+ /** 0.43: the app's name in the note ("Kastlan stays open here"). Without it, "this page
101
+ * stays open". */
102
+ appName?: string;
103
+ /** Which one is under way: its spinner, no second press. `"resume"` since 0.42. */
104
+ pending?: "manage" | "cancel" | "resume" | null;
40
105
  /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */
41
106
  labels?: LabelOverride<BillingLabels>;
42
107
  }
@@ -56,6 +121,18 @@ export interface SubscriptionActionsProps extends Omit<ComponentPropsWithRef<"di
56
121
  * Before the payer has reached the provider (`atProvider={false}`, a trial without a
57
122
  * card, a beta) there is no portal to open: the part says so in a caption instead of
58
123
  * offering two buttons that could only end in `409 billing_not_at_provider`.
124
+ *
125
+ * 0.42 (§16): where the company's buyer is another admin (`portal="buyer_only"`), the
126
+ * portal stays the buyer's — "Payment and invoices" is a caption that names them, and
127
+ * "Cancel subscription" is the in-app cancel (`onCancelInApp`). While a cancel is
128
+ * scheduled (`cancelAtPeriodEnd`) and `onResume` is given, "Keep subscription" stands in
129
+ * the cancel's place, whoever the viewer is.
130
+ *
131
+ * 0.43 (§17): the portal opens in a new tab (`openProviderPage` in the app's handler):
132
+ * its actions carry the external-link mark, and a muted note says so — "Payment and
133
+ * invoices open at {provider} in a new tab; {app} stays open here." The in-app cancel and
134
+ * "Keep subscription" stay unmarked. After an in-app cancel or resume, `cancelRequested`
135
+ * (`useCancelRequest`) shows what was asked until the provider's webhook lands.
59
136
  */
60
137
  export function SubscriptionActions({
61
138
  manageHref,
@@ -64,12 +141,22 @@ export function SubscriptionActions({
64
141
  onCancel,
65
142
  onPortal,
66
143
  atProvider = true,
144
+ portal = "open",
145
+ buyerName,
146
+ onCancelInApp,
147
+ cancelAtPeriodEnd = false,
148
+ onResume,
149
+ cancelRequested = null,
150
+ portalNewTab = true,
151
+ providerName = "Paddle",
152
+ appName,
67
153
  pending,
68
154
  labels: labelsProp,
69
155
  className,
70
156
  ...rest
71
157
  }: SubscriptionActionsProps) {
72
158
  const labels = useBillingLabels(labelsProp);
159
+ const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
73
160
  if (!atProvider) {
74
161
  return (
75
162
  <div {...rest} data-at-provider="false" className={cn("text-sm text-muted", className)}>
@@ -77,33 +164,81 @@ export function SubscriptionActions({
77
164
  </div>
78
165
  );
79
166
  }
167
+ const buyerOnly = portal === "buyer_only";
80
168
  const manageCall = onManage ?? (onPortal ? () => onPortal("overview") : undefined);
81
- const cancelCall = onCancel ?? (onPortal ? () => onPortal("cancel") : undefined);
82
- const manage =
83
- manageHref !== undefined ? (
84
- <Button href={manageHref} variant="secondary">
85
- {labels.manage}
86
- </Button>
87
- ) : manageCall !== undefined ? (
88
- <Button type="button" variant="secondary" onClick={manageCall} pending={pending === "manage"}>
89
- {labels.manage}
90
- </Button>
91
- ) : null;
169
+ const portalCancel = onCancel ?? (onPortal ? () => onPortal("cancel") : undefined);
170
+ // Under "buyer_only" the in-app cancel only; under "open" the portal's first, the
171
+ // in-app cancel only where no way to the portal's is given.
172
+ const cancelCall = buyerOnly ? onCancelInApp : (portalCancel ?? (cancelHref === undefined ? onCancelInApp : undefined));
173
+ // 0.43: what was asked in the app wins over the row until the row moves.
174
+ const scheduled = cancelRequested === "cancel" ? true : cancelRequested === "resume" ? false : cancelAtPeriodEnd;
175
+ // 0.43 (§17): the portal's actions are marked, a link opens a tab of its own.
176
+ const newTab = portalNewTab && !buyerOnly;
177
+ const words = (text: string, external: boolean, link: boolean): ReactNode =>
178
+ external ? (
179
+ <>
180
+ {text}
181
+ <ExternalMark reader={link ? undefined : common.opensInNewTab} />
182
+ </>
183
+ ) : (
184
+ text
185
+ );
186
+ const portalCancelShown = !buyerOnly && (cancelHref !== undefined || portalCancel !== undefined);
187
+ const manage = buyerOnly ? (
188
+ <p className="basis-full text-sm text-muted" data-buyer-holds="">
189
+ {labels.buyerHolds(buyerName || undefined)}
190
+ </p>
191
+ ) : manageHref !== undefined ? (
192
+ <Button href={manageHref} variant="secondary" external={newTab || undefined} data-portal-action="manage">
193
+ {words(labels.manage, newTab, true)}
194
+ </Button>
195
+ ) : manageCall !== undefined ? (
196
+ <Button
197
+ type="button"
198
+ variant="secondary"
199
+ onClick={manageCall}
200
+ pending={pending === "manage"}
201
+ className={newTab ? "relative" : undefined}
202
+ data-portal-action="manage"
203
+ >
204
+ {words(labels.manage, newTab, false)}
205
+ </Button>
206
+ ) : null;
92
207
  const cancel =
93
- cancelHref !== undefined ? (
94
- <Button href={cancelHref} variant="link" tone="danger">
95
- {labels.cancel}
208
+ scheduled && onResume !== undefined ? (
209
+ <Button type="button" variant="link" onClick={onResume} pending={pending === "resume"} data-keep-subscription="">
210
+ {labels.keepSubscription}
211
+ </Button>
212
+ ) : cancelRequested === "cancel" ? null : cancelHref !== undefined && !buyerOnly ? (
213
+ <Button href={cancelHref} variant="link" tone="danger" external={newTab || undefined} data-portal-action="cancel">
214
+ {words(labels.cancel, newTab, true)}
96
215
  </Button>
97
216
  ) : cancelCall !== undefined ? (
98
- <Button type="button" variant="link" tone="danger" onClick={cancelCall} pending={pending === "cancel"}>
99
- {labels.cancel}
217
+ <Button
218
+ type="button"
219
+ variant="link"
220
+ tone="danger"
221
+ onClick={cancelCall}
222
+ pending={pending === "cancel"}
223
+ className={newTab && portalCancelShown ? "relative" : undefined}
224
+ data-portal-action={portalCancelShown ? "cancel" : undefined}
225
+ >
226
+ {words(labels.cancel, newTab && portalCancelShown, false)}
100
227
  </Button>
101
228
  ) : null;
102
229
  if (!manage && !cancel) return null;
230
+ // The note while a way to the portal shows: "Payment and invoices open at Paddle in a
231
+ // new tab; Kastlan stays open here."
232
+ const portalShown = !buyerOnly && (manage !== null || (cancel !== null && portalCancelShown && !(scheduled && onResume)));
103
233
  return (
104
234
  <div {...rest} className={cn("flex flex-wrap items-center gap-x-4 gap-y-2", className)}>
105
235
  {manage}
106
236
  {cancel}
237
+ {newTab && portalShown && (
238
+ <p className="basis-full text-sm text-muted" data-portal-note="">
239
+ {labels.portalOpensNewTab(providerName, appName || undefined)}
240
+ </p>
241
+ )}
107
242
  </div>
108
243
  );
109
244
  }
@@ -6,7 +6,7 @@ import type { DateInput, FormatDateStyle, FormatRelativeTimeOptions } from "../l
6
6
  import { useKitDateFormatter, useKitLabels, useKitLocale, type LabelOverride } from "../i18n/kit-labels";
7
7
  import { toLocalIso } from "../lib/dates";
8
8
  import { Chip } from "./chip";
9
- import type { ChipShape, ChipSize, ChipTone, ChipVariant } from "./chip";
9
+ import type { ChipLinkProps, ChipShape, ChipSize, ChipTone, ChipVariant } from "./chip";
10
10
  import type { DataTableColumn } from "./data-table";
11
11
  import { DEFAULT_ACCOUNT_STATE_LABELS } from "./account-chips-labels";
12
12
 
@@ -47,8 +47,29 @@ interface AccountChipLook {
47
47
  export interface RoleDefinition {
48
48
  label: string;
49
49
  tone?: ChipTone;
50
- /** A lucide component or an element — Kurvenschmiede marks its admins with a shield. */
50
+ /**
51
+ * A lucide component or an element — Kurvenschmiede marks its admins with a shield.
52
+ * Unlike {@link href} and {@link variant}, it shows EVERYWHERE the role is drawn: in
53
+ * {@link AccountRoleChip} and in {@link RoleChip} (the roster's column) alike. That is
54
+ * intended (0.43, keksdose's question): the glyph is part of how the role reads, so the
55
+ * shield on the header's chip is the shield in the users list. An app that wants the
56
+ * glyph in one place only hands the other a second vocabulary without `icon`.
57
+ */
51
58
  icon?: LucideIcon | ReactElement;
59
+ /**
60
+ * 0.42.0 (docs/admin-harmonization.md §2.1, settings §12.3): the role's tool — an
61
+ * admin's `/admin`, a reviewer's `/translations`. {@link AccountRoleChip} is a link to
62
+ * it; a role without one gets a plain chip. {@link RoleChip} ignores it: a roster's
63
+ * chips name a role, they are not a way in.
64
+ */
65
+ href?: string;
66
+ /**
67
+ * 0.42.0 (keksdose build review): this role's look in {@link AccountRoleChip} — keksdose
68
+ * draws only ADMIN `solid` and its REVIEWER and MEMBER `soft`. Default `solid`, the
69
+ * header's look; the chip's own `variant` prop wins over it. {@link RoleChip} ignores
70
+ * it: a roster's column keeps one look, set on the column.
71
+ */
72
+ variant?: ChipVariant;
52
73
  }
53
74
 
54
75
  /**
@@ -107,6 +128,72 @@ export function RoleChip<R extends string = string>({
107
128
  );
108
129
  }
109
130
 
131
+ /* ── AccountRoleChip ───────────────────────────────────────────────────────── */
132
+
133
+ export interface AccountRoleChipProps<R extends string = string> extends RoleChipProps<R> {
134
+ /**
135
+ * Runs on the way to the role's tool — the account menu's `close`, so the panel does
136
+ * not stay open over the page it just navigated to (keksdose F7). Only called on a
137
+ * link chip.
138
+ */
139
+ onNavigate?: () => void;
140
+ /** Your router's link for the role's `href`. Default: the `<UiKitProvider
141
+ * linkComponent>`, then `<a>` — {@link Chip}'s `renderLink`. */
142
+ renderLink?: (props: ChipLinkProps) => ReactElement;
143
+ }
144
+
145
+ /**
146
+ * The role in the account menu's header (0.42.0, docs/admin-harmonization.md §2.1, §7):
147
+ * {@link RoleChip} in keksdose's look — `solid`, `sm`, `square`, `caps`, the role's icon
148
+ * and tone — made for `TopBarActionMenu`'s `header.extra`:
149
+ *
150
+ * extra: (close) => <AccountRoleChip value={me.role} roles={ROLES} onNavigate={close} />
151
+ *
152
+ * A role whose {@link RoleDefinition.href} is set is a shortcut to its tool: the chip is a
153
+ * link (through `renderLink`, else the provider's link, as {@link Chip} draws links) and
154
+ * calls `onNavigate` on the click. Any other role is a plain chip — every account shows
155
+ * its role (§7, "Always a badge"). The words and tones are the app's vocabulary; the kit
156
+ * adds none. `solid` stays the default, so a role the app gives no look still reads as
157
+ * the header's tag; a role that should sit quieter (keksdose's MEMBER and REVIEWER) sets
158
+ * {@link RoleDefinition.variant} `soft` in the vocabulary. Carries `data-role` with the key.
159
+ */
160
+ export function AccountRoleChip<R extends string = string>({
161
+ value: role,
162
+ roles,
163
+ onNavigate,
164
+ renderLink,
165
+ size = "sm",
166
+ variant,
167
+ shape = "square",
168
+ caps = true,
169
+ className,
170
+ ...rest
171
+ }: AccountRoleChipProps<R>) {
172
+ if (role === null || role === undefined || role === "") return null;
173
+ const def = roles[role];
174
+ const look = {
175
+ ...rest,
176
+ "data-role": role,
177
+ tone: def?.tone ?? "neutral",
178
+ icon: def?.icon,
179
+ size,
180
+ // The prop, then the role's own look, then the header's `solid`.
181
+ variant: variant ?? def?.variant ?? "solid",
182
+ shape,
183
+ caps,
184
+ className,
185
+ } as const;
186
+ const label = def?.label ?? role;
187
+ if (def?.href) {
188
+ return (
189
+ <Chip {...look} href={def.href} renderLink={renderLink} onClick={onNavigate ? () => onNavigate() : undefined}>
190
+ {label}
191
+ </Chip>
192
+ );
193
+ }
194
+ return <Chip {...look}>{label}</Chip>;
195
+ }
196
+
110
197
  /* ── AccountStateChip ──────────────────────────────────────────────────────── */
111
198
 
112
199
  /**
@@ -17,8 +17,9 @@ export interface EmptyStateProps extends Omit<ComponentPropsWithRef<"div">, "chi
17
17
  * keksdose's error boundary IS the page when it trips, and its `<h2>` went missing
18
18
  * when it moved onto EmptyState — so the page's heading outline lost the one line
19
19
  * that says what happened. Only the element changes; the look is the box's.
20
+ * `h1` (0.42.0) for an empty state that is the whole page — `AdminForbidden`.
20
21
  */
21
- headingAs?: "h2" | "h3" | "h4" | "h5" | "h6";
22
+ headingAs?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
22
23
  /** A glyph ABOVE the title — kastlan's InboxEmptyState (an inbox), keksdose's
23
24
  * offline card (a cloud with a slash). Sized by the box (`[&_svg]:size-8`) and
24
25
  * muted, so five call sites cannot pick five sizes; hidden from assistive tech,
@@ -71,6 +71,9 @@ export interface FeedbackMenuProps {
71
71
  /** Which edge of the trigger the panel lines up with, `start` or `end` in the reading
72
72
  * direction (see `HoverMenu`, whose type this is). Default `end`. */
73
73
  align?: HoverMenuProps["align"];
74
+ /** 0.43: `data-*` attributes go on the trigger — a tour's anchor
75
+ * (`data-tour="feedback-menu"`), a test hook. */
76
+ [key: `data-${string}`]: string | number | boolean | undefined;
74
77
  }
75
78
 
76
79
  /**
@@ -101,9 +104,9 @@ export interface FeedbackMenuProps {
101
104
  * to the top like one with none of what makes it actionable. The server refuses it with a
102
105
  * 422 anyway (§3.4).
103
106
  *
104
- * The menu forwards nothing to its trigger, as `TopBarActionMenu` does not: keksdose's
105
- * tour anchor (`data-tour="feedback-menu"`) stays on a wrapping `<span>`. Needs a router,
106
- * as every `TopBarActionMenu` with link rows does.
107
+ * Since 0.43 its `data-*` attributes go on the trigger (keksdose's tour anchor
108
+ * `data-tour="feedback-menu"` needs no wrapping `<span>` any more). Needs a router, as
109
+ * every `TopBarActionMenu` with link rows does.
107
110
  */
108
111
  export function FeedbackMenu({
109
112
  onFile,
@@ -115,6 +118,7 @@ export function FeedbackMenu({
115
118
  iconBadge,
116
119
  labels: labelsProp,
117
120
  align,
121
+ ...attributes
118
122
  }: FeedbackMenuProps) {
119
123
  const labels = useKitLabels("feedbackMenu", DEFAULT_FEEDBACK_MENU_LABELS, labelsProp);
120
124
  const categoryLabels = useFeedbackCategoryLabels();
@@ -167,6 +171,7 @@ export function FeedbackMenu({
167
171
  aria-label={labels.trigger}
168
172
  entries={entries}
169
173
  align={align}
174
+ triggerAttributes={Object.fromEntries(Object.entries(attributes).filter(([k]) => k.startsWith("data-")))}
170
175
  />
171
176
  );
172
177
  }
@@ -208,7 +208,12 @@ function writeQuery(
208
208
  }
209
209
  }
210
210
  queued.lastPushed = push;
211
- void navigate(`?${search}`, push ? undefined : { replace: true, state: carriedState(queued.state) });
211
+ // The hash is spelled out: `navigate("?…")` drops it (keksdose dev#495; build review
212
+ // 0.43, keksdose N2 — the tours page's filters).
213
+ void navigate(
214
+ { search: `?${search}`, hash: location.hash },
215
+ push ? undefined : { replace: true, state: carriedState(queued.state) },
216
+ );
212
217
  }
213
218
 
214
219
  /** The rendered query, and a write that composes with every other one on the router. */
@@ -13,6 +13,7 @@ import {
13
13
  } from "../components/ui-labels";
14
14
  import { DEFAULT_WIZARD_LABELS } from "../wizard/wizard-labels";
15
15
  import { DEFAULT_TOUR_LABELS } from "../tour/tour-labels";
16
+ import { DEFAULT_TOUR_CATALOGUE_LABELS } from "../tour/tour-catalogue-labels";
16
17
  import { DEFAULT_COMMAND_PALETTE_LABELS } from "../search/command-palette-labels";
17
18
  import { DEFAULT_GLOBAL_SEARCH_LABELS } from "../search/global-search-labels";
18
19
  import { DEFAULT_SERIES_CHART_LABELS } from "../components/series-chart-labels";
@@ -98,6 +99,8 @@ import { DEFAULT_DEVICE_RESET_LABELS } from "../settings/device-reset-setting-la
98
99
  import { DEFAULT_ROW_ACTIONS_LABELS } from "../components/row-actions-labels";
99
100
  import { DEFAULT_APP_SHELL_MORE_LABELS } from "../shell/app-shell-labels";
100
101
  import { DEFAULT_ACCOUNT_STATE_LABELS } from "../components/account-chips-labels";
102
+ import { DEFAULT_ADMIN_AREA_LABELS } from "../admin/admin-area-labels";
103
+ import { DEFAULT_TOP_BAR_PARTS_LABELS } from "../shell/top-bar-parts-labels";
101
104
  import { DEFAULT_SHARE_CARD_LABELS } from "../components/share-card-labels";
102
105
  import { DEFAULT_REAUTH_DIALOG_LABELS } from "../components/reauth-dialog-labels";
103
106
  import { DEFAULT_SERVER_WAKE_LABELS } from "../components/server-wake-labels";
@@ -165,6 +168,7 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
165
168
  file: DEFAULT_FILE_LABELS,
166
169
  wizard: DEFAULT_WIZARD_LABELS,
167
170
  tour: DEFAULT_TOUR_LABELS,
171
+ tourCatalogue: DEFAULT_TOUR_CATALOGUE_LABELS,
168
172
  commandPalette: DEFAULT_COMMAND_PALETTE_LABELS,
169
173
  globalSearch: DEFAULT_GLOBAL_SEARCH_LABELS,
170
174
  seriesChart: DEFAULT_SERIES_CHART_LABELS,
@@ -256,4 +260,6 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
256
260
  signChip: DEFAULT_SIGN_CHIP_LABELS,
257
261
  columnMapper: DEFAULT_COLUMN_MAPPER_LABELS,
258
262
  notifications: DEFAULT_NOTIFICATIONS_LABELS,
263
+ adminArea: DEFAULT_ADMIN_AREA_LABELS,
264
+ topBarParts: DEFAULT_TOP_BAR_PARTS_LABELS,
259
265
  };
@@ -626,6 +626,34 @@ export function germanLabels(numberLocale: string): UiKitLabels {
626
626
  awaitClickHint: "Klicken Sie auf das hervorgehobene Element, um fortzufahren",
627
627
  step: (current, total) => `${n(current)} / ${n(total)}`,
628
628
  },
629
+ // 0.43.0 — docs/tours-harmonization.md §3, §8: the tours page. keksdose's reviewed words.
630
+ tourCatalogue: {
631
+ searchLabel: "Touren durchsuchen",
632
+ searchPlaceholder: "Touren durchsuchen…",
633
+ groupLabel: "Thema",
634
+ groupAll: (count) => `Alle Themen (${n(count)})`,
635
+ groupOption: (group, count) => `${group} (${n(count)})`,
636
+ stateLabel: "Touren filtern",
637
+ stateAll: (count) => `Alle (${n(count)})`,
638
+ stateTodo: (count) => `Offen (${n(count)})`,
639
+ stateDone: (count) => `Abgeschlossen (${n(count)})`,
640
+ showing: (shown, total) => `${n(shown)} von ${n(total)} Touren`,
641
+ noMatches: "Keine Tour passt",
642
+ noMatchesHint: "Versuchen Sie ein anderes Wort, oder setzen Sie die Filter zurück.",
643
+ clearFilters: "Filter zurücksetzen",
644
+ badgeNew: "Neu",
645
+ badgeUpdated: "Aktualisiert",
646
+ resetOn: (date, reason) => (reason ? `Zurückgesetzt am ${date}: ${reason}` : `Zurückgesetzt am ${date}`),
647
+ points: (count) => `+${n(count)} Pkt.`,
648
+ steps: (count) => (count === 1 ? `${n(count)} Schritt` : `${n(count)} Schritte`),
649
+ duration: (minutes) => `Etwa ${n(minutes)} Min.`,
650
+ completedOn: (date) => `Abgeschlossen am ${date}`,
651
+ completed: "Abgeschlossen",
652
+ notCompleted: "Nicht abgeschlossen",
653
+ markUndone: "Als nicht erledigt markieren",
654
+ start: "Starten",
655
+ replay: "Erneut",
656
+ },
629
657
  commandPalette: {
630
658
  clear: "Suche löschen",
631
659
  submit: "Suchen",
@@ -1729,6 +1757,12 @@ export function germanLabels(numberLocale: string): UiKitLabels {
1729
1757
  "Ihre Zahlung wird verarbeitet. Ihr Tarif beginnt, sobald sie bestätigt ist; diese Seite aktualisiert sich von selbst.",
1730
1758
  choosePlan: "Tarif wählen",
1731
1759
  updatePayment: "Zahlungsmethode aktualisieren",
1760
+ // 0.42 (§16.7 A): for an admin who isn't the buyer. "Das Admin-Konto", which gives the
1761
+ // reader no gender.
1762
+ paymentFailedAskBuyer: (buyer) =>
1763
+ buyer
1764
+ ? `Die Zahlung ist fehlgeschlagen. ${buyer} kann die Zahlungsmethode aktualisieren.`
1765
+ : "Die Zahlung ist fehlgeschlagen. Die Zahlungsmethode kann über das Admin-Konto aktualisiert werden, mit dem das Abonnement abgeschlossen wurde.",
1732
1766
  checkAgain: "Erneut prüfen",
1733
1767
  lockReason: "Ihr Tarif ist abgelaufen. Wählen Sie einen Tarif, um wieder Änderungen vorzunehmen.",
1734
1768
  // As `characterCount.limitReached` ("Zeichenlimit erreicht").
@@ -1763,6 +1797,22 @@ export function germanLabels(numberLocale: string): UiKitLabels {
1763
1797
  switchCredited: "Gutschrift auf die nächsten Belastungen",
1764
1798
  switchNextCharge: (date) => `Nächste Belastung am ${date}`,
1765
1799
  switchConfirm: "Tarif ändern",
1800
+ // 0.42 (§16): the portal for the buyer only, the in-app cancel, the grant warning.
1801
+ buyerHolds: (buyer) =>
1802
+ buyer
1803
+ ? `Zahlungsmethode und Rechnungen liegen bei ${buyer}: Das Abonnement wurde über dieses Konto abgeschlossen.`
1804
+ : "Zahlungsmethode und Rechnungen liegen beim Admin-Konto, mit dem das Abonnement abgeschlossen wurde.",
1805
+ cancelConfirmTitle: "Abonnement kündigen",
1806
+ cancelConfirmBody: (date) => `Ihr Abonnement endet am ${date}. Bis dahin funktioniert alles wie bisher.`,
1807
+ // §312k BGB: the confirmation button says exactly this.
1808
+ cancelConfirmAction: "Jetzt kündigen",
1809
+ keepSubscription: "Abonnement behalten",
1810
+ portalOpensNewTab: (provider, app) =>
1811
+ `Zahlung und Rechnungen öffnen sich bei ${provider} in einem neuen Tab; ${app ? `${app} bleibt hier offen` : "diese Seite bleibt offen"}.`,
1812
+ grantWarningTitle: "Ihr kostenloser Zugang läuft weiter",
1813
+ grantWarningBody:
1814
+ "Ihr kostenloser Zugang hat kein Enddatum. Solange er läuft, ändert ein Kauf Ihren Tarif nicht: Sie würden für einen Tarif bezahlen, der nicht gilt.",
1815
+ grantWarningAction: "Trotzdem kaufen",
1766
1816
  waitingChanges: (count) =>
1767
1817
  count === 1
1768
1818
  ? `${n(count)} Änderung wartet auf einen Tarif`
@@ -1839,5 +1889,82 @@ export function germanLabels(numberLocale: string): UiKitLabels {
1839
1889
  more: "Mehr",
1840
1890
  moreTitle: "Weitere Seiten",
1841
1891
  },
1892
+ // 0.42.0 — docs/admin-harmonization.md §3.1: the admin area's shared words. "Verwaltung"
1893
+ // as settings §5 names the page; "mit Administratorrechten" rather than "für
1894
+ // Administratoren", which would give the reader a gender (as `transferOwnership`).
1895
+ adminArea: {
1896
+ title: "Verwaltung",
1897
+ waiting: (count) => `${n(count)} offen`,
1898
+ forbiddenTitle: "Nur mit Administratorrechten",
1899
+ forbiddenBody: (appName) => `Hier wird ${appName} verwaltet. Ihr Konto kann diese Seite nicht öffnen.`,
1900
+ forbiddenHome: "Zur Startseite",
1901
+ forbiddenReviewer: "Übersetzungen prüfen",
1902
+ sections: {
1903
+ overview: { title: "Übersicht", help: "Was gerade wartet, auf einen Blick." },
1904
+ users: { title: "Benutzer", help: "Wer ein Konto hat und was jede Person darf." },
1905
+ invitations: { title: "Einladungen", help: "Laden Sie Personen per E-Mail in ihrer Sprache ein; senden Sie offene Einladungen erneut oder widerrufen Sie sie." },
1906
+ billing: { title: "Abrechnung", help: "Tarif, Zahlung und Rechnungen." },
1907
+ activity: { title: "Aktivität", help: "Was in der Verwaltung getan wurde, das Neueste zuerst." },
1908
+ },
1909
+ tiles: {
1910
+ feedbackWaiting: "Zu sichten",
1911
+ deletionRequests: "Löschanfragen",
1912
+ invitationsPending: "Offene Einladungen",
1913
+ users: "Benutzer",
1914
+ usersNew: (count) => `${n(count)} neu in 7 Tagen`,
1915
+ },
1916
+ overviewError: "Die Übersicht konnte nicht geladen werden.",
1917
+ retry: "Erneut versuchen",
1918
+ tools: "Werkzeuge",
1919
+ },
1920
+ // 0.42.0 — docs/top-bar-harmonization.md §3.1: the top bar's shared parts. keksdose's
1921
+ // reviewed sync words where they are not about its budgets; "Tarif" as `billing`.
1922
+ topBarParts: {
1923
+ tours: {
1924
+ menu: "Geführte Touren",
1925
+ newDot: "Neue Touren",
1926
+ thisPage: "Auf dieser Seite",
1927
+ newTours: "Neue Touren",
1928
+ allTours: "Alle Touren…",
1929
+ badgeNew: "Neu",
1930
+ completed: "Abgeschlossen",
1931
+ badgeUpdated: "Aktualisiert",
1932
+ updatedDot: "Aktualisierte Touren",
1933
+ newAndUpdated: "Neu und aktualisiert",
1934
+ },
1935
+ sync: {
1936
+ blocked: (count) =>
1937
+ count === 1
1938
+ ? `${n(count)} Änderung kann von diesem Gerät nicht gesendet werden`
1939
+ : `${n(count)} Änderungen können von diesem Gerät nicht gesendet werden`,
1940
+ offline: "Offline – Änderungen werden auf diesem Gerät gespeichert und bei erneuter Verbindung synchronisiert",
1941
+ refused: "Der Server hat den letzten Abgleich abgelehnt",
1942
+ syncing: "Wird synchronisiert…",
1943
+ pending: (count) =>
1944
+ count === 1
1945
+ ? `${n(count)} Änderung wartet auf Synchronisierung`
1946
+ : `${n(count)} Änderungen warten auf Synchronisierung`,
1947
+ syncedAt: (time) => `Synchronisiert um ${time}`,
1948
+ idle: "Aktuell",
1949
+ syncHint: "Tippen, um jetzt zu synchronisieren",
1950
+ offlineHint: "offline gibt es nichts zu synchronisieren",
1951
+ withHint: (state, hint) => `${state} – ${hint}`,
1952
+ scoped: (scope, state) => `${scope}: ${state}`,
1953
+ offlineShort: "Offline",
1954
+ syncingShort: "Sync läuft…",
1955
+ waitingShort: (count) => `${n(count)} ausstehend`,
1956
+ refusedShort: "Nicht synchronisiert",
1957
+ offlineUnsaved: "Offline – Änderungen können erst nach erneuter Verbindung gespeichert werden",
1958
+ saving: "Wird gespeichert…",
1959
+ },
1960
+ dev: {
1961
+ roleHeading: "Dev · Rolle",
1962
+ switchPlan: "Tarif wechseln",
1963
+ plan: (value) => `Tarif: ${value}`,
1964
+ planHeading: "Dev · Tarif",
1965
+ backTo: (role) => `Zurück zu ${role}`,
1966
+ billingOff: "Die Abrechnung ist lokal aus: Tarife ändern nichts",
1967
+ },
1968
+ },
1842
1969
  };
1843
1970
  }
@@ -25,6 +25,7 @@ import type { CharacterCountLabels, PasswordRevealLabels } from "../components/f
25
25
  import type { TabsLabels } from "../components/tabs";
26
26
  import type { WizardLabels } from "../wizard/types";
27
27
  import type { TourLabels } from "../tour/tour";
28
+ import type { TourCatalogueLabels } from "../tour/tour-catalogue-labels";
28
29
  import type { CommandPaletteLabels } from "../search/command-palette";
29
30
  import type { GlobalSearchLabels } from "../search/global-search";
30
31
  import type { MonthPickerLabels } from "../components/month-picker";
@@ -118,6 +119,8 @@ import type { PhoneInputLabels } from "../components/phone-input";
118
119
  import type { SignChipLabels } from "../components/sign-chip";
119
120
  import type { ColumnMapperLabels } from "../components/column-mapper";
120
121
  import type { NotificationsLabels } from "../notifications/notifications-labels";
122
+ import type { AdminAreaLabels } from "../admin/admin-area-labels";
123
+ import type { TopBarPartsLabels } from "../shell/top-bar-parts-labels";
121
124
 
122
125
  /**
123
126
  * EVERY string the kit renders, as one typed tree — and an optional provider that
@@ -319,6 +322,8 @@ export interface UiKitLabels {
319
322
  file: FileLabels;
320
323
  wizard: WizardLabels;
321
324
  tour: TourLabels;
325
+ /** 0.43.0: the tours page — `TourCatalogue` and its card (docs/tours-harmonization.md §3). */
326
+ tourCatalogue: TourCatalogueLabels;
322
327
  commandPalette: CommandPaletteLabels;
323
328
  globalSearch: GlobalSearchLabels;
324
329
  seriesChart: SeriesChartLabels;
@@ -475,6 +480,13 @@ export interface UiKitLabels {
475
480
  * device's push, the device list, and the unsubscribe page
476
481
  * (docs/notifications-harmonization.md §4.2). */
477
482
  notifications: NotificationsLabels;
483
+ /** 0.42.0: the admin area's shared words — the account menu's "Administration" row, the
484
+ * 403 page, the shared sections, the overview's tiles and the Tools list
485
+ * (docs/admin-harmonization.md §3.1). */
486
+ adminArea: AdminAreaLabels;
487
+ /** 0.42.0: the top bar's shared parts — `TourMenu`, `SyncStatusIndicator`, `DevSwitchers`
488
+ * (docs/top-bar-harmonization.md §3.1). */
489
+ topBarParts: TopBarPartsLabels;
478
490
  }
479
491
 
480
492
  /**