@cosmicdrift/kumiko-renderer 0.328.1 → 0.330.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/README.md +13 -0
- package/dist/app/kumiko-screen.js +17 -14
- package/dist/components/grid-cell-for-field.d.ts +2 -1
- package/dist/components/grid-cell-for-field.js +8 -1
- package/dist/components/render-edit-types.d.ts +4 -2
- package/dist/components/render-edit.js +23 -14
- package/dist/components/render-field.js +3 -6
- package/dist/i18n-defaults.js +8 -0
- package/dist/primitives.d.ts +20 -4
- package/package.json +5 -5
- package/src/changes.json +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @cosmicdrift/kumiko-renderer
|
|
2
|
+
|
|
3
|
+
Platform-agnostic React renderer for Kumiko screens: primitives contract, hooks, `KumikoScreen`, navigation and SSE abstractions. No DOM, no `EventSource`, no `react-dom`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @cosmicdrift/kumiko-renderer
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Pair it with `@cosmicdrift/kumiko-renderer-web` for the browser.
|
|
12
|
+
|
|
13
|
+
Documentation: https://kumiko.rocks
|
|
@@ -154,10 +154,12 @@ function pageSizeOptionsFor(currentLimit) {
|
|
|
154
154
|
? PAGE_SIZE_OPTIONS
|
|
155
155
|
: [...PAGE_SIZE_OPTIONS, currentLimit].sort((a, b) => a - b);
|
|
156
156
|
}
|
|
157
|
-
function useNavigateToCreateFor(schema, entityName) {
|
|
157
|
+
function useNavigateToCreateFor(schema, entityName, createScreen) {
|
|
158
158
|
const nav = useNav();
|
|
159
159
|
const host = useReturnHost();
|
|
160
160
|
const editScreenId = useMemo(() => {
|
|
161
|
+
if (createScreen !== undefined)
|
|
162
|
+
return createScreen;
|
|
161
163
|
// allowCreate:false = update-only Edit-Screen (Create läuft über einen
|
|
162
164
|
// Lifecycle-Write) — der zählt nicht als „+ Neu"-Ziel. singleton:true
|
|
163
165
|
// öffnet ohne entityId den vorhandenen Record (EntityEditSingletonBody)
|
|
@@ -167,7 +169,7 @@ function useNavigateToCreateFor(schema, entityName) {
|
|
|
167
169
|
s.allowCreate !== false &&
|
|
168
170
|
s.singleton !== true);
|
|
169
171
|
return edit !== undefined ? lastSegment(edit.id) : undefined;
|
|
170
|
-
}, [schema.screens, entityName]);
|
|
172
|
+
}, [schema.screens, entityName, createScreen]);
|
|
171
173
|
const navigate = useCallback(() => {
|
|
172
174
|
if (editScreenId !== undefined)
|
|
173
175
|
navigateWithReturnTo(nav, { screenId: editScreenId }, host);
|
|
@@ -730,12 +732,13 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
730
732
|
const handleCancel = useCallback(() => navigateToReturnOr(nav, returnTarget, navigateToList), [nav, returnTarget, navigateToList]);
|
|
731
733
|
const handleDelete = useCallback(async () => {
|
|
732
734
|
const res = await dispatcher.write(deleteCommand, { id: entityId });
|
|
733
|
-
if (res.isSuccess)
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
735
|
+
if (!res.isSuccess)
|
|
736
|
+
return res.error;
|
|
737
|
+
// Never return onto the just-deleted record.
|
|
738
|
+
const target = returnTarget?.entityId !== entityId ? returnTarget : undefined;
|
|
739
|
+
navigateToReturnOr(nav, target, navigateToList);
|
|
740
|
+
onDeleted?.();
|
|
741
|
+
return undefined;
|
|
739
742
|
}, [dispatcher, deleteCommand, entityId, navigateToList, onDeleted, returnTarget, nav]);
|
|
740
743
|
// Never seed a tenant-declared money field with entityDefaultCurrency while
|
|
741
744
|
// its real tenant currency is still in flight (fw#2933) — wait instead. A
|
|
@@ -956,7 +959,7 @@ function EntityListScreen({ schema, screen, translate, onRowClick, }) {
|
|
|
956
959
|
}
|
|
957
960
|
function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
958
961
|
const featureName = schema.featureName;
|
|
959
|
-
const onCreate = useNavigateToCreateFor(schema, screen.entity);
|
|
962
|
+
const onCreate = useNavigateToCreateFor(schema, screen.entity, screen.createScreen);
|
|
960
963
|
const { Banner } = usePrimitives();
|
|
961
964
|
const queryType = entityQueryCommand(featureName, screen.entity, "list");
|
|
962
965
|
const nav = useNav();
|
|
@@ -1861,17 +1864,17 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
1861
1864
|
return Metric !== undefined ? (_jsx(Metric, { label: label, value: value, testId: testId, ...(onPress !== undefined && { onPress }) }, field)) : (_jsxs(GridCell, { children: [_jsx(Text, { variant: "small", testId: `${testId}-label`, children: label }), _jsx(Text, { testId: `${testId}-value`, children: value })] }, field));
|
|
1862
1865
|
});
|
|
1863
1866
|
// Metrics are a dl band when the primitive exists; the old Grid stays as the fallback.
|
|
1864
|
-
const metricsBlock = (subtitle) => MetricBand !== undefined ? (_jsx(MetricBand, { testId: "kumiko-screen-projection-detail-metrics", subtitle: subtitle, children: metricItems })) : (_jsx(Grid, { columns: screen.metrics?.length ?? 1, testId: "kumiko-screen-projection-detail-metrics", children: metricItems }));
|
|
1867
|
+
const metricsBlock = (subtitle, lead) => MetricBand !== undefined ? (_jsx(MetricBand, { testId: "kumiko-screen-projection-detail-metrics", subtitle: subtitle, ...(lead !== undefined && { lead }), children: metricItems })) : (_jsx(Grid, { columns: screen.metrics?.length ?? 1, testId: "kumiko-screen-projection-detail-metrics", children: metricItems }));
|
|
1865
1868
|
const usesPageHeaderSlot = pageHeaderSlotAvailable && PageHeader !== undefined;
|
|
1866
1869
|
const actionErrorBanner = actionError !== null && (_jsx(Banner, { variant: "error", testId: "render-edit-action-error", children: actionError }));
|
|
1867
|
-
const renderHeaderContent = (headerSlot) => (_jsxs(_Fragment, { children: [usesPageHeaderSlot
|
|
1870
|
+
const renderHeaderContent = (headerSlot) => (_jsxs(_Fragment, { children: [usesPageHeaderSlot ? (_jsxs(_Fragment, { children: [_jsx(PageHeader, { ...(header !== undefined && { title: String(record[header.title] ?? "") }), ...(header?.status !== undefined &&
|
|
1868
1871
|
StatusBadge !== undefined && {
|
|
1869
1872
|
status: (_jsx(StatusBadge, { value: String(record[header.status] ?? ""), tone: headerStatusTone(header, record), testId: "kumiko-screen-projection-detail-status" })),
|
|
1870
1873
|
}), ...(hasHeaderActions && {
|
|
1871
1874
|
actions: (_jsx(Grid, { columns: "auto", testId: "kumiko-screen-projection-detail-actions", children: _jsx(HeaderActionsBar, { actions: headerActionsList, Button: Button, Dialog: Dialog, ActionOverflowMenu: ActionOverflowMenu, onError: setActionError, collapseAfterPrimary: true }) })),
|
|
1872
|
-
}) }), (hasMetrics || headerSubtitleSlot !== undefined) &&
|
|
1875
|
+
}) }), MetricBand === undefined && headerSlot, (hasMetrics || headerSubtitleSlot !== undefined || headerSlot !== undefined) &&
|
|
1873
1876
|
(MetricBand !== undefined || hasMetrics) &&
|
|
1874
|
-
metricsBlock(headerSubtitleSlot), actionErrorBanner] })) : ((hasHeaderCard || headerSlot !== undefined) && (_jsxs(Card, { slots: {
|
|
1877
|
+
metricsBlock(headerSubtitleSlot, MetricBand !== undefined ? headerSlot : undefined), actionErrorBanner] })) : ((hasHeaderCard || headerSlot !== undefined) && (_jsxs(Card, { slots: {
|
|
1875
1878
|
...(headerTitleSlot !== undefined && { title: headerTitleSlot }),
|
|
1876
1879
|
...(headerSubtitleSlot !== undefined && { subtitle: headerSubtitleSlot }),
|
|
1877
1880
|
...(headerSlot !== undefined && { headerContent: headerSlot }),
|
|
@@ -2187,7 +2190,7 @@ function ConfigEditBody({ schema, screen, translate, }) {
|
|
|
2187
2190
|
if (initial === null) {
|
|
2188
2191
|
return (_jsx(Banner, { padded: true, variant: "loading", testId: "kumiko-screen-loading", children: "Loading\u2026" }));
|
|
2189
2192
|
}
|
|
2190
|
-
return (_jsx(RenderEdit, { screen: synthScreen, entity: synthEntity, featureName: schema.featureName, initial: initial, customSubmit: customSubmit, ...(screen.submitLabel !== undefined && { submitLabel: screen.submitLabel }), ...(translate !== undefined && { translate }), labelAppendix: (fieldName) => {
|
|
2193
|
+
return (_jsx(RenderEdit, { screen: synthScreen, entity: synthEntity, featureName: schema.featureName, initial: initial, customSubmit: customSubmit, ...(screenFillsHeight(screen) && { fillScreenHeight: true }), ...(screen.submitLabel !== undefined && { submitLabel: screen.submitLabel }), ...(translate !== undefined && { translate }), labelAppendix: (fieldName) => {
|
|
2191
2194
|
const cascade = cascades[fieldName];
|
|
2192
2195
|
if (cascade === undefined)
|
|
2193
2196
|
return undefined;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EditFieldViewModel, FieldIssue } from "@cosmicdrift/kumiko-headless";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
|
-
import type { usePrimitives } from "../primitives.js";
|
|
3
|
+
import type { FieldCellWidth, usePrimitives } from "../primitives.js";
|
|
4
4
|
export type GridCellForFieldProps = {
|
|
5
5
|
readonly field: EditFieldViewModel;
|
|
6
6
|
readonly columns: number;
|
|
@@ -22,4 +22,5 @@ export type GridCellForFieldProps = {
|
|
|
22
22
|
readonly changed?: boolean;
|
|
23
23
|
readonly flow?: boolean;
|
|
24
24
|
};
|
|
25
|
+
export declare function fieldCellWidth(field: EditFieldViewModel): FieldCellWidth;
|
|
25
26
|
export declare function GridCellForField({ field, columns, issues, onChange, GridCell, featureName, labelAppendix, fieldAppendix, allIssues, valueDisplay, row, changed, flow, }: GridCellForFieldProps): ReactNode;
|
|
@@ -6,6 +6,8 @@ const FIELD_CELL_WIDTH_BY_TYPE = {
|
|
|
6
6
|
bigInt: "number",
|
|
7
7
|
money: "money",
|
|
8
8
|
date: "date",
|
|
9
|
+
timestamp: "timestamp",
|
|
10
|
+
locatedTimestamp: "timestamp",
|
|
9
11
|
select: "select",
|
|
10
12
|
multiSelect: "select",
|
|
11
13
|
boolean: "auto",
|
|
@@ -17,9 +19,14 @@ const FIELD_CELL_WIDTH_BY_TYPE = {
|
|
|
17
19
|
image: "full",
|
|
18
20
|
images: "full",
|
|
19
21
|
};
|
|
20
|
-
|
|
22
|
+
const READONLY_FULL_WIDTH_TYPES = new Set(["text", "uuid"]);
|
|
23
|
+
// Exported for unit tests.
|
|
24
|
+
export function fieldCellWidth(field) {
|
|
21
25
|
if (field.span !== undefined || (field.type === "text" && field.multiline))
|
|
22
26
|
return "full";
|
|
27
|
+
// A readonly value (UUID, long code) is plain text that must not be cut at the input width.
|
|
28
|
+
if (field.readOnly && READONLY_FULL_WIDTH_TYPES.has(field.type))
|
|
29
|
+
return "full";
|
|
23
30
|
return FIELD_CELL_WIDTH_BY_TYPE[field.type] ?? "text";
|
|
24
31
|
}
|
|
25
32
|
export function GridCellForField({ field, columns, issues, onChange, GridCell, featureName, labelAppendix, fieldAppendix, allIssues, valueDisplay, row, changed, flow, }) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EntityDefinition, EntityEditScreenDefinition, IconKey, RowAction, RowActionDrawer } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
-
import type { FormSnapshot, FormValues, SubmitResult, Translate } from "@cosmicdrift/kumiko-headless";
|
|
2
|
+
import type { DispatcherError, FormSnapshot, FormValues, SubmitResult, Translate } from "@cosmicdrift/kumiko-headless";
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
4
|
import type * as z from "zod";
|
|
5
5
|
export type RenderEditProps<TValues extends FormValues, TCtx = unknown> = {
|
|
@@ -38,7 +38,9 @@ export type RenderEditProps<TValues extends FormValues, TCtx = unknown> = {
|
|
|
38
38
|
* onto form fields — see SubmitConfig.serverFieldPathPrefix. Only needed
|
|
39
39
|
* when buildPayload nests the form values under a key. */
|
|
40
40
|
readonly serverFieldPathPrefix?: string;
|
|
41
|
-
|
|
41
|
+
/** Returning the rejected write's error shows it in the form-error banner;
|
|
42
|
+
* returning nothing means the delete went through. */
|
|
43
|
+
readonly onDelete?: () => Promise<DispatcherError | void> | DispatcherError | void;
|
|
42
44
|
readonly onCancel?: () => void;
|
|
43
45
|
readonly onReload?: () => void;
|
|
44
46
|
/** Fires when the form gains or loses unsaved input (field changes or a
|
|
@@ -3,7 +3,7 @@ import { evalFieldCondition, isFieldsEditSection, normalizeEditField, PROJECTION
|
|
|
3
3
|
import { computeEditViewModel } from "@cosmicdrift/kumiko-headless";
|
|
4
4
|
import { RenderEditActionButton } from "./render-edit-action-button.js";
|
|
5
5
|
import { AllFieldsRequiredProvider } from "./render-field.js";
|
|
6
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
+
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
7
7
|
import { ExtensionFormRegistryProvider, useExtensionFormHost, } from "../app/extension-form-submit.js";
|
|
8
8
|
import { extensionSectionName, useExtensionSectionComponent } from "../app/extension-sections.js";
|
|
9
9
|
import { useOptionalDispatcher } from "../context/dispatcher-context.js";
|
|
@@ -520,7 +520,7 @@ export function RenderEdit(props) {
|
|
|
520
520
|
section.visible), [vm.sections, fieldsFilter]);
|
|
521
521
|
const allFieldsRequired = useMemo(() => {
|
|
522
522
|
const fillable = filteredSections.flatMap((section) => section.kind === "fields"
|
|
523
|
-
? section.fields.filter((field) => field.visible && !field.readOnly)
|
|
523
|
+
? section.fields.filter((field) => field.visible && !field.readOnly && field.type !== "boolean")
|
|
524
524
|
: []);
|
|
525
525
|
return fillable.length > 0 && fillable.every((field) => field.required);
|
|
526
526
|
}, [filteredSections]);
|
|
@@ -957,6 +957,12 @@ export function RenderEdit(props) {
|
|
|
957
957
|
return id == null || id === "";
|
|
958
958
|
})();
|
|
959
959
|
const formMode = isCreate ? "create" : "edit";
|
|
960
|
+
const recordTitleValue = !isCreate && screen.recordTitleField !== undefined
|
|
961
|
+
? initial[screen.recordTitleField]
|
|
962
|
+
: undefined;
|
|
963
|
+
const recordTitle = typeof recordTitleValue === "string" && recordTitleValue.trim() !== ""
|
|
964
|
+
? recordTitleValue
|
|
965
|
+
: undefined;
|
|
960
966
|
const resolveScreenText = (suffix) => {
|
|
961
967
|
for (const key of [
|
|
962
968
|
`screen:${screen.id}.${formMode}.${suffix}`,
|
|
@@ -1006,7 +1012,7 @@ export function RenderEdit(props) {
|
|
|
1006
1012
|
})
|
|
1007
1013
|
: translate("kumiko.wizard.step", { current: currentStep + 1, total: lastStepIndex + 1 });
|
|
1008
1014
|
})();
|
|
1009
|
-
return (_jsxs(ExtensionFormRegistryProvider, { value: extensionFormRegistry, children: [titleInShell && (_jsx(PageHeader, { title: formTitle, ...(headerMenuItems.length > 0 &&
|
|
1015
|
+
return (_jsxs(ExtensionFormRegistryProvider, { value: extensionFormRegistry, children: [titleInShell && (_jsx(PageHeader, { title: formTitle, ...(recordTitle !== undefined && { recordTitle }), ...(headerMenuItems.length > 0 &&
|
|
1010
1016
|
ActionOverflowMenu !== undefined && {
|
|
1011
1017
|
actions: (_jsx(ActionOverflowMenu, { label: translate("kumiko.list.row-actions.more"), items: headerMenuItems, testId: "render-edit-header-menu" })),
|
|
1012
1018
|
}) })), _jsx(Form, { onSubmit: () => void handleSubmit(), ...(hideSectionTitles !== true && !titleInShell && { title: formTitle }), ...(formSubtitle !== undefined && !stepRail && { subtitle: formSubtitle }), ...(isScreenForm && { screenForm: true }), ...(unsavedCount > 0 && { unsavedCount }), ...(sectionNavItems.length > 0 && { sectionNav: sectionNavItems }), ...(stepRail && {
|
|
@@ -1078,17 +1084,12 @@ export function RenderEdit(props) {
|
|
|
1078
1084
|
// over TValues; controller is mount-lifetime-stable, see onControlsReady above.
|
|
1079
1085
|
patch: patchAndScheduleDraftSave, validate: scopedValidate, hideTitle: hideSectionTitles, ...(hideSectionTitles !== true &&
|
|
1080
1086
|
sectionActionsEl !== undefined && { actions: sectionActionsEl }) }, section.title));
|
|
1081
|
-
//
|
|
1082
|
-
//
|
|
1083
|
-
//
|
|
1084
|
-
// stays on Card. No title here: the tab strip already labels the
|
|
1085
|
-
// panel (matches the non-tabs Section branch above via hideTitle).
|
|
1086
|
-
// testId stays only on the inner Section — this outer Card is
|
|
1087
|
+
// Tabs mode: unframed like relatedList tabs; the Card chrome is
|
|
1088
|
+
// only kept when section actions need a header to live in.
|
|
1089
|
+
// testId stays only on the inner Section — the outer wrapper is
|
|
1087
1090
|
// purely chrome, giving it the same testId would register two
|
|
1088
1091
|
// elements under one id.
|
|
1089
|
-
const wrapped = hideSectionTitles
|
|
1090
|
-
slots: { headerActions: sectionActionsEl },
|
|
1091
|
-
}), children: mount }, section.title)) : (mount);
|
|
1092
|
+
const wrapped = hideSectionTitles !== true ? (mount) : sectionActionsEl !== undefined ? (_jsx(Card, { slots: { headerActions: sectionActionsEl }, children: mount }, section.title)) : (_jsx(Fragment, { children: mount }, section.title));
|
|
1092
1093
|
return wrapWizardStep(section.title, wrapped);
|
|
1093
1094
|
}
|
|
1094
1095
|
if (section.kind === "relatedList") {
|
|
@@ -1163,7 +1164,15 @@ export function RenderEdit(props) {
|
|
|
1163
1164
|
}), formError !== null && (_jsx(Banner, { variant: "error", testId: "render-edit-form-error", actions: formError.code === "version_conflict" && onReload !== undefined ? (_jsx(Button, { variant: "secondary", onClick: () => {
|
|
1164
1165
|
onReload();
|
|
1165
1166
|
setFormError(null);
|
|
1166
|
-
}, testId: "render-edit-form-error-reload", children: translate("kumiko.actions.reload") })) : undefined, children: _jsx(Text, { testId: "render-edit-form-error-key", children: translate(formError.i18nKey) }) })), extensionErrorKey !== null && (_jsx(Banner, { variant: "error", testId: "render-edit-extension-error", children: _jsx(Text, { testId: "render-edit-extension-error-key", children: translate(extensionErrorKey) }) })), actionError !== null && (_jsx(Banner, { variant: "error", testId: "render-edit-action-error", children: actionError })), onDelete !== undefined && (_jsx(Dialog, { open: confirmDeleteOpen, onOpenChange: setConfirmDeleteOpen, title: translate("kumiko.actions.delete-confirm"), confirmLabel: translate("kumiko.actions.delete"), variant: "danger", onConfirm: async () => {
|
|
1167
|
-
await onDelete();
|
|
1167
|
+
}, testId: "render-edit-form-error-reload", children: translate("kumiko.actions.reload") })) : undefined, children: _jsx(Text, { testId: "render-edit-form-error-key", children: translate(formError.i18nKey, formError.i18nParams) }) })), extensionErrorKey !== null && (_jsx(Banner, { variant: "error", testId: "render-edit-extension-error", children: _jsx(Text, { testId: "render-edit-extension-error-key", children: translate(extensionErrorKey) }) })), actionError !== null && (_jsx(Banner, { variant: "error", testId: "render-edit-action-error", children: actionError })), onDelete !== undefined && (_jsx(Dialog, { open: confirmDeleteOpen, onOpenChange: setConfirmDeleteOpen, title: translate("kumiko.actions.delete-confirm"), confirmLabel: translate("kumiko.actions.delete"), variant: "danger", onConfirm: async () => {
|
|
1168
|
+
const rejection = await onDelete();
|
|
1169
|
+
// A rejected delete (e.g. a preDelete hook) carries its reason in
|
|
1170
|
+
// the field issues; the top-level key is only "validation failed".
|
|
1171
|
+
if (rejection !== undefined) {
|
|
1172
|
+
const issue = rejection.details?.fields?.[0];
|
|
1173
|
+
setFormError(issue === undefined
|
|
1174
|
+
? rejection
|
|
1175
|
+
: { ...rejection, i18nKey: issue.i18nKey, i18nParams: issue.params });
|
|
1176
|
+
}
|
|
1168
1177
|
}, testId: "render-edit-delete-dialog" }))] }) })] }));
|
|
1169
1178
|
}
|
|
@@ -54,7 +54,7 @@ export function RenderField({ field, issues, onChange, featureName, labelAppendi
|
|
|
54
54
|
t,
|
|
55
55
|
row,
|
|
56
56
|
}));
|
|
57
|
-
return (_jsx(Field, { id: id, label: field.label, required: field.required && !allFieldsRequired, ...(issues !== undefined && { issues }), ...(labelAppendix !== undefined && { labelAppendix }), ...(fieldAppendix !== undefined && { fieldAppendix }), ...(changed === true && { changed }), testId: `field-${field.field}`, children: control }));
|
|
57
|
+
return (_jsx(Field, { id: id, label: field.label, required: field.required && field.type !== "boolean" && !allFieldsRequired, ...(issues !== undefined && { issues }), ...(labelAppendix !== undefined && { labelAppendix }), ...(fieldAppendix !== undefined && { fieldAppendix }), ...(changed === true && { changed }), testId: `field-${field.field}`, children: control }));
|
|
58
58
|
}
|
|
59
59
|
// Where a reference field's option rows come from, and which of their keys
|
|
60
60
|
// holds the display text — the entity's own list handler plus `refLabelField`
|
|
@@ -319,14 +319,11 @@ function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner,
|
|
|
319
319
|
case "bigInt": {
|
|
320
320
|
const icon = resolveFieldIcon(field);
|
|
321
321
|
const unit = resolveNumberUnit(field.unit, row);
|
|
322
|
-
return (_jsx(Input, { kind: "number", ...common, value: numberValue(field.value), onChange: (v) => onChange(v), ...(icon !== undefined && { icon }), ...(unit !== undefined && { unit }) }));
|
|
322
|
+
return (_jsx(Input, { kind: "number", ...common, value: numberValue(field.value), onChange: (v) => onChange(v), locale: appLocale, ...(field.grouping !== undefined && { grouping: field.grouping }), ...(field.type === "bigInt" && { integer: true }), ...(icon !== undefined && { icon }), ...(unit !== undefined && { unit }) }));
|
|
323
323
|
}
|
|
324
324
|
case "decimal": {
|
|
325
|
-
// step="any" disables the native stepMismatch constraint — without it
|
|
326
|
-
// <input type="number"> defaults to step=1 and blocks form submit on
|
|
327
|
-
// any fractional value via silent browser-native validation.
|
|
328
325
|
const icon = resolveFieldIcon(field);
|
|
329
|
-
return (_jsx(Input, { kind: "number", ...common, value: numberValue(field.value), onChange: (v) => onChange(v),
|
|
326
|
+
return (_jsx(Input, { kind: "number", ...common, value: numberValue(field.value), onChange: (v) => onChange(v), locale: appLocale, ...(field.grouping !== undefined && { grouping: field.grouping }), ...(icon !== undefined && { icon }) }));
|
|
330
327
|
}
|
|
331
328
|
case "tz":
|
|
332
329
|
return (_jsx(Input, { kind: "tz", ...common, value: stringValue(field.value), onChange: (v) => onChange(v) }));
|
package/dist/i18n-defaults.js
CHANGED
|
@@ -81,7 +81,15 @@ export const kumikoDefaultTranslations = {
|
|
|
81
81
|
"kumiko.combobox.empty": "No matches.",
|
|
82
82
|
"kumiko.combobox.loading": "Loading…",
|
|
83
83
|
"kumiko.combobox.placeholder": "—",
|
|
84
|
+
"kumiko.dashboard.chart.forecast": "Forecast",
|
|
85
|
+
"kumiko.dashboard.chart.markers": "Events",
|
|
84
86
|
"kumiko.dashboard.filter.all": "All",
|
|
87
|
+
"kumiko.dashboard.panel.error.isolated": "The other panels are not affected.",
|
|
88
|
+
"kumiko.dashboard.panel.error.retry": "Try again",
|
|
89
|
+
"kumiko.dashboard.panel.error.title": "{label} could not be loaded",
|
|
90
|
+
"kumiko.dashboard.time-range": "Time range",
|
|
91
|
+
"kumiko.dashboard.today": "Today",
|
|
92
|
+
"kumiko.dashboard.updated-at": "As of {time}",
|
|
85
93
|
"kumiko.app-boot.loading": "Loading…",
|
|
86
94
|
"kumiko.app-boot.unauthorized": "You need to sign in to see this.",
|
|
87
95
|
"kumiko.app-boot.failed": "Couldn't load the app. Check your connection and try again.",
|
package/dist/primitives.d.ts
CHANGED
|
@@ -39,6 +39,10 @@ export type ButtonProps = {
|
|
|
39
39
|
/** Barrierefreies Label — Pflicht bei icon-only-Buttons (children ist nur
|
|
40
40
|
* ein Icon/Zeichen), sonst hat der Button keinen zugänglichen Namen. */
|
|
41
41
|
readonly ariaLabel?: string;
|
|
42
|
+
/** Tooltip — web renders the native `title` attribute, native impls may ignore it. */
|
|
43
|
+
readonly title?: string;
|
|
44
|
+
/** Toggle state for toggle buttons — web sets `aria-pressed`; leave unset for plain buttons. */
|
|
45
|
+
readonly pressed?: boolean;
|
|
42
46
|
/** Breite — default="auto" (inhaltsbreit). "full" streckt CTA-Buttons auf
|
|
43
47
|
* die Container-Breite (Karten/Panels). Andere Breiten sind Layout-Sache
|
|
44
48
|
* des Containers, kein Button-Prop (Kit hält arbiträres Sizing draußen). */
|
|
@@ -207,6 +211,12 @@ export type InputProps = {
|
|
|
207
211
|
/** `<input step>`. "any" disables the native stepMismatch constraint
|
|
208
212
|
* (needed for decimal fields — integer fields leave this unset). */
|
|
209
213
|
readonly step?: number | "any";
|
|
214
|
+
/** BCP-47 tag for the grouped display and the decimal separator when typing. */
|
|
215
|
+
readonly locale?: string;
|
|
216
|
+
/** `false` shows the value without a thousands separator (e.g. a year). */
|
|
217
|
+
readonly grouping?: boolean;
|
|
218
|
+
/** Integer-only entry (bigInt): fractional input is rejected on blur. */
|
|
219
|
+
readonly integer?: boolean;
|
|
210
220
|
/** Resolved display suffix (static or from a sibling field) — never
|
|
211
221
|
* part of the numeric value. */
|
|
212
222
|
readonly unit?: string;
|
|
@@ -471,8 +481,9 @@ export type DataTableRowAction = {
|
|
|
471
481
|
readonly onTrigger: (row: ListRowViewModel) => Promise<void> | void;
|
|
472
482
|
/** Conditional Visibility pro Row (z.B. "Start" nur wenn status==="scheduled"). */
|
|
473
483
|
readonly isVisible?: (row: ListRowViewModel) => boolean;
|
|
474
|
-
/** This action is what a click on the row does.
|
|
475
|
-
*
|
|
484
|
+
/** This action is what a click on the row does. With `onRowClick` the table
|
|
485
|
+
* (except `"inline"` mode or editable cells) and the card layout drop it
|
|
486
|
+
* from the row menu, since the row itself is already the click target. */
|
|
476
487
|
readonly rowClick?: boolean;
|
|
477
488
|
/** Resolved icon (author `RowAction.icon` or the id-derived default) —
|
|
478
489
|
* drives both the icon-left-of-text render and the icon-only collapse
|
|
@@ -526,7 +537,8 @@ export type DataTableProps = {
|
|
|
526
537
|
* sich nur um Render + Confirm-Dialog. */
|
|
527
538
|
readonly rowActions?: readonly DataTableRowAction[];
|
|
528
539
|
/** How the row-action column renders:
|
|
529
|
-
* - `"adaptive"` (default): with `onRowClick` all actions
|
|
540
|
+
* - `"adaptive"` (default): with `onRowClick` all actions except the
|
|
541
|
+
* `rowClick` one go in the kebab (no kebab or column if none remain);
|
|
530
542
|
* without it the primary action is a link button and the rest go in the
|
|
531
543
|
* kebab (a single action gets no kebab).
|
|
532
544
|
* - `"inline"`: ALWAYS inline buttons, left-aligned, even with >2 (no
|
|
@@ -864,7 +876,7 @@ export type GridProps = {
|
|
|
864
876
|
readonly flow?: boolean;
|
|
865
877
|
};
|
|
866
878
|
/** Width class of a field cell inside a `flow` grid: sized by what the field holds. */
|
|
867
|
-
export type FieldCellWidth = "text" | "number" | "money" | "date" | "select" | "full" | "auto";
|
|
879
|
+
export type FieldCellWidth = "text" | "number" | "money" | "date" | "timestamp" | "select" | "full" | "auto";
|
|
868
880
|
/** Span-Wrapper für ein Kind innerhalb eines Grid. Web: `style={{gridColumn: span N}}`,
|
|
869
881
|
* Native: eigenes Width-Rechnen. */
|
|
870
882
|
export type GridCellProps = {
|
|
@@ -1131,6 +1143,8 @@ export type JsonViewProps = {
|
|
|
1131
1143
|
/** Band above the tabs of a record detail: optional subtitle line plus the
|
|
1132
1144
|
* metrics as a definition list. `children` are `Metric` tiles. */
|
|
1133
1145
|
export type MetricBandProps = {
|
|
1146
|
+
/** Free-form block rendered unstyled as the first full-width row of the band. */
|
|
1147
|
+
readonly lead?: ReactNode;
|
|
1134
1148
|
readonly subtitle?: ReactNode;
|
|
1135
1149
|
readonly children: ReactNode;
|
|
1136
1150
|
readonly testId?: string;
|
|
@@ -1141,6 +1155,8 @@ export type MetricBandProps = {
|
|
|
1141
1155
|
* (see `usePageHeaderSlotAvailable`) callers keep their previous placement. */
|
|
1142
1156
|
export type PageHeaderProps = {
|
|
1143
1157
|
readonly title?: string;
|
|
1158
|
+
/** Name of the loaded record; shown as an extra breadcrumb before the last one. */
|
|
1159
|
+
readonly recordTitle?: string;
|
|
1144
1160
|
readonly status?: ReactNode;
|
|
1145
1161
|
readonly actions?: ReactNode;
|
|
1146
1162
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.330.0",
|
|
4
4
|
"description": "Platform-agnostic React renderer for Kumiko screens. Contains the shared logic — primitives-contract, hooks, KumikoScreen, navigation & SSE abstractions — that any platform-specific renderer (web, native) composes. No DOM, no EventSource, no react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
20
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "0.330.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.330.0",
|
|
20
|
+
"@cosmicdrift/kumiko-types": "0.330.0",
|
|
21
21
|
"react": "^19.2.6",
|
|
22
22
|
"temporal-polyfill": "^0.3.2",
|
|
23
23
|
"zod": "^4.4.3"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@types/react-dom": "^19.2.3",
|
|
29
29
|
"jsdom": "^29.1.1",
|
|
30
30
|
"react-dom": "^19.2.6",
|
|
31
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
31
|
+
"@cosmicdrift/kumiko-locale-de": "0.330.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.330.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "entityEdit screens show the server's reason when a delete is rejected",
|
|
6
|
+
"detail": "The reason (for example from a preDelete hook) now appears in the form-error banner instead of the confirm dialog closing silently. `RenderEdit`'s `onDelete` may return the rejected write's `DispatcherError` for this."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.330.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "Screen schema gains createScreen, recordTitleField and configEdit fillHeight; extension tabs render unframed",
|
|
12
|
+
"detail": "`createScreen` on an entityList names the screen opened by the create button. `recordTitleField` on an entityEdit shows a field value as the record title in the header breadcrumb (list > record > screen title). `fillHeight` on a configEdit (default true) switches it to the screen-form layout with a pinned footer; `false` restores the card layout. All three are optional and validated at boot. Extension tabs on a projectionDetail no longer wrap their content in a Card, like relatedList tabs. A `headerSlot` now renders inside the page-header slot instead of replacing the header.",
|
|
13
|
+
"migration": "Additive. Set `createScreen: \"<screen-id>\"` on an entityList and `recordTitleField: \"<field>\"` on an entityEdit to use them. configEdit screens now fill the shell height; set `fillHeight: false` to keep the card layout. Apps with screenshot tests of extension tabs lose the surrounding card frame."
|
|
14
|
+
},
|
|
2
15
|
{
|
|
3
16
|
"version": "0.328.0",
|
|
4
17
|
"type": "breaking",
|