@cosmicdrift/kumiko-renderer 0.387.0 → 0.389.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/action-form-body.js +10 -2
- package/dist/app/action-form-support.d.ts +1 -0
- package/dist/app/action-form-support.js +1 -1
- package/dist/app/base-path-context.d.ts +9 -0
- package/dist/app/base-path-context.js +13 -0
- package/dist/app/base-path.d.ts +3 -0
- package/dist/app/base-path.js +27 -0
- package/dist/app/config-edit-body.d.ts +1 -1
- package/dist/app/config-edit-body.js +19 -6
- package/dist/app/config-edit-without-secret-fields.d.ts +2 -0
- package/dist/app/config-edit-without-secret-fields.js +28 -0
- package/dist/app/entity-edit-prefill.d.ts +11 -0
- package/dist/app/entity-edit-prefill.js +9 -4
- package/dist/app/entity-list-facet-specs.d.ts +3 -0
- package/dist/app/entity-list-facet-specs.js +65 -0
- package/dist/app/entity-list-screen.js +74 -206
- package/dist/app/extension-sections.d.ts +5 -3
- package/dist/app/form-schema.js +1 -1
- package/dist/app/header-actions-bar.js +1 -1
- package/dist/app/kumiko-screen.js +6 -14
- package/dist/app/list-bulk-actions.d.ts +18 -0
- package/dist/app/list-bulk-actions.js +54 -0
- package/dist/app/locked-screen-header.d.ts +9 -0
- package/dist/app/locked-screen-header.js +33 -0
- package/dist/app/projection-list-body.js +51 -46
- package/dist/app/row-actions.d.ts +3 -0
- package/dist/app/row-actions.js +10 -5
- package/dist/app/screen-shared.js +9 -2
- package/dist/app/use-entity-range-facets.d.ts +24 -0
- package/dist/app/use-entity-range-facets.js +64 -0
- package/dist/app/use-projection-date-range.d.ts +12 -0
- package/dist/app/use-projection-date-range.js +21 -0
- package/dist/app/use-screen-list-rows.d.ts +37 -0
- package/dist/app/use-screen-list-rows.js +104 -0
- package/dist/app/use-screen-list-table.d.ts +24 -0
- package/dist/app/use-screen-list-table.js +46 -0
- package/dist/components/edit-mounts.d.ts +2 -2
- package/dist/components/embedded-list-field.js +1 -1
- package/dist/components/list-group-page-bridges.d.ts +3 -0
- package/dist/components/list-group-page-bridges.js +31 -0
- package/dist/components/list-state-store.d.ts +16 -0
- package/dist/components/list-state-store.js +31 -0
- package/dist/components/related-list-section.js +51 -90
- package/dist/components/render-edit-action-button.d.ts +1 -3
- package/dist/components/render-edit-action-button.js +2 -2
- package/dist/components/render-edit.js +11 -8
- package/dist/components/render-field.js +11 -7
- package/dist/components/render-list-toolbar-action.d.ts +27 -0
- package/dist/components/render-list-toolbar-action.js +40 -0
- package/dist/components/render-list.d.ts +13 -29
- package/dist/components/render-list.js +72 -67
- package/dist/current-user.d.ts +2 -0
- package/dist/data-table-aggregate-types.d.ts +47 -0
- package/dist/data-table-aggregate-types.js +1 -0
- package/dist/data-table-bulk-types.d.ts +27 -0
- package/dist/data-table-bulk-types.js +1 -0
- package/dist/data-table-facet-types.d.ts +36 -0
- package/dist/data-table-facet-types.js +1 -0
- package/dist/data-table-view-types.d.ts +73 -0
- package/dist/data-table-view-types.js +1 -0
- package/dist/flow-diagram/render.js +9 -1
- package/dist/headless/form/form-controller.js +8 -0
- package/dist/headless/form/types.d.ts +1 -0
- package/dist/headless/format/index.js +89 -64
- package/dist/headless/view-model/edit.js +5 -1
- package/dist/headless/view-model/list.js +184 -149
- package/dist/headless/view-model/types.d.ts +7 -2
- package/dist/hooks/list-column-layout.d.ts +19 -0
- package/dist/hooks/list-column-layout.js +36 -0
- package/dist/hooks/list-group-pages.d.ts +26 -0
- package/dist/hooks/list-group-pages.js +37 -0
- package/dist/hooks/list-reset.d.ts +8 -0
- package/dist/hooks/list-reset.js +15 -0
- package/dist/hooks/list-server-groups.d.ts +39 -0
- package/dist/hooks/list-server-groups.js +84 -0
- package/dist/hooks/list-state-schema.d.ts +28 -0
- package/dist/hooks/list-state-schema.js +76 -0
- package/dist/hooks/live-refetch-scheduler.js +4 -0
- package/dist/hooks/use-bulk-run.d.ts +41 -0
- package/dist/hooks/use-bulk-run.js +65 -0
- package/dist/hooks/use-group-load-state.d.ts +11 -0
- package/dist/hooks/use-group-load-state.js +31 -0
- package/dist/hooks/use-list-aggregates.d.ts +38 -0
- package/dist/hooks/use-list-aggregates.js +107 -0
- package/dist/hooks/use-list-table.d.ts +38 -0
- package/dist/hooks/use-list-table.js +91 -0
- package/dist/hooks/use-list-view-state.d.ts +43 -0
- package/dist/hooks/use-list-view-state.js +161 -0
- package/dist/hooks/use-reference-lookup.d.ts +9 -1
- package/dist/hooks/use-reference-lookup.js +17 -3
- package/dist/hooks/use-related-list-rows.d.ts +27 -0
- package/dist/hooks/use-related-list-rows.js +81 -0
- package/dist/hooks/use-related-list-state.d.ts +15 -0
- package/dist/hooks/use-related-list-state.js +31 -0
- package/dist/hooks/use-remembered-list-params.d.ts +13 -0
- package/dist/hooks/use-remembered-list-params.js +53 -0
- package/dist/hooks/use-remembered-local-list-state.d.ts +26 -0
- package/dist/hooks/use-remembered-local-list-state.js +63 -0
- package/dist/i18n-defaults.js +59 -2
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/primitives.d.ts +30 -47
- package/dist/web/app/create-app.d.ts +5 -0
- package/dist/web/app/create-app.js +15 -7
- package/dist/web/app/dashboard-body.js +17 -1
- package/dist/web/app/dashboard-panels.js +12 -2
- package/dist/web/app/history-leave-guard.d.ts +6 -0
- package/dist/web/app/history-leave-guard.js +115 -0
- package/dist/web/app/list-state-store-boot.d.ts +6 -0
- package/dist/web/app/list-state-store-boot.js +15 -0
- package/dist/web/app/nav.js +7 -54
- package/dist/web/app/session-list-state-store.d.ts +15 -0
- package/dist/web/app/session-list-state-store.js +117 -0
- package/dist/web/index.d.ts +2 -1
- package/dist/web/index.js +2 -1
- package/dist/web/layout/shell-header.js +2 -1
- package/dist/web/layout/uniform-bar-controls.d.ts +1 -0
- package/dist/web/layout/uniform-bar-controls.js +4 -0
- package/dist/web/lib/export-xlsx-request.d.ts +11 -0
- package/dist/web/lib/export-xlsx-request.js +46 -0
- package/dist/web/lib/file-ref-download.js +4 -1
- package/dist/web/primitives/action-menu.js +1 -1
- package/dist/web/primitives/code-input.d.ts +6 -0
- package/dist/web/primitives/code-input.js +31 -0
- package/dist/web/primitives/data-table/actions-column.d.ts +16 -0
- package/dist/web/primitives/data-table/actions-column.js +34 -0
- package/dist/web/primitives/data-table/bulk-selection-bar.d.ts +20 -0
- package/dist/web/primitives/data-table/bulk-selection-bar.js +37 -0
- package/dist/web/primitives/data-table/card-sort-select.d.ts +14 -0
- package/dist/web/primitives/data-table/card-sort-select.js +40 -0
- package/dist/web/primitives/data-table/column-layout-mode.d.ts +5 -0
- package/dist/web/primitives/data-table/column-layout-mode.js +10 -0
- package/dist/web/primitives/data-table/column-picker-section.d.ts +7 -0
- package/dist/web/primitives/data-table/column-picker-section.js +35 -0
- package/dist/web/primitives/data-table/column-resize-handle.d.ts +14 -0
- package/dist/web/primitives/data-table/column-resize-handle.js +46 -0
- package/dist/web/primitives/data-table/density.d.ts +14 -0
- package/dist/web/primitives/data-table/density.js +20 -0
- package/dist/web/primitives/data-table/export-button.d.ts +5 -0
- package/dist/web/primitives/data-table/export-button.js +37 -0
- package/dist/web/primitives/data-table/facet-cluster.d.ts +8 -0
- package/dist/web/primitives/data-table/facet-cluster.js +37 -0
- package/dist/web/primitives/data-table/group-band.d.ts +28 -0
- package/dist/web/primitives/data-table/group-band.js +53 -0
- package/dist/web/primitives/data-table/measure-value.d.ts +10 -0
- package/dist/web/primitives/data-table/measure-value.js +28 -0
- package/dist/web/primitives/data-table/pin-section.d.ts +8 -0
- package/dist/web/primitives/data-table/pin-section.js +20 -0
- package/dist/web/primitives/data-table/reset-all-button.d.ts +4 -0
- package/dist/web/primitives/data-table/reset-all-button.js +8 -0
- package/dist/web/primitives/data-table/server-group-header.d.ts +11 -0
- package/dist/web/primitives/data-table/server-group-header.js +51 -0
- package/dist/web/primitives/data-table/sortable-header.d.ts +17 -0
- package/dist/web/primitives/data-table/sortable-header.js +41 -0
- package/dist/web/primitives/data-table/table-chrome.d.ts +13 -0
- package/dist/web/primitives/data-table/table-chrome.js +30 -0
- package/dist/web/primitives/data-table/table-footer.d.ts +7 -0
- package/dist/web/primitives/data-table/table-footer.js +20 -0
- package/dist/web/primitives/data-table/toolbar-end-cluster.d.ts +10 -0
- package/dist/web/primitives/data-table/toolbar-end-cluster.js +13 -0
- package/dist/web/primitives/data-table/totals-footer.d.ts +24 -0
- package/dist/web/primitives/data-table/totals-footer.js +48 -0
- package/dist/web/primitives/data-table/use-bulk-selection.d.ts +21 -0
- package/dist/web/primitives/data-table/use-bulk-selection.js +66 -0
- package/dist/web/primitives/data-table/use-column-pins.d.ts +32 -0
- package/dist/web/primitives/data-table/use-column-pins.js +117 -0
- package/dist/web/primitives/data-table/use-column-sizing.d.ts +22 -0
- package/dist/web/primitives/data-table/use-column-sizing.js +44 -0
- package/dist/web/primitives/data-table/use-measured-column-widths.d.ts +15 -0
- package/dist/web/primitives/data-table/use-measured-column-widths.js +83 -0
- package/dist/web/primitives/data-table/use-row-keyboard-navigation.d.ts +22 -0
- package/dist/web/primitives/data-table/use-row-keyboard-navigation.js +65 -0
- package/dist/web/primitives/data-table/use-row-selection.d.ts +14 -0
- package/dist/web/primitives/data-table/use-row-selection.js +48 -0
- package/dist/web/primitives/data-table/use-view-columns.d.ts +6 -0
- package/dist/web/primitives/data-table/use-view-columns.js +17 -0
- package/dist/web/primitives/data-table/view-menu.d.ts +8 -0
- package/dist/web/primitives/data-table/view-menu.js +45 -0
- package/dist/web/primitives/data-table-cell.d.ts +0 -22
- package/dist/web/primitives/data-table-cell.js +1 -58
- package/dist/web/primitives/data-table.d.ts +1 -1
- package/dist/web/primitives/data-table.js +125 -146
- package/dist/web/primitives/facet-filter.js +1 -1
- package/dist/web/primitives/file-thumbnail.d.ts +3 -0
- package/dist/web/primitives/file-thumbnail.js +5 -0
- package/dist/web/primitives/file-value-list.d.ts +1 -1
- package/dist/web/primitives/file-value-list.js +5 -5
- package/dist/web/primitives/input.js +6 -1
- package/dist/web/primitives/layout-elements.js +6 -4
- package/dist/web/primitives/number-range-facet.d.ts +7 -0
- package/dist/web/primitives/number-range-facet.js +33 -0
- package/dist/web/primitives/page-header.js +3 -1
- package/dist/web/primitives/row-actions.js +3 -3
- package/dist/web/primitives/use-focus-on-mount.d.ts +2 -0
- package/dist/web/primitives/use-focus-on-mount.js +9 -0
- package/dist/web/widgets/bounded-load-cache.d.ts +5 -1
- package/dist/web/widgets/bounded-load-cache.js +49 -21
- package/dist/web/widgets/choice-cards.d.ts +17 -0
- package/dist/web/widgets/choice-cards.js +12 -0
- package/dist/web/widgets/collapsible-section.js +1 -1
- package/dist/web/widgets/detail-list.d.ts +5 -1
- package/dist/web/widgets/detail-list.js +5 -1
- package/dist/web/widgets/index.d.ts +1 -0
- package/dist/web/widgets/index.js +1 -0
- package/dist/web/widgets/pdf-preview.js +10 -5
- package/dist/web/widgets/plan-card.d.ts +2 -0
- package/dist/web/widgets/plan-card.js +1 -1
- package/dist/web/widgets/upload-zone.js +13 -5
- package/package.json +5 -5
- package/src/changes.json +158 -0
|
@@ -2,6 +2,8 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { useEffect } from "react";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
4
|
import { usePageHeaderSlot } from "../layout/page-header-slot.js";
|
|
5
|
+
import { UNIFORM_BAR_CONTROLS_CLASSES } from "../layout/uniform-bar-controls.js";
|
|
6
|
+
import { cn } from "../lib/cn.js";
|
|
5
7
|
export function DefaultPageHeader({ title, recordTitle, status, actions, overflowItems, hideBreadcrumb, breadcrumbScreenId, }) {
|
|
6
8
|
const slot = usePageHeaderSlot();
|
|
7
9
|
const setOverflowItems = slot?.setOverflowItems;
|
|
@@ -42,7 +44,7 @@ export function DefaultPageHeader({ title, recordTitle, status, actions, overflo
|
|
|
42
44
|
return () => setOverflowItems(undefined);
|
|
43
45
|
}, [setOverflowItems, overflowItems]);
|
|
44
46
|
if (slot === null) {
|
|
45
|
-
return (_jsxs("div", { "data-kumiko-layout": "page-header-inline", className: "flex items-center gap-2 px-4 py-2", children: [status, _jsx("div", { className: "ml-auto flex items-center gap-2", children: actions })] }));
|
|
47
|
+
return (_jsxs("div", { "data-kumiko-layout": "page-header-inline", className: "flex items-center gap-2 px-4 py-2", children: [status, _jsx("div", { className: cn("ml-auto flex items-center gap-2", UNIFORM_BAR_CONTROLS_CLASSES), children: actions })] }));
|
|
46
48
|
}
|
|
47
49
|
return (_jsxs(_Fragment, { children: [status !== undefined &&
|
|
48
50
|
slot.statusElement !== null &&
|
|
@@ -37,19 +37,19 @@ export function RowActionsCell({ row, actions, mode = "adaptive", rowIsLink = fa
|
|
|
37
37
|
// otherwise "inline" is exactly the wall-to-wall text-button problem
|
|
38
38
|
// this feature exists to fix. An author-set `display` takes that choice over.
|
|
39
39
|
const iconOnly = !hasExplicitDisplay && shouldRenderActionsIconOnly(visible);
|
|
40
|
-
return (_jsx("div", { className: "flex w-full items-center gap-1 justify-start", children: visible.map((a) => (_jsx(RowActionButton, { row: row, action: a, iconOnly: iconOnly, compact: compact }, a.id))) }));
|
|
40
|
+
return (_jsx("div", { className: "flex w-full flex-nowrap items-center gap-1 justify-start whitespace-nowrap", children: visible.map((a) => (_jsx(RowActionButton, { row: row, action: a, iconOnly: iconOnly, compact: compact }, a.id))) }));
|
|
41
41
|
}
|
|
42
42
|
if (hasExplicitDisplay) {
|
|
43
43
|
const inline = visible.filter((a) => a.display !== undefined);
|
|
44
44
|
const rest = visible.filter((a) => a.display === undefined);
|
|
45
|
-
return (_jsxs("div", { className: "inline-flex flex-
|
|
45
|
+
return (_jsxs("div", { className: "inline-flex flex-nowrap items-center gap-1 justify-end whitespace-nowrap", children: [inline.map((a) => (_jsx(RowActionButton, { row: row, action: a, compact: compact }, a.id))), rest.length > 0 && _jsx(RowActionsKebab, { row: row, actions: rest })] }));
|
|
46
46
|
}
|
|
47
47
|
if (rowIsLink) {
|
|
48
48
|
return (_jsx("div", { className: "inline-flex items-center justify-end", children: _jsx(RowActionsKebab, { row: row, actions: visible }) }));
|
|
49
49
|
}
|
|
50
50
|
const primary = primaryRowAction(visible);
|
|
51
51
|
const rest = visible.filter((a) => a.id !== primary?.id);
|
|
52
|
-
return (_jsxs("div", { className: "inline-flex flex-
|
|
52
|
+
return (_jsxs("div", { className: "inline-flex flex-nowrap items-center gap-1 justify-end whitespace-nowrap", children: [primary !== undefined && _jsx(RowActionButton, { row: row, action: primary, asLink: true }), rest.length > 0 && _jsx(RowActionsKebab, { row: row, actions: rest })] }));
|
|
53
53
|
}
|
|
54
54
|
// Shared trigger state between inline button + kebab item: busy flag
|
|
55
55
|
// (while the async onTrigger runs) + confirm-pending action. Both
|
|
@@ -1 +1,5 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type BoundedLoadCache<T> = {
|
|
2
|
+
readonly load: (key: string, load: () => Promise<T>) => Promise<T>;
|
|
3
|
+
readonly retain: (key: string) => () => void;
|
|
4
|
+
};
|
|
5
|
+
export declare function createBoundedLoadCache<T>(max: number, dispose: (value: T) => void, shouldKeep?: (value: T) => boolean): BoundedLoadCache<T>;
|
|
@@ -1,25 +1,53 @@
|
|
|
1
|
-
|
|
2
|
-
// so several documents on one page keep their own loading tasks alive.
|
|
3
|
-
export function createBoundedLoadCache(max, dispose) {
|
|
1
|
+
export function createBoundedLoadCache(max, dispose, shouldKeep = () => true) {
|
|
4
2
|
const entries = new Map();
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
entries.delete(oldestKey);
|
|
20
|
-
void oldest.then(dispose);
|
|
21
|
-
}
|
|
3
|
+
const users = new Map();
|
|
4
|
+
const everRetained = new Set();
|
|
5
|
+
const evictIdle = (protectedKey) => {
|
|
6
|
+
for (const [key, entry] of entries) {
|
|
7
|
+
// skip: back within the bound, nothing more to evict
|
|
8
|
+
if (entries.size <= max)
|
|
9
|
+
return;
|
|
10
|
+
const idle = (users.get(key) ?? 0) === 0;
|
|
11
|
+
const settled = everRetained.has(key) || entries.size > max * 2;
|
|
12
|
+
if (key === protectedKey || !idle || !settled)
|
|
13
|
+
continue;
|
|
14
|
+
entries.delete(key);
|
|
15
|
+
everRetained.delete(key);
|
|
16
|
+
void entry.then(dispose);
|
|
22
17
|
}
|
|
23
|
-
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
load: (key, load) => {
|
|
21
|
+
const cached = entries.get(key);
|
|
22
|
+
if (cached !== undefined)
|
|
23
|
+
return cached;
|
|
24
|
+
const loading = load();
|
|
25
|
+
entries.set(key, loading);
|
|
26
|
+
// A failed load must not stick: the next mount retries instead of replaying the rejection.
|
|
27
|
+
const forget = () => {
|
|
28
|
+
if (entries.get(key) === loading)
|
|
29
|
+
entries.delete(key);
|
|
30
|
+
};
|
|
31
|
+
loading.then((value) => {
|
|
32
|
+
if (!shouldKeep(value))
|
|
33
|
+
forget();
|
|
34
|
+
}, forget);
|
|
35
|
+
evictIdle(key);
|
|
36
|
+
return loading;
|
|
37
|
+
},
|
|
38
|
+
retain: (key) => {
|
|
39
|
+
users.set(key, (users.get(key) ?? 0) + 1);
|
|
40
|
+
everRetained.add(key);
|
|
41
|
+
return () => {
|
|
42
|
+
const remaining = (users.get(key) ?? 1) - 1;
|
|
43
|
+
if (remaining > 0) {
|
|
44
|
+
users.set(key, remaining);
|
|
45
|
+
// skip: other consumers still hold the entry
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
users.delete(key);
|
|
49
|
+
evictIdle();
|
|
50
|
+
};
|
|
51
|
+
},
|
|
24
52
|
};
|
|
25
53
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/** Card-styled radio group for mutually exclusive choices that need a description or price line. */
|
|
3
|
+
export declare function ChoiceCards<T extends string>({ value, options, onChange, ariaLabel, testId, }: {
|
|
4
|
+
/** `null`: nothing selected. */
|
|
5
|
+
readonly value: T | null;
|
|
6
|
+
readonly options: readonly {
|
|
7
|
+
readonly value: T;
|
|
8
|
+
readonly title: string;
|
|
9
|
+
readonly description?: string;
|
|
10
|
+
/** Right-aligned next to the title, e.g. a price. */
|
|
11
|
+
readonly aside?: string;
|
|
12
|
+
readonly disabled?: boolean;
|
|
13
|
+
}[];
|
|
14
|
+
readonly onChange: (value: T) => void;
|
|
15
|
+
readonly ariaLabel?: string;
|
|
16
|
+
readonly testId?: string;
|
|
17
|
+
}): ReactNode;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Check } from "lucide-react";
|
|
3
|
+
import { useId } from "react";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
/** Card-styled radio group for mutually exclusive choices that need a description or price line. */
|
|
6
|
+
export function ChoiceCards({ value, options, onChange, ariaLabel, testId, }) {
|
|
7
|
+
const groupName = useId();
|
|
8
|
+
return (_jsx("div", { role: "radiogroup", "aria-label": ariaLabel, "data-testid": testId, className: "grid grid-cols-1 gap-2 sm:grid-cols-2", children: options.map((option) => {
|
|
9
|
+
const selected = option.value === value;
|
|
10
|
+
return (_jsxs("label", { "data-testid": testId !== undefined ? `${testId}-${option.value}` : undefined, className: cn("relative flex cursor-pointer flex-col gap-1 rounded-lg border bg-card p-4 transition-colors", "hover:bg-muted/50 has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring", selected ? "border-primary ring-1 ring-primary" : "border-border", option.disabled && "cursor-not-allowed opacity-50"), children: [_jsx("input", { type: "radio", className: "sr-only", name: groupName, value: option.value, checked: selected, disabled: option.disabled, onChange: () => onChange(option.value) }), _jsxs("span", { className: "flex items-start justify-between gap-2", children: [_jsx("span", { className: "font-semibold", children: option.title }), _jsxs("span", { className: "flex items-center gap-2", children: [option.aside !== undefined && (_jsx("span", { className: "font-semibold tabular-nums", children: option.aside })), _jsx("span", { "aria-hidden": "true", className: cn("flex size-5 shrink-0 items-center justify-center rounded-full border", selected ? "border-primary bg-primary text-primary-foreground" : "border-input"), children: selected && _jsx(Check, { className: "size-3" }) })] })] }), option.description !== undefined && (_jsx("span", { className: "text-sm text-muted-foreground", children: option.description }))] }, option.value));
|
|
11
|
+
}) }));
|
|
12
|
+
}
|
|
@@ -13,5 +13,5 @@ export function CollapsibleSection({ title, defaultOpen = false, children, testI
|
|
|
13
13
|
if (defaultOpen)
|
|
14
14
|
setOpen(true);
|
|
15
15
|
}, [defaultOpen]);
|
|
16
|
-
return (_jsxs("details", { "data-testid": testId, className: "group rounded-lg border bg-card", open: open, onToggle: (e) => setOpen(e.currentTarget.open), children: [_jsxs("summary", { className: "flex cursor-pointer select-none items-center justify-between gap-2 rounded-lg px-4
|
|
16
|
+
return (_jsxs("details", { "data-testid": testId, className: "group rounded-lg border bg-card", open: open, onToggle: (e) => setOpen(e.currentTarget.open), children: [_jsxs("summary", { className: "flex h-9 cursor-pointer select-none items-center justify-between gap-2 rounded-lg px-4 text-sm font-medium hover:bg-muted/50 [&::-webkit-details-marker]:hidden", children: [_jsx("span", { children: title }), _jsx(ChevronRight, { "aria-hidden": "true", className: "size-4 text-muted-foreground transition-transform group-open:rotate-90" })] }), _jsx("div", { className: "border-t px-4 py-3", children: children })] }));
|
|
17
17
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Read-only key-value list for detail views (dimmed label on the left, value on the right). The
|
|
4
|
+
* value is a ReactNode, so badges and chips are supported. `emphasize` highlights a row, such as
|
|
5
|
+
* the final result in a calculator.
|
|
6
|
+
*/
|
|
3
7
|
export declare function DetailList({ rows, testId, }: {
|
|
4
8
|
readonly rows: readonly {
|
|
5
9
|
/** Optional stable key -- falls back to the label when omitted (fine as
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Read-only key-value list for detail views (dimmed label on the left, value on the right). The
|
|
5
|
+
* value is a ReactNode, so badges and chips are supported. `emphasize` highlights a row, such as
|
|
6
|
+
* the final result in a calculator.
|
|
7
|
+
*/
|
|
4
8
|
export function DetailList({ rows, testId, }) {
|
|
5
9
|
return (_jsx("dl", { "data-testid": testId, className: "@container flex flex-col divide-y", children: rows.map((row) => (_jsxs("div", { className: "grid grid-cols-1 gap-0.5 py-2.5 @md:grid-cols-[200px_1fr] @md:gap-4", children: [_jsx("dt", { className: cn("text-sm text-muted-foreground", row.emphasize === true && "font-semibold text-foreground"), children: row.label }), _jsx("dd", { className: cn("text-sm font-medium", row.emphasize === true && "font-semibold"), children: row.value })] }, row.id ?? row.label))) }));
|
|
6
10
|
}
|
|
@@ -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";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Suspense, use, useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { Suspense, use, useCallback, useEffect, useMemo, useState } from "react";
|
|
3
3
|
import { useTranslation } from "../../i18n.js";
|
|
4
4
|
import { createBoundedLoadCache } from "./bounded-load-cache.js";
|
|
5
5
|
// pdf.js runs in the main thread ("fake worker"): the worker module is bundled
|
|
@@ -7,10 +7,10 @@ import { createBoundedLoadCache } from "./bounded-load-cache.js";
|
|
|
7
7
|
// CDN URL is ever fetched (self-hosting and CSP stay intact).
|
|
8
8
|
const RENDER_SCALE = 96 / 72;
|
|
9
9
|
const MAX_CACHED_DOCUMENTS = 4;
|
|
10
|
-
const
|
|
10
|
+
const documentCache = 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);
|
|
@@ -42,9 +42,11 @@ async function loadDocument(source) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
// The promise must be stable across renders for use().
|
|
45
|
+
function documentKey(source) {
|
|
46
|
+
return "base64" in source ? `base64:${source.base64}` : `url:${source.url}`;
|
|
47
|
+
}
|
|
45
48
|
function documentFor(source) {
|
|
46
|
-
|
|
47
|
-
return cachedDocument(key, () => loadDocument(source));
|
|
49
|
+
return documentCache.load(documentKey(source), () => loadDocument(source));
|
|
48
50
|
}
|
|
49
51
|
function PageView({ document, pageNumber, }) {
|
|
50
52
|
const t = useTranslation();
|
|
@@ -128,6 +130,9 @@ function pageFor(document, pageNumber) {
|
|
|
128
130
|
function Pages({ source }) {
|
|
129
131
|
const t = useTranslation();
|
|
130
132
|
const loaded = use(documentFor(source));
|
|
133
|
+
// A mounted preview keeps its document out of the eviction: destroying it blanks its pages.
|
|
134
|
+
const key = documentKey(source);
|
|
135
|
+
useEffect(() => documentCache.retain(key), [key]);
|
|
131
136
|
if (!loaded.ok) {
|
|
132
137
|
return _jsx("p", { className: "text-sm text-status-bad", children: t("kumiko.widget.pdf-preview.renderFailed") });
|
|
133
138
|
}
|
|
@@ -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) })] }),
|
|
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.
|
|
3
|
+
"version": "0.389.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.
|
|
52
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
51
|
+
"@cosmicdrift/kumiko-framework": "0.389.0",
|
|
52
|
+
"@cosmicdrift/kumiko-types": "0.389.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.389.0",
|
|
96
|
+
"@cosmicdrift/kumiko-testing": "0.389.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,162 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.389.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "DataTable view menu, remembered list state and compact density",
|
|
6
|
+
"detail": "Lists with a table block show a view menu (columns, density, reset view) and a reset-all button. Remembered query parameters are copied into the URL once; links with explicit parameters win. Compact density uses 32px rows with 4px cell padding. New keys kumiko.list.reset-all and kumiko.list.view.* in all locale packages."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.389.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "remembered column pins and widths in the list view state",
|
|
12
|
+
"detail": "useListTable passes pins, widths, minWidths and keyboardNavigation to the DataTable view options. Pins and widths live in the list-state store next to columns and density; a saved pin set replaces the schema pins, so a user can release a pinned column. New keys kumiko.list.view.pinning, kumiko.list.view.pin-left, kumiko.list.view.pin-right and kumiko.list.column.resize in all locale packages."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.389.0",
|
|
16
|
+
"type": "improvement",
|
|
17
|
+
"title": "DataTable resize grip, pinned focus ring and bounded widths",
|
|
18
|
+
"detail": "Column widths stay between minWidth and 2000 px, also when read back from the session. The resize grip shows on header hover and focus and carries aria-valuenow, aria-valuemin and aria-valuemax. Pinned and actions cells repeat the keyboard focus ring, and a pressed pin button is highlighted."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"version": "0.389.0",
|
|
22
|
+
"type": "improvement",
|
|
23
|
+
"title": "Server-side groups, totals row and number range facet in the DataTable",
|
|
24
|
+
"detail": "With table.grouping the groups load via one aggregate query; each open group near the viewport loads its rows with the list query plus a group filter, page by page with \"Load more\", at most four groups at once, a collapsed group sends no query. Headers show the server count and measures, the global pager gives way in both pagination modes, and the view menu offers \"Group by\" with userChoice. table.totals renders a live footer row over the filters and search. A range facet shows from/to inputs, keeps <screenId>.f.<field>.min/.max in the URL and sends gte/lte filters, money in minor units. Infinite lists now also start over when a facet changes. New keys kumiko.list.group.*, kumiko.list.measure.*, kumiko.list.totals.label, kumiko.list.range.* and kumiko.list.notice.* in all locale packages."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"version": "0.389.0",
|
|
28
|
+
"type": "improvement",
|
|
29
|
+
"title": "DataTable row selection and bulk run",
|
|
30
|
+
"detail": "With bulkActions the table gets a checkbox column, a header checkbox for the loaded rows (at most 100), a checkbox per card and a bar with \"n selected\" and the actions. A run sends one /api/write per row, at most 4 at a time; rows hidden by the row action's visible are skipped. Cancel stops new writes. A banner shows succeeded, failed, skipped and not run; failed and not-run rows stay selected and the list reloads. With table.keyboardNavigation, Space toggles the focused row. New keys kumiko.list.bulk.* in all locale packages."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"version": "0.389.0",
|
|
34
|
+
"type": "fix",
|
|
35
|
+
"title": "entityList writeHandler row actions keep display",
|
|
36
|
+
"detail": "entityList built its writeHandler row actions inline and dropped display; it now uses the same builder as projection and related lists."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"version": "0.389.0",
|
|
40
|
+
"type": "improvement",
|
|
41
|
+
"title": "Export button in the DataTable toolbar",
|
|
42
|
+
"detail": "Lists with table.export show an Export button next to the view menu. It exports the current search, sort and facets with the visible columns in their order and shows a hint when the list exceeds the row limit. New keys kumiko.list.export, kumiko.list.export-busy, kumiko.list.export-too-many and kumiko.list.export-failed in all locale packages."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"version": "0.389.0",
|
|
46
|
+
"type": "improvement",
|
|
47
|
+
"title": "All primary toolbar actions move into the list page header",
|
|
48
|
+
"detail": "Previously at most one primary toolbar action was promoted, and only when the list had no create button. Now every primary action without a confirm dialog sits in the page header, left of the create button (which stays rightmost), in declaration order. Promoted actions no longer appear in the list toolbar. On phones they become entries of the header overflow menu when a create button is present, so the create button keeps its place."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"version": "0.389.0",
|
|
52
|
+
"type": "improvement",
|
|
53
|
+
"title": "renderer { format: \"file\", previewOpenByDefault: true } opens the first PDF preview",
|
|
54
|
+
"detail": "FileValueList takes a previewOpenByDefault prop, set declaratively through the file renderer spec, so the inline preview of the first file starts open. The preview is only mounted for PDFs. Without the option the toggle behaves as before."
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"version": "0.389.0",
|
|
58
|
+
"type": "breaking",
|
|
59
|
+
"title": "DataTable rowGrouping removed",
|
|
60
|
+
"detail": "The client-side rowGrouping prop and the DataTableRowGrouping type are removed from the DataTable contract; grouping runs only through serverGroups. A custom DataTable primitive drops its rowGrouping handling.",
|
|
61
|
+
"migration": "delete rowGrouping from custom DataTable primitives and their callers; group lists through table.grouping on the screen"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"version": "0.389.0",
|
|
65
|
+
"type": "improvement",
|
|
66
|
+
"title": "Totals row keeps its leading cells pinned",
|
|
67
|
+
"detail": "With pinned columns, the empty totals cells under the checkbox and expand columns stick to the left like the cells above them, and the cell under the actions column sticks to the right with the same background."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"version": "0.389.0",
|
|
71
|
+
"type": "fix",
|
|
72
|
+
"title": "Success message resolves reference labels by id",
|
|
73
|
+
"detail": "resolveSuccessMessageParams requests the chosen record by id, so a tenant picked in a tier-admin form with more than 200 tenants is named in the success message instead of showing its raw id. Sources that ignore both inputs keep the previous capped-list behaviour."
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"version": "0.389.0",
|
|
77
|
+
"type": "fix",
|
|
78
|
+
"title": "Locked fallback chains show the rendered screen in the header",
|
|
79
|
+
"detail": "When a screen with visibleWhen falls back to another screen that is locked too, the page header and breadcrumb now name the screen that actually renders instead of the locked intermediate fallback."
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"version": "0.389.0",
|
|
83
|
+
"type": "fix",
|
|
84
|
+
"title": "Provider forms stay usable without secrets access",
|
|
85
|
+
"detail": "The merged provider form (config keys plus secrets) previously showed only an error banner if the viewer was denied the secrets list. Now the secret fields are left out and host, port, sender and provider choice can still be saved."
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"version": "0.389.0",
|
|
89
|
+
"type": "fix",
|
|
90
|
+
"title": "PDF previews keep their document while mounted",
|
|
91
|
+
"detail": "createBoundedLoadCache now returns { load, retain }. PdfPreview retains its document while mounted, and the cache evicts only unused entries, so a fifth document no longer destroys one that is still rendered."
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"version": "0.389.0",
|
|
95
|
+
"type": "fix",
|
|
96
|
+
"title": "Leave guard keeps the history clean on Back and Forward",
|
|
97
|
+
"detail": "Confirming the leave dialog after a browser Back or Forward now uses history.back(), so the target is no longer duplicated in the history stack. Popstate events that arrive while a leave request is open restore the URL and leave the open dialog untouched."
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"version": "0.389.0",
|
|
101
|
+
"type": "improvement",
|
|
102
|
+
"title": "basePath option for createKumikoApp and the auth pages",
|
|
103
|
+
"detail": "createKumikoApp accepts basePath. The default auth pages gate matches its paths below the prefix, and the login screen builds its default forgot-password and unlock hrefs with it, so mail links to /admin/unlock-account no longer end on the login screen. New exports BasePathProvider, useBasePath, normalizeBasePath, stripBasePath and prependBasePath."
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"version": "0.389.0",
|
|
107
|
+
"type": "improvement",
|
|
108
|
+
"title": "secrets-denied test fixture without a cast",
|
|
109
|
+
"detail": "The fixture's field map already fits the annotated ConfigEditScreenDefinition, so the cast and its marker are gone. No runtime change."
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"version": "0.388.0",
|
|
113
|
+
"type": "improvement",
|
|
114
|
+
"title": "List view model and cell formats as tables",
|
|
115
|
+
"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."
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"version": "0.388.0",
|
|
119
|
+
"type": "fix",
|
|
120
|
+
"title": "Single-line row actions, stable form columns and select width, system-scope configEdit",
|
|
121
|
+
"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."
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"version": "0.388.0",
|
|
125
|
+
"type": "fix",
|
|
126
|
+
"title": "Uniform control height in page headers and list toolbars, spacing for projectionList descriptions",
|
|
127
|
+
"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."
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"version": "0.388.0",
|
|
131
|
+
"type": "breaking",
|
|
132
|
+
"title": "Input gets kind \"code\" and autoFocus on kind \"text\"",
|
|
133
|
+
"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.",
|
|
134
|
+
"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"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"version": "0.388.0",
|
|
138
|
+
"type": "fix",
|
|
139
|
+
"title": "Renderer low-severity robustness fixes",
|
|
140
|
+
"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."
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"version": "0.388.0",
|
|
144
|
+
"type": "fix",
|
|
145
|
+
"title": "Renderer review fixes for lookups, dropzone, markSaved, actionForm reset and dashboard gates",
|
|
146
|
+
"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."
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"version": "0.388.0",
|
|
150
|
+
"type": "improvement",
|
|
151
|
+
"title": "useTenantCurrency hook",
|
|
152
|
+
"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."
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"version": "0.388.0",
|
|
156
|
+
"type": "improvement",
|
|
157
|
+
"title": "ChoiceCards widget and PlanCard price prefix",
|
|
158
|
+
"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\"."
|
|
159
|
+
},
|
|
2
160
|
{
|
|
3
161
|
"version": "0.385.0",
|
|
4
162
|
"type": "improvement",
|