@cosmicdrift/kumiko-renderer 0.3.0 → 0.4.1

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,82 @@
1
1
  # @cosmicdrift/kumiko-renderer
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 010b410: feat(auth-email-password): "Bestätigungs-Mail erneut senden" im LoginScreen
8
+
9
+ LoginScreen bietet bei reason=email_not_verified jetzt einen Resend-Link
10
+ im Fehler-Banner — der existierende `requestEmailVerification`-Endpoint
11
+ wird direkt aufgerufen, der Banner wechselt nach Erfolg zum Info-Variant
12
+ ("Wir haben dir eine neue Bestätigungs-Mail geschickt.").
13
+
14
+ UX-Details:
15
+
16
+ - Bei 429 → inline-Hint "Bitte warte kurz und versuche es erneut."
17
+ - Bei Netzwerk/sonstigen Fehlern → inline-Hint "Konnte nicht senden."
18
+ - Anti-Typo-Gate: ändert der User die Email-Eingabe nach dem Login-Fail,
19
+ verschwindet der Resend-Link — sonst würde Resend silent-success an die
20
+ geänderte (potentiell typoed) Adresse gehen ohne User-Feedback.
21
+ - Andere Failure-Codes (invalid_credentials etc.) zeigen weiterhin keinen
22
+ Resend-Link.
23
+
24
+ i18n: 4 neue Keys (DE+EN) im `auth.login.resend*`-Namespace, additive.
25
+ Apps die ihre Translations override-en müssen nichts ändern.
26
+
27
+ Additive UI-Feature — keine API-Breaks, keine Schema-Migration.
28
+
29
+ - Updated dependencies [010b410]
30
+ - @cosmicdrift/kumiko-framework@0.4.1
31
+ - @cosmicdrift/kumiko-headless@0.4.1
32
+
33
+ ## 0.4.0
34
+
35
+ ### Minor Changes
36
+
37
+ - 825e7d2: Visual-Tree V.1.4 → V.1.6 — Feature-complete Editor + Folder-Hierarchy + Roving-tabindex.
38
+
39
+ **V.1.4** — explicit `folder?: string` Schema-Field auf text-block-entity. Slug bleibt
40
+ kebab-only validiert, Folder explizit gesetzt. Tree gruppiert via `groupBlocksByFolder`
41
+ (ersetzt `groupBlocksBySlugPrefix`). `Subscribe<T>` Signature um optional `emitError`
42
+ erweitert für explicit async-error-Pfade. ProviderBranch zeigt Error-Banner mit
43
+ Retry-Button. Drift-Test pinnt seedTextBlock-vs-set.write Slug-Validation.
44
+
45
+ **V.1.4b** — URL-State-Routing für Editor-Target via `nav.searchParams`. F5 + Back-Button
46
+ stellen den Editor-State wieder her. Format: `?t=text-content:edit&a_slug=...&a_lang=...`.
47
+ Plus `useDispatchTarget` hook ersetzt globalen `dispatchTarget` als empfohlenen Production-
48
+ Pfad (legacy bleibt für Test-Hooks).
49
+
50
+ **V.1.5** — Arrow-Key-Navigation (`<aside role="tree">`, ARIA-tree-Pattern) + SSE-driven
51
+ Tree-Refresh. `ClientFeatureDefinition.treeEntities?: string[]` listet Entity-Namen pro
52
+ Provider; live-events triggern provider-re-mount → Stale-Tree-state="stub"→"filled"
53
+ flippt nach save automatisch.
54
+
55
+ **V.1.5c+d** — Active-Node-Highlight (explicit blue + 2px border-l + scrollIntoView),
56
+ VS-Code-Polish (compact spacing, focus-visible, folder-icon-color text-amber, indent-
57
+ guides per ancestor-depth), Folder-Wrapper für legal-pages ("📁 Legal" + slug-first
58
+ Verschachtelung) und text-content ("📁 Content").
59
+
60
+ **V.1.6** — Multi-level Folder-Splitting (`folder="page/marketing"` → nested folders,
61
+ walk-or-create-pattern, folder/leaf-collision-tolerant). Roving-tabindex (nur focused-
62
+ treeitem hat tabIndex=0, Tab cyclt aus dem Tree raus).
63
+
64
+ 35/35 kumiko check PASS, 13/13 group-blocks + 22/22 text-content integration tests grün.
65
+ Browser + Keyboard lokal validated.
66
+
67
+ **Breaking**: `TreeContext` Type entfernt (V.1.2 SR2-Rip — war nie genutzt). Provider sind
68
+ session-bound: `TreeChildrenSubscribe = () => Subscribe<T>` statt `(ctx) => Subscribe<T>`.
69
+
70
+ **V.1.7-Followups**: useEffect-deps in VisualTree-focus-init (Performance), Cancellation-
71
+ Token in TreeProvider's fetch (emit-after-unmount-warning), inline-rename, drag-drop,
72
+ file-icons per slug-extension, parent-jump bei ArrowLeft auf collapsed-item.
73
+
74
+ ### Patch Changes
75
+
76
+ - Updated dependencies [825e7d2]
77
+ - @cosmicdrift/kumiko-framework@0.4.0
78
+ - @cosmicdrift/kumiko-headless@0.4.0
79
+
3
80
  ## 0.3.0
4
81
 
5
82
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
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.3.0",
19
- "@cosmicdrift/kumiko-headless": "0.3.0",
18
+ "@cosmicdrift/kumiko-framework": "0.4.1",
19
+ "@cosmicdrift/kumiko-headless": "0.4.1",
20
20
  "react": "^19.2.6"
21
21
  },
22
22
  "devDependencies": {
@@ -1,3 +1,4 @@
1
+ import type { ConfigCascade, ConfigValueSource } from "@cosmicdrift/kumiko-framework/engine";
1
2
  import type {
2
3
  ActionFormScreenDefinition,
3
4
  ConfigEditScreenDefinition,
@@ -918,8 +919,12 @@ function ActionFormBody({
918
919
  // Banner/Submit-Button-State sind identisch zu entityEdit. Der einzige
919
920
  // Pfad-Unterschied ist customSubmit das mehrere config:write:set-Calls
920
921
  // orchestriert.
922
+ //
923
+ // Response enthält seit config-seeding auch `source` (z.B. "system-row")
924
+ // für das ConfigSourceBadge. Type wird lokal gehalten da er nur hier
925
+ // relevant ist — kein eigener Export nötig.
921
926
  type ConfigValueResponse = Readonly<
922
- Record<string, { value: string | number | boolean | undefined; scope: string }>
927
+ Record<string, { value: string | number | boolean | undefined; scope: string; source: string }>
923
928
  >;
924
929
 
925
930
  function ConfigEditBody({
@@ -931,7 +936,7 @@ function ConfigEditBody({
931
936
  readonly screen: ConfigEditScreenDefinition;
932
937
  readonly translate?: Translate;
933
938
  }): ReactNode {
934
- const { Banner } = usePrimitives();
939
+ const { Banner, ConfigSourceBadge, ConfigCascadeView } = usePrimitives();
935
940
  const dispatcher = useDispatcher();
936
941
 
937
942
  // Detail-Load: config:query:values returnt ALLE Keys des Tenants.
@@ -939,6 +944,8 @@ function ConfigEditBody({
939
944
  // unsere Form-Field-Werte.
940
945
  const valuesQuery = useQuery<ConfigValueResponse>("config:query:values", {});
941
946
 
947
+ const cascadeQuery = useQuery<Record<string, ConfigCascade>>("config:query:cascade", {});
948
+
942
949
  const synthEntity = useMemo(() => synthesizeConfigEditEntity(screen.fields), [screen.fields]);
943
950
  const synthScreen = useMemo(() => synthesizeConfigEditScreen(screen), [screen]);
944
951
 
@@ -978,6 +985,34 @@ function ConfigEditBody({
978
985
  return out as FormValues;
979
986
  }, [valuesQuery.data, screen.fields, screen.configKeys]);
980
987
 
988
+ // Sources-Lookup: qualifiedKey → ConfigValueSource für das
989
+ // ConfigSourceBadge. Wird via fieldAppendix an RenderEdit
990
+ // durchgereicht.
991
+ const sources = useMemo<Record<string, ConfigValueSource>>(() => {
992
+ if (valuesQuery.data === null) return {};
993
+ const out: Record<string, ConfigValueSource> = {};
994
+ for (const [shortName, qualified] of Object.entries(screen.configKeys)) {
995
+ const source = valuesQuery.data[qualified]?.source as ConfigValueSource | undefined; // @cast-boundary engine-payload
996
+ if (source !== undefined) out[shortName] = source;
997
+ }
998
+ return out;
999
+ }, [valuesQuery.data, screen.configKeys]);
1000
+
1001
+ // Cascade-Lookup: qualifiedKey → ConfigCascade für die
1002
+ // Cascade-Anzeige unter jedem Feld. Defensive `levels`-Shape-Check
1003
+ // damit fremde Query-Mocks (z.B. configEdit-Unit-Tests die nur
1004
+ // `config:query:values` mocken) nicht durch das Cascade-Rendering
1005
+ // crashen.
1006
+ const cascades = useMemo<Record<string, ConfigCascade>>(() => {
1007
+ if (!cascadeQuery.data) return {};
1008
+ const out: Record<string, ConfigCascade> = {};
1009
+ for (const [shortName, qualified] of Object.entries(screen.configKeys)) {
1010
+ const cascade = cascadeQuery.data[qualified];
1011
+ if (cascade && Array.isArray(cascade.levels)) out[shortName] = cascade;
1012
+ }
1013
+ return out;
1014
+ }, [cascadeQuery.data, screen.configKeys]);
1015
+
981
1016
  // Multi-Write Submit: ein einzelner /api/batch Call mit N
982
1017
  // config:write:set Commands. Server-side ist batch atomic
983
1018
  // (transaktional: alle Writes in einer DB-TX, all-or-nothing) und
@@ -1041,6 +1076,27 @@ function ConfigEditBody({
1041
1076
  customSubmit={customSubmit}
1042
1077
  {...(screen.submitLabel !== undefined && { submitLabel: screen.submitLabel })}
1043
1078
  {...(translate !== undefined && { translate })}
1079
+ fieldAppendix={(fieldName: string) => {
1080
+ const source = sources[fieldName];
1081
+ const cascade = cascades[fieldName];
1082
+ return (
1083
+ <>
1084
+ {source ? <ConfigSourceBadge source={source} /> : null}
1085
+ {cascade !== undefined ? (
1086
+ <ConfigCascadeView
1087
+ cascade={cascade}
1088
+ screenScope={screen.scope}
1089
+ qualifiedKey={screen.configKeys[fieldName]}
1090
+ onReset={async (key, scope) => {
1091
+ await dispatcher.write("config:write:reset", { key, scope });
1092
+ await valuesQuery.refetch?.();
1093
+ await cascadeQuery.refetch?.();
1094
+ }}
1095
+ />
1096
+ ) : null}
1097
+ </>
1098
+ );
1099
+ }}
1044
1100
  />
1045
1101
  );
1046
1102
  }
@@ -56,6 +56,10 @@ export type RenderEditProps<TValues extends FormValues, TCtx = unknown> = {
56
56
  * damit "Speichern" durch domain-spezifischere Strings ersetzt
57
57
  * werden kann ("Genehmigen" / "Versenden" / etc.). */
58
58
  readonly submitLabel?: string;
59
+ /** Pro-Field-Zusatz-Inhalt nach dem Label (z.B. ConfigSourceBadge).
60
+ * Wird mit dem Field-Namen aufgerufen, returnt ReactNode oder
61
+ * undefined. */
62
+ readonly fieldAppendix?: (fieldName: string) => ReactNode | undefined;
59
63
  };
60
64
 
61
65
  function deriveFormFields<TValues extends FormValues, TCtx>(
@@ -100,6 +104,7 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
100
104
  onCancel,
101
105
  onReload,
102
106
  submitLabel,
107
+ fieldAppendix,
103
108
  } = props;
104
109
  const { customSubmit } = props;
105
110
  // Translate-Fallback: wenn der Caller keine Translate-Fn übergibt,
@@ -267,6 +272,10 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
267
272
  }}
268
273
  GridCell={GridCell}
269
274
  featureName={featureName}
275
+ {...(fieldAppendix !== undefined && {
276
+ labelAppendix: fieldAppendix(field.field),
277
+ fieldAppendix: fieldAppendix(field.field),
278
+ })}
270
279
  />
271
280
  ))}
272
281
  </Grid>
@@ -323,6 +332,8 @@ type GridCellForFieldProps = {
323
332
  /** Tier 2.7e-3: durchgereicht damit Reference-Felder die richtige
324
333
  * Lookup-Query-QN bauen können (`<feature>:query:<refEntity>:list`). */
325
334
  readonly featureName: string;
335
+ readonly labelAppendix?: ReactNode;
336
+ readonly fieldAppendix?: ReactNode;
326
337
  };
327
338
 
328
339
  function GridCellForField({
@@ -332,6 +343,8 @@ function GridCellForField({
332
343
  onChange,
333
344
  GridCell,
334
345
  featureName,
346
+ labelAppendix,
347
+ fieldAppendix,
335
348
  }: GridCellForFieldProps): ReactNode {
336
349
  const effectiveSpan = field.span !== undefined ? Math.min(field.span, columns) : 1;
337
350
  return (
@@ -341,6 +354,8 @@ function GridCellForField({
341
354
  {...(issues !== undefined && { issues })}
342
355
  onChange={onChange}
343
356
  featureName={featureName}
357
+ {...(labelAppendix !== undefined && { labelAppendix })}
358
+ {...(fieldAppendix !== undefined && { fieldAppendix })}
344
359
  />
345
360
  </GridCell>
346
361
  );
@@ -14,14 +14,27 @@ import { usePrimitives } from "../primitives";
14
14
  export type RenderFieldProps = {
15
15
  readonly field: EditFieldViewModel;
16
16
  readonly issues?: readonly FieldIssue[];
17
- readonly onChange: (value: unknown) => void;
17
+ readonly onChange: (val: unknown) => void;
18
18
  /** Nur bei type:"reference" relevant — Feature-Name für die Lookup-
19
19
  * Query-QN (`<feature>:query:<refEntity>:list`). Andere Field-Types
20
20
  * ignorieren das Prop. */
21
21
  readonly featureName?: string;
22
+ /** Optionaler Zusatz-Inhalt der nach dem Label gerendert wird (z.B.
23
+ * ConfigSourceBadge). */
24
+ readonly labelAppendix?: ReactNode;
25
+ /** Optionaler Zusatz-Inhalt der nach dem Input gerendert wird (z.B.
26
+ * ConfigCascade). */
27
+ readonly fieldAppendix?: ReactNode;
22
28
  };
23
29
 
24
- export function RenderField({ field, issues, onChange, featureName }: RenderFieldProps): ReactNode {
30
+ export function RenderField({
31
+ field,
32
+ issues,
33
+ onChange,
34
+ featureName,
35
+ labelAppendix,
36
+ fieldAppendix,
37
+ }: RenderFieldProps): ReactNode {
25
38
  const { Field, Input } = usePrimitives();
26
39
  if (!field.visible) return null;
27
40
 
@@ -51,6 +64,8 @@ export function RenderField({ field, issues, onChange, featureName }: RenderFiel
51
64
  label={field.label}
52
65
  required={field.required}
53
66
  {...(issues !== undefined && { issues })}
67
+ {...(labelAppendix !== undefined && { labelAppendix })}
68
+ {...(fieldAppendix !== undefined && { fieldAppendix })}
54
69
  testId={`field-${field.field}`}
55
70
  >
56
71
  {control}
@@ -29,6 +29,11 @@
29
29
  // nicht), aber der App-Code hat ein einheitliches Primitive-Vokabular
30
30
  // über Core + Custom.
31
31
 
32
+ import type {
33
+ ConfigCascade,
34
+ ConfigScope,
35
+ ConfigValueSource,
36
+ } from "@cosmicdrift/kumiko-framework/engine";
32
37
  import type {
33
38
  FieldIssue,
34
39
  ListColumnViewModel,
@@ -88,6 +93,8 @@ export type FieldProps = {
88
93
  readonly label: string;
89
94
  readonly required?: boolean;
90
95
  readonly issues?: readonly FieldIssue[];
96
+ readonly labelAppendix?: ReactNode;
97
+ readonly fieldAppendix?: ReactNode;
91
98
  readonly children: ReactNode;
92
99
  readonly testId?: string;
93
100
  };
@@ -466,6 +473,22 @@ export type DialogProps = {
466
473
  readonly testId?: string;
467
474
  };
468
475
 
476
+ /** Source-badge for one cascade step (User / Tenant / System / …).
477
+ * Used inline next to a config value to indicate where it came from. */
478
+ export type ConfigSourceBadgeProps = {
479
+ readonly source: ConfigValueSource;
480
+ };
481
+
482
+ /** Collapsible cascade-view that lives under a config-edit input.
483
+ * Shows the active level inline; click expands the full cascade and
484
+ * exposes a Reset-button if the active level matches `screenScope`. */
485
+ export type ConfigCascadeViewProps = {
486
+ readonly cascade: ConfigCascade;
487
+ readonly screenScope: ConfigScope;
488
+ readonly onReset?: (key: string, scope: ConfigScope) => void;
489
+ readonly qualifiedKey?: string;
490
+ };
491
+
469
492
  // ---- Core-Registry (Kumiko-eigene Primitives) ----
470
493
 
471
494
  export type CorePrimitives = {
@@ -481,6 +504,8 @@ export type CorePrimitives = {
481
504
  readonly Text: ComponentType<TextProps>;
482
505
  readonly Heading: ComponentType<HeadingProps>;
483
506
  readonly Dialog: ComponentType<DialogProps>;
507
+ readonly ConfigSourceBadge: ComponentType<ConfigSourceBadgeProps>;
508
+ readonly ConfigCascadeView: ComponentType<ConfigCascadeViewProps>;
484
509
  };
485
510
 
486
511
  /** Offene Extension-Zone für App-eigene Primitives. Devs erweitern