@cosmicdrift/kumiko-bundled-features 0.79.3 → 0.80.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.79.3",
3
+ "version": "0.80.0",
4
4
  "description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -84,11 +84,11 @@
84
84
  "./step-dispatcher": "./src/step-dispatcher/index.ts"
85
85
  },
86
86
  "dependencies": {
87
- "@cosmicdrift/kumiko-dispatcher-live": "0.79.3",
88
- "@cosmicdrift/kumiko-framework": "0.79.3",
89
- "@cosmicdrift/kumiko-headless": "0.79.3",
90
- "@cosmicdrift/kumiko-renderer": "0.79.3",
91
- "@cosmicdrift/kumiko-renderer-web": "0.79.3",
87
+ "@cosmicdrift/kumiko-dispatcher-live": "0.80.0",
88
+ "@cosmicdrift/kumiko-framework": "0.80.0",
89
+ "@cosmicdrift/kumiko-headless": "0.80.0",
90
+ "@cosmicdrift/kumiko-renderer": "0.80.0",
91
+ "@cosmicdrift/kumiko-renderer-web": "0.80.0",
92
92
  "@mollie/api-client": "^4.5.0",
93
93
  "@node-rs/argon2": "^2.0.2",
94
94
  "@types/nodemailer": "^8.0.0",
@@ -15,6 +15,7 @@
15
15
  // authMutedLinkClass — Subtle-Link-Style.
16
16
  // parseUrlToken — URL-Param-Helper (window.location.search).
17
17
 
18
+ import { usePrimitives } from "@cosmicdrift/kumiko-renderer";
18
19
  import { BareFormProvider, cn } from "@cosmicdrift/kumiko-renderer-web";
19
20
  import { createContext, type ReactNode, useContext } from "react";
20
21
 
@@ -51,17 +52,30 @@ export type AuthCardProps = {
51
52
  };
52
53
 
53
54
  export function AuthCard({ title, subtitle, children }: AuthCardProps): ReactNode {
55
+ const { Card } = usePrimitives();
54
56
  const shell = useAuthShell() ?? defaultAuthShell;
57
+ // h1 (Seiten-Hauptüberschrift) via Header-Slot erhalten — die Card-Default-
58
+ // Header wäre h3. padded:false = Form sitzt randlos wie bisher (bare form).
55
59
  const card = (
56
- <div className="w-full max-w-sm rounded-lg border bg-card text-card-foreground shadow-sm">
57
- {(title !== undefined || subtitle !== undefined) && (
58
- <div className="flex flex-col space-y-1.5 p-6 pb-4">
59
- {title !== undefined && <h1 className="text-xl font-semibold tracking-tight">{title}</h1>}
60
- {subtitle !== undefined && <p className="text-sm text-muted-foreground">{subtitle}</p>}
61
- </div>
62
- )}
60
+ <Card
61
+ className="w-full max-w-sm"
62
+ options={{ padded: false }}
63
+ slots={{
64
+ header:
65
+ title !== undefined || subtitle !== undefined ? (
66
+ <div className="flex flex-col space-y-1.5 p-6 pb-4">
67
+ {title !== undefined && (
68
+ <h1 className="text-xl font-semibold tracking-tight">{title}</h1>
69
+ )}
70
+ {subtitle !== undefined && (
71
+ <p className="text-sm text-muted-foreground">{subtitle}</p>
72
+ )}
73
+ </div>
74
+ ) : undefined,
75
+ }}
76
+ >
63
77
  <BareFormProvider>{children}</BareFormProvider>
64
- </div>
78
+ </Card>
65
79
  );
66
80
  return shell(card);
67
81
  }
@@ -27,7 +27,7 @@ export function ConfirmAccountDeletionScreen({
27
27
  }: ConfirmAccountDeletionScreenProps): ReactNode {
28
28
  const t = useTranslation();
29
29
  const dispatcher = useDispatcher();
30
- const { Button, Banner } = usePrimitives();
30
+ const { Button, Banner, Card } = usePrimitives();
31
31
  const [token] = useState(readToken);
32
32
  const [phase, setPhase] = useState<Phase>(token.length > 0 ? "idle" : "missing");
33
33
 
@@ -42,12 +42,19 @@ export function ConfirmAccountDeletionScreen({
42
42
  };
43
43
 
44
44
  return (
45
- <div className="w-full max-w-sm mx-auto rounded-lg border bg-card text-card-foreground shadow-sm">
46
- <div className="flex flex-col space-y-1.5 p-6 pb-4">
47
- <h1 className="text-xl font-semibold tracking-tight">
48
- {title ?? t("userDataRights.deletion.confirm.title")}
49
- </h1>
50
- </div>
45
+ <Card
46
+ className="w-full max-w-sm mx-auto"
47
+ options={{ padded: false }}
48
+ slots={{
49
+ header: (
50
+ <div className="flex flex-col space-y-1.5 p-6 pb-4">
51
+ <h1 className="text-xl font-semibold tracking-tight">
52
+ {title ?? t("userDataRights.deletion.confirm.title")}
53
+ </h1>
54
+ </div>
55
+ ),
56
+ }}
57
+ >
51
58
  <div className="p-6 pt-0 flex flex-col gap-4">
52
59
  {phase === "success" ? (
53
60
  <Banner variant="info">
@@ -83,6 +90,6 @@ export function ConfirmAccountDeletionScreen({
83
90
  </>
84
91
  )}
85
92
  </div>
86
- </div>
93
+ </Card>
87
94
  );
88
95
  }
@@ -130,7 +130,26 @@ function ExportSection(): ReactNode {
130
130
  }, [inProgress, refetch]);
131
131
 
132
132
  return (
133
- <Section title={t("userDataRights.privacyCenter.export.title")} testId="privacy-export">
133
+ <Section
134
+ title={t("userDataRights.privacyCenter.export.title")}
135
+ testId="privacy-export"
136
+ actions={
137
+ !inProgress ? (
138
+ <Button
139
+ onClick={() => void request()}
140
+ disabled={submitting}
141
+ loading={submitting}
142
+ testId="privacy-export-request"
143
+ >
144
+ {done
145
+ ? t("userDataRights.privacyCenter.export.requestNew")
146
+ : submitting
147
+ ? t("userDataRights.privacyCenter.export.requesting")
148
+ : t("userDataRights.privacyCenter.export.request")}
149
+ </Button>
150
+ ) : undefined
151
+ }
152
+ >
134
153
  <p className="text-sm text-muted-foreground">
135
154
  {t("userDataRights.privacyCenter.export.intro")}
136
155
  </p>
@@ -171,20 +190,6 @@ function ExportSection(): ReactNode {
171
190
  </Banner>
172
191
  )}
173
192
  <StatusBanner status={status} />
174
- {!inProgress && (
175
- <Button
176
- onClick={() => void request()}
177
- disabled={submitting}
178
- loading={submitting}
179
- testId="privacy-export-request"
180
- >
181
- {done
182
- ? t("userDataRights.privacyCenter.export.requestNew")
183
- : submitting
184
- ? t("userDataRights.privacyCenter.export.requesting")
185
- : t("userDataRights.privacyCenter.export.request")}
186
- </Button>
187
- )}
188
193
  </Section>
189
194
  );
190
195
  }
@@ -219,6 +224,17 @@ function RestrictionSection({
219
224
  <Section
220
225
  title={t("userDataRights.privacyCenter.restriction.title")}
221
226
  testId="privacy-restriction"
227
+ actions={
228
+ restricted ? undefined : (
229
+ <Button
230
+ variant="danger"
231
+ onClick={() => setDialogOpen(true)}
232
+ testId="privacy-restriction-restrict"
233
+ >
234
+ {t("userDataRights.privacyCenter.restriction.restrict")}
235
+ </Button>
236
+ )
237
+ }
222
238
  >
223
239
  {restricted ? (
224
240
  <Banner variant="error" testId="privacy-restriction-active">
@@ -230,13 +246,6 @@ function RestrictionSection({
230
246
  {t("userDataRights.privacyCenter.restriction.explainer")}
231
247
  </p>
232
248
  <StatusBanner status={status} />
233
- <Button
234
- variant="danger"
235
- onClick={() => setDialogOpen(true)}
236
- testId="privacy-restriction-restrict"
237
- >
238
- {t("userDataRights.privacyCenter.restriction.restrict")}
239
- </Button>
240
249
  <Dialog
241
250
  open={dialogOpen}
242
251
  onOpenChange={setDialogOpen}
@@ -291,7 +300,29 @@ function DeletionSection({
291
300
  };
292
301
 
293
302
  return (
294
- <Section title={t("userDataRights.privacyCenter.deletion.title")} testId="privacy-deletion">
303
+ <Section
304
+ title={t("userDataRights.privacyCenter.deletion.title")}
305
+ testId="privacy-deletion"
306
+ actions={
307
+ deletionRequested ? (
308
+ <Button
309
+ variant="secondary"
310
+ onClick={() => void cancelDeletion()}
311
+ testId="privacy-deletion-cancel"
312
+ >
313
+ {t("userDataRights.privacyCenter.deletion.cancel")}
314
+ </Button>
315
+ ) : (
316
+ <Button
317
+ variant="danger"
318
+ onClick={() => setDialogOpen(true)}
319
+ testId="privacy-deletion-delete"
320
+ >
321
+ {t("userDataRights.privacyCenter.deletion.delete")}
322
+ </Button>
323
+ )
324
+ }
325
+ >
295
326
  {deletionRequested ? (
296
327
  <>
297
328
  <Banner variant="error" testId="privacy-deletion-requested">
@@ -300,13 +331,6 @@ function DeletionSection({
300
331
  })}
301
332
  </Banner>
302
333
  <StatusBanner status={status} />
303
- <Button
304
- variant="secondary"
305
- onClick={() => void cancelDeletion()}
306
- testId="privacy-deletion-cancel"
307
- >
308
- {t("userDataRights.privacyCenter.deletion.cancel")}
309
- </Button>
310
334
  </>
311
335
  ) : (
312
336
  <>
@@ -314,13 +338,6 @@ function DeletionSection({
314
338
  {t("userDataRights.privacyCenter.deletion.explainer")}
315
339
  </p>
316
340
  <StatusBanner status={status} />
317
- <Button
318
- variant="danger"
319
- onClick={() => setDialogOpen(true)}
320
- testId="privacy-deletion-delete"
321
- >
322
- {t("userDataRights.privacyCenter.deletion.delete")}
323
- </Button>
324
341
  <Dialog
325
342
  open={dialogOpen}
326
343
  onOpenChange={setDialogOpen}
@@ -21,7 +21,7 @@ export function RequestAccountDeletionScreen({
21
21
  }: RequestAccountDeletionScreenProps): ReactNode {
22
22
  const t = useTranslation();
23
23
  const dispatcher = useDispatcher();
24
- const { Form, Field, Input, Button, Banner } = usePrimitives();
24
+ const { Form, Field, Input, Button, Banner, Card } = usePrimitives();
25
25
  const [email, setEmail] = useState("");
26
26
  const [submitting, setSubmitting] = useState(false);
27
27
  const [done, setDone] = useState(false);
@@ -50,12 +50,19 @@ export function RequestAccountDeletionScreen({
50
50
  };
51
51
 
52
52
  return (
53
- <div className="w-full max-w-sm mx-auto rounded-lg border bg-card text-card-foreground shadow-sm">
54
- <div className="flex flex-col space-y-1.5 p-6 pb-4">
55
- <h1 className="text-xl font-semibold tracking-tight">
56
- {title ?? t("userDataRights.deletion.request.title")}
57
- </h1>
58
- </div>
53
+ <Card
54
+ className="w-full max-w-sm mx-auto"
55
+ options={{ padded: false }}
56
+ slots={{
57
+ header: (
58
+ <div className="flex flex-col space-y-1.5 p-6 pb-4">
59
+ <h1 className="text-xl font-semibold tracking-tight">
60
+ {title ?? t("userDataRights.deletion.request.title")}
61
+ </h1>
62
+ </div>
63
+ ),
64
+ }}
65
+ >
59
66
  {done ? (
60
67
  <div className="p-6 pt-0">
61
68
  <Banner variant="info">
@@ -91,6 +98,6 @@ export function RequestAccountDeletionScreen({
91
98
  </Form>
92
99
  </div>
93
100
  )}
94
- </div>
101
+ </Card>
95
102
  );
96
103
  }
@@ -243,7 +243,7 @@ function DangerZoneSection({
243
243
  readonly onChanged: () => void;
244
244
  }): ReactNode {
245
245
  const t = useTranslation();
246
- const { Button, Banner, Dialog, Heading } = usePrimitives();
246
+ const { Button, Banner, Dialog, Card } = usePrimitives();
247
247
  const dispatcher = useDispatcher();
248
248
  const [dialogOpen, setDialogOpen] = useState(false);
249
249
  const [status, setStatus] = useState<SectionStatus>({ kind: "idle" });
@@ -270,52 +270,54 @@ function DangerZoneSection({
270
270
  onChanged();
271
271
  };
272
272
 
273
+ const footer = deletionRequested ? (
274
+ <Button
275
+ variant="secondary"
276
+ onClick={() => void cancelDeletion()}
277
+ testId="profile-danger-cancel"
278
+ >
279
+ {t("profile.danger.cancelDeletion")}
280
+ </Button>
281
+ ) : (
282
+ <Button variant="danger" onClick={() => setDialogOpen(true)} testId="profile-danger-delete">
283
+ {t("profile.danger.delete")}
284
+ </Button>
285
+ );
286
+
273
287
  return (
274
- <section
275
- data-testid="profile-danger"
276
- className="flex flex-col gap-4 rounded-lg border border-destructive/40 bg-card p-6"
288
+ <Card
289
+ testId="profile-danger"
290
+ className="border-destructive/40"
291
+ slots={{ title: t("profile.danger.title"), footer }}
277
292
  >
278
- <Heading variant="section">{t("profile.danger.title")}</Heading>
279
- {deletionRequested ? (
280
- <>
281
- <Banner variant="error" testId="profile-danger-requested">
282
- {t("profile.danger.requested", {
283
- date: formatDeletionDate(me.gracePeriodEnd),
284
- })}
285
- </Banner>
286
- <StatusBanner status={status} />
287
- <Button
288
- variant="secondary"
289
- onClick={() => void cancelDeletion()}
290
- testId="profile-danger-cancel"
291
- >
292
- {t("profile.danger.cancelDeletion")}
293
- </Button>
294
- </>
295
- ) : (
296
- <>
297
- <p className="text-sm text-muted-foreground">{t("profile.danger.explainer")}</p>
298
- <StatusBanner status={status} />
299
- <Button
300
- variant="danger"
301
- onClick={() => setDialogOpen(true)}
302
- testId="profile-danger-delete"
303
- >
304
- {t("profile.danger.delete")}
305
- </Button>
306
- <Dialog
307
- open={dialogOpen}
308
- onOpenChange={setDialogOpen}
309
- title={t("profile.danger.dialogTitle")}
310
- description={t("profile.danger.dialogDescription")}
311
- variant="danger"
312
- confirmLabel={t("profile.danger.delete")}
313
- onConfirm={requestDeletion}
314
- testId="profile-danger-dialog"
315
- />
316
- </>
317
- )}
318
- </section>
293
+ <div className="flex flex-col gap-4">
294
+ {deletionRequested ? (
295
+ <>
296
+ <Banner variant="error" testId="profile-danger-requested">
297
+ {t("profile.danger.requested", {
298
+ date: formatDeletionDate(me.gracePeriodEnd),
299
+ })}
300
+ </Banner>
301
+ <StatusBanner status={status} />
302
+ </>
303
+ ) : (
304
+ <>
305
+ <p className="text-sm text-muted-foreground">{t("profile.danger.explainer")}</p>
306
+ <StatusBanner status={status} />
307
+ <Dialog
308
+ open={dialogOpen}
309
+ onOpenChange={setDialogOpen}
310
+ title={t("profile.danger.dialogTitle")}
311
+ description={t("profile.danger.dialogDescription")}
312
+ variant="danger"
313
+ confirmLabel={t("profile.danger.delete")}
314
+ onConfirm={requestDeletion}
315
+ testId="profile-danger-dialog"
316
+ />
317
+ </>
318
+ )}
319
+ </div>
320
+ </Card>
319
321
  );
320
322
  }
321
323