@cosmicdrift/kumiko-renderer 0.326.1 → 0.328.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.326.1",
3
+ "version": "0.328.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.326.1",
19
- "@cosmicdrift/kumiko-headless": "0.326.1",
20
- "@cosmicdrift/kumiko-types": "0.326.1",
18
+ "@cosmicdrift/kumiko-framework": "0.328.0",
19
+ "@cosmicdrift/kumiko-headless": "0.328.0",
20
+ "@cosmicdrift/kumiko-types": "0.328.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.326.1"
31
+ "@cosmicdrift/kumiko-locale-de": "0.328.0"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",
@@ -0,0 +1,118 @@
1
+ // fw#3381: an entityEdit screen pins its action bar to the shell height by
2
+ // default (Form fillHeight + stickyActions); `fillHeight: false` opts out.
3
+ import { describe, expect, test } from "bun:test";
4
+ import type {
5
+ EntityDefinition,
6
+ EntityEditScreenDefinition,
7
+ } from "@cosmicdrift/kumiko-framework/ui-types";
8
+ import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
9
+ import { render } from "@testing-library/react";
10
+ import type { ComponentType, ReactNode } from "react";
11
+ import { DispatcherProvider } from "../../context/dispatcher-context";
12
+ import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
13
+ import { type CorePrimitives, type FormProps, PrimitivesProvider } from "../../primitives";
14
+ import type { FeatureSchema } from "../feature-schema";
15
+ import { KumikoScreen } from "../kumiko-screen";
16
+ import { NavProvider } from "../nav";
17
+
18
+ const noop = (): ReactNode => null;
19
+ const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
20
+
21
+ function stubDispatcher(): Dispatcher {
22
+ return {
23
+ write: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["write"],
24
+ query: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["query"],
25
+ batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
26
+ statusStore: {
27
+ getState: () => "online",
28
+ subscribe: () => () => {},
29
+ } as unknown as Dispatcher["statusStore"],
30
+ async *stream() {},
31
+ pendingWrites: () => [],
32
+ pendingFiles: () => [],
33
+ };
34
+ }
35
+
36
+ function buildSchema(fillHeight?: boolean): FeatureSchema {
37
+ const entity: EntityDefinition = {
38
+ fields: {
39
+ name: { type: "text", maxLength: 200, required: false, searchable: false, sortable: false },
40
+ },
41
+ };
42
+ const screen: EntityEditScreenDefinition = {
43
+ id: "unit-edit",
44
+ type: "entityEdit",
45
+ entity: "unit",
46
+ layout: { sections: [{ columns: 1, fields: ["name"] }] },
47
+ ...(fillHeight !== undefined && { fillHeight }),
48
+ };
49
+ return {
50
+ featureName: "housing",
51
+ entities: { unit: entity },
52
+ screens: [screen],
53
+ } as FeatureSchema;
54
+ }
55
+
56
+ function captureFormProps(fillHeight?: boolean): FormProps {
57
+ let captured: FormProps | undefined;
58
+ const capturingForm: ComponentType<FormProps> = (props) => {
59
+ captured = props;
60
+ return null;
61
+ };
62
+ const primitives: CorePrimitives = {
63
+ Button: noop,
64
+ Banner: passChildren,
65
+ Field: passChildren,
66
+ Input: noop,
67
+ DataTable: noop,
68
+ Form: capturingForm,
69
+ Section: passChildren,
70
+ Card: passChildren,
71
+ Grid: passChildren,
72
+ GridCell: passChildren,
73
+ Text: passChildren,
74
+ Heading: noop,
75
+ Dialog: noop,
76
+ Modal: noop,
77
+ Lightbox: noop,
78
+ ConfigSourceBadge: noop,
79
+ ConfigCascadeView: noop,
80
+ Link: noop,
81
+ };
82
+ render(
83
+ <LocaleProvider resolver={createStaticLocaleResolver({ locale: "de-DE" })}>
84
+ <DispatcherProvider dispatcher={stubDispatcher()}>
85
+ <NavProvider
86
+ value={{
87
+ route: { screenId: "housing:unit-edit" },
88
+ navigate: () => {},
89
+ replace: () => {},
90
+ hrefFor: () => "",
91
+ searchParams: {},
92
+ setSearchParams: () => {},
93
+ }}
94
+ >
95
+ <PrimitivesProvider value={primitives}>
96
+ <KumikoScreen schema={buildSchema(fillHeight)} qn="housing:screen:unit-edit" />
97
+ </PrimitivesProvider>
98
+ </NavProvider>
99
+ </DispatcherProvider>
100
+ </LocaleProvider>,
101
+ );
102
+ if (captured === undefined) throw new Error("Form was not rendered");
103
+ return captured;
104
+ }
105
+
106
+ describe("entityEdit fillHeight (fw#3381)", () => {
107
+ test("default: form fills the height with a pinned action bar", () => {
108
+ const props = captureFormProps();
109
+ expect(props.fillHeight).toBe(true);
110
+ expect(props.stickyActions).toBe(true);
111
+ });
112
+
113
+ test("fillHeight: false restores page scroll", () => {
114
+ const props = captureFormProps(false);
115
+ expect(props.fillHeight).toBeFalsy();
116
+ expect(props.stickyActions).toBeFalsy();
117
+ });
118
+ });
@@ -39,7 +39,7 @@ function stubDispatcher(): Dispatcher {
39
39
  };
40
40
  }
41
41
 
42
- function buildSchema(): FeatureSchema {
42
+ function buildSchema(fillHeight?: boolean): FeatureSchema {
43
43
  const entity: EntityDefinition = {
44
44
  fields: {
45
45
  name: { type: "text", maxLength: 200, required: false, searchable: false, sortable: false },
@@ -50,6 +50,7 @@ function buildSchema(): FeatureSchema {
50
50
  type: "entityList",
51
51
  entity: "org",
52
52
  columns: ["name"],
53
+ ...(fillHeight !== undefined && { fillHeight }),
53
54
  };
54
55
  return {
55
56
  featureName: "orgs",
@@ -58,54 +59,70 @@ function buildSchema(): FeatureSchema {
58
59
  } as FeatureSchema;
59
60
  }
60
61
 
62
+ async function captureListProps(fillHeight?: boolean): Promise<DataTableProps> {
63
+ let captured: DataTableProps | undefined;
64
+ const capturingDataTable: ComponentType<DataTableProps> = (props) => {
65
+ captured = props;
66
+ return null;
67
+ };
68
+ const primitives: CorePrimitives = {
69
+ Button: noop,
70
+ Banner: passChildren,
71
+ Field: passChildren,
72
+ Input: noop,
73
+ DataTable: capturingDataTable,
74
+ Form: passChildren,
75
+ Section: passChildren,
76
+ Card: passChildren,
77
+ Grid: passChildren,
78
+ GridCell: passChildren,
79
+ Text: passChildren,
80
+ Heading: noop,
81
+ Dialog: noop,
82
+ Modal: noop,
83
+ Lightbox: noop,
84
+ ConfigSourceBadge: noop,
85
+ ConfigCascadeView: noop,
86
+ Link: noop,
87
+ };
88
+ render(
89
+ <LocaleProvider resolver={createStaticLocaleResolver({ locale: "de-DE" })}>
90
+ <DispatcherProvider dispatcher={stubDispatcher()}>
91
+ <NavProvider
92
+ value={{
93
+ route: { screenId: "orgs:org-list" },
94
+ navigate: () => {},
95
+ replace: () => {},
96
+ hrefFor: () => "",
97
+ searchParams: {},
98
+ setSearchParams: () => {},
99
+ }}
100
+ >
101
+ <PrimitivesProvider value={primitives}>
102
+ <KumikoScreen schema={buildSchema(fillHeight)} qn="orgs:screen:org-list" />
103
+ </PrimitivesProvider>
104
+ </NavProvider>
105
+ </DispatcherProvider>
106
+ </LocaleProvider>,
107
+ );
108
+
109
+ await waitFor(() => expect(captured).toBeDefined());
110
+ if (captured === undefined) throw new Error("DataTable was not rendered");
111
+ return captured;
112
+ }
113
+
61
114
  describe("entityList screen padding (fw#2640)", () => {
62
115
  test("the list screen marks its table as the screen body", async () => {
63
- let capturedScreenPadding: boolean | undefined;
64
- const capturingDataTable: ComponentType<DataTableProps> = (props) => {
65
- capturedScreenPadding = props.screenPadding;
66
- return null;
67
- };
68
- const primitives: CorePrimitives = {
69
- Button: noop,
70
- Banner: passChildren,
71
- Field: passChildren,
72
- Input: noop,
73
- DataTable: capturingDataTable,
74
- Form: passChildren,
75
- Section: passChildren,
76
- Card: passChildren,
77
- Grid: passChildren,
78
- GridCell: passChildren,
79
- Text: passChildren,
80
- Heading: noop,
81
- Dialog: noop,
82
- Modal: noop,
83
- Lightbox: noop,
84
- ConfigSourceBadge: noop,
85
- ConfigCascadeView: noop,
86
- Link: noop,
87
- };
88
- render(
89
- <LocaleProvider resolver={createStaticLocaleResolver({ locale: "de-DE" })}>
90
- <DispatcherProvider dispatcher={stubDispatcher()}>
91
- <NavProvider
92
- value={{
93
- route: { screenId: "orgs:org-list" },
94
- navigate: () => {},
95
- replace: () => {},
96
- hrefFor: () => "",
97
- searchParams: {},
98
- setSearchParams: () => {},
99
- }}
100
- >
101
- <PrimitivesProvider value={primitives}>
102
- <KumikoScreen schema={buildSchema()} qn="orgs:screen:org-list" />
103
- </PrimitivesProvider>
104
- </NavProvider>
105
- </DispatcherProvider>
106
- </LocaleProvider>,
107
- );
116
+ expect((await captureListProps()).screenPadding).toBe(true);
117
+ });
118
+ });
119
+
120
+ describe("entityList fillHeight (fw#3381)", () => {
121
+ test("fills the shell height by default (scrollBody)", async () => {
122
+ expect((await captureListProps()).scrollBody).toBe(true);
123
+ });
108
124
 
109
- await waitFor(() => expect(capturedScreenPadding).toBe(true));
125
+ test("fillHeight: false restores page scroll (no scrollBody)", async () => {
126
+ expect((await captureListProps(false)).scrollBody).toBeUndefined();
110
127
  });
111
128
  });
@@ -53,6 +53,8 @@ export function synthesizeActionFormScreen(
53
53
  // Only ActionFormScreenDefinition carries slots/fieldLabels; `in` narrows
54
54
  // without a cast since SecretMintScreenDefinition has neither.
55
55
  ...("slots" in screen && screen.slots !== undefined && { slots: screen.slots }),
56
+ ...("fillHeight" in screen &&
57
+ screen.fillHeight !== undefined && { fillHeight: screen.fillHeight }),
56
58
  ...("fieldLabels" in screen &&
57
59
  screen.fieldLabels !== undefined && { fieldLabels: screen.fieldLabels }),
58
60
  };