@cosmicdrift/kumiko-headless 0.65.0 → 0.67.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 +2 -2
- package/src/nav/__tests__/resolve.test.ts +43 -0
- package/src/nav/resolve.ts +4 -0
- package/src/nav/types.ts +10 -1
- package/src/view-model/edit.ts +11 -1
- package/src/view-model/types.ts +13 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-headless",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.67.0",
|
|
4
4
|
"description": "Headless UI logic for Kumiko — Dispatcher contract, Form-Controller, View-Model, Nav-Resolver. Plattform- und React-frei; jeder Renderer (renderer, renderer-web, renderer-native, …) komponiert darauf.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
35
|
+
"@cosmicdrift/kumiko-framework": "0.67.0",
|
|
36
36
|
"zod": "^4.4.3"
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
|
@@ -199,4 +199,47 @@ describe("resolveNavigation", () => {
|
|
|
199
199
|
expect(tree[0]?.children[0]?.qualifiedName).toBe("a:nav:mid");
|
|
200
200
|
expect(tree[0]?.children[0]?.children[0]?.qualifiedName).toBe("a:nav:leaf");
|
|
201
201
|
});
|
|
202
|
+
|
|
203
|
+
// Visual-Tree-Merge: die polymorphen Felder (target/actions/createAction/
|
|
204
|
+
// provider) müssen durch resolveNavigation durchgereicht werden, sonst
|
|
205
|
+
// kann der eine Renderer dynamische/dispatch-Knoten nicht bauen.
|
|
206
|
+
test("polymorphic fields pass through: target, actions, createAction, provider", () => {
|
|
207
|
+
const editTarget = { featureId: "text-content", action: "edit", args: { slug: "hero" } };
|
|
208
|
+
const createTarget = { featureId: "text-content", action: "create", args: { folder: "page" } };
|
|
209
|
+
const source = buildSource([
|
|
210
|
+
{
|
|
211
|
+
id: "tc:nav:content",
|
|
212
|
+
label: "Content",
|
|
213
|
+
provider: true,
|
|
214
|
+
createAction: { icon: "plus", label: "New page", target: createTarget },
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: "tc:nav:hero",
|
|
218
|
+
label: "Hero",
|
|
219
|
+
parent: "tc:nav:content",
|
|
220
|
+
target: editTarget,
|
|
221
|
+
actions: [{ icon: "trash", label: "Delete", target: editTarget }],
|
|
222
|
+
},
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
const tree = resolveNavigation({ source });
|
|
226
|
+
const content = tree[0];
|
|
227
|
+
expect(content?.provider).toBe(true);
|
|
228
|
+
expect(content?.createAction?.target).toEqual(createTarget);
|
|
229
|
+
const hero = content?.children[0];
|
|
230
|
+
expect(hero?.target).toEqual(editTarget);
|
|
231
|
+
expect(hero?.screen).toBeUndefined();
|
|
232
|
+
expect(hero?.actions).toHaveLength(1);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Knoten ohne die neuen Felder dürfen sie NICHT als undefined-Keys tragen
|
|
236
|
+
// (conditional-spread): hält die resolved Node sauber + Snapshot-stabil.
|
|
237
|
+
test("absent polymorphic fields are omitted, not set to undefined", () => {
|
|
238
|
+
const source = buildSource([{ id: "a:nav:plain", label: "Plain", screen: "a:screen:x" }]);
|
|
239
|
+
const node = resolveNavigation({ source })[0];
|
|
240
|
+
expect(node).not.toBeUndefined();
|
|
241
|
+
expect("target" in (node ?? {})).toBe(false);
|
|
242
|
+
expect("provider" in (node ?? {})).toBe(false);
|
|
243
|
+
expect("actions" in (node ?? {})).toBe(false);
|
|
244
|
+
});
|
|
202
245
|
});
|
package/src/nav/resolve.ts
CHANGED
|
@@ -31,6 +31,10 @@ export function resolveNavigation(options: ResolveNavigationOptions): NavTree {
|
|
|
31
31
|
children,
|
|
32
32
|
...(entry.icon !== undefined && { icon: entry.icon }),
|
|
33
33
|
...(entry.screen !== undefined && { screen: entry.screen }),
|
|
34
|
+
...(entry.target !== undefined && { target: entry.target }),
|
|
35
|
+
...(entry.actions !== undefined && { actions: entry.actions }),
|
|
36
|
+
...(entry.createAction !== undefined && { createAction: entry.createAction }),
|
|
37
|
+
...(entry.provider !== undefined && { provider: entry.provider }),
|
|
34
38
|
};
|
|
35
39
|
}
|
|
36
40
|
|
package/src/nav/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NavDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
1
|
+
import type { NavDefinition, TargetRef, TreeAction } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
|
|
3
3
|
// A single resolved nav entry as the renderer consumes it. Labels are NOT
|
|
4
4
|
// translated yet — nav can be used in SSR contexts where the locale isn't
|
|
@@ -13,6 +13,15 @@ export type NavNode = {
|
|
|
13
13
|
readonly label: string;
|
|
14
14
|
readonly icon?: string;
|
|
15
15
|
readonly screen?: string;
|
|
16
|
+
// Polymorphes Klick-Ziel (EditorPanel via dispatch) — Alternative zu
|
|
17
|
+
// `screen`. Knoten trägt screen XOR target.
|
|
18
|
+
readonly target?: TargetRef;
|
|
19
|
+
// Hover-Actions + „+"-Affordance, durchgereicht aus der NavDefinition.
|
|
20
|
+
readonly actions?: readonly TreeAction[];
|
|
21
|
+
readonly createAction?: TreeAction;
|
|
22
|
+
// true → Children kommen zur Laufzeit aus einem nav-provider (lazy +
|
|
23
|
+
// SSE-live), nicht aus `children`. Renderer macht den Knoten expandable.
|
|
24
|
+
readonly provider?: boolean;
|
|
16
25
|
readonly order: number;
|
|
17
26
|
readonly children: readonly NavNode[];
|
|
18
27
|
};
|
package/src/view-model/edit.ts
CHANGED
|
@@ -119,6 +119,12 @@ export function computeEditViewModel<
|
|
|
119
119
|
fieldDef.type === "reference"
|
|
120
120
|
? ((fieldDef as unknown as { multiple?: boolean }).multiple ?? false)
|
|
121
121
|
: undefined;
|
|
122
|
+
// file/image: accept/maxSize ins ViewModel + entityType/fieldName für
|
|
123
|
+
// den Upload-POST (Endpoint validiert gegen die richtige Field-Def).
|
|
124
|
+
const isFileType = fieldDef.type === "file" || fieldDef.type === "image";
|
|
125
|
+
const fileDef = isFileType
|
|
126
|
+
? (fieldDef as unknown as { accept?: readonly string[]; maxSize?: string })
|
|
127
|
+
: undefined;
|
|
122
128
|
const view: EditFieldViewModel = {
|
|
123
129
|
field: normalized.field,
|
|
124
130
|
label,
|
|
@@ -140,12 +146,16 @@ export function computeEditViewModel<
|
|
|
140
146
|
...(refFeature !== undefined && { refFeature }),
|
|
141
147
|
...(refLabelField !== undefined && { refLabelField }),
|
|
142
148
|
...(refMultiple !== undefined && { refMultiple }),
|
|
149
|
+
...(fileDef?.accept !== undefined && { accept: fileDef.accept }),
|
|
150
|
+
...(fileDef?.maxSize !== undefined && { maxSize: fileDef.maxSize }),
|
|
151
|
+
...(isFileType && { entityType: screen.entity, fieldName: normalized.field }),
|
|
143
152
|
};
|
|
144
153
|
return view;
|
|
145
154
|
});
|
|
146
155
|
return {
|
|
147
156
|
kind: "fields" as const,
|
|
148
|
-
|
|
157
|
+
// Titellose Section (flache Form) → kein h3; nur übersetzen wenn gesetzt.
|
|
158
|
+
...(sectionSpec.title !== undefined && { title: translate(sectionSpec.title) }),
|
|
149
159
|
columns: sectionSpec.columns ?? 1,
|
|
150
160
|
fields,
|
|
151
161
|
};
|
package/src/view-model/types.ts
CHANGED
|
@@ -132,6 +132,17 @@ export type EditFieldViewModel = {
|
|
|
132
132
|
/** Nur bei `type: "reference"` — Multi-Mode (Tier 2.7e-Multi):
|
|
133
133
|
* Wert ist UUID-Array, Renderer mountet Multi-Combobox mit Tags. */
|
|
134
134
|
readonly refMultiple?: boolean;
|
|
135
|
+
/** Nur bei `type: "file" | "image"` — erlaubte Extensions/MIME (z.B.
|
|
136
|
+
* ["jpg","png"]) + Max-Größe ("5mb"). Der Renderer setzt das `accept`-
|
|
137
|
+
* Attribut + zeigt die Grenze; die Validierung läuft serverseitig im
|
|
138
|
+
* Upload-Endpoint gegen die Field-Def. */
|
|
139
|
+
readonly accept?: readonly string[];
|
|
140
|
+
readonly maxSize?: string;
|
|
141
|
+
/** Nur bei `type: "file" | "image"` — Entity-Name + Field-Name für den
|
|
142
|
+
* Upload-POST (`/api/files`), damit der Endpoint maxSize/accept gegen
|
|
143
|
+
* die richtige Field-Def prüfen kann. */
|
|
144
|
+
readonly entityType?: string;
|
|
145
|
+
readonly fieldName?: string;
|
|
135
146
|
};
|
|
136
147
|
|
|
137
148
|
// Discriminated by `kind` — mirrors EditSectionSpec on the engine side.
|
|
@@ -141,7 +152,8 @@ export type EditSectionViewModel = EditFieldsSectionViewModel | EditExtensionSec
|
|
|
141
152
|
|
|
142
153
|
export type EditFieldsSectionViewModel = {
|
|
143
154
|
readonly kind: "fields";
|
|
144
|
-
|
|
155
|
+
/** Optional — eine titellose Section rendert nur ihre Felder (flache Form). */
|
|
156
|
+
readonly title?: string;
|
|
145
157
|
readonly columns: number;
|
|
146
158
|
readonly fields: readonly EditFieldViewModel[];
|
|
147
159
|
};
|