@cosmicdrift/kumiko-renderer 0.224.1 → 0.225.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.225.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,8 +15,8 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "0.225.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.225.0",
|
|
20
20
|
"react": "^19.2.6",
|
|
21
21
|
"temporal-polyfill": "^0.3.2",
|
|
22
22
|
"zod": "^4.4.3"
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@testing-library/react": "^16.3.2",
|
|
26
26
|
"@types/react": "^19.2.14",
|
|
27
27
|
"jsdom": "^29.1.1",
|
|
28
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
28
|
+
"@cosmicdrift/kumiko-locale-de": "0.225.0"
|
|
29
29
|
},
|
|
30
30
|
"repository": {
|
|
31
31
|
"type": "git",
|
|
@@ -2021,13 +2021,38 @@ function ProjectionDetailBody({
|
|
|
2021
2021
|
readonly translate?: Translate;
|
|
2022
2022
|
readonly entityId?: string;
|
|
2023
2023
|
}): ReactNode {
|
|
2024
|
-
const { Banner, Text } = usePrimitives();
|
|
2024
|
+
const { Banner, Text, Heading, Grid, GridCell, Tabs, StatusBadge } = usePrimitives();
|
|
2025
2025
|
const t = useTranslation();
|
|
2026
2026
|
const effectiveTranslate = translate ?? t;
|
|
2027
2027
|
const nav = useNav();
|
|
2028
2028
|
const idParam = screen.idParam ?? "id";
|
|
2029
|
+
const isTabsMode = screen.layout.mode === "tabs";
|
|
2030
|
+
const activeSection = useMemo(() => {
|
|
2031
|
+
if (!isTabsMode) return undefined;
|
|
2032
|
+
const tabParam = nav.searchParams["tab"];
|
|
2033
|
+
return (
|
|
2034
|
+
screen.layout.sections.find((section) => section.id === tabParam) ?? screen.layout.sections[0]
|
|
2035
|
+
);
|
|
2036
|
+
}, [isTabsMode, screen.layout.sections, nav.searchParams]);
|
|
2037
|
+
// Tabs is an optional Core-Primitive (additive rollout) — same "skip +
|
|
2038
|
+
// warn once" precedent as Drawer above, instead of crashing when a web
|
|
2039
|
+
// app hasn't upgraded its createKumikoApp wiring yet.
|
|
2040
|
+
useEffect(() => {
|
|
2041
|
+
if (isTabsMode && Tabs === undefined) {
|
|
2042
|
+
// biome-ignore lint/suspicious/noConsole: dev-warning for a setup error
|
|
2043
|
+
console.warn(
|
|
2044
|
+
`[kumiko] screen "${screen.id}" uses layout.mode: "tabs", but no <Tabs> primitive is registered — the tab strip will not render. createKumikoApp() from kumiko-renderer-web wires it automatically.`,
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
}, [isTabsMode, Tabs, screen.id]);
|
|
2029
2048
|
const entity = useMemo(() => synthesizeProjectionDetailEntity(screen.layout), [screen.layout]);
|
|
2030
|
-
const detailScreen = useMemo(() =>
|
|
2049
|
+
const detailScreen = useMemo(() => {
|
|
2050
|
+
const source =
|
|
2051
|
+
activeSection !== undefined
|
|
2052
|
+
? { ...screen, layout: { ...screen.layout, sections: [activeSection] } }
|
|
2053
|
+
: screen;
|
|
2054
|
+
return synthesizeProjectionDetailScreen(source);
|
|
2055
|
+
}, [screen, activeSection]);
|
|
2031
2056
|
const detailQuery = useQuery<Readonly<Record<string, unknown>>>(
|
|
2032
2057
|
screen.query,
|
|
2033
2058
|
entityId !== undefined ? { [idParam]: entityId } : {},
|
|
@@ -2221,17 +2246,74 @@ function ProjectionDetailBody({
|
|
|
2221
2246
|
);
|
|
2222
2247
|
}
|
|
2223
2248
|
return (
|
|
2224
|
-
|
|
2225
|
-
screen
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2249
|
+
<>
|
|
2250
|
+
{screen.header !== undefined && (
|
|
2251
|
+
<>
|
|
2252
|
+
<Heading variant="page" testId="kumiko-screen-projection-detail-title">
|
|
2253
|
+
{String(record[screen.header.title] ?? "")}
|
|
2254
|
+
</Heading>
|
|
2255
|
+
{screen.header.subtitle !== undefined && (
|
|
2256
|
+
<Text variant="muted" testId="kumiko-screen-projection-detail-subtitle">
|
|
2257
|
+
{String(record[screen.header.subtitle] ?? "")}
|
|
2258
|
+
</Text>
|
|
2259
|
+
)}
|
|
2260
|
+
{screen.header.status !== undefined &&
|
|
2261
|
+
(StatusBadge !== undefined ? (
|
|
2262
|
+
<StatusBadge
|
|
2263
|
+
value={String(record[screen.header.status] ?? "")}
|
|
2264
|
+
testId="kumiko-screen-projection-detail-status"
|
|
2265
|
+
/>
|
|
2266
|
+
) : (
|
|
2267
|
+
<Text testId="kumiko-screen-projection-detail-status">
|
|
2268
|
+
{String(record[screen.header.status] ?? "")}
|
|
2269
|
+
</Text>
|
|
2270
|
+
))}
|
|
2271
|
+
</>
|
|
2272
|
+
)}
|
|
2273
|
+
{screen.metrics !== undefined && screen.metrics.length > 0 && (
|
|
2274
|
+
<Grid columns={screen.metrics.length} testId="kumiko-screen-projection-detail-metrics">
|
|
2275
|
+
{screen.metrics.map((metric) => {
|
|
2276
|
+
const labelKey = screen.fieldLabels?.[metric];
|
|
2277
|
+
return (
|
|
2278
|
+
<GridCell key={metric}>
|
|
2279
|
+
<Text
|
|
2280
|
+
variant="small"
|
|
2281
|
+
testId={`kumiko-screen-projection-detail-metric-${metric}-label`}
|
|
2282
|
+
>
|
|
2283
|
+
{labelKey !== undefined ? effectiveTranslate(labelKey) : metric}
|
|
2284
|
+
</Text>
|
|
2285
|
+
<Text testId={`kumiko-screen-projection-detail-metric-${metric}-value`}>
|
|
2286
|
+
{String(record[metric] ?? "")}
|
|
2287
|
+
</Text>
|
|
2288
|
+
</GridCell>
|
|
2289
|
+
);
|
|
2290
|
+
})}
|
|
2291
|
+
</Grid>
|
|
2292
|
+
)}
|
|
2293
|
+
{isTabsMode && Tabs !== undefined && activeSection !== undefined && (
|
|
2294
|
+
<Tabs
|
|
2295
|
+
testId="kumiko-screen-projection-detail-tabs"
|
|
2296
|
+
items={screen.layout.sections.map((section) => ({
|
|
2297
|
+
id: section.id ?? "",
|
|
2298
|
+
label: effectiveTranslate(section.title ?? section.id ?? ""),
|
|
2299
|
+
}))}
|
|
2300
|
+
activeId={activeSection.id ?? ""}
|
|
2301
|
+
onSelect={(id) => nav.setSearchParams({ tab: id })}
|
|
2302
|
+
/>
|
|
2303
|
+
)}
|
|
2304
|
+
<RenderEdit
|
|
2305
|
+
screen={detailScreen}
|
|
2306
|
+
entity={entity}
|
|
2307
|
+
featureName={schema.featureName}
|
|
2308
|
+
initial={record as FormValues}
|
|
2309
|
+
entityId={entityId}
|
|
2310
|
+
customSubmit={async () => ({ isSuccess: true, validationBlocked: false, data: undefined })}
|
|
2311
|
+
{...(headerActions !== undefined && { actions: headerActions })}
|
|
2312
|
+
{...(translate !== undefined && { translate })}
|
|
2313
|
+
{...(isTabsMode && { hideSectionTitles: true })}
|
|
2314
|
+
valueDisplay={screen.valueDisplay ?? "text"}
|
|
2315
|
+
/>
|
|
2316
|
+
</>
|
|
2235
2317
|
);
|
|
2236
2318
|
}
|
|
2237
2319
|
// ---- actionForm (Tier 2.7d) ----
|
|
@@ -45,11 +45,13 @@ export function RelatedListSection({
|
|
|
45
45
|
parentId,
|
|
46
46
|
featureName,
|
|
47
47
|
translate,
|
|
48
|
+
hideTitle,
|
|
48
49
|
}: {
|
|
49
50
|
readonly section: EditRelatedListSectionViewModel;
|
|
50
51
|
readonly parentId: string;
|
|
51
52
|
readonly featureName: string;
|
|
52
53
|
readonly translate?: Translate;
|
|
54
|
+
readonly hideTitle?: boolean;
|
|
53
55
|
}): ReactNode {
|
|
54
56
|
const { Banner, Section } = usePrimitives();
|
|
55
57
|
const t = useTranslation();
|
|
@@ -92,7 +94,7 @@ export function RelatedListSection({
|
|
|
92
94
|
: undefined;
|
|
93
95
|
|
|
94
96
|
return (
|
|
95
|
-
<Section title={section.title} testId={`related-list-${section.title}`}>
|
|
97
|
+
<Section title={hideTitle ? undefined : section.title} testId={`related-list-${section.title}`}>
|
|
96
98
|
{rowsQuery.loading && rowsQuery.data === null ? (
|
|
97
99
|
<Banner padded variant="loading" testId="related-list-loading">
|
|
98
100
|
Loading…
|
|
@@ -132,6 +132,11 @@ export type RenderEditProps<TValues extends FormValues, TCtx = unknown> = {
|
|
|
132
132
|
* unaffected either way, so this only changes forms that already have
|
|
133
133
|
* readOnly fields. */
|
|
134
134
|
readonly valueDisplay?: "form" | "text";
|
|
135
|
+
/** Suppresses every section's own title (fields-section header, relatedList
|
|
136
|
+
* Section title) — for a host that already renders the section label
|
|
137
|
+
* itself elsewhere (e.g. a tab strip whose label duplicates it).
|
|
138
|
+
* Omitting this prop keeps unchanged behavior. */
|
|
139
|
+
readonly hideSectionTitles?: boolean;
|
|
135
140
|
};
|
|
136
141
|
|
|
137
142
|
export type RenderEditAction = {
|
|
@@ -221,6 +221,7 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
|
|
|
221
221
|
disabled = false,
|
|
222
222
|
hideActions,
|
|
223
223
|
valueDisplay = "form",
|
|
224
|
+
hideSectionTitles,
|
|
224
225
|
} = props;
|
|
225
226
|
const { customSubmit } = props;
|
|
226
227
|
// Translate-Fallback: wenn der Caller keine Translate-Fn übergibt,
|
|
@@ -1162,6 +1163,7 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
|
|
|
1162
1163
|
parentId={parentId}
|
|
1163
1164
|
featureName={featureName}
|
|
1164
1165
|
translate={translate}
|
|
1166
|
+
hideTitle={hideSectionTitles}
|
|
1165
1167
|
/>
|
|
1166
1168
|
);
|
|
1167
1169
|
}
|
|
@@ -1169,7 +1171,11 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
|
|
|
1169
1171
|
// Section-Header unterdrücken wenn er den Form-Titel der
|
|
1170
1172
|
// Action-Bar 1:1 wiederholen würde (typisch bei Single-Section-
|
|
1171
1173
|
// ActionForms, deren Section-Label = Screen-Titel ist).
|
|
1172
|
-
const sectionTitle =
|
|
1174
|
+
const sectionTitle = hideSectionTitles
|
|
1175
|
+
? undefined
|
|
1176
|
+
: section.title === formTitle
|
|
1177
|
+
? undefined
|
|
1178
|
+
: section.title;
|
|
1173
1179
|
// Titellose Sections kollidieren sonst auf key/testId — Index-Fallback.
|
|
1174
1180
|
const sectionKey = section.title ?? `section-${sectionIndex}`;
|
|
1175
1181
|
const sectionEl = (
|
package/src/index.ts
CHANGED
package/src/primitives.tsx
CHANGED
|
@@ -907,9 +907,12 @@ export type CardProps = {
|
|
|
907
907
|
};
|
|
908
908
|
|
|
909
909
|
/** Determinate progress bar (e.g. wizard step progress). `value` is a
|
|
910
|
-
* 0..1 fraction, not a percentage — implementations scale for display.
|
|
910
|
+
* 0..1 fraction, not a percentage — implementations scale for display.
|
|
911
|
+
* `tone` controls the fill color; omitted defaults to "default" (backward
|
|
912
|
+
* compatible with callers predating this field). */
|
|
911
913
|
export type ProgressProps = {
|
|
912
914
|
readonly value: number;
|
|
915
|
+
readonly tone?: "default" | "warn" | "danger";
|
|
913
916
|
readonly testId?: string;
|
|
914
917
|
};
|
|
915
918
|
|
|
@@ -941,6 +944,26 @@ export type WizardStepGroupProps = {
|
|
|
941
944
|
readonly children: ReactNode;
|
|
942
945
|
};
|
|
943
946
|
|
|
947
|
+
/** Tab strip for a projectionDetail `layout.mode: "tabs"`. Renders only the
|
|
948
|
+
* strip, not the content — the caller mounts the active section separately.
|
|
949
|
+
* Keyboard/ARIA (role=tablist/tab, arrow-key navigation) is the
|
|
950
|
+
* implementation's job. */
|
|
951
|
+
export type TabsProps = {
|
|
952
|
+
readonly items: readonly { readonly id: string; readonly label: string }[];
|
|
953
|
+
readonly activeId: string;
|
|
954
|
+
readonly onSelect: (id: string) => void;
|
|
955
|
+
readonly testId?: string;
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
export type StatusTone = "ok" | "warn" | "bad" | "critical" | "muted";
|
|
959
|
+
|
|
960
|
+
/** Status pill. `value` is the raw column value — tone-mapping is the app's job. */
|
|
961
|
+
export type StatusBadgeProps = {
|
|
962
|
+
readonly value: string;
|
|
963
|
+
readonly tone?: StatusTone;
|
|
964
|
+
readonly testId?: string;
|
|
965
|
+
};
|
|
966
|
+
|
|
944
967
|
// ---- Core-Registry (Kumiko-eigene Primitives) ----
|
|
945
968
|
|
|
946
969
|
export type CorePrimitives = {
|
|
@@ -982,6 +1005,14 @@ export type CorePrimitives = {
|
|
|
982
1005
|
* CorePrimitives mocks in tests keep compiling — additive rollout of
|
|
983
1006
|
* a new primitive shouldn't force every test double to grow a stub. */
|
|
984
1007
|
readonly WizardStepGroup?: ComponentType<WizardStepGroupProps>;
|
|
1008
|
+
/** Optional (unlike the other Core-Primitives) so existing partial
|
|
1009
|
+
* CorePrimitives mocks in tests keep compiling — additive rollout of
|
|
1010
|
+
* a new primitive shouldn't force every test double to grow a stub. */
|
|
1011
|
+
readonly Tabs?: ComponentType<TabsProps>;
|
|
1012
|
+
/** Optional (unlike the other Core-Primitives) so existing partial
|
|
1013
|
+
* CorePrimitives mocks in tests keep compiling — additive rollout of
|
|
1014
|
+
* a new primitive shouldn't force every test double to grow a stub. */
|
|
1015
|
+
readonly StatusBadge?: ComponentType<StatusBadgeProps>;
|
|
985
1016
|
};
|
|
986
1017
|
|
|
987
1018
|
/** Offene Extension-Zone für App-eigene Primitives. Devs erweitern
|