@cosmicdrift/kumiko-renderer 0.387.0 → 0.388.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.
Files changed (68) hide show
  1. package/dist/app/action-form-body.js +10 -2
  2. package/dist/app/action-form-support.d.ts +1 -0
  3. package/dist/app/action-form-support.js +1 -1
  4. package/dist/app/config-edit-body.js +6 -2
  5. package/dist/app/entity-edit-prefill.d.ts +11 -0
  6. package/dist/app/entity-edit-prefill.js +9 -4
  7. package/dist/app/extension-sections.d.ts +5 -3
  8. package/dist/app/form-schema.js +1 -1
  9. package/dist/app/header-actions-bar.js +1 -1
  10. package/dist/components/edit-mounts.d.ts +2 -2
  11. package/dist/components/embedded-list-field.js +1 -1
  12. package/dist/components/related-list-section.js +14 -1
  13. package/dist/components/render-edit-action-button.d.ts +1 -3
  14. package/dist/components/render-edit-action-button.js +2 -2
  15. package/dist/components/render-edit.js +11 -8
  16. package/dist/components/render-field.js +5 -2
  17. package/dist/components/render-list.js +31 -18
  18. package/dist/flow-diagram/render.js +9 -1
  19. package/dist/headless/form/form-controller.js +8 -0
  20. package/dist/headless/form/types.d.ts +1 -0
  21. package/dist/headless/format/index.js +89 -64
  22. package/dist/headless/view-model/edit.js +2 -0
  23. package/dist/headless/view-model/list.js +184 -149
  24. package/dist/headless/view-model/types.d.ts +3 -0
  25. package/dist/hooks/live-refetch-scheduler.js +4 -0
  26. package/dist/hooks/use-reference-lookup.d.ts +9 -1
  27. package/dist/hooks/use-reference-lookup.js +17 -3
  28. package/dist/i18n-defaults.js +6 -2
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.js +1 -0
  31. package/dist/primitives.d.ts +19 -0
  32. package/dist/web/app/create-app.js +8 -3
  33. package/dist/web/app/dashboard-body.js +17 -1
  34. package/dist/web/app/dashboard-panels.js +12 -2
  35. package/dist/web/index.d.ts +1 -1
  36. package/dist/web/index.js +1 -1
  37. package/dist/web/layout/shell-header.js +2 -1
  38. package/dist/web/layout/uniform-bar-controls.d.ts +1 -0
  39. package/dist/web/layout/uniform-bar-controls.js +4 -0
  40. package/dist/web/lib/file-ref-download.js +4 -1
  41. package/dist/web/primitives/action-menu.js +1 -1
  42. package/dist/web/primitives/code-input.d.ts +6 -0
  43. package/dist/web/primitives/code-input.js +31 -0
  44. package/dist/web/primitives/data-table.js +4 -3
  45. package/dist/web/primitives/facet-filter.js +1 -1
  46. package/dist/web/primitives/file-thumbnail.d.ts +3 -0
  47. package/dist/web/primitives/file-thumbnail.js +5 -0
  48. package/dist/web/primitives/input.js +6 -1
  49. package/dist/web/primitives/layout-elements.js +6 -4
  50. package/dist/web/primitives/page-header.js +3 -1
  51. package/dist/web/primitives/row-actions.js +3 -3
  52. package/dist/web/primitives/use-focus-on-mount.d.ts +2 -0
  53. package/dist/web/primitives/use-focus-on-mount.js +9 -0
  54. package/dist/web/widgets/bounded-load-cache.d.ts +1 -1
  55. package/dist/web/widgets/bounded-load-cache.js +10 -4
  56. package/dist/web/widgets/choice-cards.d.ts +17 -0
  57. package/dist/web/widgets/choice-cards.js +12 -0
  58. package/dist/web/widgets/collapsible-section.js +1 -1
  59. package/dist/web/widgets/detail-list.d.ts +5 -1
  60. package/dist/web/widgets/detail-list.js +5 -1
  61. package/dist/web/widgets/index.d.ts +1 -0
  62. package/dist/web/widgets/index.js +1 -0
  63. package/dist/web/widgets/pdf-preview.js +1 -1
  64. package/dist/web/widgets/plan-card.d.ts +2 -0
  65. package/dist/web/widgets/plan-card.js +1 -1
  66. package/dist/web/widgets/upload-zone.js +13 -5
  67. package/package.json +5 -5
  68. package/src/changes.json +49 -0
@@ -1,6 +1,7 @@
1
1
  export { AiTextArea, type AiTextAreaProps, AiTextField, type AiTextFieldProps, } from "./ai-text-field.tsx";
2
2
  export type { StackedAreaDateFormat, StackedAreaInitialWindow, StackedAreaRangeOption, StackedAreaRanges, StackedAreaWindowSelection, } from "./chart-window.ts";
3
3
  export { type ChartMarker, type ChartSeries, type ChartTone, SegmentBarChart, type SegmentBarRow, StackedAreaChart, StackedBarChart, StatusBarChart, type StatusBarEntry, smoothPath, TimeseriesChart, type TimeseriesHeight, type TimeseriesPoint, type TimeseriesReferenceLine, type TimeseriesXAxis, type TimeseriesYAxis, } from "./charts.tsx";
4
+ export { ChoiceCards } from "./choice-cards.tsx";
4
5
  export { CollapsibleSection } from "./collapsible-section.tsx";
5
6
  export { type DashboardListColumn, DashboardListTable } from "./dashboard-list.tsx";
6
7
  export { DetailList } from "./detail-list.tsx";
@@ -4,6 +4,7 @@
4
4
  // styleguide sample.
5
5
  export { AiTextArea, AiTextField, } from "./ai-text-field.js";
6
6
  export { SegmentBarChart, StackedAreaChart, StackedBarChart, StatusBarChart, smoothPath, TimeseriesChart, } from "./charts.js";
7
+ export { ChoiceCards } from "./choice-cards.js";
7
8
  export { CollapsibleSection } from "./collapsible-section.js";
8
9
  export { DashboardListTable } from "./dashboard-list.js";
9
10
  export { DetailList } from "./detail-list.js";
@@ -10,7 +10,7 @@ const MAX_CACHED_DOCUMENTS = 4;
10
10
  const cachedDocument = createBoundedLoadCache(MAX_CACHED_DOCUMENTS, (loaded) => {
11
11
  if (loaded.ok)
12
12
  void loaded.loadingTask.destroy();
13
- });
13
+ }, (loaded) => loaded.ok);
14
14
  function decodeBase64(base64) {
15
15
  const binary = atob(base64);
16
16
  const bytes = new Uint8Array(binary.length);
@@ -2,6 +2,8 @@ import type { ReactNode } from "react";
2
2
  export type PlanCardPrice = {
3
3
  readonly amount: string;
4
4
  readonly period?: string;
5
+ /** Shown before the amount, e.g. "from" when prices differ per provider. */
6
+ readonly prefix?: string;
5
7
  };
6
8
  export type PlanCardActionSlot = {
7
9
  readonly label: string;
@@ -6,7 +6,7 @@ export function PlanCard({ title, description, price, features, current = false,
6
6
  const t = useTranslation();
7
7
  const compact = variant === "compact";
8
8
  const footer = cta !== undefined || ctaLink !== undefined || secondaryAction !== undefined ? (_jsxs("div", { className: "flex w-full flex-col gap-2", children: [ctaLink !== undefined && (_jsx(Link, { href: ctaLink.href, variant: "button", target: ctaLink.target, className: "w-full justify-center", testId: testId !== undefined ? `${testId}-cta` : "plan-card-cta", children: ctaLink.label })), ctaLink === undefined && cta !== undefined && (_jsx(Button, { width: "full", disabled: cta.disabled, onClick: cta.onClick, testId: testId !== undefined ? `${testId}-cta` : "plan-card-cta", children: cta.label })), secondaryAction !== undefined && (_jsx(Button, { variant: "secondary", width: "full", disabled: secondaryAction.disabled, onClick: secondaryAction.onClick, children: secondaryAction.label }))] })) : undefined;
9
- return (_jsxs(Card, { testId: testId ?? "plan-card", className: cn(current && "border-primary"), slots: { footer }, children: [_jsxs("div", { className: cn("flex items-start justify-between gap-2", compact && "items-center"), children: [_jsx("h2", { className: cn("font-semibold", compact ? "text-base" : "text-lg"), children: title }), current && (_jsx("span", { "data-testid": testId !== undefined ? `${testId}-current` : "plan-card-current", className: "inline-block rounded-xl bg-primary/10 px-2.5 py-1 text-xs font-semibold text-primary", children: t("kumiko.planCard.current") }))] }), description !== undefined && !compact && (_jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: description })), price !== undefined && (_jsx("div", { className: cn(compact ? "mt-1" : "mt-1 mb-3"), children: price === null ? (_jsx("span", { "data-testid": testId !== undefined ? `${testId}-price-unavailable` : "plan-card-price-unavailable", className: "text-sm text-muted-foreground", children: t("kumiko.planCard.priceUnavailable") })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-2xl font-bold", children: price.amount }), price.period !== undefined && (_jsx("span", { className: "ml-1 text-sm text-muted-foreground", children: price.period }))] })) })), !compact && features !== undefined && features.length > 0 && (_jsx("ul", { className: "space-y-1 text-sm", children: features.map((feature, index) => (_jsxs("li", { className: "flex gap-2", children: [_jsx("span", { "aria-hidden": "true", children: "\u2713" }), _jsx("span", { children: feature })] }, index))) }))] }));
9
+ return (_jsxs(Card, { testId: testId ?? "plan-card", className: cn(current && "border-primary"), slots: { footer }, children: [_jsxs("div", { className: cn("flex items-start justify-between gap-2", compact && "items-center"), children: [_jsx("h2", { className: cn("font-semibold", compact ? "text-base" : "text-lg"), children: title }), current && (_jsx("span", { "data-testid": testId !== undefined ? `${testId}-current` : "plan-card-current", className: "inline-block rounded-xl bg-primary/10 px-2.5 py-1 text-xs font-semibold text-primary", children: t("kumiko.planCard.current") }))] }), description !== undefined && !compact && (_jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: description })), price !== undefined && (_jsx("div", { className: cn(compact ? "mt-1" : "mt-1 mb-3"), children: price === null ? (_jsx("span", { "data-testid": testId !== undefined ? `${testId}-price-unavailable` : "plan-card-price-unavailable", className: "text-sm text-muted-foreground", children: t("kumiko.planCard.priceUnavailable") })) : (_jsxs(_Fragment, { children: [price.prefix !== undefined && (_jsx("span", { className: "mr-1 text-sm text-muted-foreground", children: price.prefix })), _jsx("span", { className: "text-2xl font-bold", children: price.amount }), price.period !== undefined && (_jsx("span", { className: "ml-1 text-sm text-muted-foreground", children: price.period }))] })) })), !compact && features !== undefined && features.length > 0 && (_jsx("ul", { className: "space-y-1 text-sm", children: features.map((feature, index) => (_jsxs("li", { className: "flex gap-2", children: [_jsx("span", { "aria-hidden": "true", children: "\u2713" }), _jsx("span", { children: feature })] }, index))) }))] }));
10
10
  }
11
11
  export function PlanGrid({ children, testId }) {
12
12
  return (_jsx("div", { "data-testid": testId, className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3", children: children }));
@@ -114,6 +114,18 @@ export function UploadZone({ onUpload, title, hint, accept, multiple = true, cap
114
114
  if (row.value !== undefined)
115
115
  onRemove?.(row.value);
116
116
  }
117
+ // Values the owner already held before any upload of this zone (editing a record with files).
118
+ // Hidden while an upload runs: its value reaches `doneValues` one render before its row is done.
119
+ const uploading = rows.some((row) => row.status === "uploading");
120
+ const existingRows = doneValues === undefined || uploading
121
+ ? []
122
+ : doneValues
123
+ .filter((value) => !rows.some((row) => row.value === value))
124
+ .map((value) => ({ id: `existing:${value}`, fileName: value, status: "done", value }));
125
+ const visibleRows = [
126
+ ...existingRows,
127
+ ...rows.filter((row) => row.value === undefined || doneValues === undefined || doneValues.includes(row.value)),
128
+ ];
117
129
  const acceptAttr = toAcceptAttr(accept);
118
130
  return (_jsxs("div", { "data-testid": testId, className: "flex flex-col gap-4", children: [_jsxs("label", { htmlFor: inputId, onDragOver: (e) => {
119
131
  e.preventDefault();
@@ -123,9 +135,5 @@ export function UploadZone({ onUpload, title, hint, accept, multiple = true, cap
123
135
  if (!(e.relatedTarget instanceof Node) || !e.currentTarget.contains(e.relatedTarget)) {
124
136
  setDragOver(false);
125
137
  }
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))) }))] }));
138
+ }, 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) })] }), visibleRows.length > 0 && (_jsx("ul", { className: "flex flex-col gap-2", children: visibleRows.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))) }))] }));
131
139
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer",
3
- "version": "0.387.0",
3
+ "version": "0.388.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": "MIT",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -48,8 +48,8 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@cosmicdrift/kumiko-framework": "0.387.0",
52
- "@cosmicdrift/kumiko-types": "0.387.0",
51
+ "@cosmicdrift/kumiko-framework": "0.388.0",
52
+ "@cosmicdrift/kumiko-types": "0.388.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.387.0",
96
- "@cosmicdrift/kumiko-testing": "0.387.0",
95
+ "@cosmicdrift/kumiko-locale-de": "0.388.0",
96
+ "@cosmicdrift/kumiko-testing": "0.388.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,53 @@
1
1
  [
2
+ {
3
+ "version": "0.388.0",
4
+ "type": "improvement",
5
+ "title": "List view model and cell formats as tables",
6
+ "detail": "computeListViewModel resolves each column through an ordered list of resolvers and applyFormatSpec dispatches through a table of cell formatters. Output is unchanged; a format named like an Object.prototype member such as \"toString\" still takes the unknown-format path."
7
+ },
8
+ {
9
+ "version": "0.388.0",
10
+ "type": "fix",
11
+ "title": "Single-line row actions, stable form columns and select width, system-scope configEdit",
12
+ "detail": "Row actions never wrap onto a second line; the template overview lets long titles break and cuts the slug earlier so the table keeps room at 1280 px. In screen forms a section with explicitly declared columns (two or more) lays out as that many columns, and a select cell is as wide as a text cell instead of growing with its help text. A configEdit screen with scope system now reads the system level, so a SystemAdmin no longer sees the value of their own tenant row."
13
+ },
14
+ {
15
+ "version": "0.388.0",
16
+ "type": "fix",
17
+ "title": "Uniform control height in page headers and list toolbars, spacing for projectionList descriptions",
18
+ "detail": "Buttons in the page header (create, toolbarActions, detail actions, form actions and app-added buttons such as the AI button) and in list toolbars (search, filters, secondary actions) now share one height, independent of the size a screen or app requested. A toolbarAction promoted into the page header no longer renders smaller than the standard create button. A projectionList without a toolbar no longer glues its description to the page header. The header of a CollapsibleSection (\"Erweitert\") has the same height as the form controls next to it."
19
+ },
20
+ {
21
+ "version": "0.388.0",
22
+ "type": "breaking",
23
+ "title": "Input gets kind \"code\" and autoFocus on kind \"text\"",
24
+ "detail": "Input has a new kind \"code\" (length, onComplete, autoFocus, autoComplete defaulting to one-time-code), a single real input shown as separate cells; onComplete fires once when input, paste or autofill reaches full length and the value holds digits only. kind \"text\" gains autoFocus.",
25
+ "migration": "custom Input implementations (PrimitivesProvider override) with an exhaustive switch over props.kind need a case \"code\"; delegating to DefaultInput from @cosmicdrift/kumiko-renderer/web is enough"
26
+ },
27
+ {
28
+ "version": "0.388.0",
29
+ "type": "fix",
30
+ "title": "Renderer low-severity robustness fixes",
31
+ "detail": "The list search echo handling no longer mutates state during render and no longer re-sends an already sent value. A failing live refetch no longer drops a queued follow-up run. Downloaded blob URLs are revoked after a delay, failed PDF loads are retried on the next mount, the file meta cache is cleared when the app gate unmounts, and renderFlow rejects duplicate node ids and unknown icons. The es/fr/it titles of the reset-to-system action now say the tenant template is reset to the system template."
32
+ },
33
+ {
34
+ "version": "0.388.0",
35
+ "type": "fix",
36
+ "title": "Renderer review fixes for lookups, dropzone, markSaved, actionForm reset and dashboard gates",
37
+ "detail": "Reference labels show \"Unknown\" only when the lookup page was complete; a related-list column keeps sortable on fields without a sortable key; a files dropzone lists and can remove files already in the value; extension markSaved accepts the written field names, no longer clears other sections' dirty state; an actionForm with load and resetOnSuccess reloads the record before resetting; fallback panels no longer flash before the visibility gate answers; identical text blocks get unique keys; the missing-screen warning names the route id."
38
+ },
39
+ {
40
+ "version": "0.388.0",
41
+ "type": "improvement",
42
+ "title": "useTenantCurrency hook",
43
+ "detail": "The tenant currency resolution of the entity edit prefill is exported as a hook with an explicit fallback for errors and missing values and no query when no currency is needed; the mail preview uses it only for templates with a money variable."
44
+ },
45
+ {
46
+ "version": "0.388.0",
47
+ "type": "improvement",
48
+ "title": "ChoiceCards widget and PlanCard price prefix",
49
+ "detail": "ChoiceCards is a card-styled radio group (title, description, aside, native radios, keyboard operable) for exclusive choices that need more than a segment label. PlanCardPrice takes an optional prefix shown before the amount, for example \"from\"."
50
+ },
2
51
  {
3
52
  "version": "0.385.0",
4
53
  "type": "improvement",