@cosmicdrift/kumiko-renderer 0.377.0 → 0.378.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/kumiko-screen.js +3 -1
- 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/render-edit-types.d.ts +4 -1
- package/dist/components/render-edit.js +14 -1
- package/dist/components/render-field.js +8 -1
- 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/view-model/edit.d.ts +4 -1
- package/dist/headless/view-model/edit.js +14 -3
- package/dist/headless/view-model/types.d.ts +1 -1
- package/dist/i18n-defaults.js +6 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/primitives.d.ts +10 -0
- package/dist/web/index.d.ts +2 -2
- package/dist/web/index.js +1 -1
- package/dist/web/primitives/file-thumbnail.d.ts +6 -0
- package/dist/web/primitives/file-thumbnail.js +19 -19
- 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/index.js +3 -1
- 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/package.json +14 -5
- package/src/changes.json +39 -0
- package/src/web/styles.css +13 -9
package/dist/primitives.d.ts
CHANGED
|
@@ -1346,6 +1346,12 @@ export type JsonViewProps = {
|
|
|
1346
1346
|
readonly indent?: number;
|
|
1347
1347
|
readonly testId?: string;
|
|
1348
1348
|
};
|
|
1349
|
+
/** Read-only list of stored files (FileRef ids): name, download link and, for
|
|
1350
|
+
* PDFs, an inline preview. Used for `renderer: { format: "file" }` on a detail field. */
|
|
1351
|
+
export type FileValueListProps = {
|
|
1352
|
+
readonly fileIds: readonly string[];
|
|
1353
|
+
readonly testId?: string;
|
|
1354
|
+
};
|
|
1349
1355
|
/** Band above the tabs of a record detail: optional subtitle line plus the
|
|
1350
1356
|
* metrics as a definition list. `children` are `Metric` tiles. */
|
|
1351
1357
|
export type MetricBandProps = {
|
|
@@ -1528,6 +1534,10 @@ export type CorePrimitives = {
|
|
|
1528
1534
|
* CorePrimitives mocks in tests keep compiling — additive rollout of
|
|
1529
1535
|
* a new primitive shouldn't force every test double to grow a stub. */
|
|
1530
1536
|
readonly JsonView?: ComponentType<JsonViewProps>;
|
|
1537
|
+
/** Optional (unlike the other Core-Primitives) so existing partial
|
|
1538
|
+
* CorePrimitives mocks in tests keep compiling — additive rollout of
|
|
1539
|
+
* a new primitive shouldn't force every test double to grow a stub. */
|
|
1540
|
+
readonly FileValueList?: ComponentType<FileValueListProps>;
|
|
1531
1541
|
/** Optional (unlike the other Core-Primitives) so existing partial
|
|
1532
1542
|
* CorePrimitives mocks in tests keep compiling — additive rollout of
|
|
1533
1543
|
* a new primitive shouldn't force every test double to grow a stub. */
|
package/dist/web/index.d.ts
CHANGED
|
@@ -74,5 +74,5 @@ export { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "./ui/resiz
|
|
|
74
74
|
export { SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider } from "./ui/sidebar.tsx";
|
|
75
75
|
export type { KumikoBuild } from "./version/update-checker.tsx";
|
|
76
76
|
export { readLoadedBuild } from "./version/update-checker.tsx";
|
|
77
|
-
export type { AiTextAreaProps, AiTextFieldProps, BooleanFieldProps, ComparisonMetric, DateFieldProps, DrawerProps, FeedRow, FileFieldProps, FloatingPanelGeometry, FloatingPanelProps, InfinityListProps, InfinityListSelection, ModeSwitchVariant, NumberFieldProps, PhotoSlotSpec, PhotoSlotsProps, PlanCardActionSlot, PlanCardLinkSlot, PlanCardPrice, PlanCardProps, PlanGridProps, ProgressListRow, PublicShellProps, PublicShellVariant, QueryTableColumn, QueryTableProps, RangeFieldProps, ResultColumn, SelectFieldProps, SideBySideTableCell, SideBySideTableColumn, SideBySideTableRow, StatDelta, StatTone, StatusBadgeTone, StatusBarEntry, StatusTone, TextareaFieldProps, TextFieldProps, TimeseriesHeight, TimeseriesPoint, TimeseriesReferenceLine, TimeseriesXAxis, TimeseriesYAxis, UploadZoneProps, } from "./widgets/index.ts";
|
|
78
|
-
export { AiTextArea, AiTextField, BooleanField, CollapsibleSection, ComparisonTable, DateField, DetailList, Drawer, EmptyState, ErrorState, FeedList, FileField, FloatingPanel, InfinityList, LoadingState, MiniStat, ModeSwitch, MoneyField, NumberField, PercentField, PhotoSlots, PlanCard, PlanGrid, ProgressBar, ProgressList, PublicShell, QueryTable, RangeField, ResultPanel, ResultTable, SectionCard, SelectField, SideBySideTable, Sparkline, STATUS_TONE_TEXT, StatCard, StatusBadge, StatusBarChart, StepBar, smoothPath, TextareaField, TextField, TimeseriesChart, UploadZone, useDraft, } from "./widgets/index.ts";
|
|
77
|
+
export type { AiTextAreaProps, AiTextFieldProps, BooleanFieldProps, ComparisonMetric, DateFieldProps, DrawerProps, FeedRow, FileFieldProps, FloatingPanelGeometry, FloatingPanelProps, InfinityListProps, InfinityListSelection, ModeSwitchVariant, NumberFieldProps, PdfPreviewSource, PhotoSlotSpec, PhotoSlotsProps, PlanCardActionSlot, PlanCardLinkSlot, PlanCardPrice, PlanCardProps, PlanGridProps, ProgressListRow, PublicShellProps, PublicShellVariant, QueryTableColumn, QueryTableProps, RangeFieldProps, ResultColumn, SelectFieldProps, SideBySideTableCell, SideBySideTableColumn, SideBySideTableRow, StatDelta, StatTone, StatusBadgeTone, StatusBarEntry, StatusTone, TextareaFieldProps, TextFieldProps, TimeseriesHeight, TimeseriesPoint, TimeseriesReferenceLine, TimeseriesXAxis, TimeseriesYAxis, UploadZoneProps, } from "./widgets/index.ts";
|
|
78
|
+
export { AiTextArea, AiTextField, BooleanField, CollapsibleSection, ComparisonTable, DateField, DetailList, Drawer, EmptyState, ErrorState, FeedList, FileField, FloatingPanel, InfinityList, LoadingState, MiniStat, ModeSwitch, MoneyField, NumberField, PdfPreview, PercentField, PhotoSlots, PlanCard, PlanGrid, ProgressBar, ProgressList, PublicShell, QueryTable, RangeField, ResultPanel, ResultTable, SectionCard, SelectField, SideBySideTable, Sparkline, STATUS_TONE_TEXT, StatCard, StatusBadge, StatusBarChart, StepBar, smoothPath, TextareaField, TextField, TimeseriesChart, UploadZone, useDraft, } from "./widgets/index.ts";
|
package/dist/web/index.js
CHANGED
|
@@ -50,4 +50,4 @@ export { applyTokensToCssVars, defaultTokens, lightTokens, useBrowserTokensApi,
|
|
|
50
50
|
export { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "./ui/resizable.js";
|
|
51
51
|
export { SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider } from "./ui/sidebar.js";
|
|
52
52
|
export { readLoadedBuild } from "./version/update-checker.js";
|
|
53
|
-
export { AiTextArea, AiTextField, BooleanField, CollapsibleSection, ComparisonTable, DateField, DetailList, Drawer, EmptyState, ErrorState, FeedList, FileField, FloatingPanel, InfinityList, LoadingState, MiniStat, ModeSwitch, MoneyField, NumberField, PercentField, PhotoSlots, PlanCard, PlanGrid, ProgressBar, ProgressList, PublicShell, QueryTable, RangeField, ResultPanel, ResultTable, SectionCard, SelectField, SideBySideTable, Sparkline, STATUS_TONE_TEXT, StatCard, StatusBadge, StatusBarChart, StepBar, smoothPath, TextareaField, TextField, TimeseriesChart, UploadZone, useDraft, } from "./widgets/index.js";
|
|
53
|
+
export { AiTextArea, AiTextField, BooleanField, CollapsibleSection, ComparisonTable, DateField, DetailList, Drawer, EmptyState, ErrorState, FeedList, FileField, FloatingPanel, InfinityList, LoadingState, MiniStat, ModeSwitch, MoneyField, NumberField, PdfPreview, PercentField, PhotoSlots, PlanCard, PlanGrid, ProgressBar, ProgressList, PublicShell, QueryTable, RangeField, ResultPanel, ResultTable, SectionCard, SelectField, SideBySideTable, Sparkline, STATUS_TONE_TEXT, StatCard, StatusBadge, StatusBarChart, StepBar, smoothPath, TextareaField, TextField, TimeseriesChart, UploadZone, useDraft, } from "./widgets/index.js";
|
|
@@ -8,6 +8,11 @@ export declare function FileThumbnail({ fileId, variant, className, testId, }: {
|
|
|
8
8
|
readonly className?: string;
|
|
9
9
|
readonly testId?: string;
|
|
10
10
|
}): ReactNode;
|
|
11
|
+
type FileMeta = {
|
|
12
|
+
readonly fileName: string;
|
|
13
|
+
readonly mimeType: string | undefined;
|
|
14
|
+
};
|
|
15
|
+
export declare function useFileMeta(fileId: string): FileMeta | null;
|
|
11
16
|
export declare function FileNameLink({ fileId, fallbackLabel, className, }: {
|
|
12
17
|
readonly fileId: string;
|
|
13
18
|
readonly fallbackLabel: string;
|
|
@@ -23,3 +28,4 @@ export declare function FileDisplayCell({ value, display, fallbackLabel, compact
|
|
|
23
28
|
readonly fallbackLabel: string;
|
|
24
29
|
readonly compact?: boolean;
|
|
25
30
|
}): ReactNode;
|
|
31
|
+
export {};
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { isUuid } from "@cosmicdrift/kumiko-types/identifiers";
|
|
3
3
|
import { ImageIcon } from "lucide-react";
|
|
4
4
|
import { useEffect, useState } from "react";
|
|
5
|
+
import { fileIdsOf } from "../../file-ids-of.js";
|
|
5
6
|
import { cn } from "../lib/cn.js";
|
|
6
7
|
// Stored values are untrusted: only UUIDs reach a request, and path segments are
|
|
7
8
|
// encoded so an id like "../x" or "a?b" cannot redirect the request.
|
|
@@ -33,42 +34,41 @@ function ThumbnailChain({ fileId, variant, className, testId, }) {
|
|
|
33
34
|
export function FileThumbnail({ fileId, variant, className = "h-24 w-32", testId, }) {
|
|
34
35
|
return (_jsx(ThumbnailChain, { fileId: fileId, variant: variant, className: className, testId: testId }, `${fileId}:${variant ?? ""}`));
|
|
35
36
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
function parseFileMeta(value) {
|
|
38
|
+
if (typeof value !== "object" || value === null || !("fileName" in value))
|
|
39
|
+
return null;
|
|
40
|
+
if (typeof value.fileName !== "string")
|
|
41
|
+
return null;
|
|
42
|
+
const mimeType = "mimeType" in value && typeof value.mimeType === "string" ? value.mimeType : undefined;
|
|
43
|
+
return { fileName: value.fileName, mimeType };
|
|
41
44
|
}
|
|
42
|
-
export function
|
|
43
|
-
const [
|
|
45
|
+
export function useFileMeta(fileId) {
|
|
46
|
+
const [meta, setMeta] = useState(null);
|
|
44
47
|
const validId = isUuid(fileId);
|
|
45
48
|
useEffect(() => {
|
|
46
49
|
let cancelled = false;
|
|
47
|
-
|
|
50
|
+
setMeta(null);
|
|
48
51
|
if (!validId)
|
|
49
52
|
return;
|
|
50
53
|
fetch(`${fileOriginalUrl(fileId)}/meta`)
|
|
51
54
|
.then((res) => (res.ok ? res.json() : null))
|
|
52
|
-
.then((
|
|
53
|
-
if (!cancelled
|
|
54
|
-
|
|
55
|
+
.then((json) => {
|
|
56
|
+
if (!cancelled)
|
|
57
|
+
setMeta(parseFileMeta(json));
|
|
55
58
|
})
|
|
56
59
|
.catch(() => { });
|
|
57
60
|
return () => {
|
|
58
61
|
cancelled = true;
|
|
59
62
|
};
|
|
60
63
|
}, [fileId, validId]);
|
|
61
|
-
|
|
64
|
+
return meta;
|
|
65
|
+
}
|
|
66
|
+
export function FileNameLink({ fileId, fallbackLabel, className, }) {
|
|
67
|
+
const fileName = useFileMeta(fileId)?.fileName ?? null;
|
|
68
|
+
if (!isUuid(fileId))
|
|
62
69
|
return _jsx("span", { className: cn("text-sm", className), children: fallbackLabel });
|
|
63
70
|
return (_jsx("a", { href: fileOriginalUrl(fileId), target: "_blank", rel: "noreferrer", className: cn("text-sm underline underline-offset-2", className), children: fileName ?? fallbackLabel }));
|
|
64
71
|
}
|
|
65
|
-
function fileIdsOf(value) {
|
|
66
|
-
if (typeof value === "string")
|
|
67
|
-
return value === "" ? [] : [value];
|
|
68
|
-
if (!Array.isArray(value))
|
|
69
|
-
return [];
|
|
70
|
-
return value.filter((entry) => typeof entry === "string" && entry !== "");
|
|
71
|
-
}
|
|
72
72
|
// Height-bound, width follows the aspect ratio within a clamp, so a portrait and
|
|
73
73
|
// a landscape photo both read as a preview without stretching the column.
|
|
74
74
|
const TABLE_THUMBNAIL_CLASS = "h-16 min-w-16 max-w-24 object-cover";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { isUuid } from "@cosmicdrift/kumiko-types/identifiers";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useTranslation } from "../../index.js";
|
|
5
|
+
import { Button } from "../ui/button.js";
|
|
6
|
+
import { PdfPreview } from "../widgets/pdf-preview.js";
|
|
7
|
+
import { fileOriginalUrl, useFileMeta } from "./file-thumbnail.js";
|
|
8
|
+
const PDF_MIME_TYPE = "application/pdf";
|
|
9
|
+
function FileValueRow({ fileId, testId, }) {
|
|
10
|
+
const t = useTranslation();
|
|
11
|
+
const meta = useFileMeta(fileId);
|
|
12
|
+
const [previewOpen, setPreviewOpen] = useState(false);
|
|
13
|
+
if (!isUuid(fileId))
|
|
14
|
+
return _jsx("span", { className: "text-sm", children: fileId });
|
|
15
|
+
const downloadUrl = fileOriginalUrl(fileId);
|
|
16
|
+
return (_jsxs("li", { className: "flex flex-col gap-2", "data-testid": testId, children: [_jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsx("span", { className: "text-sm", children: meta?.fileName ?? fileId.slice(0, 8) }), _jsx("a", { href: downloadUrl, download: meta?.fileName ?? "", className: "text-primary text-sm underline underline-offset-2", ...(testId !== undefined && { "data-testid": `${testId}-download` }), children: t("kumiko.field.file-list.download") }), meta?.mimeType === PDF_MIME_TYPE && (_jsx(Button, { type: "button", variant: "link", size: "sm", "aria-expanded": previewOpen, onClick: () => setPreviewOpen((open) => !open), ...(testId !== undefined && { "data-testid": `${testId}-preview-toggle` }), children: t(previewOpen ? "kumiko.field.file-list.hidePreview" : "kumiko.field.file-list.preview") }))] }), previewOpen && (_jsx(PdfPreview, { source: { url: downloadUrl }, testId: `file-preview-${fileId}` }))] }));
|
|
17
|
+
}
|
|
18
|
+
export function FileValueList({ fileIds, testId }) {
|
|
19
|
+
if (fileIds.length === 0)
|
|
20
|
+
return null;
|
|
21
|
+
return (_jsx("ul", { className: "m-0 flex list-none flex-col gap-3 p-0", "data-testid": testId, children: fileIds.map((fileId) => (_jsx(FileValueRow, { fileId: fileId, ...(testId !== undefined && { testId: `${testId}-${fileId}` }) }, fileId))) }));
|
|
22
|
+
}
|
|
@@ -32,6 +32,7 @@ import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMe
|
|
|
32
32
|
import { EmbeddedListInput } from "./embedded-list-input.js";
|
|
33
33
|
import { CardLeadingImage, FileDisplayCell } from "./file-thumbnail.js";
|
|
34
34
|
import { FileUploadInput } from "./file-upload.js";
|
|
35
|
+
import { FileValueList } from "./file-value-list.js";
|
|
35
36
|
import { DefaultJsonView } from "./json-view.js";
|
|
36
37
|
import { screenFormColumnWidthClassName, screenPaddingClassName, screenWidthClassName, } from "./layout.js";
|
|
37
38
|
import { DefaultLightbox } from "./lightbox.js";
|
|
@@ -1920,7 +1921,7 @@ export function DefaultCard({ slots, options, className, testId, dataAttributes,
|
|
|
1920
1921
|
// The one region allowed to shrink and scroll internally — mirrors
|
|
1921
1922
|
// FormSections' fillHeight === true && "min-h-0" treatment of the
|
|
1922
1923
|
// section it wraps.
|
|
1923
|
-
fillHeight && "min-h-0 flex flex-col"), children: children })), s.footer !== undefined && (_jsx("div", { className: cn(cardFooter, footerInset && cardFooterInset, footerBordered && cardFooterBorder, fillHeight && "shrink-0"), children: s.footer }))] }));
|
|
1924
|
+
fillHeight && "min-h-0 flex flex-col"), children: children })), s.footer !== undefined && s.footer !== null && s.footer !== false && (_jsx("div", { className: cn(cardFooter, footerInset && cardFooterInset, footerBordered && cardFooterBorder, fillHeight && "shrink-0"), children: s.footer }))] }));
|
|
1924
1925
|
return (_jsx(InsideScreenFormColumnContext.Provider, { value: false, children: screenBody ? _jsx(FormScreenShell, { children: card }) : card }));
|
|
1925
1926
|
}
|
|
1926
1927
|
// otpauth:// enrollment URI → scannable QR (errorCorrectionLevel "H", ~30%
|
|
@@ -2011,6 +2012,7 @@ export const defaultPrimitives = {
|
|
|
2011
2012
|
MetricBand: DefaultMetricBand,
|
|
2012
2013
|
PageHeader: DefaultPageHeader,
|
|
2013
2014
|
JsonView: DefaultJsonView,
|
|
2015
|
+
FileValueList,
|
|
2014
2016
|
FillContainer: DefaultFillContainer,
|
|
2015
2017
|
ActionOverflowMenu,
|
|
2016
2018
|
SecretReveal: DefaultSecretReveal,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function createBoundedLoadCache<T>(max: number, dispose: (value: T) => void): (key: string, load: () => Promise<T>) => Promise<T>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Evicts the oldest entry once `max` is exceeded and disposes only that one,
|
|
2
|
+
// so several documents on one page keep their own loading tasks alive.
|
|
3
|
+
export function createBoundedLoadCache(max, dispose) {
|
|
4
|
+
const entries = new Map();
|
|
5
|
+
return (key, load) => {
|
|
6
|
+
const cached = entries.get(key);
|
|
7
|
+
if (cached !== undefined)
|
|
8
|
+
return cached;
|
|
9
|
+
const loading = load();
|
|
10
|
+
entries.set(key, loading);
|
|
11
|
+
// A failed load must not stick: the next mount retries instead of replaying the rejection.
|
|
12
|
+
loading.catch(() => {
|
|
13
|
+
if (entries.get(key) === loading)
|
|
14
|
+
entries.delete(key);
|
|
15
|
+
});
|
|
16
|
+
if (entries.size > max) {
|
|
17
|
+
const [oldestKey, oldest] = entries.entries().next().value ?? [];
|
|
18
|
+
if (oldestKey !== undefined && oldest !== undefined) {
|
|
19
|
+
entries.delete(oldestKey);
|
|
20
|
+
void oldest.then(dispose);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return loading;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Maximize2Icon, Minimize2Icon } from "lucide-react";
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
4
|
import { useTranslation } from "../../index.js";
|
|
5
5
|
import { clamp } from "../lib/clamp.js";
|
|
6
6
|
import { cn } from "../lib/cn.js";
|
|
@@ -78,6 +78,39 @@ function sidePanelClass(side, narrow, variant, belowHeader) {
|
|
|
78
78
|
return `inset-y-8 right-8 h-auto ${WIDTH_CLASS} rounded-[2rem] border shadow-2xl overflow-hidden`;
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
+
const DRAWER_INSET_PROPERTY = {
|
|
82
|
+
left: "--kumiko-drawer-inset-left",
|
|
83
|
+
right: "--kumiko-drawer-inset-right",
|
|
84
|
+
};
|
|
85
|
+
// Publishes the width a non-modal side drawer occupies so the shell content (styles.css) can
|
|
86
|
+
// narrow instead of sitting underneath it. Read from the layout box (offsetLeft/offsetWidth), not
|
|
87
|
+
// getBoundingClientRect: the slide-in animation moves the painted rect, and the observer would
|
|
88
|
+
// not fire again once it settles.
|
|
89
|
+
// ponytail: one owner per side, so two non-modal drawers open on the same side overwrite each
|
|
90
|
+
// other's value; a registry that takes the max is needed once a screen stacks them.
|
|
91
|
+
function usePublishDrawerInset(sheet, side, active) {
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (!active || sheet === null || (side !== "left" && side !== "right"))
|
|
94
|
+
return;
|
|
95
|
+
const property = DRAWER_INSET_PROPERTY[side];
|
|
96
|
+
const root = document.documentElement;
|
|
97
|
+
const publish = () => {
|
|
98
|
+
const inset = side === "right"
|
|
99
|
+
? window.innerWidth - sheet.offsetLeft
|
|
100
|
+
: sheet.offsetLeft + sheet.offsetWidth;
|
|
101
|
+
root.style.setProperty(property, `${Math.max(0, Math.round(inset))}px`);
|
|
102
|
+
};
|
|
103
|
+
publish();
|
|
104
|
+
const observer = new ResizeObserver(publish);
|
|
105
|
+
observer.observe(sheet);
|
|
106
|
+
window.addEventListener("resize", publish);
|
|
107
|
+
return () => {
|
|
108
|
+
observer.disconnect();
|
|
109
|
+
window.removeEventListener("resize", publish);
|
|
110
|
+
root.style.removeProperty(property);
|
|
111
|
+
};
|
|
112
|
+
}, [sheet, side, active]);
|
|
113
|
+
}
|
|
81
114
|
/** Slide-in panel beside a list (e.g. mail reader next to the inbox) —
|
|
82
115
|
* thin wrapper over the Sheet primitive with header/body/footer slots
|
|
83
116
|
* so screens skip per-route Radix boilerplate. */
|
|
@@ -95,6 +128,8 @@ export function Drawer({ open, onOpenChange, side = "right", title, description,
|
|
|
95
128
|
: Math.min(maxWidthPx, Math.round(window.innerWidth * 0.9));
|
|
96
129
|
const [resizedWidthPx, setResizedWidthPx] = useState(() => clamp(resize?.defaultWidthPx ?? defaultWidthFromViewport(), minWidthPx, effectiveMaxWidthPx()));
|
|
97
130
|
const [maximized, setMaximized] = useState(false);
|
|
131
|
+
const [sheet, setSheet] = useState(null);
|
|
132
|
+
usePublishDrawerInset(sheet, side, open && !modal && !narrow);
|
|
98
133
|
const blurPx = backdrop?.blurPx ?? DEFAULT_BLUR_PX;
|
|
99
134
|
const dimPercent = backdrop?.dimPercent ?? DEFAULT_DIM_PERCENT;
|
|
100
135
|
const overlayStyle = {
|
|
@@ -134,7 +169,7 @@ export function Drawer({ open, onOpenChange, side = "right", title, description,
|
|
|
134
169
|
// instead of resizing relative to what's on screen.
|
|
135
170
|
setResizedWidthPx((current) => clamp((maximized ? effectiveWidthPx : current) + delta, minWidthPx, effectiveMaxWidthPx()));
|
|
136
171
|
};
|
|
137
|
-
return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, modal: modal, children: _jsxs(DrawerSheetContent, { side: side, "data-testid": testId, overlayStyle: overlayStyle,
|
|
172
|
+
return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, modal: modal, children: _jsxs(DrawerSheetContent, { ref: setSheet, side: side, "data-testid": testId, overlayStyle: overlayStyle,
|
|
138
173
|
// Radix already drops the overlay and the focus trap for a
|
|
139
174
|
// non-modal Root; only the dismiss-on-outside-interaction is left
|
|
140
175
|
// to suppress, so a click into the page behind keeps the panel open.
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.378.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.378.0",
|
|
52
|
+
"@cosmicdrift/kumiko-types": "0.378.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.378.0",
|
|
96
|
+
"@cosmicdrift/kumiko-testing": "0.378.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,43 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.378.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Non-modal drawers narrow the content instead of covering it",
|
|
6
|
+
"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."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.378.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "Files fields show download and PDF preview",
|
|
12
|
+
"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.",
|
|
13
|
+
"migration": "PdfPreview is now exported from @cosmicdrift/kumiko-renderer/web (props: source {base64}|{url}, testId)."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"version": "0.378.0",
|
|
17
|
+
"type": "improvement",
|
|
18
|
+
"title": "FormScreenLayout gives custom screens the standard form screen layout",
|
|
19
|
+
"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.",
|
|
20
|
+
"migration": "Custom form screens can replace FormScreenShell + Card with FormScreenLayout + Section."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"version": "0.378.0",
|
|
24
|
+
"type": "fix",
|
|
25
|
+
"title": "Card renders no footer bar for an empty footer slot",
|
|
26
|
+
"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."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"version": "0.378.0",
|
|
30
|
+
"type": "improvement",
|
|
31
|
+
"title": "flow-diagram subpath",
|
|
32
|
+
"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."
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"version": "0.378.0",
|
|
36
|
+
"type": "breaking",
|
|
37
|
+
"title": "Renderer stylesheet no longer scans the document designer",
|
|
38
|
+
"detail": "The renderer stylesheet stopped scanning the document designer sources by relative path, which broke in isolated installs.",
|
|
39
|
+
"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."
|
|
40
|
+
},
|
|
2
41
|
{
|
|
3
42
|
"version": "0.377.0",
|
|
4
43
|
"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. */
|