@cosmicdrift/kumiko-renderer 0.377.0 → 0.379.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/dist/app/action-form-shim.js +1 -2
- package/dist/app/action-form-support.js +16 -4
- package/dist/app/kumiko-screen.js +51 -38
- package/dist/app/list-empty-content.d.ts +1 -0
- package/dist/app/list-empty-content.js +5 -0
- package/dist/app/row-actions.d.ts +7 -1
- package/dist/app/row-actions.js +6 -1
- package/dist/app/secret-mint-body.js +3 -2
- package/dist/components/embedded-list-field.js +16 -7
- package/dist/components/file-list-field.js +4 -0
- package/dist/components/form-derivation-patch.d.ts +8 -0
- package/dist/components/form-derivation-patch.js +24 -0
- package/dist/components/form-screen-layout.d.ts +13 -0
- package/dist/components/form-screen-layout.js +13 -0
- package/dist/components/grid-cell-for-field.d.ts +4 -1
- package/dist/components/grid-cell-for-field.js +6 -2
- package/dist/components/list-empty-state.d.ts +4 -1
- package/dist/components/list-empty-state.js +4 -1
- package/dist/components/reference-create-dialog.js +1 -1
- package/dist/components/related-list-section.js +4 -1
- package/dist/components/render-edit-logic.d.ts +1 -1
- package/dist/components/render-edit-logic.js +8 -2
- package/dist/components/render-edit-types.d.ts +7 -1
- package/dist/components/render-edit.js +38 -6
- package/dist/components/render-field.d.ts +3 -1
- package/dist/components/render-field.js +27 -8
- package/dist/components/render-list.d.ts +3 -1
- package/dist/components/render-list.js +29 -8
- package/dist/components/write-form-section.js +4 -1
- package/dist/field-issue-text.d.ts +4 -0
- package/dist/field-issue-text.js +14 -0
- package/dist/file-ids-of.d.ts +1 -0
- package/dist/file-ids-of.js +7 -0
- package/dist/flow-diagram/icons.d.ts +3 -0
- package/dist/flow-diagram/icons.js +14 -0
- package/dist/flow-diagram/index.d.ts +2 -0
- package/dist/flow-diagram/index.js +1 -0
- package/dist/flow-diagram/render.d.ts +3 -0
- package/dist/flow-diagram/render.js +234 -0
- package/dist/flow-diagram/types.d.ts +18 -0
- package/dist/flow-diagram/types.js +1 -0
- package/dist/headless/dispatcher/index.d.ts +1 -0
- package/dist/headless/dispatcher/index.js +1 -0
- package/dist/headless/dispatcher/request-id.d.ts +1 -0
- package/dist/headless/dispatcher/request-id.js +15 -0
- package/dist/headless/form/form-controller.js +16 -1
- package/dist/headless/form/zod-bridge.js +7 -0
- package/dist/headless/index.d.ts +2 -1
- package/dist/headless/index.js +2 -1
- package/dist/headless/plural-key.d.ts +1 -0
- package/dist/headless/plural-key.js +5 -0
- package/dist/headless/view-model/edit.d.ts +8 -1
- package/dist/headless/view-model/edit.js +23 -7
- package/dist/headless/view-model/types.d.ts +11 -3
- package/dist/hooks/live-refetch-scheduler.d.ts +6 -0
- package/dist/hooks/live-refetch-scheduler.js +45 -0
- package/dist/hooks/use-query.js +8 -4
- package/dist/hooks/use-reference-lookup.d.ts +1 -0
- package/dist/hooks/use-reference-lookup.js +6 -3
- package/dist/i18n-defaults.js +28 -3
- package/dist/index.d.ts +6 -2
- package/dist/index.js +4 -1
- package/dist/leave-guard-dialog.d.ts +2 -0
- package/dist/leave-guard-dialog.js +28 -0
- package/dist/leave-guard.d.ts +15 -0
- package/dist/leave-guard.js +39 -0
- package/dist/live/dispatcher-live.js +1 -16
- package/dist/primitives.d.ts +39 -0
- package/dist/sse/live-events.d.ts +3 -0
- package/dist/sse/live-events.js +3 -0
- package/dist/web/app/create-app.js +2 -2
- package/dist/web/app/dashboard-body.js +13 -1
- package/dist/web/app/nav.js +51 -9
- package/dist/web/index.d.ts +2 -2
- package/dist/web/index.js +1 -1
- package/dist/web/layout/nav-tree.js +13 -3
- package/dist/web/layout/shell-header.js +3 -3
- package/dist/web/lib/file-upload-form-data.d.ts +2 -0
- package/dist/web/lib/file-upload-form-data.js +2 -0
- package/dist/web/lib/upload-error-key.d.ts +7 -0
- package/dist/web/lib/upload-error-key.js +23 -0
- package/dist/web/lib/upload-file-ref.d.ts +1 -1
- package/dist/web/primitives/embedded-list-input.js +5 -4
- package/dist/web/primitives/file-thumbnail.d.ts +6 -0
- package/dist/web/primitives/file-thumbnail.js +19 -19
- package/dist/web/primitives/file-upload.d.ts +2 -1
- package/dist/web/primitives/file-upload.js +11 -7
- package/dist/web/primitives/file-value-list.d.ts +3 -0
- package/dist/web/primitives/file-value-list.js +22 -0
- package/dist/web/primitives/files-dropzone.d.ts +16 -0
- package/dist/web/primitives/files-dropzone.js +52 -0
- package/dist/web/primitives/index.js +65 -30
- package/dist/web/primitives/segmented-select.js +8 -2
- package/dist/web/sse/live-events.d.ts +13 -0
- package/dist/web/sse/live-events.js +88 -23
- package/dist/web/widgets/bounded-load-cache.d.ts +1 -0
- package/dist/web/widgets/bounded-load-cache.js +25 -0
- package/dist/web/widgets/drawer.js +37 -2
- package/dist/web/widgets/index.d.ts +1 -0
- package/dist/web/widgets/index.js +1 -0
- package/dist/web/widgets/pdf-preview.d.ts +10 -0
- package/dist/web/widgets/pdf-preview.js +140 -0
- package/dist/web/widgets/upload-zone.d.ts +11 -2
- package/dist/web/widgets/upload-zone.js +30 -7
- package/package.json +14 -5
- package/src/changes.json +143 -0
- package/src/web/styles.css +13 -9
|
@@ -10,6 +10,7 @@ export { FloatingPanel, type FloatingPanelGeometry, type FloatingPanelProps, } f
|
|
|
10
10
|
export { BooleanField, type BooleanFieldProps, DateField, type DateFieldProps, FileField, type FileFieldProps, MoneyField, NumberField, type NumberFieldProps, PercentField, RangeField, type RangeFieldProps, SelectField, type SelectFieldProps, TextareaField, type TextareaFieldProps, TextField, type TextFieldProps, } from "./form-fields.tsx";
|
|
11
11
|
export { InfinityList, type InfinityListProps, type InfinityListSelection, } from "./infinity-list.tsx";
|
|
12
12
|
export { ModeSwitch, type ModeSwitchVariant } from "./mode-switch.tsx";
|
|
13
|
+
export { PdfPreview, type PdfPreviewSource } from "./pdf-preview.tsx";
|
|
13
14
|
export { type PhotoSlotSpec, PhotoSlots, type PhotoSlotsProps } from "./photo-slots.tsx";
|
|
14
15
|
export { PlanCard, type PlanCardActionSlot, type PlanCardLinkSlot, type PlanCardPrice, type PlanCardProps, PlanGrid, type PlanGridProps, } from "./plan-card.tsx";
|
|
15
16
|
export { ProgressBar } from "./progress-bar.tsx";
|
|
@@ -13,6 +13,7 @@ export { FloatingPanel, } from "./floating-panel.js";
|
|
|
13
13
|
export { BooleanField, DateField, FileField, MoneyField, NumberField, PercentField, RangeField, SelectField, TextareaField, TextField, } from "./form-fields.js";
|
|
14
14
|
export { InfinityList, } from "./infinity-list.js";
|
|
15
15
|
export { ModeSwitch } from "./mode-switch.js";
|
|
16
|
+
export { PdfPreview } from "./pdf-preview.js";
|
|
16
17
|
export { PhotoSlots } from "./photo-slots.js";
|
|
17
18
|
export { PlanCard, PlanGrid, } from "./plan-card.js";
|
|
18
19
|
export { ProgressBar } from "./progress-bar.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export type PdfPreviewSource = {
|
|
3
|
+
readonly base64: string;
|
|
4
|
+
} | {
|
|
5
|
+
readonly url: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function PdfPreview({ source, testId, }: {
|
|
8
|
+
readonly source: PdfPreviewSource;
|
|
9
|
+
readonly testId?: string;
|
|
10
|
+
}): ReactNode;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense, use, useCallback, useMemo, useState } from "react";
|
|
3
|
+
import { useTranslation } from "../../i18n.js";
|
|
4
|
+
import { createBoundedLoadCache } from "./bounded-load-cache.js";
|
|
5
|
+
// pdf.js runs in the main thread ("fake worker"): the worker module is bundled
|
|
6
|
+
// with the app and handed over through globalThis.pdfjsWorker, so no worker or
|
|
7
|
+
// CDN URL is ever fetched (self-hosting and CSP stay intact).
|
|
8
|
+
const RENDER_SCALE = 96 / 72;
|
|
9
|
+
const MAX_CACHED_DOCUMENTS = 4;
|
|
10
|
+
const cachedDocument = createBoundedLoadCache(MAX_CACHED_DOCUMENTS, (loaded) => {
|
|
11
|
+
if (loaded.ok)
|
|
12
|
+
void loaded.loadingTask.destroy();
|
|
13
|
+
});
|
|
14
|
+
function decodeBase64(base64) {
|
|
15
|
+
const binary = atob(base64);
|
|
16
|
+
const bytes = new Uint8Array(binary.length);
|
|
17
|
+
for (let index = 0; index < binary.length; index++)
|
|
18
|
+
bytes[index] = binary.charCodeAt(index);
|
|
19
|
+
return bytes;
|
|
20
|
+
}
|
|
21
|
+
async function loadBytes(source) {
|
|
22
|
+
if ("base64" in source)
|
|
23
|
+
return decodeBase64(source.base64);
|
|
24
|
+
const res = await fetch(source.url, { credentials: "same-origin" });
|
|
25
|
+
if (!res.ok)
|
|
26
|
+
throw new Error(`pdf fetch failed (${res.status})`);
|
|
27
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
28
|
+
}
|
|
29
|
+
async function loadDocument(source) {
|
|
30
|
+
try {
|
|
31
|
+
const [pdfjs, worker, data] = await Promise.all([
|
|
32
|
+
import("pdfjs-dist"),
|
|
33
|
+
import("pdfjs-dist/build/pdf.worker.min.mjs"),
|
|
34
|
+
loadBytes(source),
|
|
35
|
+
]);
|
|
36
|
+
Object.assign(globalThis, { pdfjsWorker: worker });
|
|
37
|
+
const loadingTask = pdfjs.getDocument({ data });
|
|
38
|
+
return { ok: true, document: await loadingTask.promise, loadingTask };
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return { ok: false };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// The promise must be stable across renders for use().
|
|
45
|
+
function documentFor(source) {
|
|
46
|
+
const key = "base64" in source ? `base64:${source.base64}` : `url:${source.url}`;
|
|
47
|
+
return cachedDocument(key, () => loadDocument(source));
|
|
48
|
+
}
|
|
49
|
+
function PageView({ document, pageNumber, }) {
|
|
50
|
+
const t = useTranslation();
|
|
51
|
+
const page = use(pageFor(document, pageNumber));
|
|
52
|
+
// Stable identity: the mount callbacks below depend on it, and a new viewport per render re-mounts canvas and text layer.
|
|
53
|
+
const viewport = useMemo(() => page.getViewport({ scale: RENDER_SCALE }), [page]);
|
|
54
|
+
const [availableWidth, setAvailableWidth] = useState(null);
|
|
55
|
+
const measureFrame = useCallback((frame) => {
|
|
56
|
+
if (frame === null)
|
|
57
|
+
return;
|
|
58
|
+
const observer = new ResizeObserver(() => setAvailableWidth(frame.clientWidth));
|
|
59
|
+
observer.observe(frame);
|
|
60
|
+
setAvailableWidth(frame.clientWidth);
|
|
61
|
+
return () => observer.disconnect();
|
|
62
|
+
}, []);
|
|
63
|
+
// The sheet keeps its natural pixel size and is scaled as a whole, so canvas and text layer stay aligned.
|
|
64
|
+
const fitScale = availableWidth === null || availableWidth <= 0
|
|
65
|
+
? 1
|
|
66
|
+
: Math.min(1, availableWidth / viewport.width);
|
|
67
|
+
const mountCanvas = useCallback((canvas) => {
|
|
68
|
+
if (canvas === null)
|
|
69
|
+
return;
|
|
70
|
+
const pixelRatio = globalThis.devicePixelRatio || 1;
|
|
71
|
+
canvas.width = Math.floor(viewport.width * pixelRatio);
|
|
72
|
+
canvas.height = Math.floor(viewport.height * pixelRatio);
|
|
73
|
+
const task = page.render({
|
|
74
|
+
canvas,
|
|
75
|
+
viewport,
|
|
76
|
+
transform: pixelRatio === 1 ? undefined : [pixelRatio, 0, 0, pixelRatio, 0, 0],
|
|
77
|
+
});
|
|
78
|
+
task.promise.catch(() => undefined);
|
|
79
|
+
return () => task.cancel();
|
|
80
|
+
}, [page, viewport]);
|
|
81
|
+
const mountTextLayer = useCallback((container) => {
|
|
82
|
+
if (container === null)
|
|
83
|
+
return;
|
|
84
|
+
container.style.setProperty("--total-scale-factor", String(RENDER_SCALE));
|
|
85
|
+
container.style.setProperty("--scale-factor", String(RENDER_SCALE));
|
|
86
|
+
let layer = null;
|
|
87
|
+
let cancelled = false;
|
|
88
|
+
void import("pdfjs-dist").then(({ TextLayer }) => {
|
|
89
|
+
// skip: cleanup ran before pdfjs finished loading
|
|
90
|
+
if (cancelled)
|
|
91
|
+
return undefined;
|
|
92
|
+
// TextLayer.cancel() keeps rendered spans; a re-mount must not append a second layer.
|
|
93
|
+
container.replaceChildren();
|
|
94
|
+
const textLayer = new TextLayer({
|
|
95
|
+
textContentSource: page.streamTextContent(),
|
|
96
|
+
container,
|
|
97
|
+
viewport,
|
|
98
|
+
});
|
|
99
|
+
layer = textLayer;
|
|
100
|
+
return textLayer.render().catch(() => undefined);
|
|
101
|
+
});
|
|
102
|
+
return () => {
|
|
103
|
+
cancelled = true;
|
|
104
|
+
layer?.cancel();
|
|
105
|
+
};
|
|
106
|
+
}, [page, viewport]);
|
|
107
|
+
return (_jsxs("figure", { className: "m-0 flex w-full flex-col items-center gap-2 rounded-lg bg-muted p-4", "data-testid": `pdf-page-${pageNumber}`, children: [_jsx("div", { ref: measureFrame, className: "w-full", children: _jsx("div", { className: "mx-auto", style: { width: viewport.width * fitScale, height: viewport.height * fitScale }, children: _jsxs("div", { className: "relative border border-border bg-white shadow-md", style: {
|
|
108
|
+
width: viewport.width,
|
|
109
|
+
height: viewport.height,
|
|
110
|
+
transform: `scale(${fitScale})`,
|
|
111
|
+
transformOrigin: "top left",
|
|
112
|
+
}, children: [_jsx("canvas", { ref: mountCanvas, style: { width: viewport.width, height: viewport.height }, className: "block" }), _jsx("div", { ref: mountTextLayer, className: "absolute inset-0 overflow-hidden leading-none [&_span]:absolute [&_span]:origin-top-left [&_span]:cursor-text [&_span]:whitespace-pre [&_span]:text-transparent [&_span::selection]:bg-primary/30" })] }) }) }), _jsx("figcaption", { className: "text-xs text-muted-foreground", children: t("kumiko.widget.pdf-preview.pageOf", { page: pageNumber, pages: document.numPages }) })] }));
|
|
113
|
+
}
|
|
114
|
+
const pageCache = new WeakMap();
|
|
115
|
+
function pageFor(document, pageNumber) {
|
|
116
|
+
let pages = pageCache.get(document);
|
|
117
|
+
if (pages === undefined) {
|
|
118
|
+
pages = new Map();
|
|
119
|
+
pageCache.set(document, pages);
|
|
120
|
+
}
|
|
121
|
+
let page = pages.get(pageNumber);
|
|
122
|
+
if (page === undefined) {
|
|
123
|
+
page = document.getPage(pageNumber);
|
|
124
|
+
pages.set(pageNumber, page);
|
|
125
|
+
}
|
|
126
|
+
return page;
|
|
127
|
+
}
|
|
128
|
+
function Pages({ source }) {
|
|
129
|
+
const t = useTranslation();
|
|
130
|
+
const loaded = use(documentFor(source));
|
|
131
|
+
if (!loaded.ok) {
|
|
132
|
+
return _jsx("p", { className: "text-sm text-status-bad", children: t("kumiko.widget.pdf-preview.renderFailed") });
|
|
133
|
+
}
|
|
134
|
+
const pageNumbers = Array.from({ length: loaded.document.numPages }, (_, index) => index + 1);
|
|
135
|
+
return (_jsx(_Fragment, { children: pageNumbers.map((pageNumber) => (_jsx(Suspense, { fallback: null, children: _jsx(PageView, { document: loaded.document, pageNumber: pageNumber }) }, pageNumber))) }));
|
|
136
|
+
}
|
|
137
|
+
export function PdfPreview({ source, testId = "pdf-preview", }) {
|
|
138
|
+
const t = useTranslation();
|
|
139
|
+
return (_jsx("div", { className: "flex flex-col items-stretch gap-4 overflow-auto", "data-testid": testId, children: _jsx(Suspense, { fallback: _jsx("p", { className: "text-sm text-muted-foreground", children: t("kumiko.widget.pdf-preview.loading") }), children: _jsx(Pages, { source: source }) }) }));
|
|
140
|
+
}
|
|
@@ -3,7 +3,7 @@ export type UploadZoneProps = {
|
|
|
3
3
|
/** Uploads a single file (POST + optional follow-up mutation). Throws on
|
|
4
4
|
* failure — the message ends up in the row's error line. Runs in
|
|
5
5
|
* parallel per file within a batch (no sequential waiting). */
|
|
6
|
-
readonly onUpload: (file: File) => Promise<void>;
|
|
6
|
+
readonly onUpload: (file: File) => Promise<string | undefined | void>;
|
|
7
7
|
readonly title: ReactNode;
|
|
8
8
|
readonly hint?: ReactNode;
|
|
9
9
|
readonly accept?: readonly string[];
|
|
@@ -14,6 +14,15 @@ export type UploadZoneProps = {
|
|
|
14
14
|
readonly capture?: "environment" | "user";
|
|
15
15
|
readonly disabled?: boolean;
|
|
16
16
|
readonly testId?: string;
|
|
17
|
+
/** Turns a failed upload into the row's error text. Default: the error's own message. */
|
|
18
|
+
readonly describeError?: (cause: unknown) => string;
|
|
19
|
+
/** Controlled use, for a form field: a string returned by `onUpload` becomes the row's value
|
|
20
|
+
* and done rows get a remove button that calls this with it. The zone does not drop the row;
|
|
21
|
+
* the owner removes the value from `doneValues`, which the zone then follows. */
|
|
22
|
+
readonly onRemove?: (value: string) => void;
|
|
23
|
+
/** The values the owner currently holds. Done rows whose value left the list disappear, and
|
|
24
|
+
* going from some to none (an external reset) clears every row, error rows included. */
|
|
25
|
+
readonly doneValues?: readonly string[];
|
|
17
26
|
};
|
|
18
27
|
export declare function matchesAccept(file: File, accept?: readonly string[]): boolean;
|
|
19
28
|
/** Drop zone + multi-file picker with a per-file status row (uploading/done/
|
|
@@ -21,4 +30,4 @@ export declare function matchesAccept(file: File, accept?: readonly string[]): b
|
|
|
21
30
|
* independently — unlike `FileField` (single file, FileRef value inside a
|
|
22
31
|
* form field). `onUpload` decides what "upload" means (storage POST, ingest
|
|
23
32
|
* mutation, or both); the zone only tracks uploading/done/error. */
|
|
24
|
-
export declare function UploadZone({ onUpload, title, hint, accept, multiple, capture, disabled, testId, }: UploadZoneProps): ReactNode;
|
|
33
|
+
export declare function UploadZone({ onUpload, title, hint, accept, multiple, capture, disabled, testId, describeError, onRemove, doneValues, }: UploadZoneProps): ReactNode;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { CheckCircle2, FileUp, Loader2, TriangleAlert, Upload } from "lucide-react";
|
|
3
|
-
import { useId, useRef, useState } from "react";
|
|
2
|
+
import { CheckCircle2, FileUp, Loader2, TriangleAlert, Upload, X } from "lucide-react";
|
|
3
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
4
4
|
import { useTranslation } from "../../index.js";
|
|
5
5
|
import { toAcceptAttr } from "../lib/accept-attr.js";
|
|
6
6
|
import { cn } from "../lib/cn.js";
|
|
@@ -44,7 +44,7 @@ function partitionByAccept(files, accept) {
|
|
|
44
44
|
* independently — unlike `FileField` (single file, FileRef value inside a
|
|
45
45
|
* form field). `onUpload` decides what "upload" means (storage POST, ingest
|
|
46
46
|
* mutation, or both); the zone only tracks uploading/done/error. */
|
|
47
|
-
export function UploadZone({ onUpload, title, hint, accept, multiple = true, capture, disabled, testId, }) {
|
|
47
|
+
export function UploadZone({ onUpload, title, hint, accept, multiple = true, capture, disabled, testId, describeError, onRemove, doneValues, }) {
|
|
48
48
|
const t = useTranslation();
|
|
49
49
|
const inputId = useId();
|
|
50
50
|
const inputRef = useRef(null);
|
|
@@ -54,16 +54,31 @@ export function UploadZone({ onUpload, title, hint, accept, multiple = true, cap
|
|
|
54
54
|
// preview leaves it undefined. These ids are React keys/row ids, not
|
|
55
55
|
// globally unique identifiers, so a per-instance counter is enough.
|
|
56
56
|
const nextRowId = useRef(0);
|
|
57
|
+
const previousDoneCount = useRef(0);
|
|
58
|
+
const doneCount = doneValues?.length;
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (doneCount === undefined)
|
|
61
|
+
return;
|
|
62
|
+
if (doneCount === 0 && previousDoneCount.current > 0)
|
|
63
|
+
setRows([]);
|
|
64
|
+
previousDoneCount.current = doneCount;
|
|
65
|
+
}, [doneCount]);
|
|
57
66
|
async function uploadOne(file) {
|
|
58
67
|
const rowId = String(nextRowId.current++);
|
|
59
68
|
const uploadFile = await resizeImageBeforeUpload(file);
|
|
60
69
|
setRows((prev) => [...prev, { id: rowId, fileName: uploadFile.name, status: "uploading" }]);
|
|
61
70
|
try {
|
|
62
|
-
await onUpload(uploadFile);
|
|
63
|
-
setRows((prev) => prev.map((row) =>
|
|
71
|
+
const value = await onUpload(uploadFile);
|
|
72
|
+
setRows((prev) => prev.map((row) => row.id === rowId
|
|
73
|
+
? { ...row, status: "done", ...(typeof value === "string" && { value }) }
|
|
74
|
+
: row));
|
|
64
75
|
}
|
|
65
76
|
catch (cause) {
|
|
66
|
-
const message =
|
|
77
|
+
const message = describeError !== undefined
|
|
78
|
+
? describeError(cause)
|
|
79
|
+
: cause instanceof Error
|
|
80
|
+
? cause.message
|
|
81
|
+
: t("kumiko.widget.upload.error");
|
|
67
82
|
setRows((prev) => prev.map((row) => (row.id === rowId ? { ...row, status: "error", error: message } : row)));
|
|
68
83
|
}
|
|
69
84
|
}
|
|
@@ -95,6 +110,10 @@ export function UploadZone({ onUpload, title, hint, accept, multiple = true, cap
|
|
|
95
110
|
return;
|
|
96
111
|
void uploadFiles(e.dataTransfer.files);
|
|
97
112
|
}
|
|
113
|
+
function removeRow(row) {
|
|
114
|
+
if (row.value !== undefined)
|
|
115
|
+
onRemove?.(row.value);
|
|
116
|
+
}
|
|
98
117
|
const acceptAttr = toAcceptAttr(accept);
|
|
99
118
|
return (_jsxs("div", { "data-testid": testId, className: "flex flex-col gap-4", children: [_jsxs("label", { htmlFor: inputId, onDragOver: (e) => {
|
|
100
119
|
e.preventDefault();
|
|
@@ -104,5 +123,9 @@ export function UploadZone({ onUpload, title, hint, accept, multiple = true, cap
|
|
|
104
123
|
if (!(e.relatedTarget instanceof Node) || !e.currentTarget.contains(e.relatedTarget)) {
|
|
105
124
|
setDragOver(false);
|
|
106
125
|
}
|
|
107
|
-
}, onDrop: handleDrop, "data-testid": testId !== undefined ? `${testId}-dropzone` : undefined, className: cn("flex w-full cursor-pointer flex-col items-center gap-3 rounded-lg border-2 border-dashed p-10 text-center transition-colors has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2", dragOver ? "border-primary bg-muted/40" : "border-border", disabled === true && "cursor-not-allowed opacity-50"), children: [_jsx(Upload, { className: "size-8 text-muted-foreground", "aria-hidden": "true" }), _jsx("span", { className: "text-sm", children: title }), hint !== undefined && _jsx("span", { className: "text-sm text-muted-foreground", children: hint }), _jsx("input", { ref: inputRef, id: inputId, type: "file", multiple: multiple, className: "sr-only", "data-testid": testId !== undefined ? `${testId}-input` : undefined, disabled: disabled, ...(acceptAttr !== undefined && { accept: acceptAttr }), ...(capture !== undefined && { capture }), onChange: (e) => void uploadFiles(e.target.files) })] }), rows.length > 0 && (_jsx("ul", { className: "flex flex-col gap-2", children: rows
|
|
126
|
+
}, onDrop: handleDrop, "data-testid": testId !== undefined ? `${testId}-dropzone` : undefined, className: cn("flex w-full cursor-pointer flex-col items-center gap-3 rounded-lg border-2 border-dashed p-10 text-center transition-colors has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2", dragOver ? "border-primary bg-muted/40" : "border-border", disabled === true && "cursor-not-allowed opacity-50"), children: [_jsx(Upload, { className: "size-8 text-muted-foreground", "aria-hidden": "true" }), _jsx("span", { className: "text-sm", children: title }), hint !== undefined && _jsx("span", { className: "text-sm text-muted-foreground", children: hint }), _jsx("input", { ref: inputRef, id: inputId, type: "file", multiple: multiple, className: "sr-only", "data-testid": testId !== undefined ? `${testId}-input` : undefined, disabled: disabled, ...(acceptAttr !== undefined && { accept: acceptAttr }), ...(capture !== undefined && { capture }), onChange: (e) => void uploadFiles(e.target.files) })] }), rows.length > 0 && (_jsx("ul", { className: "flex flex-col gap-2", children: rows
|
|
127
|
+
.filter((row) => row.value === undefined ||
|
|
128
|
+
doneValues === undefined ||
|
|
129
|
+
doneValues.includes(row.value))
|
|
130
|
+
.map((row) => (_jsxs("li", { "data-testid": testId !== undefined ? `${testId}-row` : undefined, className: "flex items-center justify-between gap-3 rounded-md border border-border px-4 py-2", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx(FileUp, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": "true" }), _jsx("span", { className: "truncate text-sm", children: row.fileName })] }), _jsxs("output", { className: "flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground", children: [STATUS_ICON[row.status], _jsx("span", { className: "sr-only", children: t(STATUS_LABEL_KEY[row.status]) }), row.status === "error" && row.error !== undefined && _jsx("span", { children: row.error }), row.value !== undefined && onRemove !== undefined && disabled !== true && (_jsx("button", { type: "button", className: "rounded-sm p-0.5 text-muted-foreground hover:text-foreground", "aria-label": t("kumiko.widget.upload.remove"), "data-testid": testId !== undefined ? `${testId}-remove` : undefined, onClick: () => removeRow(row), children: _jsx(X, { className: "size-3.5", "aria-hidden": "true" }) }))] })] }, row.id))) }))] }));
|
|
108
131
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.379.0",
|
|
4
4
|
"description": "React renderer for Kumiko screens. The root export holds the platform-agnostic logic (primitives contract, hooks, KumikoScreen, navigation and SSE abstractions); ./headless the DOM-free UI logic, ./live the HTTP dispatcher, ./web the browser bindings (HTML primitives, history navigation, EventSource live events, createKumikoApp).",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
"types": "./dist/headless/index.d.ts",
|
|
18
18
|
"default": "./dist/headless/index.js"
|
|
19
19
|
},
|
|
20
|
+
"./flow-diagram": {
|
|
21
|
+
"types": "./dist/flow-diagram/index.d.ts",
|
|
22
|
+
"default": "./dist/flow-diagram/index.js"
|
|
23
|
+
},
|
|
20
24
|
"./headless/apex": {
|
|
21
25
|
"types": "./dist/headless/apex/index.d.ts",
|
|
22
26
|
"default": "./dist/headless/apex/index.js"
|
|
@@ -44,8 +48,8 @@
|
|
|
44
48
|
}
|
|
45
49
|
},
|
|
46
50
|
"dependencies": {
|
|
47
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
48
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
51
|
+
"@cosmicdrift/kumiko-framework": "0.379.0",
|
|
52
|
+
"@cosmicdrift/kumiko-types": "0.379.0",
|
|
49
53
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
50
54
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
51
55
|
"@radix-ui/react-label": "^2.1.8",
|
|
@@ -64,6 +68,7 @@
|
|
|
64
68
|
"clsx": "^2.1.1",
|
|
65
69
|
"cmdk": "^1.1.1",
|
|
66
70
|
"lucide-react": "^1.14.0",
|
|
71
|
+
"pdfjs-dist": "6.3.289",
|
|
67
72
|
"qrcode": "^1.5.4",
|
|
68
73
|
"radix-ui": "^1.4.3",
|
|
69
74
|
"react": "^19.2.6",
|
|
@@ -87,8 +92,8 @@
|
|
|
87
92
|
}
|
|
88
93
|
},
|
|
89
94
|
"devDependencies": {
|
|
90
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
91
|
-
"@cosmicdrift/kumiko-testing": "0.
|
|
95
|
+
"@cosmicdrift/kumiko-locale-de": "0.379.0",
|
|
96
|
+
"@cosmicdrift/kumiko-testing": "0.379.0",
|
|
92
97
|
"@tailwindcss/cli": "^4.3.0",
|
|
93
98
|
"@testing-library/react": "^16.3.2",
|
|
94
99
|
"@testing-library/user-event": "^14.6.1",
|
|
@@ -118,6 +123,10 @@
|
|
|
118
123
|
"types": "./dist/headless/index.d.ts",
|
|
119
124
|
"default": "./dist/headless/index.js"
|
|
120
125
|
},
|
|
126
|
+
"./flow-diagram": {
|
|
127
|
+
"types": "./dist/flow-diagram/index.d.ts",
|
|
128
|
+
"default": "./dist/flow-diagram/index.js"
|
|
129
|
+
},
|
|
121
130
|
"./headless/apex": {
|
|
122
131
|
"types": "./dist/headless/apex/index.d.ts",
|
|
123
132
|
"default": "./dist/headless/apex/index.js"
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,147 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.379.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "actionForm success box shows the message and every fact on its own line",
|
|
6
|
+
"detail": "The success message and the successFacts of an actionForm no longer run together (\"Fertig.Gebucht: 20Übersprungen: 0\"). The message is its own line, a fact with one value is one \"Label: value\" line, a fact with several lines (for example file names) is a label line followed by one line per value instead of a comma chain. New test ids action-form-success-text and action-form-success-fact-<n>-line-<i>.",
|
|
7
|
+
"migration": "None. Tests that read the textContent of a multi-line fact now see the label and the lines without the former \", \" separators."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.379.0",
|
|
11
|
+
"type": "improvement",
|
|
12
|
+
"title": "Empty list with an active default filter offers \"Show all\"",
|
|
13
|
+
"detail": "A list that is empty only because its defaultFilters are on says so and offers a Show all button instead of the \"create the first one\" text. The hard-coded Loading… banners use the kumiko.widget.loading key, so they follow the locale."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"version": "0.379.0",
|
|
17
|
+
"type": "fix",
|
|
18
|
+
"title": "Count messages use singular and plural forms",
|
|
19
|
+
"detail": "Nine count messages (template usage, mail preview entries, layout origin and footer, tag delete confirmation, embedded-list paste warnings) now come as .one and .other keys in en, de, es, fr and it, so \"1 template\" no longer reads \"1 templates\". Apps that override these keys must rename them to .one and .other."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"version": "0.379.0",
|
|
23
|
+
"type": "fix",
|
|
24
|
+
"title": "Shell header: the parent crumb no longer slides under a long title",
|
|
25
|
+
"detail": "The page title truncates on its own; the parent crumb stays whole and is left out when the space beside the header actions gets narrower than 20rem (a container query on the header's left side instead of the sm viewport breakpoint). The h1 carries the full title as tooltip."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"version": "0.379.0",
|
|
29
|
+
"type": "fix",
|
|
30
|
+
"title": "A rejected empty embedded list shows its error at the list, once, without a banner-only failure",
|
|
31
|
+
"detail": "The empty state of an embedded list now shows list-level issues (min items, required), and the surrounding field no longer prints them a second time. An issue on a derived head total that is not laid out (an empty list leaves it without a value) no longer adds the generic form banner while another issue is shown; it still shows the banner when it is the only issue. A list below its minItems now reads \"At least 1 row required.\" / \"At least N rows required.\" (errors.validation.list_min_rows.one and .other) instead of the generic too-small text. Only embedded lists get the row wording (their schema reports a custom issue with params.i18nKey and minimum, which both zod bridges now forward); other arrays such as a required multiSelect keep errors.validation.too_small.",
|
|
32
|
+
"migration": "None."
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"version": "0.379.0",
|
|
36
|
+
"type": "fix",
|
|
37
|
+
"title": "An embedded list does not repeat its section title as a visible label",
|
|
38
|
+
"detail": "When the label of an embedded-list field equals the title of the section it sits in (for example \"Positionen\" under \"Positionen\"), entityEdit and actionForm screens now keep the label for screen readers only instead of printing it twice. Applies by default, no setting.",
|
|
39
|
+
"migration": "None."
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"version": "0.379.0",
|
|
43
|
+
"type": "improvement",
|
|
44
|
+
"title": "Embedded-list footer totals can sum several columns under their own label",
|
|
45
|
+
"detail": "EmbeddedFieldDef.totals entries are now a column name (as before) or { label, from } with an i18n label and the sub-fields to add up, for example Net { from: [\"amount\"] }, VAT { from: [\"vatAmount\"] } and Gross { from: [\"amount\", \"vatAmount\"] }. Display only, the server derivation is unchanged. The boot validator rejects an empty from and any from entry that is not a number, money or decimal sub-field. Money sums use the same format as string totals. The ui-walkthrough receive-invoice screen and the embedded recipe now lay out only the Positionen list with a footer of Net, VAT and Gross; the derived head totals are still computed on the server but not laid out, and the list requires at least one line (minItems 1).",
|
|
46
|
+
"migration": "Optional. Replace a totals string list with labelled entries where the footer should show Net, VAT and Gross (solon invoice screens)."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"version": "0.379.0",
|
|
50
|
+
"type": "fix",
|
|
51
|
+
"title": "projectionDetail shows the record again after a record action",
|
|
52
|
+
"detail": "After a writeHandler action on a projectionDetail (header or section action) the screen refetched the record but the shown fields kept their old values until a reload. The actions now reload through the same path as the reload button, which re-applies the refetched record to a form without user input.",
|
|
53
|
+
"migration": "None."
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"version": "0.379.0",
|
|
57
|
+
"type": "improvement",
|
|
58
|
+
"title": "Live updates reconnect, resync and coalesce refetches",
|
|
59
|
+
"detail": "The web EventSource reconnects with exponential backoff and jitter after a refused handshake unless the session ended, rewires every subscribed entity on the new connection and sends each listener a LIVE_RESYNC_EVENT_TYPE event after a reconnect, because frames during the gap are lost. useQuery and the nav tree refetch in the background and collapse a burst of live events into one trailing run (LIVE_REFETCH_COALESCE_MS); an event during a running fetch queues exactly one follow-up. A failed refetch keeps the last data on screen and shows QueryErrorBanner with a retry button above it."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"version": "0.379.0",
|
|
63
|
+
"type": "breaking",
|
|
64
|
+
"title": "Forms ask before unsaved input is discarded",
|
|
65
|
+
"detail": "A dirty entity form or writeForm section now asks with the discard dialog before an in-app navigation, a projectionDetail tab switch, browser back or forward, and the browser asks on tab close or reload (beforeunload). A successful save clears the form before its follow-up navigation, and wizards with layout.draft keep their draft instead of asking. requestLeave, hasUnsavedInput and useUnsavedInputSource are exported for custom screens.",
|
|
66
|
+
"migration": "End-to-end tests that fill a form and then navigate away without saving now meet the dialog (testId leave-guard-dialog); confirm it or save first. page.goto from a dirty form triggers the browser's beforeunload prompt."
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"version": "0.379.0",
|
|
70
|
+
"type": "breaking",
|
|
71
|
+
"title": "Form writes carry a requestId and reuse it only for a safe retry",
|
|
72
|
+
"detail": "The form controller passes { requestId } as third argument to dispatcher.write. After a transport failure without a server answer, a submit with the same values reuses the requestId, so the server idempotency guard does not run the write twice; any server answer or changed values get a new one. generateRequestId moved to the headless dispatcher module.",
|
|
73
|
+
"migration": "Tests that assert a mocked dispatcher.write with toHaveBeenCalledWith(type, payload) must accept the third argument, e.g. toHaveBeenCalledWith(type, payload, expect.objectContaining({ requestId: expect.any(String) })). Custom Dispatcher implementations should forward options.requestId."
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"version": "0.379.0",
|
|
77
|
+
"type": "improvement",
|
|
78
|
+
"title": "Failed submits focus the first invalid field, clearer format errors",
|
|
79
|
+
"detail": "After a failed submit the web form focuses and scrolls to the first field with aria-invalid (FormProps.focusFirstErrorKey; native forms ignore it). Format errors for email, url, date, datetime and uuid name the expected format with an example, the error line no longer shifts the labels of neighbouring fields, and SegmentedControl options with up to four entries get equal widths."
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"version": "0.379.0",
|
|
83
|
+
"type": "fix",
|
|
84
|
+
"title": "Toolbar navigate actions are hidden when the user may not open the target screen",
|
|
85
|
+
"detail": "A toolbarAction of kind navigate on entityList, projectionList and relatedList now follows the access rule of its target screen, the same check metric jumps use. Before, the button showed for every role and led to an access-denied page.",
|
|
86
|
+
"migration": "None. Gate the target screen with its own access rule; the toolbar button follows it."
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"version": "0.379.0",
|
|
90
|
+
"type": "improvement",
|
|
91
|
+
"title": "actionForm loads a singleton row without a record id",
|
|
92
|
+
"detail": "load.singleton: true on an actionForm sends the load query without idParam and prefills the form from the tenant's single row, like projectionDetail. A route id is never forwarded. The boot validator rejects singleton together with idParam."
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"version": "0.379.0",
|
|
96
|
+
"type": "improvement",
|
|
97
|
+
"title": "Dashboard text block link shown only to given roles",
|
|
98
|
+
"detail": "A text block of kind link ({ text, screen, roles? }) renders a link to a screen and is hidden unless the user holds one of the roles. This only gates the display; the target screen still checks access on the server. The boot validator rejects an unknown target screen and an empty roles list."
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"version": "0.379.0",
|
|
102
|
+
"type": "fix",
|
|
103
|
+
"title": "DataTable no longer hides its last column under the sticky actions column",
|
|
104
|
+
"detail": "Header and text cells wrap instead of staying on one line (badges, dates and row actions keep their own nowrap), and a row-action group wraps when the column is narrow. A table whose cells could not shrink scrolled sideways without a visible scrollbar and its last column slid under the sticky actions column. Long values now wrap (up to max-w-xs per column) instead of being cut with an ellipsis."
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"version": "0.378.0",
|
|
108
|
+
"type": "fix",
|
|
109
|
+
"title": "Non-modal drawers narrow the content instead of covering it",
|
|
110
|
+
"detail": "A Drawer with modal={false} on a wide viewport now publishes the width it occupies as --kumiko-drawer-inset-right or --kumiko-drawer-inset-left on the document root, and the shell content area (main next to the sidebar) takes it as inline padding, so error banners, right columns and the pinned form footer end beside the drawer. Modal drawers and narrow viewports keep overlaying. Two non-modal drawers open on the same side overwrite each other's value."
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"version": "0.378.0",
|
|
114
|
+
"type": "improvement",
|
|
115
|
+
"title": "Files fields show download and PDF preview",
|
|
116
|
+
"detail": "A read-only detail field with renderer { format: \"file\" } (projectionDetail, readOnly edit field) lists each file with its name and a download link; PDFs get an inline preview toggle. New optional primitive FileValueList, and PdfPreview (moved here from the document designer) with a per-source bounded document cache so several PDFs can share a page. i18n keys kumiko.field.file-list.download/preview/hidePreview and kumiko.widget.pdf-preview.* ship in de, es, fr and it.",
|
|
117
|
+
"migration": "PdfPreview is now exported from @cosmicdrift/kumiko-renderer/web (props: source {base64}|{url}, testId)."
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"version": "0.378.0",
|
|
121
|
+
"type": "improvement",
|
|
122
|
+
"title": "FormScreenLayout gives custom screens the standard form screen layout",
|
|
123
|
+
"detail": "New universal component FormScreenLayout (children, description, actions, secondaryActions, onSubmit, width, testId) renders the same left-aligned form column, section stack and pinned footer as an entityEdit screen. Fill it with Section primitives (title, icon, subtitle); description becomes the form subtitle.",
|
|
124
|
+
"migration": "Custom form screens can replace FormScreenShell + Card with FormScreenLayout + Section."
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"version": "0.378.0",
|
|
128
|
+
"type": "fix",
|
|
129
|
+
"title": "Card renders no footer bar for an empty footer slot",
|
|
130
|
+
"detail": "slots.footer set to false or null (for example `hasDone && <Button/>`) left an empty grey footer bar. The bar now renders only when the footer slot has content."
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"version": "0.378.0",
|
|
134
|
+
"type": "improvement",
|
|
135
|
+
"title": "flow-diagram subpath",
|
|
136
|
+
"detail": "\"@cosmicdrift/kumiko-renderer/flow-diagram\" exports renderFlow(spec) and the FlowSpec, FlowNode, FlowEdge, FlowIcon types, a pure string function that renders an animated SVG flow diagram."
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"version": "0.378.0",
|
|
140
|
+
"type": "breaking",
|
|
141
|
+
"title": "Renderer stylesheet no longer scans the document designer",
|
|
142
|
+
"detail": "The renderer stylesheet stopped scanning the document designer sources by relative path, which broke in isolated installs.",
|
|
143
|
+
"migration": "Apps that embed the document designer add `@import \"@cosmicdrift/kumiko-document-designer/web/styles.css\";` after the renderer stylesheet import in their styles.css."
|
|
144
|
+
},
|
|
2
145
|
{
|
|
3
146
|
"version": "0.377.0",
|
|
4
147
|
"type": "improvement",
|
package/src/web/styles.css
CHANGED
|
@@ -34,12 +34,13 @@
|
|
|
34
34
|
imported through. Here that is `<package>/src/web` in both layouts, so:
|
|
35
35
|
- `../` is the renderer's own src (shared screens and components) and
|
|
36
36
|
`../../dist` its compiled twin;
|
|
37
|
-
- the sibling
|
|
38
|
-
ships its own compiled CSS) need two variants: `../../../X` in the
|
|
37
|
+
- the sibling package bundled-features (no compiled CSS of its own) needs two variants: `../../../X` in the
|
|
39
38
|
workspace (unscoped dir name) and `../../../kumiko-X` in a standalone
|
|
40
39
|
consumer (scoped registry name under node_modules/@cosmicdrift/).
|
|
41
40
|
A path that doesn't exist in a given layout is simply not matched, no
|
|
42
|
-
error.
|
|
41
|
+
error. The document designer is not scanned from here: it ships its own
|
|
42
|
+
`@cosmicdrift/kumiko-document-designer/web/styles.css`, which an app imports
|
|
43
|
+
after this file. samples is monorepo-only (in-repo sample build), a no-op elsewhere.
|
|
43
44
|
|
|
44
45
|
The published packages ship compiled dist (class names survive as string
|
|
45
46
|
literals in the .js), so every src line has a dist twin; src stays for
|
|
@@ -51,10 +52,6 @@
|
|
|
51
52
|
@source "../../../bundled-features/dist/**/*.js";
|
|
52
53
|
@source "../../../kumiko-bundled-features/src/**/*.{ts,tsx}";
|
|
53
54
|
@source "../../../kumiko-bundled-features/dist/**/*.js";
|
|
54
|
-
@source "../../../document-designer/src/**/*.{ts,tsx}";
|
|
55
|
-
@source "../../../document-designer/dist/**/*.js";
|
|
56
|
-
@source "../../../kumiko-document-designer/src/**/*.{ts,tsx}";
|
|
57
|
-
@source "../../../kumiko-document-designer/dist/**/*.js";
|
|
58
55
|
@source "../../../../samples/**/src/**/*.{ts,tsx}";
|
|
59
56
|
/* In the monorepo `tsc --build` emits composite output (incl. compiled tests
|
|
60
57
|
and stale files of deleted components) into dist; scanning it would make the
|
|
@@ -62,8 +59,6 @@
|
|
|
62
59
|
@source not "../../dist/**/__tests__/**";
|
|
63
60
|
@source not "../../../bundled-features/dist/**/__tests__/**";
|
|
64
61
|
@source not "../../../kumiko-bundled-features/dist/**/__tests__/**";
|
|
65
|
-
@source not "../../../document-designer/dist/**/__tests__/**";
|
|
66
|
-
@source not "../../../kumiko-document-designer/dist/**/__tests__/**";
|
|
67
62
|
|
|
68
63
|
/* Dark-Mode via class-selector (nicht media). Erlaubt JS-Toggle
|
|
69
64
|
und überschreibt System-Preference wenn die App will. */
|
|
@@ -301,6 +296,15 @@
|
|
|
301
296
|
--shell-header-height: 3.5rem;
|
|
302
297
|
}
|
|
303
298
|
|
|
299
|
+
/* A non-modal side Drawer publishes the width it occupies (widgets/drawer.tsx); the content area
|
|
300
|
+
narrows by that much instead of sitting underneath. Only <main>: the drawer starts below the
|
|
301
|
+
header, and the pinned form footer lives inside <main>. No transition: the sheet slides by
|
|
302
|
+
transform, its width never animates. */
|
|
303
|
+
[data-slot="sidebar-inset"] > main {
|
|
304
|
+
padding-inline-end: var(--kumiko-drawer-inset-right, 0px);
|
|
305
|
+
padding-inline-start: var(--kumiko-drawer-inset-left, 0px);
|
|
306
|
+
}
|
|
307
|
+
|
|
304
308
|
/* Light-Mode-Overrides — aktiv wenn `<html>` KEINE `.dark` Klasse
|
|
305
309
|
hat. Dark ist der Default (siehe @theme oben); toggled der User
|
|
306
310
|
auf light, greifen diese Werte. */
|