@cosmicdrift/kumiko-renderer 0.224.2 → 0.226.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.224.2",
3
+ "version": "0.226.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.224.2",
19
- "@cosmicdrift/kumiko-headless": "0.224.2",
18
+ "@cosmicdrift/kumiko-framework": "0.226.0",
19
+ "@cosmicdrift/kumiko-headless": "0.226.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.224.2"
28
+ "@cosmicdrift/kumiko-locale-de": "0.226.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(() => synthesizeProjectionDetailScreen(screen), [screen]);
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
- <RenderEdit
2225
- screen={detailScreen}
2226
- entity={entity}
2227
- featureName={schema.featureName}
2228
- initial={record as FormValues}
2229
- entityId={entityId}
2230
- customSubmit={async () => ({ isSuccess: true, validationBlocked: false, data: undefined })}
2231
- {...(headerActions !== undefined && { actions: headerActions })}
2232
- {...(translate !== undefined && { translate })}
2233
- valueDisplay={screen.valueDisplay ?? "text"}
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 = section.title === formTitle ? undefined : section.title;
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
@@ -198,7 +198,10 @@ export type {
198
198
  ProgressProps,
199
199
  RuntimeRenderer,
200
200
  SectionProps,
201
+ StatusBadgeProps,
202
+ StatusTone,
201
203
  StepBarProps,
204
+ TabsProps,
202
205
  TextProps,
203
206
  WizardStepGroupProps,
204
207
  } from "./primitives";
@@ -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