@cosmicdrift/kumiko-renderer-web 0.196.0 → 0.197.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-renderer-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.197.0",
|
|
4
4
|
"description": "Web-platform bindings for @cosmicdrift/kumiko-renderer. HTML default-primitives, browser history-based navigation, EventSource-backed live events, and a one-call createKumikoApp that mounts the whole stack via react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
"./styles.css": "./src/styles.css"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
20
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
21
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
19
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.197.0",
|
|
20
|
+
"@cosmicdrift/kumiko-headless": "0.197.0",
|
|
21
|
+
"@cosmicdrift/kumiko-renderer": "0.197.0",
|
|
22
22
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
23
23
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
24
24
|
"@radix-ui/react-label": "^2.1.8",
|
|
@@ -492,6 +492,48 @@ describe("DataTable", () => {
|
|
|
492
492
|
expect(screen.queryByTestId("dt-pager-page-60")).not.toBeNull();
|
|
493
493
|
expect(screen.queryByTestId("dt-pager-page-30")).not.toBeNull();
|
|
494
494
|
});
|
|
495
|
+
|
|
496
|
+
test("Default-Locale (en): Status-Text + aria-Labels aus i18n-Fallback statt hartcodiert", () => {
|
|
497
|
+
render(
|
|
498
|
+
<DataTable
|
|
499
|
+
columns={cols}
|
|
500
|
+
rows={oneRow}
|
|
501
|
+
testId="dt"
|
|
502
|
+
pager={{ page: 1, limit: 50, total: 3000, onPageChange: mock() }}
|
|
503
|
+
/>,
|
|
504
|
+
);
|
|
505
|
+
expect(screen.getByTestId("dt-pager-status").textContent).toBe("1–50 of 3,000");
|
|
506
|
+
expect(screen.getByTestId("dt-pager-prev").getAttribute("aria-label")).toBe("Previous page");
|
|
507
|
+
expect(screen.getByTestId("dt-pager-next").getAttribute("aria-label")).toBe("Next page");
|
|
508
|
+
expect(screen.getByTestId("dt-pager-page-2").getAttribute("aria-label")).toBe("Page 2");
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
test("de-Locale: Status-Text + aria-Labels kommen aus i18n statt hartcodiertem Englisch (issue #2007)", async () => {
|
|
512
|
+
const { LocaleProvider, createStaticLocaleResolver, kumikoDefaultTranslations } =
|
|
513
|
+
await import("@cosmicdrift/kumiko-renderer");
|
|
514
|
+
render(
|
|
515
|
+
<LocaleProvider
|
|
516
|
+
resolver={createStaticLocaleResolver({ locale: "de" })}
|
|
517
|
+
fallbackBundles={[kumikoDefaultTranslations]}
|
|
518
|
+
>
|
|
519
|
+
<DataTable
|
|
520
|
+
columns={cols}
|
|
521
|
+
rows={oneRow}
|
|
522
|
+
testId="dt"
|
|
523
|
+
pager={{ page: 1, limit: 50, total: 3000, onPageChange: mock() }}
|
|
524
|
+
/>
|
|
525
|
+
</LocaleProvider>,
|
|
526
|
+
);
|
|
527
|
+
// toLocaleString() without a locale arg stays runtime-default (not the
|
|
528
|
+
// resolver locale) — out of scope for #2007, which only addresses the
|
|
529
|
+
// untranslated word/aria-labels.
|
|
530
|
+
expect(screen.getByTestId("dt-pager-status").textContent).toBe("1–50 von 3,000");
|
|
531
|
+
expect(screen.getByTestId("dt-pager-prev").getAttribute("aria-label")).toBe(
|
|
532
|
+
"Vorherige Seite",
|
|
533
|
+
);
|
|
534
|
+
expect(screen.getByTestId("dt-pager-next").getAttribute("aria-label")).toBe("Nächste Seite");
|
|
535
|
+
expect(screen.getByTestId("dt-pager-page-2").getAttribute("aria-label")).toBe("Seite 2");
|
|
536
|
+
});
|
|
495
537
|
});
|
|
496
538
|
|
|
497
539
|
// Infinite-scroll sentinel: renders a sentinel div, shows a spinner when
|
package/src/primitives/index.tsx
CHANGED
|
@@ -1153,11 +1153,15 @@ function InfiniteSentinel({
|
|
|
1153
1153
|
);
|
|
1154
1154
|
}
|
|
1155
1155
|
|
|
1156
|
-
// Pager —
|
|
1157
|
-
// pagination="pages". Layout:
|
|
1158
|
-
//
|
|
1159
|
-
//
|
|
1160
|
-
//
|
|
1156
|
+
// Pager — classic page pager (← 1 … N →) for DataTables with
|
|
1157
|
+
// pagination="pages". Layout: status text on the left ("X – Y of Z"),
|
|
1158
|
+
// page buttons in the middle, prev/next arrows on the outside. The
|
|
1159
|
+
// window-of-7 doesn't show all pages for large lists — the user sees
|
|
1160
|
+
// the current range plus first/last as anchors.
|
|
1161
|
+
//
|
|
1162
|
+
// Status text and prev/next/page aria-labels go through t(...), which
|
|
1163
|
+
// requires a LocaleProvider in the tree — same requirement as
|
|
1164
|
+
// InfiniteSentinel above, already established for DataTable consumers.
|
|
1161
1165
|
function Pager({
|
|
1162
1166
|
page,
|
|
1163
1167
|
limit,
|
|
@@ -1171,6 +1175,7 @@ function Pager({
|
|
|
1171
1175
|
readonly onPageChange: (next: number) => void;
|
|
1172
1176
|
readonly testId?: string;
|
|
1173
1177
|
}): ReactNode {
|
|
1178
|
+
const t = useTranslation();
|
|
1174
1179
|
const totalPages = Math.max(1, Math.ceil(total / limit));
|
|
1175
1180
|
const safePage = Math.max(1, Math.min(page, totalPages));
|
|
1176
1181
|
const from = (safePage - 1) * limit + 1;
|
|
@@ -1183,11 +1188,15 @@ function Pager({
|
|
|
1183
1188
|
className="flex items-center justify-between mt-3 gap-3 text-sm text-muted-foreground"
|
|
1184
1189
|
>
|
|
1185
1190
|
<div data-testid={testId !== undefined ? `${testId}-status` : undefined}>
|
|
1186
|
-
{
|
|
1191
|
+
{t("kumiko.pager.status", {
|
|
1192
|
+
from: from.toLocaleString(),
|
|
1193
|
+
to: to.toLocaleString(),
|
|
1194
|
+
total: total.toLocaleString(),
|
|
1195
|
+
})}
|
|
1187
1196
|
</div>
|
|
1188
1197
|
<div className="flex items-center gap-1">
|
|
1189
1198
|
<PagerButton
|
|
1190
|
-
ariaLabel="
|
|
1199
|
+
ariaLabel={t("kumiko.pager.previousPage")}
|
|
1191
1200
|
disabled={safePage <= 1}
|
|
1192
1201
|
onClick={() => onPageChange(safePage - 1)}
|
|
1193
1202
|
testId={testId !== undefined ? `${testId}-prev` : undefined}
|
|
@@ -1206,7 +1215,7 @@ function Pager({
|
|
|
1206
1215
|
) : (
|
|
1207
1216
|
<PagerButton
|
|
1208
1217
|
key={entry}
|
|
1209
|
-
ariaLabel={
|
|
1218
|
+
ariaLabel={t("kumiko.pager.page", { entry })}
|
|
1210
1219
|
ariaCurrent={entry === safePage ? "page" : undefined}
|
|
1211
1220
|
active={entry === safePage}
|
|
1212
1221
|
onClick={() => onPageChange(entry)}
|
|
@@ -1217,7 +1226,7 @@ function Pager({
|
|
|
1217
1226
|
),
|
|
1218
1227
|
)}
|
|
1219
1228
|
<PagerButton
|
|
1220
|
-
ariaLabel="
|
|
1229
|
+
ariaLabel={t("kumiko.pager.nextPage")}
|
|
1221
1230
|
disabled={safePage >= totalPages}
|
|
1222
1231
|
onClick={() => onPageChange(safePage + 1)}
|
|
1223
1232
|
testId={testId !== undefined ? `${testId}-next` : undefined}
|
|
@@ -1755,7 +1764,11 @@ function DefaultSection({
|
|
|
1755
1764
|
<section
|
|
1756
1765
|
data-testid={testId}
|
|
1757
1766
|
className={cn(
|
|
1758
|
-
|
|
1767
|
+
// py-4 (not the standalone card's py-6): the border-t between
|
|
1768
|
+
// sections already carries the section break, so the vertical
|
|
1769
|
+
// gap only needs to read as roughly double the gap-4 field row
|
|
1770
|
+
// spacing, not triple it.
|
|
1771
|
+
"flex flex-col gap-4 px-6 py-4",
|
|
1759
1772
|
variant === "destructive" && "border-l-2 border-destructive/40",
|
|
1760
1773
|
)}
|
|
1761
1774
|
>
|
package/src/ui/sheet.tsx
CHANGED
|
@@ -105,10 +105,13 @@ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
105
105
|
<div
|
|
106
106
|
data-slot="sheet-footer"
|
|
107
107
|
// Matches the card footer convention (primitives/index.tsx cardFooter
|
|
108
|
-
// + cardFooterBorder)
|
|
109
|
-
//
|
|
108
|
+
// + cardFooterBorder) for padding/border/button-row shape, but keeps
|
|
109
|
+
// the panel's own `bg-background` instead of the card footer's
|
|
110
|
+
// `bg-muted/30` — the sheet panel isn't a card, so its footer reads
|
|
111
|
+
// as the same surface as the body above it. The border-t alone marks
|
|
112
|
+
// the footer boundary.
|
|
110
113
|
className={cn(
|
|
111
|
-
"mt-auto flex items-center justify-end gap-2 border-t bg-
|
|
114
|
+
"mt-auto flex items-center justify-end gap-2 border-t bg-background px-[var(--card-padding)] py-4",
|
|
112
115
|
className,
|
|
113
116
|
)}
|
|
114
117
|
{...props}
|
|
@@ -33,6 +33,35 @@ describe("Drawer", () => {
|
|
|
33
33
|
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
describe("showCloseButton", () => {
|
|
37
|
+
test("default true: Close-Button im Header vorhanden", () => {
|
|
38
|
+
render(
|
|
39
|
+
<Drawer open={true} onOpenChange={() => {}} title="Mail" testId="drawer">
|
|
40
|
+
<div>Body</div>
|
|
41
|
+
</Drawer>,
|
|
42
|
+
);
|
|
43
|
+
expect(screen.getByRole("button", { name: "Close" })).toBeTruthy();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("false: kein Close-Button, Titel und Maximieren bleiben", () => {
|
|
47
|
+
render(
|
|
48
|
+
<Drawer
|
|
49
|
+
open={true}
|
|
50
|
+
onOpenChange={() => {}}
|
|
51
|
+
title="Mail"
|
|
52
|
+
testId="drawer"
|
|
53
|
+
showCloseButton={false}
|
|
54
|
+
resize={{}}
|
|
55
|
+
>
|
|
56
|
+
<div>Body</div>
|
|
57
|
+
</Drawer>,
|
|
58
|
+
);
|
|
59
|
+
expect(screen.queryByRole("button", { name: "Close" })).toBeNull();
|
|
60
|
+
expect(screen.getByText("Mail")).toBeTruthy();
|
|
61
|
+
expect(screen.getByRole("button", { name: /maximize drawer width/i })).toBeTruthy();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
36
65
|
describe("backdrop", () => {
|
|
37
66
|
function getOverlay(): HTMLElement {
|
|
38
67
|
const overlay = document.querySelector('[data-slot="sheet-overlay"]');
|
|
@@ -122,18 +151,18 @@ describe("Drawer", () => {
|
|
|
122
151
|
);
|
|
123
152
|
expect(screen.getByRole("separator").getAttribute("aria-valuenow")).toBe("800");
|
|
124
153
|
});
|
|
125
|
-
test("Startbreite folgt max(
|
|
126
|
-
withViewportWidth(
|
|
154
|
+
test("Startbreite folgt max(600px, 37.5vw) wenn resize.defaultWidthPx fehlt", () => {
|
|
155
|
+
withViewportWidth(1920, () => {
|
|
127
156
|
render(
|
|
128
157
|
<Drawer open={true} onOpenChange={() => {}} side="right" testId="drawer" resize={{}}>
|
|
129
158
|
<div>Body</div>
|
|
130
159
|
</Drawer>,
|
|
131
160
|
);
|
|
132
|
-
expect(screen.getByRole("separator").getAttribute("aria-valuenow")).toBe("
|
|
161
|
+
expect(screen.getByRole("separator").getAttribute("aria-valuenow")).toBe("720");
|
|
133
162
|
});
|
|
134
163
|
});
|
|
135
164
|
|
|
136
|
-
test("Startbreite clamped auf effectiveMaxWidthPx wenn
|
|
165
|
+
test("Startbreite clamped auf effectiveMaxWidthPx wenn 37.5vw ueber MAX_WIDTH_PX hinauslaeuft", () => {
|
|
137
166
|
withViewportWidth(8000, () => {
|
|
138
167
|
render(
|
|
139
168
|
<Drawer open={true} onOpenChange={() => {}} side="right" testId="drawer" resize={{}}>
|
|
@@ -166,14 +195,14 @@ describe("Drawer", () => {
|
|
|
166
195
|
});
|
|
167
196
|
});
|
|
168
197
|
|
|
169
|
-
test("Startbreite clamped auf
|
|
198
|
+
test("Startbreite clamped auf 600px Minimum bei schmalem Viewport", () => {
|
|
170
199
|
withViewportWidth(800, () => {
|
|
171
200
|
render(
|
|
172
201
|
<Drawer open={true} onOpenChange={() => {}} side="right" testId="drawer" resize={{}}>
|
|
173
202
|
<div>Body</div>
|
|
174
203
|
</Drawer>,
|
|
175
204
|
);
|
|
176
|
-
expect(screen.getByRole("separator").getAttribute("aria-valuenow")).toBe("
|
|
205
|
+
expect(screen.getByRole("separator").getAttribute("aria-valuenow")).toBe("600");
|
|
177
206
|
});
|
|
178
207
|
});
|
|
179
208
|
|
package/src/widgets/drawer.tsx
CHANGED
|
@@ -21,6 +21,10 @@ export type DrawerProps = {
|
|
|
21
21
|
readonly footer?: ReactNode;
|
|
22
22
|
readonly children: ReactNode;
|
|
23
23
|
readonly testId?: string;
|
|
24
|
+
/** Header close (X). Default `true`. Turn off when the caller's own
|
|
25
|
+
* footer already has a dedicated close/cancel action, so there is only
|
|
26
|
+
* one way to dismiss the drawer. */
|
|
27
|
+
readonly showCloseButton?: boolean;
|
|
24
28
|
/** Opt-in drag-to-resize + maximize toggle (left/right sides only). */
|
|
25
29
|
readonly resize?: {
|
|
26
30
|
readonly defaultWidthPx?: number;
|
|
@@ -37,10 +41,18 @@ export type DrawerProps = {
|
|
|
37
41
|
|
|
38
42
|
const MIN_WIDTH_PX = 320;
|
|
39
43
|
const MAX_WIDTH_PX = 1000;
|
|
40
|
-
// Must match the static `max(
|
|
44
|
+
// Must match the static `max(600px,37.5vw)` in floatingSideClass — Tailwind's
|
|
41
45
|
// JIT can't read these at runtime, so the two are kept in sync by hand.
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
// 600px (vs. the old 520px floor) gives a two-column field row (e.g.
|
|
47
|
+
// street/number, zip/city) room to breathe, while staying well under half
|
|
48
|
+
// of a 1280px viewport so a narrow window isn't dominated by the drawer.
|
|
49
|
+
// 37.5vw (vs. the old 25vw) keeps that same 600px on a 1280–1600px window
|
|
50
|
+
// (25vw was still under the floor there, making the ratio dead weight up
|
|
51
|
+
// to 2400px) but actually grows past the floor by ~1600px, so a 1920px
|
|
52
|
+
// window — where the two-column row is cramped just as often — lands
|
|
53
|
+
// around 720px instead of being stuck at 600px alongside a 1280px window.
|
|
54
|
+
const DEFAULT_WIDTH_MIN_PX = 600;
|
|
55
|
+
const DEFAULT_WIDTH_VIEWPORT_RATIO = 0.375;
|
|
44
56
|
const DEFAULT_BLUR_PX = 0;
|
|
45
57
|
const DEFAULT_DIM_PERCENT = 20;
|
|
46
58
|
|
|
@@ -55,13 +67,13 @@ function defaultWidthFromViewport(): number {
|
|
|
55
67
|
function floatingSideClass(side: "left" | "right" | "top" | "bottom"): string {
|
|
56
68
|
switch (side) {
|
|
57
69
|
case "left":
|
|
58
|
-
return "inset-y-8 left-8 h-auto w-[max(
|
|
70
|
+
return "inset-y-8 left-8 h-auto w-[max(600px,37.5vw)] max-w-[85vw] sm:max-w-[max(600px,37.5vw)] rounded-[2rem] border shadow-2xl overflow-hidden";
|
|
59
71
|
case "top":
|
|
60
72
|
return "inset-x-8 top-8 h-auto max-h-[80vh] rounded-[2rem] border shadow-2xl overflow-hidden";
|
|
61
73
|
case "bottom":
|
|
62
74
|
return "inset-x-8 bottom-8 h-auto max-h-[80vh] rounded-[2rem] border shadow-2xl overflow-hidden";
|
|
63
75
|
default:
|
|
64
|
-
return "inset-y-8 right-8 h-auto w-[max(
|
|
76
|
+
return "inset-y-8 right-8 h-auto w-[max(600px,37.5vw)] max-w-[85vw] sm:max-w-[max(600px,37.5vw)] rounded-[2rem] border shadow-2xl overflow-hidden";
|
|
65
77
|
}
|
|
66
78
|
}
|
|
67
79
|
|
|
@@ -77,6 +89,7 @@ export function Drawer({
|
|
|
77
89
|
footer,
|
|
78
90
|
children,
|
|
79
91
|
testId,
|
|
92
|
+
showCloseButton = true,
|
|
80
93
|
resize,
|
|
81
94
|
backdrop,
|
|
82
95
|
}: DrawerProps): ReactNode {
|
|
@@ -146,6 +159,7 @@ export function Drawer({
|
|
|
146
159
|
side={side}
|
|
147
160
|
data-testid={testId}
|
|
148
161
|
overlayStyle={overlayStyle}
|
|
162
|
+
showCloseButton={showCloseButton}
|
|
149
163
|
className={floatingSideClass(side)}
|
|
150
164
|
style={canResize ? { width: effectiveWidthPx, maxWidth: "none" } : undefined}
|
|
151
165
|
>
|