@cosmicdrift/kumiko-renderer 0.2.3 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,63 @@
1
1
  # @cosmicdrift/kumiko-renderer
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0.3.0 bringt zwei neue Subsysteme (Step-Engine Tier-3 + Visual-Tree) plus
8
+ eine AST-Codemod-Pipeline als Vorarbeit für den L2-AI-Layer.
9
+
10
+ ### Breaking Changes
11
+
12
+ - `skipTransitionGuard` → `unsafeSkipTransitionGuard` (Rename in
13
+ feature-ast + engine). Der `unsafe`-Prefix macht die Tragweite des
14
+ Casts sichtbar und ist konsistent zur `unsafeProjectionUpsert`- und
15
+ `r.rawTable`-Konvention. Migration: 1:1-Ersetzung, keine Verhaltens-Änderung.
16
+
17
+ ### Features
18
+
19
+ - **Step-Engine M.4 — Tier-3 Workflow-Engine.** Neue Step-Vocabulary
20
+ `wait`, `waitForEvent`, `retry` ermöglicht persistierte Long-Running-Flows
21
+ über Job-Boundaries hinweg. Q7 Snapshot-at-Start hängt jedem Step-Run
22
+ einen SHA-256-Fingerprint des Aggregat-Zustands an, sodass Replays
23
+ deterministisch gegen den ursprünglichen Eingangszustand laufen.
24
+ - **Visual-Tree V.1.x — Tree-API + Editor-Panel.** Neue `VisualTree`-
25
+ Component plus TreeProvider-Pattern; erste TreeProviders für
26
+ `text-content` und `legal-pages` (CMS-light + Impressum/Privacy).
27
+ Fundament für den späteren No-Code-Designer (~3000 LOC, 98 Tests).
28
+ - **Codemod-Pipeline.** AST-basierte Patcher-Module für strukturelle
29
+ Feature-Edits — wird vom kommenden L2-AI-Layer als Tool-Surface
30
+ verwendet, ist aber eigenständig nutzbar für ts-morph-style Migrationen.
31
+ - **user-data-rights Sample-Recipe.** DSGVO Art. 15/17/18/20 vollständig
32
+ als Sample-Recipe (`samples/recipes/`) inklusive README — zeigt die
33
+ Export- und Forget-Pipeline gegen den `compliance-profiles`-Default
34
+ (`eu-dsgvo`).
35
+
36
+ ### Fixes
37
+
38
+ - `tier-engine`: auto-default-tier-Hook benutzt jetzt `ctx.db.raw` für
39
+ Event-Store-Operationen (#37, vorher: stiller Bug, 22 Tage live).
40
+ - `engine`: unsafe-projection-upsert nutzt `as never` statt `as any` —
41
+ schmaler Cast-Surface, weniger Compiler-Knebel.
42
+ - `visual-tree`: runtime-isolation marker für client-konsumierte Files,
43
+ damit der Multi-Entry-Build den richtigen Bundle-Split bekommt.
44
+ - `feature-ast`: vollständiger `unsafeSkipTransitionGuard`-Rename (war
45
+ in zwei Modulen noch der alte Name).
46
+ - `framework`: Error-Reasons + `noConsole`-Lint + No-Date-API-Guard
47
+ wieder push-ready.
48
+
49
+ ### Library-Updates
50
+
51
+ hono 4.12, jose 6.2, stripe 22.1, meilisearch 0.58, marked 18,
52
+ bun-types 1.3.13, lucide-react 1.14, bullmq 5.76, ioredis 5.10,
53
+ i18next 26.0, react + radix-ui-primitives auf aktuelle Minors.
54
+
55
+ ### Patch Changes
56
+
57
+ - Updated dependencies
58
+ - @cosmicdrift/kumiko-framework@0.3.0
59
+ - @cosmicdrift/kumiko-headless@0.3.0
60
+
3
61
  ## 0.2.3
4
62
 
5
63
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer",
3
- "version": "0.2.3",
3
+ "version": "0.3.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>",
@@ -9,17 +9,20 @@
9
9
  "runtime": "client"
10
10
  },
11
11
  "exports": {
12
- ".": "./src/index.ts"
12
+ ".": {
13
+ "types": "./src/index.ts",
14
+ "default": "./src/index.ts"
15
+ }
13
16
  },
14
17
  "dependencies": {
15
- "@cosmicdrift/kumiko-framework": "0.2.3",
16
- "@cosmicdrift/kumiko-headless": "0.2.3",
17
- "react": "^19.2.0"
18
+ "@cosmicdrift/kumiko-framework": "0.3.0",
19
+ "@cosmicdrift/kumiko-headless": "0.3.0",
20
+ "react": "^19.2.6"
18
21
  },
19
22
  "devDependencies": {
20
- "@testing-library/react": "^16.3.0",
21
- "@types/react": "^19.2.0",
22
- "jsdom": "^29.1.0"
23
+ "@testing-library/react": "^16.3.2",
24
+ "@types/react": "^19.2.14",
25
+ "jsdom": "^29.1.1"
23
26
  },
24
27
  "repository": {
25
28
  "type": "git",
@@ -17,16 +17,16 @@ function makeStatefulResolver(initial: string): LocaleResolver {
17
17
  let current = initial;
18
18
  const listeners = new Set<() => void>();
19
19
  return {
20
- translate: (key) => key,
20
+ translate: (key: string) => key,
21
21
  locale: () => current,
22
22
  timeZone: () => "UTC",
23
- subscribe: (l) => {
23
+ subscribe: (l: () => void) => {
24
24
  listeners.add(l);
25
25
  return () => {
26
26
  listeners.delete(l);
27
27
  };
28
28
  },
29
- setLocale: (next) => {
29
+ setLocale: (next: string) => {
30
30
  current = next;
31
31
  for (const l of listeners) l();
32
32
  },
@@ -45,7 +45,7 @@ describe("useTranslation — lookup order", () => {
45
45
  test("App-Resolver wins when it returns a non-key value", () => {
46
46
  const resolver: LocaleResolver = {
47
47
  ...createStaticLocaleResolver({ locale: "de" }),
48
- translate: (key) => (key === "hello" ? "Resolved by app" : key),
48
+ translate: (key: string) => (key === "hello" ? "Resolved by app" : key),
49
49
  };
50
50
  const { result } = renderHook(() => useTranslation(), { wrapper: wrap(resolver) });
51
51
  expect(result.current("hello")).toBe("Resolved by app");
@@ -4,7 +4,10 @@ import type {
4
4
  EntityDefinition,
5
5
  EntityEditScreenDefinition,
6
6
  EntityListScreenDefinition,
7
+ RowAction,
8
+ RowActionWriteHandler,
7
9
  ScreenDefinition,
10
+ ToolbarAction,
8
11
  } from "@cosmicdrift/kumiko-framework/ui-types";
9
12
  import type {
10
13
  Command,
@@ -16,12 +19,12 @@ import type {
16
19
  } from "@cosmicdrift/kumiko-headless";
17
20
  import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
18
21
  import { RenderEdit } from "../components/render-edit";
19
- import { RenderList } from "../components/render-list";
22
+ import { RenderList, type ToolbarActionButton } from "../components/render-list";
20
23
  import { useDispatcher, useOptionalDispatcher } from "../context/dispatcher-context";
21
24
  import { useListUrlState } from "../hooks/use-list-url-state";
22
25
  import { useQuery } from "../hooks/use-query";
23
26
  import { useTranslation } from "../i18n";
24
- import { usePrimitives } from "../primitives";
27
+ import { type DataTableRowAction, usePrimitives } from "../primitives";
25
28
  import { synthesizeActionFormEntity, synthesizeActionFormScreen } from "./action-form-shim";
26
29
  import { synthesizeConfigEditEntity, synthesizeConfigEditScreen } from "./config-edit-shim";
27
30
  import { useCustomScreenComponent } from "./custom-screens";
@@ -82,7 +85,10 @@ export function KumikoScreen({
82
85
  }: KumikoScreenProps): ReactNode {
83
86
  const { Banner, Text } = usePrimitives();
84
87
  const screen = useMemo(
85
- () => schema.screens.find((s) => qualifyScreenId(schema.featureName, s.id) === qn),
88
+ () =>
89
+ schema.screens.find(
90
+ (s: ScreenDefinition) => qualifyScreenId(schema.featureName, s.id) === qn,
91
+ ),
86
92
  [schema.featureName, schema.screens, qn],
87
93
  );
88
94
 
@@ -162,7 +168,9 @@ function entityWriteCommand(
162
168
  function useNavigateToListAfter(schema: FeatureSchema, entityName: string): () => void {
163
169
  const nav = useNav();
164
170
  return useCallback(() => {
165
- const list = schema.screens.find((s) => s.type === "entityList" && s.entity === entityName);
171
+ const list = schema.screens.find(
172
+ (s: ScreenDefinition) => s.type === "entityList" && s.entity === entityName,
173
+ );
166
174
  if (!list) return;
167
175
  // schema.screens.id ist QN-form (registry-stamped); nav.navigate
168
176
  // erwartet Short-Form. Sonst landet die URL doppelt-qualifiziert.
@@ -184,7 +192,9 @@ function useNavigateToCreateFor(
184
192
  ): (() => void) | undefined {
185
193
  const nav = useNav();
186
194
  const editScreenId = useMemo(() => {
187
- const edit = schema.screens.find((s) => s.type === "entityEdit" && s.entity === entityName);
195
+ const edit = schema.screens.find(
196
+ (s: ScreenDefinition) => s.type === "entityEdit" && s.entity === entityName,
197
+ );
188
198
  return edit !== undefined ? lastSegment(edit.id) : undefined;
189
199
  }, [schema.screens, entityName]);
190
200
  const navigate = useCallback(() => {
@@ -643,7 +653,7 @@ function EntityListBody({
643
653
  const rowActions = useMemo(() => {
644
654
  if (screen.rowActions === undefined) return undefined;
645
655
  return screen.rowActions
646
- .map((action) => {
656
+ .map((action: RowAction): DataTableRowAction | null => {
647
657
  // navigate-Variante braucht keinen Dispatcher; nav ist
648
658
  // immer da (Provider von createKumikoApp).
649
659
  if (action.kind === "navigate") {
@@ -671,31 +681,32 @@ function EntityListBody({
671
681
  }),
672
682
  };
673
683
  }
674
- // writeHandler-Variante (default kind, Backwards-Compat).
675
- // Braucht Dispatcher — null returnen → filter unten dropt es,
676
- // damit das useEffect-Warning oben einmal feuert + die Action
677
- // einfach nicht rendert (statt Crash).
678
684
  if (dispatcher === undefined) return null;
685
+ if (action.kind !== "writeHandler" && action.kind !== undefined) return null;
686
+ const writeAction = action as RowActionWriteHandler;
679
687
  return {
680
- id: action.id,
681
- label: effectiveTranslate(action.label),
682
- ...(action.style !== undefined && { style: action.style }),
683
- ...(action.confirm !== undefined && { confirm: effectiveTranslate(action.confirm) }),
684
- ...(action.confirmLabel !== undefined && {
685
- confirmLabel: effectiveTranslate(action.confirmLabel),
686
- }),
688
+ id: writeAction.id,
689
+ label: effectiveTranslate(writeAction.label),
690
+ style: writeAction.style,
691
+ confirm:
692
+ writeAction.confirm !== undefined ? effectiveTranslate(writeAction.confirm) : undefined,
693
+ confirmLabel:
694
+ writeAction.confirmLabel !== undefined
695
+ ? effectiveTranslate(writeAction.confirmLabel)
696
+ : undefined,
687
697
  onTrigger: async (row: ListRowViewModel) => {
688
- const buildPayload = action.payload;
698
+ const buildPayload = writeAction.payload;
689
699
  const payload =
690
700
  buildPayload !== undefined ? buildPayload(row.values) : { id: row.values["id"] };
691
- await dispatcher.write(action.handler, payload);
701
+ await dispatcher.write(writeAction.handler, payload);
692
702
  },
693
- ...(action.visible !== undefined && {
694
- isVisible: (row: ListRowViewModel) => action.visible?.(row.values, undefined) ?? true,
695
- }),
703
+ isVisible:
704
+ writeAction.visible !== undefined
705
+ ? (row: ListRowViewModel) => writeAction.visible?.(row.values, undefined) ?? true
706
+ : undefined,
696
707
  };
697
708
  })
698
- .filter((a): a is NonNullable<typeof a> => a !== null);
709
+ .filter((a: DataTableRowAction | null): a is DataTableRowAction => a !== null);
699
710
  }, [screen.rowActions, effectiveTranslate, dispatcher, nav]);
700
711
 
701
712
  // ToolbarActions: Schema → Resolved-Form (analog rowActions).
@@ -705,45 +716,34 @@ function EntityListBody({
705
716
  const toolbarActions = useMemo(() => {
706
717
  if (screen.toolbarActions === undefined) return undefined;
707
718
  return screen.toolbarActions
708
- .map(
709
- (
710
- action,
711
- ): {
712
- id: string;
713
- label: string;
714
- style?: "primary" | "secondary" | "danger";
715
- confirm?: string;
716
- confirmLabel?: string;
717
- onTrigger: () => Promise<void> | void;
718
- } | null => {
719
- if (action.kind === "navigate") {
720
- return {
721
- id: action.id,
722
- label: effectiveTranslate(action.label),
723
- ...(action.style !== undefined && { style: action.style }),
724
- onTrigger: () => nav.navigate({ screenId: action.screen }),
725
- };
726
- }
727
- // writeHandler — braucht Dispatcher. Wenn keiner mounted ist,
728
- // skippen wir die Action statt zu crashen (gleiche Logik wie
729
- // bei rowActions; einmaliger Warn-Log dort reicht).
730
- if (dispatcher === undefined) return null;
719
+ .map((action: ToolbarAction): ToolbarActionButton | null => {
720
+ if (action.kind === "navigate") {
731
721
  return {
732
722
  id: action.id,
733
723
  label: effectiveTranslate(action.label),
734
724
  ...(action.style !== undefined && { style: action.style }),
735
- ...(action.confirm !== undefined && { confirm: effectiveTranslate(action.confirm) }),
736
- ...(action.confirmLabel !== undefined && {
737
- confirmLabel: effectiveTranslate(action.confirmLabel),
738
- }),
739
- onTrigger: async () => {
740
- const payload = action.payload?.() ?? {};
741
- await dispatcher.write(action.handler, payload);
742
- },
725
+ onTrigger: () => nav.navigate({ screenId: action.screen }),
743
726
  };
744
- },
745
- )
746
- .filter((a): a is NonNullable<typeof a> => a !== null);
727
+ }
728
+ // writeHandler — braucht Dispatcher. Wenn keiner mounted ist,
729
+ // skippen wir die Action statt zu crashen (gleiche Logik wie
730
+ // bei rowActions; einmaliger Warn-Log dort reicht).
731
+ if (dispatcher === undefined) return null;
732
+ return {
733
+ id: action.id,
734
+ label: effectiveTranslate(action.label),
735
+ ...(action.style !== undefined && { style: action.style }),
736
+ ...(action.confirm !== undefined && { confirm: effectiveTranslate(action.confirm) }),
737
+ ...(action.confirmLabel !== undefined && {
738
+ confirmLabel: effectiveTranslate(action.confirmLabel),
739
+ }),
740
+ onTrigger: async () => {
741
+ const payload = action.payload?.() ?? {};
742
+ await dispatcher.write(action.handler, payload);
743
+ },
744
+ };
745
+ })
746
+ .filter((a: ToolbarActionButton | null): a is ToolbarActionButton => a !== null);
747
747
  }, [screen.toolbarActions, effectiveTranslate, nav, dispatcher]);
748
748
 
749
749
  if (rowsQuery.loading && rowsQuery.data === null) {
@@ -6,6 +6,7 @@ import { normalizeEditField } from "@cosmicdrift/kumiko-framework/ui-types";
6
6
  import type {
7
7
  DispatcherError,
8
8
  EditFieldViewModel,
9
+ EditSectionViewModel,
9
10
  FieldConditions,
10
11
  FieldIssue,
11
12
  FormSnapshot,
@@ -252,10 +253,10 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
252
253
  actions={formActions}
253
254
  testId="render-edit-form"
254
255
  >
255
- {vm.sections.map((section) => (
256
+ {vm.sections.map((section: EditSectionViewModel) => (
256
257
  <Section key={section.title} title={section.title} testId={`section-${section.title}`}>
257
258
  <Grid columns={section.columns}>
258
- {section.fields.map((field) => (
259
+ {section.fields.map((field: EditFieldViewModel) => (
259
260
  <GridCellForField
260
261
  key={field.field}
261
262
  field={field}
@@ -249,7 +249,7 @@ function renderInput({
249
249
  const labels = field.optionLabels;
250
250
  const selectOptions =
251
251
  labels !== undefined
252
- ? rawOptions.map((value) => ({ value, label: labels[value] ?? value }))
252
+ ? rawOptions.map((value: string) => ({ value, label: labels[value] ?? value }))
253
253
  : rawOptions;
254
254
  return (
255
255
  <Input
@@ -3,7 +3,11 @@ import type {
3
3
  EntityDefinition,
4
4
  EntityListScreenDefinition,
5
5
  } from "@cosmicdrift/kumiko-framework/ui-types";
6
- import type { ListRowViewModel, Translate } from "@cosmicdrift/kumiko-headless";
6
+ import type {
7
+ ListColumnViewModel,
8
+ ListRowViewModel,
9
+ Translate,
10
+ } from "@cosmicdrift/kumiko-headless";
7
11
  import { computeListViewModel } from "@cosmicdrift/kumiko-headless";
8
12
  import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
9
13
  import type { ListSort } from "../hooks/use-list-url-state";
@@ -160,8 +164,8 @@ export function RenderList(props: RenderListProps): ReactNode {
160
164
  const referenceColumns = useMemo(
161
165
  () =>
162
166
  vm.columns
163
- .filter((c) => c.type === "reference" && c.refEntity !== undefined)
164
- .map((c) => ({
167
+ .filter((c: ListColumnViewModel) => c.type === "reference" && c.refEntity !== undefined)
168
+ .map((c: ListColumnViewModel) => ({
165
169
  field: c.field,
166
170
  refEntity: c.refEntity ?? "",
167
171
  // Tier 2.7e Cross-Feature: refFeature kommt aus parseRefTarget
@@ -183,7 +187,7 @@ export function RenderList(props: RenderListProps): ReactNode {
183
187
  }, []);
184
188
  const enrichedColumns = useMemo(() => {
185
189
  if (referenceColumns.length === 0) return vm.columns;
186
- return vm.columns.map((col) => {
190
+ return vm.columns.map((col: ListColumnViewModel) => {
187
191
  if (col.type !== "reference") return col;
188
192
  // Author-deklarierter Renderer übersteuert immer — Default greift
189
193
  // nur wenn keiner gesetzt ist.
@@ -284,16 +288,18 @@ export function RenderList(props: RenderListProps): ReactNode {
284
288
  // ListSort = DataTableSort (use-list-url-state aliased) — kein Cast nötig.
285
289
  return (
286
290
  <>
287
- {referenceColumns.map((rc) => (
288
- <ReferenceLookupBridge
289
- key={rc.field}
290
- field={rc.field}
291
- refEntity={rc.refEntity}
292
- labelField={rc.labelField}
293
- featureName={rc.refFeature}
294
- onMap={handleLookupMap}
295
- />
296
- ))}
291
+ {referenceColumns.map(
292
+ (rc: { field: string; refEntity: string; refFeature: string; labelField: string }) => (
293
+ <ReferenceLookupBridge
294
+ key={rc.field}
295
+ field={rc.field}
296
+ refEntity={rc.refEntity}
297
+ labelField={rc.labelField}
298
+ featureName={rc.refFeature}
299
+ onMap={handleLookupMap}
300
+ />
301
+ ),
302
+ )}
297
303
  <DataTable
298
304
  columns={enrichedVm.columns}
299
305
  rows={enrichedVm.rows}
package/src/i18n.tsx CHANGED
@@ -148,7 +148,7 @@ export function createStaticLocaleResolver(
148
148
  const locale = options.locale ?? "en";
149
149
  const timeZone = options.timeZone ?? "UTC";
150
150
  return {
151
- translate: (key) => key,
151
+ translate: (key: string) => key,
152
152
  locale: () => locale,
153
153
  timeZone: () => timeZone,
154
154
  // No-op subscribe: unsere Locale ist statisch, es gibt nie ein