@cosmicdrift/kumiko-renderer 0.330.0 → 0.330.2
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/kumiko-screen.js +12 -4
- package/dist/components/grid-cell-for-field.js +1 -1
- package/dist/components/render-edit.d.ts +2 -0
- package/dist/components/render-edit.js +18 -15
- package/dist/components/write-form-section.js +3 -4
- package/dist/primitives.d.ts +4 -1
- package/package.json +5 -5
- package/src/changes.json +14 -0
|
@@ -5,7 +5,7 @@ import { resolveActionIcon } from "@cosmicdrift/kumiko-types/action-icon";
|
|
|
5
5
|
import { TENANT_CURRENCY_CONFIG_KEY } from "@cosmicdrift/kumiko-types/fields";
|
|
6
6
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
7
7
|
import { extractCreatedId, extractIdField } from "../components/reference-create-dialog.js";
|
|
8
|
-
import { RenderEdit, } from "../components/render-edit.js";
|
|
8
|
+
import { RenderEdit, resolveRecordTitle, } from "../components/render-edit.js";
|
|
9
9
|
import { needsActionConfirm, RenderEditActionButton, RenderEditActionConfirmDialog, } from "../components/render-edit-action-button.js";
|
|
10
10
|
import { RenderList } from "../components/render-list.js";
|
|
11
11
|
import { useDispatcher, useOptionalDispatcher } from "../context/dispatcher-context.js";
|
|
@@ -1843,6 +1843,9 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
1843
1843
|
// this shares the same page padding/width as the record's edit card below
|
|
1844
1844
|
// it, instead of sitting flush against the screen edge (fw record-screen
|
|
1845
1845
|
// header polish).
|
|
1846
|
+
const recordTitle = screen.recordTitleField !== undefined
|
|
1847
|
+
? resolveRecordTitle(record[screen.recordTitleField])
|
|
1848
|
+
: undefined;
|
|
1846
1849
|
const header = screen.header;
|
|
1847
1850
|
const subtitleHref = resolveSubtitleHref(header, record);
|
|
1848
1851
|
// Plain Text, not Heading — the Card title slot already renders as its own
|
|
@@ -1867,15 +1870,17 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
1867
1870
|
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 }));
|
|
1868
1871
|
const usesPageHeaderSlot = pageHeaderSlotAvailable && PageHeader !== undefined;
|
|
1869
1872
|
const actionErrorBanner = actionError !== null && (_jsx(Banner, { variant: "error", testId: "render-edit-action-error", children: actionError }));
|
|
1870
|
-
const renderHeaderContent = (headerSlot) => (_jsxs(_Fragment, { children: [usesPageHeaderSlot ? (_jsxs(_Fragment, { children: [_jsx(PageHeader, { ...(header !== undefined && { title: String(record[header.title] ?? "") }), ...(header?.status !== undefined &&
|
|
1873
|
+
const renderHeaderContent = (headerSlot) => (_jsxs(_Fragment, { children: [usesPageHeaderSlot ? (_jsxs(_Fragment, { children: [_jsx(PageHeader, { ...(header !== undefined && { title: String(record[header.title] ?? "") }), ...(header === undefined && recordTitle !== undefined && { title: recordTitle }), ...(header?.status !== undefined &&
|
|
1871
1874
|
StatusBadge !== undefined && {
|
|
1872
1875
|
status: (_jsx(StatusBadge, { value: String(record[header.status] ?? ""), tone: headerStatusTone(header, record), testId: "kumiko-screen-projection-detail-status" })),
|
|
1873
1876
|
}), ...(hasHeaderActions && {
|
|
1874
1877
|
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 }) })),
|
|
1875
1878
|
}) }), MetricBand === undefined && headerSlot, (hasMetrics || headerSubtitleSlot !== undefined || headerSlot !== undefined) &&
|
|
1876
1879
|
(MetricBand !== undefined || hasMetrics) &&
|
|
1877
|
-
metricsBlock(headerSubtitleSlot, MetricBand !== undefined ? headerSlot : undefined), actionErrorBanner] })) : ((hasHeaderCard || headerSlot !== undefined) && (_jsxs(Card, { slots: {
|
|
1880
|
+
metricsBlock(headerSubtitleSlot, MetricBand !== undefined ? headerSlot : undefined), actionErrorBanner] })) : ((hasHeaderCard || headerSlot !== undefined || recordTitle !== undefined) && (_jsxs(Card, { slots: {
|
|
1878
1881
|
...(headerTitleSlot !== undefined && { title: headerTitleSlot }),
|
|
1882
|
+
...(headerTitleSlot === undefined &&
|
|
1883
|
+
recordTitle !== undefined && { title: recordTitle }),
|
|
1879
1884
|
...(headerSubtitleSlot !== undefined && { subtitle: headerSubtitleSlot }),
|
|
1880
1885
|
...(headerSlot !== undefined && { headerContent: headerSlot }),
|
|
1881
1886
|
...(hasHeaderActions && { headerActions: headerActionsContent }),
|
|
@@ -1891,7 +1896,10 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
1891
1896
|
...(count !== undefined && { count }),
|
|
1892
1897
|
};
|
|
1893
1898
|
}), activeId: activeSection.id ?? "", onSelect: (id) => nav.setSearchParams({ tab: id }) }))] }));
|
|
1894
|
-
return (_jsxs(_Fragment, { children: [_jsx(RenderEdit, { screen: detailScreen, entity: entity, featureName: schema.featureName, initial: record, entityId: effectiveEntityId, customSubmit: async () => ({ isSuccess: true, validationBlocked: false, data: undefined }), onReload: reloadDetail, onRelatedListDrawerAction: openDrawer, ...(translate !== undefined && { translate }), ...(hasTabs && { hideSectionTitles: true }), ...(screenFillsHeight(screen) && { fillScreenHeight: true }), ...((hasHeaderCard ||
|
|
1899
|
+
return (_jsxs(_Fragment, { children: [_jsx(RenderEdit, { screen: detailScreen, entity: entity, featureName: schema.featureName, initial: record, entityId: effectiveEntityId, customSubmit: async () => ({ isSuccess: true, validationBlocked: false, data: undefined }), onReload: reloadDetail, onRelatedListDrawerAction: openDrawer, ...(translate !== undefined && { translate }), ...(hasTabs && { hideSectionTitles: true }), ...(screenFillsHeight(screen) && { fillScreenHeight: true }), ...((hasHeaderCard ||
|
|
1900
|
+
hasTabs ||
|
|
1901
|
+
screen.slots?.header !== undefined ||
|
|
1902
|
+
screen.recordTitleField !== undefined) && {
|
|
1895
1903
|
headerRegion: renderHeaderContent,
|
|
1896
1904
|
}), buildSectionActions: buildSectionActions, valueDisplay: screen.valueDisplay ?? "text" }, `${entityId}:${reloadNonce}`), _jsx(DrawerHost, { schema: schema, drawerAction: drawerAction, drawerScreen: drawerScreen, ...(drawerInitialValues !== undefined && { drawerInitialValues }), userRoles: userRoles, ...(translate !== undefined && { translate }), onClose: closeDrawer, onSuccess: () => {
|
|
1897
1905
|
closeDrawer();
|
|
@@ -3,4 +3,6 @@ import type { RenderEditProps } from "./render-edit-types.js";
|
|
|
3
3
|
export type { RenderEditAction, RenderEditChangeState, RenderEditControls, RenderEditProps, } from "./render-edit-types.js";
|
|
4
4
|
import { type ReactNode } from "react";
|
|
5
5
|
export declare const PATCH_DRAFT_SAVE_DEBOUNCE_MS = 500;
|
|
6
|
+
/** A record title is a non-blank string; anything else leaves the screen title alone. */
|
|
7
|
+
export declare function resolveRecordTitle(value: unknown): string | undefined;
|
|
6
8
|
export declare function RenderEdit<TValues extends FormValues, TCtx = unknown>(props: RenderEditProps<TValues, TCtx>): ReactNode;
|
|
@@ -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 {
|
|
6
|
+
import { 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";
|
|
@@ -141,6 +141,10 @@ function EditSlotMount({ slot, slotName, screenId, entityName, entityId, values,
|
|
|
141
141
|
function formSectionDomId(sectionIndex) {
|
|
142
142
|
return `form-section-${sectionIndex}`;
|
|
143
143
|
}
|
|
144
|
+
/** A record title is a non-blank string; anything else leaves the screen title alone. */
|
|
145
|
+
export function resolveRecordTitle(value) {
|
|
146
|
+
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
147
|
+
}
|
|
144
148
|
export function RenderEdit(props) {
|
|
145
149
|
const { screen, entity, featureName, initial, writeCommand, translate: translateProp, ctx, schema, onSubmit, payloadMode = "values", buildPayload, serverFieldPathPrefix, onDelete, onCancel, onReload, onDirtyChange, onCopyLink, actions, onRelatedListDrawerAction, submitLabel, submitVariant, summary, labelAppendix, fieldAppendix, entityId: entityIdProp, extensionInitialValues, onChange, onControlsReady, fields: fieldsFilter, disabled = false, hideActions, valueDisplay = "form", hideSectionTitles, fillScreenHeight, headerRegion, buildSectionActions, } = props;
|
|
146
150
|
const { customSubmit } = props;
|
|
@@ -957,11 +961,8 @@ export function RenderEdit(props) {
|
|
|
957
961
|
return id == null || id === "";
|
|
958
962
|
})();
|
|
959
963
|
const formMode = isCreate ? "create" : "edit";
|
|
960
|
-
const
|
|
961
|
-
? initial[screen.recordTitleField]
|
|
962
|
-
: undefined;
|
|
963
|
-
const recordTitle = typeof recordTitleValue === "string" && recordTitleValue.trim() !== ""
|
|
964
|
-
? recordTitleValue
|
|
964
|
+
const recordTitle = !isCreate && screen.recordTitleField !== undefined
|
|
965
|
+
? resolveRecordTitle(initial[screen.recordTitleField])
|
|
965
966
|
: undefined;
|
|
966
967
|
const resolveScreenText = (suffix) => {
|
|
967
968
|
for (const key of [
|
|
@@ -1084,12 +1085,14 @@ export function RenderEdit(props) {
|
|
|
1084
1085
|
// over TValues; controller is mount-lifetime-stable, see onControlsReady above.
|
|
1085
1086
|
patch: patchAndScheduleDraftSave, validate: scopedValidate, hideTitle: hideSectionTitles, ...(hideSectionTitles !== true &&
|
|
1086
1087
|
sectionActionsEl !== undefined && { actions: sectionActionsEl }) }, section.title));
|
|
1087
|
-
// Tabs mode: unframed
|
|
1088
|
-
//
|
|
1089
|
-
// testId stays only on the inner
|
|
1090
|
-
//
|
|
1088
|
+
// Tabs mode: an unframed padded panel (the tab panel is the
|
|
1089
|
+
// surface), so extension content neither sits flush at the shell
|
|
1090
|
+
// edge nor touches the tab strip. testId stays only on the inner
|
|
1091
|
+
// Section — giving the wrapper the same one would register two
|
|
1091
1092
|
// elements under one id.
|
|
1092
|
-
const wrapped = hideSectionTitles !== true ? (mount) :
|
|
1093
|
+
const wrapped = hideSectionTitles !== true ? (mount) : (_jsx(Card, { options: { framed: false }, ...(sectionActionsEl !== undefined && {
|
|
1094
|
+
slots: { headerActions: sectionActionsEl },
|
|
1095
|
+
}), children: mount }, section.title));
|
|
1093
1096
|
return wrapWizardStep(section.title, wrapped);
|
|
1094
1097
|
}
|
|
1095
1098
|
if (section.kind === "relatedList") {
|
|
@@ -1126,8 +1129,8 @@ export function RenderEdit(props) {
|
|
|
1126
1129
|
}), ...(fieldAppendix !== undefined && {
|
|
1127
1130
|
fieldAppendix: fieldAppendix(field.field),
|
|
1128
1131
|
}), allIssues: snapshot.errors, valueDisplay: valueDisplay, row: snapshot.values, ...(!isCreate && field.field in snapshot.changes && { changed: true }) }, field.field))) }, gridKey));
|
|
1129
|
-
// Tabs mode
|
|
1130
|
-
//
|
|
1132
|
+
// Tabs mode: the tab panel is the surface, so field sections render
|
|
1133
|
+
// as unframed padded panels; group cards inside stay framed.
|
|
1131
1134
|
// Stays `Card`, not `Section` — Section always flattens to a
|
|
1132
1135
|
// borderless divider when rendered inside this component's own
|
|
1133
1136
|
// `<Form>` (InsideFormContext), even with `chromeless` set, so it
|
|
@@ -1140,13 +1143,13 @@ export function RenderEdit(props) {
|
|
|
1140
1143
|
// group cards (Card-in-Card) — the boot-validator rejects that
|
|
1141
1144
|
// combination in tabs mode, so `sectionActionsEl` is always
|
|
1142
1145
|
// undefined here at runtime.
|
|
1143
|
-
return wrapWizardStep(sectionKey, groupsGrid);
|
|
1146
|
+
return wrapWizardStep(sectionKey, _jsx(Card, { options: { framed: false }, children: groupsGrid }, sectionKey));
|
|
1144
1147
|
}
|
|
1145
1148
|
// No section title here (fw#3218) — the Tab strip right above
|
|
1146
1149
|
// already names this panel, so a repeated Card title would just
|
|
1147
1150
|
// duplicate it. The title row only exists to carry actions, and
|
|
1148
1151
|
// then it carries only the actions, never the title text.
|
|
1149
|
-
const cardEl = (_jsx(Card, { ...(sectionActionsEl !== undefined && {
|
|
1152
|
+
const cardEl = (_jsx(Card, { options: { framed: false }, ...(sectionActionsEl !== undefined && {
|
|
1150
1153
|
slots: { headerActions: sectionActionsEl },
|
|
1151
1154
|
}), testId: `section-${sectionKey}`, children: renderFieldGrid(section.fields, section.columns, `${sectionKey}-grid`) }, sectionKey));
|
|
1152
1155
|
return wrapWizardStep(sectionKey, cardEl);
|
|
@@ -88,11 +88,10 @@ export function WriteFormSection({ section, featureName, translate, hideTitle, o
|
|
|
88
88
|
const submitButton = (_jsx(Button, { type: "button", variant: "primary", disabled: isSubmitting, loading: isSubmitting, onClick: () => void handleSubmit(), testId: "write-form-section-submit", children: section.submitLabel ?? effectiveTranslate("kumiko.actions.save") }));
|
|
89
89
|
const titleRowActions = actions !== undefined ? (_jsxs(_Fragment, { children: [actions, submitButton] })) : (submitButton);
|
|
90
90
|
// Section always flattens to a borderless divider when rendered inside
|
|
91
|
-
// RenderEdit's own <Form> in tabs mode (hideTitle) —
|
|
92
|
-
//
|
|
93
|
-
// branches stay on Card.
|
|
91
|
+
// RenderEdit's own <Form> in tabs mode (hideTitle) — an unframed Card panel
|
|
92
|
+
// stands in, like render-edit.tsx's tabs-mode fields/extension branches.
|
|
94
93
|
if (hideTitle) {
|
|
95
|
-
return (_jsx(Card, { slots: {
|
|
94
|
+
return (_jsx(Card, { options: { framed: false }, slots: {
|
|
96
95
|
...(section.description !== undefined && { subtitle: section.description }),
|
|
97
96
|
headerActions: titleRowActions,
|
|
98
97
|
}, testId: `write-form-${section.title ?? "section"}`, children: content }));
|
package/dist/primitives.d.ts
CHANGED
|
@@ -876,7 +876,7 @@ export type GridProps = {
|
|
|
876
876
|
readonly flow?: boolean;
|
|
877
877
|
};
|
|
878
878
|
/** Width class of a field cell inside a `flow` grid: sized by what the field holds. */
|
|
879
|
-
export type FieldCellWidth = "text" | "number" | "money" | "date" | "timestamp" | "select" | "full" | "
|
|
879
|
+
export type FieldCellWidth = "text" | "number" | "money" | "date" | "timestamp" | "select" | "full" | "toggle";
|
|
880
880
|
/** Span-Wrapper für ein Kind innerhalb eines Grid. Web: `style={{gridColumn: span N}}`,
|
|
881
881
|
* Native: eigenes Width-Rechnen. */
|
|
882
882
|
export type GridCellProps = {
|
|
@@ -1029,6 +1029,9 @@ export type CardOptions = {
|
|
|
1029
1029
|
* relatedList table sizing to the panel instead of the page) instead of
|
|
1030
1030
|
* normal document-flow height. Default false. */
|
|
1031
1031
|
readonly fillHeight?: boolean;
|
|
1032
|
+
/** false drops border, background, shadow and radius but keeps padding and
|
|
1033
|
+
* header slots — a panel inside a host surface such as a tab panel. Default true. */
|
|
1034
|
+
readonly framed?: boolean;
|
|
1032
1035
|
};
|
|
1033
1036
|
/** Generische Card-Chrome (border/bg/radius/shadow + Header/Body/Footer) als
|
|
1034
1037
|
* Primitive — eine Quelle für alle Card-Flächen, statt pro App nachgebaut.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.330.
|
|
3
|
+
"version": "0.330.2",
|
|
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.330.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.330.
|
|
20
|
-
"@cosmicdrift/kumiko-types": "0.330.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "0.330.2",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.330.2",
|
|
20
|
+
"@cosmicdrift/kumiko-types": "0.330.2",
|
|
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.330.
|
|
31
|
+
"@cosmicdrift/kumiko-locale-de": "0.330.2"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.330.1",
|
|
4
|
+
"type": "breaking",
|
|
5
|
+
"title": "Boolean fields in flow forms use FieldCellWidth \"toggle\" instead of \"auto\"",
|
|
6
|
+
"detail": "In flow forms (screen forms and drawers) a boolean field's label now shares the top line with its neighbours' labels and the switch sits on the input line. The `FieldCellWidth` union loses `\"auto\"` and gains `\"toggle\"`; the default web Grid maps it to a fixed minimum width.",
|
|
7
|
+
"migration": "Custom Grid/GridCell primitives keyed by FieldCellWidth: rename `auto` to `toggle`."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.330.1",
|
|
11
|
+
"type": "improvement",
|
|
12
|
+
"title": "projectionDetail tab panels get page padding, recordTitleField on projectionDetail",
|
|
13
|
+
"detail": "Tab panels on a projectionDetail with `layout.mode: \"tabs\"` now pad their content like the rest of the page: extension, field-section, groups and writeForm tabs render as an unframed padded panel with space below the tab strip. relatedList tabs stay flush. This fixes extension tabs sitting flush against the shell edge since 0.330.0. `CardOptions.framed` (default true) drops the card frame and keeps the padding. A projectionDetail without `header` can set `recordTitleField`: the query output field then titles the page header (breadcrumb \"list > record\"). The boot check requires the field to be in the query's outputSchema and rejects it next to `header`.",
|
|
14
|
+
"migration": "Extension tab panels get their padding back, so extension components that added their own padding to make up for the flush panel in 0.330.0 should drop it. Set `recordTitleField: \"<field>\"` on a projectionDetail without `header` to show the record name in the breadcrumb."
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"version": "0.330.0",
|
|
4
18
|
"type": "fix",
|