@cosmicdrift/kumiko-renderer 0.383.0 → 0.384.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/headless/format/index.js +22 -3
- package/dist/headless/view-model/edit.js +7 -1
- package/dist/web/primitives/calendar-day-picker.d.ts +13 -0
- package/dist/web/primitives/calendar-day-picker.js +38 -0
- package/dist/web/primitives/calendar-popover.d.ts +1 -1
- package/dist/web/primitives/calendar-popover.js +5 -39
- package/dist/web/primitives/file-thumbnail.js +69 -7
- package/dist/web/primitives/index.js +5 -4
- package/dist/web/tokens.js +2 -1
- package/package.json +5 -5
- package/src/changes.json +18 -0
|
@@ -68,11 +68,11 @@ function formatDateCell(value, type, opts) {
|
|
|
68
68
|
// No `locale` fallback to a browser API here (headless has none) — passing
|
|
69
69
|
// `undefined` to Intl.NumberFormat resolves the runtime's default locale,
|
|
70
70
|
// the same value a browser's navigator.language-based guess would produce.
|
|
71
|
-
function formatNumberCell(value, locale, grouping = true) {
|
|
71
|
+
function formatNumberCell(value, locale, grouping = true, fractionDigits = {}) {
|
|
72
72
|
if (typeof value !== "number" || !Number.isFinite(value))
|
|
73
73
|
return String(value);
|
|
74
74
|
try {
|
|
75
|
-
return new Intl.NumberFormat(locale, { useGrouping: grouping }).format(value);
|
|
75
|
+
return new Intl.NumberFormat(locale, { useGrouping: grouping, ...fractionDigits }).format(value);
|
|
76
76
|
}
|
|
77
77
|
catch {
|
|
78
78
|
// Malformed BCP-47 locale tag — mirrors formatDateCell's fallback above.
|
|
@@ -152,6 +152,25 @@ function stringSpec(spec, key) {
|
|
|
152
152
|
const v = spec[key];
|
|
153
153
|
return typeof v === "string" ? v : undefined;
|
|
154
154
|
}
|
|
155
|
+
const MAX_FRACTION_DIGITS = 20;
|
|
156
|
+
function fractionDigitSpec(spec, key) {
|
|
157
|
+
const v = spec[key];
|
|
158
|
+
return typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= MAX_FRACTION_DIGITS
|
|
159
|
+
? v
|
|
160
|
+
: undefined;
|
|
161
|
+
}
|
|
162
|
+
// Out-of-range or inverted options fall back to the Intl default instead of
|
|
163
|
+
// throwing: a bad schema option must not crash the screen it renders in.
|
|
164
|
+
function fractionDigitsSpec(spec) {
|
|
165
|
+
const min = fractionDigitSpec(spec, "minimumFractionDigits");
|
|
166
|
+
const max = fractionDigitSpec(spec, "maximumFractionDigits");
|
|
167
|
+
if (min !== undefined && max !== undefined && min > max)
|
|
168
|
+
return {};
|
|
169
|
+
return {
|
|
170
|
+
...(min !== undefined && { minimumFractionDigits: min }),
|
|
171
|
+
...(max !== undefined && { maximumFractionDigits: max }),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
155
174
|
function booleanSpec(spec, key) {
|
|
156
175
|
const v = spec[key];
|
|
157
176
|
return typeof v === "boolean" ? v : undefined;
|
|
@@ -197,7 +216,7 @@ export function applyFormatSpec(spec, value, translate) {
|
|
|
197
216
|
case "number":
|
|
198
217
|
case "decimal":
|
|
199
218
|
case "bigInt":
|
|
200
|
-
return formatNumberCell(value, stringSpec(spec, "locale"), booleanSpec(spec, "grouping") ?? true);
|
|
219
|
+
return formatNumberCell(value, stringSpec(spec, "locale"), booleanSpec(spec, "grouping") ?? true, spec.format === "bigInt" ? {} : fractionDigitsSpec(spec));
|
|
201
220
|
case "unit":
|
|
202
221
|
return formatUnitCell(value, stringSpec(spec, "unit"), stringSpec(spec, "locale"), unitDisplaySpec(spec));
|
|
203
222
|
case "priority": {
|
|
@@ -42,6 +42,12 @@ function computeWriteFormSectionViewModel(sectionSpec, screenId, values, transla
|
|
|
42
42
|
...(sectionSpec.actions !== undefined && { actions: sectionSpec.actions }),
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
+
// A section whose only content is an action (a conditional field plus a delete button) stays on the screen.
|
|
46
|
+
function hasVisibleContent(fields, actions, values) {
|
|
47
|
+
if (fields.some((field) => field.visible))
|
|
48
|
+
return true;
|
|
49
|
+
return (actions ?? []).some((action) => action.visible === undefined || evalFieldCondition(action.visible, values));
|
|
50
|
+
}
|
|
45
51
|
function wasEmptiedByWriteExclusion(declaredFieldSpecs, remainingFields) {
|
|
46
52
|
return declaredFieldSpecs.length > 0 && remainingFields.length === 0;
|
|
47
53
|
}
|
|
@@ -459,7 +465,7 @@ export function computeEditViewModel(input) {
|
|
|
459
465
|
});
|
|
460
466
|
// The boot-validator rejects declared-empty sections (screens.ts), but a
|
|
461
467
|
// create-excluded field list can still empty one here: then `visible` is false.
|
|
462
|
-
const visible = fields.
|
|
468
|
+
const visible = hasVisibleContent(fields, sectionSpec.actions, values);
|
|
463
469
|
// Tabs mode renders this section as its own panel, where two columns reads
|
|
464
470
|
// better than the single-column default a stacked form keeps. Applies to
|
|
465
471
|
// entityEdit tabs as well since fw#3134, not just projectionDetail.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type CalendarDayPickerProps = {
|
|
3
|
+
readonly selected: Date | undefined;
|
|
4
|
+
readonly onSelect: (d: Date | undefined) => void;
|
|
5
|
+
readonly startMonth: Date;
|
|
6
|
+
readonly endMonth: Date;
|
|
7
|
+
readonly disabledMatchers: readonly ({
|
|
8
|
+
before: Date;
|
|
9
|
+
} | {
|
|
10
|
+
after: Date;
|
|
11
|
+
})[];
|
|
12
|
+
};
|
|
13
|
+
export default function CalendarDayPicker({ selected, onSelect, startMonth, endMonth, disabledMatchers, }: CalendarDayPickerProps): ReactNode;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { DayPicker } from "react-day-picker";
|
|
3
|
+
import { cn } from "../lib/cn.js";
|
|
4
|
+
const dropdownSelectClass = "rounded-sm border border-input bg-transparent px-1.5 py-0.5 text-sm font-medium " +
|
|
5
|
+
"cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring";
|
|
6
|
+
// rdp v9's default dropdown renders an aria-hidden <span> with the same label
|
|
7
|
+
// next to the <select>, hidden only by rdp's own style.css. Our token classes
|
|
8
|
+
// replace that CSS, so the label showed twice; a bare <select> avoids it
|
|
9
|
+
// regardless of CSS.
|
|
10
|
+
function SelectDropdown({ options, className, ...selectProps }) {
|
|
11
|
+
return (_jsx("select", { className: cn(dropdownSelectClass, className), ...selectProps, children: options?.map((opt) => (_jsx("option", { value: opt.value, disabled: opt.disabled, children: opt.label }, opt.value))) }));
|
|
12
|
+
}
|
|
13
|
+
// Month/year selects come from SelectDropdown (components prop), so only the dropdowns container is styled here.
|
|
14
|
+
const dayPickerClasses = {
|
|
15
|
+
root: "rdp-root",
|
|
16
|
+
months: "flex flex-col gap-2",
|
|
17
|
+
month: "flex flex-col gap-2",
|
|
18
|
+
month_caption: "flex justify-center items-center h-7 text-sm font-medium",
|
|
19
|
+
dropdowns: "flex items-center gap-1 text-sm font-medium",
|
|
20
|
+
nav: "flex items-center gap-1 absolute right-1 top-2",
|
|
21
|
+
button_previous: "inline-flex h-7 w-7 items-center justify-center rounded-sm hover:bg-accent",
|
|
22
|
+
button_next: "inline-flex h-7 w-7 items-center justify-center rounded-sm hover:bg-accent",
|
|
23
|
+
weekdays: "flex",
|
|
24
|
+
weekday: "w-8 text-xs font-normal text-muted-foreground",
|
|
25
|
+
week: "flex mt-1",
|
|
26
|
+
day: "w-8 h-8 p-0 text-sm",
|
|
27
|
+
day_button: "inline-flex h-8 w-8 items-center justify-center rounded-sm hover:bg-accent " +
|
|
28
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
29
|
+
selected: "[&_button]:bg-primary [&_button]:text-primary-foreground [&_button]:hover:bg-primary/90",
|
|
30
|
+
today: "[&_button]:underline",
|
|
31
|
+
outside: "text-muted-foreground/50",
|
|
32
|
+
disabled: "text-muted-foreground/30 pointer-events-none",
|
|
33
|
+
};
|
|
34
|
+
export default function CalendarDayPicker({ selected, onSelect, startMonth, endMonth, disabledMatchers, }) {
|
|
35
|
+
return (_jsx(DayPicker, { mode: "single", captionLayout: "dropdown", startMonth: startMonth, endMonth: endMonth, selected: selected,
|
|
36
|
+
// Without defaultMonth DayPicker opens on today, not on the chosen date.
|
|
37
|
+
defaultMonth: selected, ...(disabledMatchers.length > 0 && { disabled: [...disabledMatchers] }), onSelect: (d) => onSelect(d), classNames: dayPickerClasses, components: { Dropdown: SelectDropdown } }));
|
|
38
|
+
}
|
|
@@ -11,8 +11,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
// teilen beide dieselbe Kalender-Mechanik ohne ihre Eingabe-Form zu teilen.
|
|
12
12
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
13
13
|
import { Calendar as CalendarIcon } from "lucide-react";
|
|
14
|
-
import {
|
|
14
|
+
import { lazy, Suspense } from "react";
|
|
15
15
|
import { cn } from "../lib/cn.js";
|
|
16
|
+
const CalendarDayPicker = lazy(() => import("./calendar-day-picker.js"));
|
|
17
|
+
// Same footprint as the loaded calendar card, so opening the popover does not jump.
|
|
18
|
+
const CALENDAR_PLACEHOLDER_CLASS = "h-[17rem] w-56";
|
|
16
19
|
export const inFieldIconButtonClass = "absolute inset-y-0 right-0 inline-flex w-9 items-center justify-center rounded-md " +
|
|
17
20
|
"text-muted-foreground transition-colors hover:text-foreground " +
|
|
18
21
|
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring " +
|
|
@@ -35,42 +38,5 @@ export function CalendarPopover({ selected, onSelect, min, max, disabled, hasErr
|
|
|
35
38
|
...(min !== undefined ? [{ before: min }] : []),
|
|
36
39
|
...(max !== undefined ? [{ after: max }] : []),
|
|
37
40
|
];
|
|
38
|
-
return (_jsxs(PopoverPrimitive.Root, { children: [_jsx(PopoverPrimitive.Trigger, { asChild: true, children: _jsx("button", { type: "button", disabled: disabled, "aria-label": triggerLabel, "aria-invalid": hasError === true ? true : undefined, className: cn(inFieldIconButtonClass, hasError === true && "text-destructive"), children: _jsx(CalendarIcon, { className: "size-4", "aria-hidden": "true" }) }) }), _jsx(PopoverPrimitive.Portal, { children: _jsx(PopoverPrimitive.Content, { className: popoverClass, align: "end", sideOffset: 4, children: _jsx(
|
|
39
|
-
// Ohne defaultMonth zeigt DayPicker today statt selected —
|
|
40
|
-
// unintuitiv wenn der User schon ein Datum gewählt hat.
|
|
41
|
-
defaultMonth: selected, ...(disabledMatchers.length > 0 && { disabled: disabledMatchers }), onSelect: (d) => onSelect(d), classNames: dayPickerClasses, components: { Dropdown: SelectDropdown } }) }) })] }));
|
|
41
|
+
return (_jsxs(PopoverPrimitive.Root, { children: [_jsx(PopoverPrimitive.Trigger, { asChild: true, children: _jsx("button", { type: "button", disabled: disabled, "aria-label": triggerLabel, "aria-invalid": hasError === true ? true : undefined, className: cn(inFieldIconButtonClass, hasError === true && "text-destructive"), children: _jsx(CalendarIcon, { className: "size-4", "aria-hidden": "true" }) }) }), _jsx(PopoverPrimitive.Portal, { children: _jsx(PopoverPrimitive.Content, { className: popoverClass, align: "end", sideOffset: 4, children: _jsx(Suspense, { fallback: _jsx("div", { className: CALENDAR_PLACEHOLDER_CLASS, "aria-hidden": "true" }), children: _jsx(CalendarDayPicker, { selected: selected, onSelect: onSelect, startMonth: startMonth, endMonth: endMonth, disabledMatchers: disabledMatchers }) }) }) })] }));
|
|
42
42
|
}
|
|
43
|
-
const dropdownSelectClass = "rounded-sm border border-input bg-transparent px-1.5 py-0.5 text-sm font-medium " +
|
|
44
|
-
"cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring";
|
|
45
|
-
// rdp v9s Default-Dropdown rendert NEBEN dem <select> ein aria-hidden
|
|
46
|
-
// <span> mit demselben Label — sichtbar nur, wenn rdps eigene style.css
|
|
47
|
-
// das <select> transparent darüberlegt. Da wir die rdp-Klassen mit eigenen
|
|
48
|
-
// Tokens überschreiben, greift diese Positionierung nicht → Label doppelt
|
|
49
|
-
// (#369-Folgebug). Ein nacktes <select> ohne Begleit-Span vermeidet das
|
|
50
|
-
// CSS-unabhängig — eine Quelle für den sichtbaren Wert.
|
|
51
|
-
function SelectDropdown({ options, className, ...selectProps }) {
|
|
52
|
-
return (_jsx("select", { className: cn(dropdownSelectClass, className), ...selectProps, children: options?.map((opt) => (_jsx("option", { value: opt.value, disabled: opt.disabled, children: opt.label }, opt.value))) }));
|
|
53
|
-
}
|
|
54
|
-
// classNames-Map für react-day-picker v9 — überschreibt die default-
|
|
55
|
-
// Klassen mit Tailwind/shadcn-Tokens. Die Monat-/Jahr-Selects rendert
|
|
56
|
-
// SelectDropdown (components-Prop), daher hier nur der dropdowns-Container.
|
|
57
|
-
const dayPickerClasses = {
|
|
58
|
-
root: "rdp-root",
|
|
59
|
-
months: "flex flex-col gap-2",
|
|
60
|
-
month: "flex flex-col gap-2",
|
|
61
|
-
month_caption: "flex justify-center items-center h-7 text-sm font-medium",
|
|
62
|
-
dropdowns: "flex items-center gap-1 text-sm font-medium",
|
|
63
|
-
nav: "flex items-center gap-1 absolute right-1 top-2",
|
|
64
|
-
button_previous: "inline-flex h-7 w-7 items-center justify-center rounded-sm hover:bg-accent",
|
|
65
|
-
button_next: "inline-flex h-7 w-7 items-center justify-center rounded-sm hover:bg-accent",
|
|
66
|
-
weekdays: "flex",
|
|
67
|
-
weekday: "w-8 text-xs font-normal text-muted-foreground",
|
|
68
|
-
week: "flex mt-1",
|
|
69
|
-
day: "w-8 h-8 p-0 text-sm",
|
|
70
|
-
day_button: "inline-flex h-8 w-8 items-center justify-center rounded-sm hover:bg-accent " +
|
|
71
|
-
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
72
|
-
selected: "[&_button]:bg-primary [&_button]:text-primary-foreground [&_button]:hover:bg-primary/90",
|
|
73
|
-
today: "[&_button]:underline",
|
|
74
|
-
outside: "text-muted-foreground/50",
|
|
75
|
-
disabled: "text-muted-foreground/30 pointer-events-none",
|
|
76
|
-
};
|
|
@@ -29,7 +29,7 @@ function ThumbnailChain({ fileId, variant, className, testId, }) {
|
|
|
29
29
|
if (src === undefined) {
|
|
30
30
|
return (_jsx("div", { "data-testid": testId, className: cn("bg-muted text-muted-foreground flex items-center justify-center rounded-md border", className), children: _jsx(ImageIcon, { className: "size-1/3" }) }));
|
|
31
31
|
}
|
|
32
|
-
return (_jsx("img", { src: src, alt: "", "data-testid": testId, onError: () => setFailedCount((n) => n + 1), className: cn("bg-muted rounded-md border object-contain", className) }));
|
|
32
|
+
return (_jsx("img", { src: src, alt: "", loading: "lazy", decoding: "async", "data-testid": testId, onError: () => setFailedCount((n) => n + 1), className: cn("bg-muted rounded-md border object-contain", className) }));
|
|
33
33
|
}
|
|
34
34
|
export function FileThumbnail({ fileId, variant, className = "h-24 w-32", testId, }) {
|
|
35
35
|
return (_jsx(ThumbnailChain, { fileId: fileId, variant: variant, className: className, testId: testId }, `${fileId}:${variant ?? ""}`));
|
|
@@ -42,6 +42,71 @@ function parseFileMeta(value) {
|
|
|
42
42
|
const mimeType = "mimeType" in value && typeof value.mimeType === "string" ? value.mimeType : undefined;
|
|
43
43
|
return { fileName: value.fileName, mimeType };
|
|
44
44
|
}
|
|
45
|
+
const META_BATCH_SIZE = 100;
|
|
46
|
+
const META_CACHE_LIMIT = 500;
|
|
47
|
+
const metaCache = new Map();
|
|
48
|
+
let pendingMetaIds = new Map();
|
|
49
|
+
function parseBatchMeta(json) {
|
|
50
|
+
const metaById = new Map();
|
|
51
|
+
if (typeof json !== "object" || json === null || !("files" in json))
|
|
52
|
+
return metaById;
|
|
53
|
+
if (!Array.isArray(json.files))
|
|
54
|
+
return metaById;
|
|
55
|
+
for (const file of json.files) {
|
|
56
|
+
const meta = parseFileMeta(file);
|
|
57
|
+
if (meta !== null && typeof file === "object" && file !== null && "id" in file) {
|
|
58
|
+
if (typeof file.id === "string")
|
|
59
|
+
metaById.set(file.id, meta);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return metaById;
|
|
63
|
+
}
|
|
64
|
+
async function fetchMetaBatch(ids, resolvers) {
|
|
65
|
+
let metaById = null;
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetch(`/api/files/meta?ids=${ids.map(encodeURIComponent).join(",")}`);
|
|
68
|
+
if (res.ok)
|
|
69
|
+
metaById = parseBatchMeta(await res.json());
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// skip: a failed lookup shows the fallback label, like a missing file
|
|
73
|
+
}
|
|
74
|
+
for (const id of ids) {
|
|
75
|
+
// A transport failure must not stick in the cache; an id the server left out is a final answer.
|
|
76
|
+
if (metaById === null)
|
|
77
|
+
metaCache.delete(id);
|
|
78
|
+
resolvers.get(id)?.(metaById?.get(id) ?? null);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function flushPendingMetaIds() {
|
|
82
|
+
const batch = pendingMetaIds;
|
|
83
|
+
pendingMetaIds = new Map();
|
|
84
|
+
const ids = [...batch.keys()];
|
|
85
|
+
for (let start = 0; start < ids.length; start += META_BATCH_SIZE) {
|
|
86
|
+
void fetchMetaBatch(ids.slice(start, start + META_BATCH_SIZE), batch);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function loadFileMeta(fileId) {
|
|
90
|
+
const cached = metaCache.get(fileId);
|
|
91
|
+
if (cached)
|
|
92
|
+
return cached;
|
|
93
|
+
const promise = new Promise((resolve) => {
|
|
94
|
+
if (pendingMetaIds.size === 0)
|
|
95
|
+
queueMicrotask(flushPendingMetaIds);
|
|
96
|
+
pendingMetaIds.set(fileId, resolve);
|
|
97
|
+
});
|
|
98
|
+
if (metaCache.size >= META_CACHE_LIMIT) {
|
|
99
|
+
// An id still waiting for its batch keeps its entry, or its promise would never settle.
|
|
100
|
+
for (const key of metaCache.keys()) {
|
|
101
|
+
if (pendingMetaIds.has(key))
|
|
102
|
+
continue;
|
|
103
|
+
metaCache.delete(key);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
metaCache.set(fileId, promise);
|
|
108
|
+
return promise;
|
|
109
|
+
}
|
|
45
110
|
export function useFileMeta(fileId) {
|
|
46
111
|
const [meta, setMeta] = useState(null);
|
|
47
112
|
const validId = isUuid(fileId);
|
|
@@ -50,13 +115,10 @@ export function useFileMeta(fileId) {
|
|
|
50
115
|
setMeta(null);
|
|
51
116
|
if (!validId)
|
|
52
117
|
return;
|
|
53
|
-
|
|
54
|
-
.then((res) => (res.ok ? res.json() : null))
|
|
55
|
-
.then((json) => {
|
|
118
|
+
void loadFileMeta(fileId).then((loaded) => {
|
|
56
119
|
if (!cancelled)
|
|
57
|
-
setMeta(
|
|
58
|
-
})
|
|
59
|
-
.catch(() => { });
|
|
120
|
+
setMeta(loaded);
|
|
121
|
+
});
|
|
60
122
|
return () => {
|
|
61
123
|
cancelled = true;
|
|
62
124
|
};
|
|
@@ -4,9 +4,6 @@ import { PII_ERASED_SENTINEL } from "@cosmicdrift/kumiko-types/kms-adapter-types
|
|
|
4
4
|
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
5
5
|
import { cva } from "class-variance-authority";
|
|
6
6
|
import { ArrowDown, ArrowUp, ArrowUpDown, Building, CalendarDays, Check, ChevronDown, ChevronLeft, ChevronRight, Globe, Hash, KeyRound, Link, Loader2, Lock, Mail, MapPin, MoreHorizontal, Phone, Search, SlidersHorizontal, Tag, Undo2, User, X, } from "lucide-react";
|
|
7
|
-
// The main entry pulls Node-only deps (yargs/pngjs) under bundlers that ignore
|
|
8
|
-
// package.json#browser (Metro), hence the explicit browser subpath.
|
|
9
|
-
import QRCode from "qrcode/lib/browser.js";
|
|
10
7
|
import { Children, createContext, Fragment, isValidElement, useContext, useEffect, useId, useRef, useState, } from "react";
|
|
11
8
|
import { fieldIssueText } from "../../field-issue-text.js";
|
|
12
9
|
import { applyFormatSpec, EMPTY_VALUE_PLACEHOLDER, isSafeHref } from "../../headless/index.js";
|
|
@@ -1971,7 +1968,11 @@ function QrSecretValue({ value }) {
|
|
|
1971
1968
|
useEffect(() => {
|
|
1972
1969
|
let cancelled = false;
|
|
1973
1970
|
setFailed(false);
|
|
1974
|
-
|
|
1971
|
+
// Loaded on first QR render, not with the primitives. The main entry pulls
|
|
1972
|
+
// Node-only deps (yargs/pngjs) under bundlers that ignore package.json#browser
|
|
1973
|
+
// (Metro), hence the explicit browser subpath.
|
|
1974
|
+
import("qrcode/lib/browser.js")
|
|
1975
|
+
.then(({ default: QRCode }) => QRCode.toString(value, { type: "svg", errorCorrectionLevel: "H" }))
|
|
1975
1976
|
.then((result) => {
|
|
1976
1977
|
if (!cancelled)
|
|
1977
1978
|
setSvg(result);
|
package/dist/web/tokens.js
CHANGED
|
@@ -20,7 +20,8 @@ import { DEFAULT_COLOR_SCHEME_VARIABLE } from "./default-color-scheme.js";
|
|
|
20
20
|
// AppTheme.defaultColorScheme (CSS variable) applies, otherwise the HTML state.
|
|
21
21
|
//
|
|
22
22
|
// Against FOUC the host HTML also needs a synchronous inline script BEFORE
|
|
23
|
-
// the stylesheet link
|
|
23
|
+
// the stylesheet link. Under a strict CSP its hash is the sha256 in the build's
|
|
24
|
+
// csp-hashes.json, which runProdApp also hands to hostDispatch as deps.cspScriptHashes:
|
|
24
25
|
//
|
|
25
26
|
// <script>try{var s=localStorage.getItem("kumiko:theme");
|
|
26
27
|
// if(s==="dark"||(s==="auto"&&matchMedia("(prefers-color-scheme: dark)").matches))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.384.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>",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
52
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
51
|
+
"@cosmicdrift/kumiko-framework": "0.384.0",
|
|
52
|
+
"@cosmicdrift/kumiko-types": "0.384.0",
|
|
53
53
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
54
54
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
55
55
|
"@radix-ui/react-label": "^2.1.8",
|
|
@@ -92,8 +92,8 @@
|
|
|
92
92
|
}
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
96
|
-
"@cosmicdrift/kumiko-testing": "0.
|
|
95
|
+
"@cosmicdrift/kumiko-locale-de": "0.384.0",
|
|
96
|
+
"@cosmicdrift/kumiko-testing": "0.384.0",
|
|
97
97
|
"@tailwindcss/cli": "^4.3.0",
|
|
98
98
|
"@testing-library/react": "^16.3.2",
|
|
99
99
|
"@testing-library/user-event": "^14.6.1",
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.384.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "A section with a visible action is not hidden when all its fields are",
|
|
6
|
+
"detail": "Edit and detail sections (entityEdit, projectionDetail, in every app) whose fields are all hidden by visibility conditions now stay on screen while at least one of their actions is visible. Before, such a section vanished together with its action buttons; apps that relied on that to hide a section must hide the action with its own visible condition."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.384.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "Fraction digits for number and decimal formats",
|
|
12
|
+
"detail": "{ format: \"number\" | \"decimal\", maximumFractionDigits: 4 } renders 0.0004 as 0.0004 (de: 0,0004) instead of 0; minimumFractionDigits pads. Integers 0 to 20 with min not above max; anything else is ignored and the Intl default (3 digits) applies, so existing formats render unchanged."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.384.0",
|
|
16
|
+
"type": "improvement",
|
|
17
|
+
"title": "DayPicker and QR code load on demand",
|
|
18
|
+
"detail": "The calendar popover and the QR secret value import react-day-picker and qrcode lazily, so screens without a date input or an MFA setup no longer ship them. Date popovers render their grid one tick later; tests should use findBy queries."
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"version": "0.383.0",
|
|
4
22
|
"type": "improvement",
|