@cosmicdrift/kumiko-renderer 0.392.2 → 0.394.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 (57) hide show
  1. package/dist/app/entity-list-screen.js +4 -2
  2. package/dist/app/kumiko-screen.js +4 -36
  3. package/dist/app/projection-list-body.js +1 -1
  4. package/dist/app/qn.d.ts +1 -6
  5. package/dist/app/qn.js +1 -12
  6. package/dist/app/screen-visibility-gate.d.ts +19 -0
  7. package/dist/app/screen-visibility-gate.js +60 -0
  8. package/dist/app/use-embedded-screen.d.ts +2 -0
  9. package/dist/app/use-embedded-screen.js +1 -1
  10. package/dist/app/use-gated-toolbar-actions.d.ts +7 -0
  11. package/dist/app/use-gated-toolbar-actions.js +51 -0
  12. package/dist/app/use-projection-list-actions.d.ts +1 -0
  13. package/dist/app/use-projection-list-actions.js +11 -2
  14. package/dist/app/visible-when.d.ts +5 -1
  15. package/dist/app/visible-when.js +11 -0
  16. package/dist/data-table-view-types.d.ts +4 -2
  17. package/dist/hooks/list-column-layout.d.ts +3 -1
  18. package/dist/hooks/list-column-layout.js +20 -7
  19. package/dist/hooks/use-list-view-state.d.ts +1 -1
  20. package/dist/hooks/use-list-view-state.js +3 -2
  21. package/dist/i18n-defaults.js +2 -0
  22. package/dist/web/app/dashboard-body.js +1 -1
  23. package/dist/web/app/dashboard-screen-tiles.d.ts +2 -1
  24. package/dist/web/app/dashboard-screen-tiles.js +9 -7
  25. package/dist/web/app/dashboard-shared.d.ts +2 -0
  26. package/dist/web/app/use-dashboard-screen-state.js +6 -1
  27. package/dist/web/layout/shell-breadcrumb.d.ts +4 -1
  28. package/dist/web/layout/shell-breadcrumb.js +18 -3
  29. package/dist/web/layout/shell-header.js +4 -2
  30. package/dist/web/primitives/action-menu.js +2 -2
  31. package/dist/web/primitives/calendar-popover.js +1 -1
  32. package/dist/web/primitives/combobox.js +1 -1
  33. package/dist/web/primitives/data-table/table-data-cell.js +27 -1
  34. package/dist/web/primitives/data-table/table-header-row.js +1 -1
  35. package/dist/web/primitives/data-table/table-view.js +2 -2
  36. package/dist/web/primitives/data-table/use-column-sizing.d.ts +4 -2
  37. package/dist/web/primitives/data-table/use-column-sizing.js +34 -11
  38. package/dist/web/primitives/data-table/view-menu.js +1 -1
  39. package/dist/web/primitives/data-table-cell.js +1 -1
  40. package/dist/web/primitives/dialog.js +1 -1
  41. package/dist/web/primitives/dropdown-menu.d.ts +1 -1
  42. package/dist/web/primitives/dropdown-menu.js +2 -2
  43. package/dist/web/primitives/lightbox.js +1 -1
  44. package/dist/web/primitives/modal-shell.js +1 -1
  45. package/dist/web/primitives/modal.js +1 -1
  46. package/dist/web/primitives/narrow-pinned-footer.js +1 -1
  47. package/dist/web/primitives/toast.js +2 -2
  48. package/dist/web/sse/live-events.js +0 -7
  49. package/dist/web/widgets/floating-panel-geometry.d.ts +32 -0
  50. package/dist/web/widgets/floating-panel-geometry.js +71 -0
  51. package/dist/web/widgets/floating-panel.d.ts +2 -6
  52. package/dist/web/widgets/floating-panel.js +11 -55
  53. package/dist/web/widgets/infinity-list.js +16 -7
  54. package/dist/web/widgets/plan-card.d.ts +2 -0
  55. package/dist/web/widgets/plan-card.js +5 -1
  56. package/package.json +6 -15
  57. package/src/changes.json +42 -0
@@ -32,6 +32,9 @@ export function InfinityList({ query, payload, pageSize = 50, rows, nextCursor,
32
32
  // this, a slow earlier response can land after a faster later one and
33
33
  // overwrite it, leaving stale rows on screen.
34
34
  const requestSeq = useRef(0);
35
+ // Lets refreshFirstPage tell "initial load still running" from outside a state updater.
36
+ const stateKindRef = useRef(state.kind);
37
+ stateKindRef.current = state.kind;
35
38
  // IDs of the most recently loaded first page. refreshFirstPage() needs
36
39
  // this to tell "row dropped from page 1 elsewhere" (must be pruned) apart
37
40
  // from "row belongs to an already-accumulated later page" (must survive)
@@ -89,10 +92,10 @@ export function InfinityList({ query, payload, pageSize = 50, rows, nextCursor,
89
92
  // scroll position.
90
93
  // biome-ignore lint/correctness/useExhaustiveDependencies: payload goes through payloadKey
91
94
  const refreshFirstPage = useCallback(async () => {
92
- // Read, don't bump: a concurrent load() (e.g. the mount fetch still
93
- // in flight) must still land. Bumping here would make load()'s own
94
- // sequence check discard it, and the refresh below then bails on
95
- // `prev.kind !== "ready"` — the list gets stuck in loading forever.
95
+ // Read, don't bump at the start: a concurrent load() (e.g. the mount
96
+ // fetch still in flight) must not be discarded by a refresh that then
97
+ // fails or is superseded. Only a refresh that actually delivers fresh
98
+ // rows bumps (below), because those are newer than the running load's.
96
99
  const seqAtStart = requestSeq.current;
97
100
  const res = await dispatcher.query(query, { ...payload, limit: pageSize });
98
101
  if (seqAtStart !== requestSeq.current)
@@ -102,6 +105,15 @@ export function InfinityList({ query, payload, pageSize = 50, rows, nextCursor,
102
105
  return;
103
106
  const freshRows = rowsRef.current(res.data);
104
107
  const freshIds = new Set(freshRows.map((row, index) => rowIdRef.current(row, index)));
108
+ if (stateKindRef.current === "loading") {
109
+ // The initial load was issued before the change that triggered this
110
+ // refresh (the live stream reports its first open as a resync), so
111
+ // its page 1 may be stale: adopt the fresh page and drop the older load.
112
+ requestSeq.current += 1;
113
+ firstPageIdsRef.current = freshIds;
114
+ setState({ kind: "ready", rows: freshRows, cursor: nextCursorRef.current(res.data) });
115
+ return;
116
+ }
105
117
  // A row absent from freshIds is stale for one of two reasons: it was
106
118
  // dropped from page 1 (deleted, or filtered out elsewhere) and must be
107
119
  // pruned, or it belongs to an already-accumulated later page and must
@@ -110,9 +122,6 @@ export function InfinityList({ query, payload, pageSize = 50, rows, nextCursor,
110
122
  const previousFirstPageIds = firstPageIdsRef.current;
111
123
  firstPageIdsRef.current = freshIds;
112
124
  setState((prev) => {
113
- // skip: the initial load() is still in flight and owns the eventual
114
- // state — a concurrent refresh landing here has nothing accumulated
115
- // to merge into yet.
116
125
  if (prev.kind === "loading")
117
126
  return prev;
118
127
  if (prev.kind !== "ready") {
@@ -4,6 +4,8 @@ export type PlanCardPrice = {
4
4
  readonly period?: string;
5
5
  /** Shown before the amount, e.g. "from" when prices differ per provider. */
6
6
  readonly prefix?: string;
7
+ /** Small line under the price row, e.g. "plus VAT". */
8
+ readonly note?: string;
7
9
  };
8
10
  export type PlanCardActionSlot = {
9
11
  readonly label: string;
@@ -6,8 +6,12 @@ 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: [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))) }))] }));
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(PlanCardPriceRow, { price: price, compact: compact, testId: testId }), !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 }));
13
13
  }
14
+ function PlanCardPriceRow({ price, compact, testId, }) {
15
+ const t = useTranslation();
16
+ return (_jsxs("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 }))] })), price?.note !== undefined && (_jsx("p", { "data-testid": testId !== undefined ? `${testId}-price-note` : "plan-card-price-note", className: "text-xs text-muted-foreground", children: price.note }))] }));
17
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer",
3
- "version": "0.392.2",
3
+ "version": "0.394.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,17 +48,8 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@cosmicdrift/kumiko-framework": "0.392.2",
52
- "@cosmicdrift/kumiko-types": "0.392.2",
53
- "@radix-ui/react-dialog": "^1.1.15",
54
- "@radix-ui/react-dropdown-menu": "^2.1.16",
55
- "@radix-ui/react-label": "^2.1.8",
56
- "@radix-ui/react-popover": "^1.1.15",
57
- "@radix-ui/react-select": "^2.2.6",
58
- "@radix-ui/react-separator": "^1.1.7",
59
- "@radix-ui/react-slot": "^1.2.4",
60
- "@radix-ui/react-toast": "^1.2.15",
61
- "@radix-ui/react-tooltip": "^1.2.8",
51
+ "@cosmicdrift/kumiko-framework": "0.394.0",
52
+ "@cosmicdrift/kumiko-types": "0.394.0",
62
53
  "@tiptap/core": "^3.31.3",
63
54
  "@tiptap/pm": "^3.31.3",
64
55
  "@tiptap/react": "^3.31.3",
@@ -70,7 +61,7 @@
70
61
  "lucide-react": "^1.14.0",
71
62
  "pdfjs-dist": "6.3.289",
72
63
  "qrcode": "^1.5.4",
73
- "radix-ui": "^1.4.3",
64
+ "radix-ui": "^1.8.1",
74
65
  "react": "^19.2.6",
75
66
  "react-day-picker": "^10.0.0",
76
67
  "react-dom": "^19.2.6",
@@ -92,8 +83,8 @@
92
83
  }
93
84
  },
94
85
  "devDependencies": {
95
- "@cosmicdrift/kumiko-locale-de": "0.392.2",
96
- "@cosmicdrift/kumiko-testing": "0.392.2",
86
+ "@cosmicdrift/kumiko-locale-de": "0.394.0",
87
+ "@cosmicdrift/kumiko-testing": "0.394.0",
97
88
  "@tailwindcss/cli": "^4.3.0",
98
89
  "@testing-library/react": "^16.3.2",
99
90
  "@testing-library/user-event": "^14.6.1",
package/src/changes.json CHANGED
@@ -1,4 +1,46 @@
1
1
  [
2
+ {
3
+ "version": "0.394.0",
4
+ "type": "improvement",
5
+ "title": "edit screens opened without a record use screen:<id>.createTitle",
6
+ "detail": "When a detail screen is opened without an entity id and the feature translates `screen:<id>.createTitle`, the breadcrumb and page title use it instead of the edit title. managed-pages names its create state \"New page\"."
7
+ },
8
+ {
9
+ "version": "0.394.0",
10
+ "type": "improvement",
11
+ "title": "resizable tables size columns by content until the user resizes one",
12
+ "detail": "With `table.resizing` on (also through `bundledLists`), columns without a schema `width` no longer start at a flat 160px. The table keeps the auto layout and sizes them by content until the user drags or arrow-keys a column; that first resize stores the displayed widths of the other columns too, so nothing jumps. The first column of a resizable table breaks only when the table runs out of width, and a clickable first cell wraps instead of truncating, and dates and timestamps (also through a `format` renderer) stay on one line. `onWidthChange` gets an optional third argument with those widths. Stored widths from earlier sessions still apply; a browser that saved any view setting before this release still holds the old 160px widths until the view is reset. Apps with `bundledLists.table.resizing` (use-all-bundled, solon) see wider name columns and narrower leftover space on first visit."
13
+ },
14
+ {
15
+ "version": "0.394.0",
16
+ "type": "fix",
17
+ "title": "Floating panel keeps its distance to the window edge",
18
+ "detail": "Moving, resizing and window resizes clamp the panel 16px inside the viewport instead of at the border. On a window resize the panel keeps its gap to the nearer horizontal and vertical edge, so a bottom-right panel no longer sticks to the right border when the window narrows. Stored geometry stays valid and is only clamped again."
19
+ },
20
+ {
21
+ "version": "0.394.0",
22
+ "type": "fix",
23
+ "title": "One copy of Radix dismissable layers",
24
+ "detail": "Dialog, Popover, DropdownMenu and Toast now come from the radix-ui package instead of the single @radix-ui/react-* packages, which dropped nine direct dependencies. A root override pins @radix-ui/react-dialog to 1.3.1 because cmdk would otherwise pull an older dialog with its own react-dismissable-layer. Apps that import @radix-ui/react-* themselves, or add cmdk, should align their own lockfile to one react-dialog version."
25
+ },
26
+ {
27
+ "version": "0.394.0",
28
+ "type": "improvement",
29
+ "title": "A projectionDetail can be a dashboard screen panel",
30
+ "detail": "A `screen` panel or its `fallback` may point at a projectionDetail. The id comes from the host dashboard's `record` binding and is passed to the embedded screen as its entityId. Boot fails when the host dashboard has no `record`. Header, fields and Detail query render inline in the tile."
31
+ },
32
+ {
33
+ "version": "0.394.0",
34
+ "type": "improvement",
35
+ "title": "Toolbar actions accept visibleWhen",
36
+ "detail": "Every toolbar action of a projectionList or entityList takes an optional `visibleWhen` ({ query, field, eq }). The button shows only while the live query matches; it stays hidden while the query loads and when it fails. Buttons gating on the same query share one request. Boot checks the query, its output field and that it needs no input. relatedList toolbar actions reject `visibleWhen`."
37
+ },
38
+ {
39
+ "version": "0.394.0",
40
+ "type": "improvement",
41
+ "title": "Screen fallback can depend on the failure reason",
42
+ "detail": "A screen-level `fallback` is either a screen id (unchanged) or a map `{ hidden, forbidden, notFound, error }`. `forbidden` is a 403 and `notFound` a 404 on the gate query; `hidden` is an unmet condition; `error` is any other failure. A reason without an entry shows a default notice (new keys kumiko.screen.forbidden and kumiko.screen.notFound) and `error` shows an error banner with retry. Boot rejects an empty map and checks each entry like a string fallback."
43
+ },
2
44
  {
3
45
  "version": "0.392.1",
4
46
  "type": "fix",