@cosmicdrift/kumiko-renderer 0.187.0 → 0.188.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.
@@ -1,5 +1,9 @@
1
1
  import type { EntityEditScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
- import type { EditFieldViewModel, FieldIssue } from "@cosmicdrift/kumiko-headless";
2
+ import {
3
+ currencyDecimals,
4
+ type EditFieldViewModel,
5
+ type FieldIssue,
6
+ } from "@cosmicdrift/kumiko-headless";
3
7
  import { type ReactNode, useCallback, useMemo, useState } from "react";
4
8
  import { useAppFeatures } from "../app/app-features-context";
5
9
  import { toKebab } from "../app/qn";
@@ -330,15 +334,15 @@ function renderInput({
330
334
  />
331
335
  );
332
336
  case "money": {
333
- const moneyDef = field as unknown as { currency?: string; locale?: string };
337
+ const currency = field.currency ?? "EUR";
334
338
  return (
335
339
  <Input
336
340
  kind="money"
337
341
  {...common}
338
- value={numberValue(field.value)}
339
- onChange={(v) => onChange(v)}
340
- {...(moneyDef.currency !== undefined && { currency: moneyDef.currency })}
341
- locale={moneyDef.locale ?? appLocale}
342
+ value={moneyMinorValue(field.value, currency)}
343
+ onChange={(v) => onChange(moneyPayload(v, currency))}
344
+ currency={currency}
345
+ locale={appLocale}
342
346
  />
343
347
  );
344
348
  }
@@ -479,6 +483,32 @@ function numberValue(v: unknown): number | "" {
479
483
  return typeof v === "number" ? v : Number(v);
480
484
  }
481
485
 
486
+ // Read/initial value → MoneyInput's minor-units contract. The server's
487
+ // read shape is `{amount, currency, amountMinor}` in MAJOR units — deliberately
488
+ // NOT reading `amountMinor` here: the server derives it via a flat
489
+ // MINOR_UNIT_SCALE=100 (money.ts), which disagrees with currencyDecimals for
490
+ // zero-decimal currencies like JPY, so it would double-scale JPY amounts.
491
+ // Deriving from `amount * 10**currencyDecimals(currency)` keeps this the
492
+ // exact inverse of moneyPayload below. A bare number is passed through
493
+ // unchanged — legacy/malformed data, already in minor units.
494
+ function moneyMinorValue(v: unknown, currency: string): number | "" {
495
+ if (v === undefined || v === null || v === "") return "";
496
+ if (typeof v === "number") return v;
497
+ if (typeof v === "object") {
498
+ const amount = (v as { amount?: unknown }).amount;
499
+ if (typeof amount === "number") return Math.round(amount * 10 ** currencyDecimals(currency));
500
+ }
501
+ return "";
502
+ }
503
+
504
+ // MoneyInput's minor-units onChange → the server payload shape
505
+ // (`z.object({amount, currency})`, schema-builder.ts) — MAJOR units, no
506
+ // `amountMinor` (the server derives that itself on write).
507
+ function moneyPayload(minorUnits: number | undefined, currency: string): unknown {
508
+ if (minorUnits === undefined) return undefined;
509
+ return { amount: minorUnits / 10 ** currencyDecimals(currency), currency };
510
+ }
511
+
482
512
  // locatedTimestamp-Feldwert: das Read-Wrapper liefert `{ at, tz, utc }`; leer
483
513
  // (noch nicht gesetzt) → "" als Empty-Sentinel, analog money/timestamp.
484
514
  function locatedValue(v: unknown): { at: string; tz: string; utc?: string } | "" {
@@ -0,0 +1,39 @@
1
+ import { createContext, type ReactNode, useContext } from "react";
2
+
3
+ // Client-side draftId storage for RenderEdit's create-mode draftKey (issue
4
+ // #1913) — `${screen.id}:new:${draftId}` needs the same `draftId` across a
5
+ // same-tab reload so the wizard resumes the right row instead of the leader
6
+ // on the last-write-wins upsert. Contract only, platform-neutral — the
7
+ // concrete impl (window.sessionStorage on web) is injected by the platform
8
+ // package, same pattern as NavApi/window.history in app/nav.tsx. This
9
+ // package touches no browser storage itself.
10
+ export type DraftStorage = {
11
+ readonly getDraftId: (screenId: string) => string | null;
12
+ readonly setDraftId: (screenId: string, draftId: string) => void;
13
+ readonly clearDraftId: (screenId: string) => void;
14
+ };
15
+
16
+ // No-op default: without a mounted provider (unwired platform, or a test
17
+ // that doesn't care about drafts) same-tab reload just loses draftId
18
+ // persistence — RenderEdit falls back to its `form-draft:query:list`
19
+ // resume path, the same one a cleared sessionStorage takes on the web.
20
+ const noopDraftStorage: DraftStorage = {
21
+ getDraftId: () => null,
22
+ setDraftId: () => {},
23
+ clearDraftId: () => {},
24
+ };
25
+
26
+ const DraftStorageContext = createContext<DraftStorage>(noopDraftStorage);
27
+
28
+ export type DraftStorageProviderProps = {
29
+ readonly value: DraftStorage;
30
+ readonly children: ReactNode;
31
+ };
32
+
33
+ export function DraftStorageProvider({ value, children }: DraftStorageProviderProps): ReactNode {
34
+ return <DraftStorageContext value={value}>{children}</DraftStorageContext>;
35
+ }
36
+
37
+ export function useDraftStorage(): DraftStorage {
38
+ return useContext(DraftStorageContext);
39
+ }
@@ -22,6 +22,12 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
22
22
  "kumiko.actions.edit": "Bearbeiten",
23
23
  "kumiko.actions.copyLink": "Link kopieren",
24
24
  "kumiko.actions.copyLinkCopied": "Kopiert!",
25
+ "kumiko.actions.next": "Weiter",
26
+ "kumiko.actions.back": "Zurück",
27
+ "kumiko.actions.finish": "Abschließen",
28
+
29
+ // Wizard step chrome (RenderEdit layout.mode="wizard").
30
+ "kumiko.wizard.step": "Schritt {current} von {total}",
25
31
 
26
32
  // Version — Update-Awareness-Banner (UpdateChecker).
27
33
  "kumiko.version.update-available": "Eine neue Version ist verfügbar.",
@@ -130,6 +136,8 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
130
136
  "kumiko.form.error.version-conflict":
131
137
  "Datensatz wurde zwischenzeitlich geändert. Lade neu und versuche es erneut.",
132
138
  "kumiko.form.extension.save-failed": "Ein Zusatzfeld konnte nicht gespeichert werden.",
139
+ "kumiko.form.draft.resume-multiple":
140
+ "Mehrere offene Entwürfe für dieses Formular gefunden. Welchen möchtest du fortsetzen?",
133
141
 
134
142
  // Validation — Default-Reason-Codes aus dem Framework. App-Code
135
143
  // kann eigene Codes via Validation-Hooks reinwerfen; die hier sind
@@ -192,6 +200,11 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
192
200
  "kumiko.actions.edit": "Edit",
193
201
  "kumiko.actions.copyLink": "Copy link",
194
202
  "kumiko.actions.copyLinkCopied": "Copied!",
203
+ "kumiko.actions.next": "Next",
204
+ "kumiko.actions.back": "Back",
205
+ "kumiko.actions.finish": "Finish",
206
+
207
+ "kumiko.wizard.step": "Step {current} of {total}",
195
208
 
196
209
  "kumiko.version.update-available": "A new version is available.",
197
210
 
@@ -283,6 +296,8 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
283
296
  "kumiko.form.error.version-conflict":
284
297
  "Record was modified in the meantime. Reload and try again.",
285
298
  "kumiko.form.extension.save-failed": "A custom field could not be saved.",
299
+ "kumiko.form.draft.resume-multiple":
300
+ "Found multiple open drafts for this form. Which one do you want to resume?",
286
301
 
287
302
  "kumiko.validation.required": "Required.",
288
303
  "kumiko.validation.invalid": "Invalid value.",
package/src/index.ts CHANGED
@@ -69,7 +69,11 @@ export { lastSegment } from "./app/qn";
69
69
  export type { VariableChipsProps } from "./app/variable-chips";
70
70
  export { VariableChips } from "./app/variable-chips";
71
71
  export { dispatcherErrorText, WriteFailedError } from "./app/write-failed-error";
72
- export type { RenderEditProps } from "./components/render-edit";
72
+ export type {
73
+ RenderEditChangeState,
74
+ RenderEditControls,
75
+ RenderEditProps,
76
+ } from "./components/render-edit";
73
77
  export { RenderEdit } from "./components/render-edit";
74
78
  export type { RenderFieldProps } from "./components/render-field";
75
79
  export { RenderField } from "./components/render-field";
@@ -82,6 +86,8 @@ export {
82
86
  useDispatcherStatus,
83
87
  useOptionalDispatcher,
84
88
  } from "./context/dispatcher-context";
89
+ export type { DraftStorage, DraftStorageProviderProps } from "./context/draft-storage-context";
90
+ export { DraftStorageProvider, useDraftStorage } from "./context/draft-storage-context";
85
91
  export type { UserRolesProviderProps } from "./context/user-roles-context";
86
92
  export { UserRolesProvider, useUserRoles } from "./context/user-roles-context";
87
93
  export { formatWhen } from "./format-when";
@@ -169,6 +175,7 @@ export type {
169
175
  ModalProps,
170
176
  PrimitivesProviderProps,
171
177
  PrimitivesRegistry,
178
+ ProgressProps,
172
179
  RuntimeRenderer,
173
180
  SectionProps,
174
181
  TextProps,
@@ -863,6 +863,13 @@ export type CardProps = {
863
863
  readonly testId?: string;
864
864
  };
865
865
 
866
+ /** Determinate progress bar (e.g. wizard step progress). `value` is a
867
+ * 0..1 fraction, not a percentage — implementations scale for display. */
868
+ export type ProgressProps = {
869
+ readonly value: number;
870
+ readonly testId?: string;
871
+ };
872
+
866
873
  // ---- Core-Registry (Kumiko-eigene Primitives) ----
867
874
 
868
875
  export type CorePrimitives = {
@@ -888,6 +895,10 @@ export type CorePrimitives = {
888
895
  readonly ConfigSourceBadge: ComponentType<ConfigSourceBadgeProps>;
889
896
  readonly ConfigCascadeView: ComponentType<ConfigCascadeViewProps>;
890
897
  readonly Link: ComponentType<LinkProps>;
898
+ /** Optional (unlike the other Core-Primitives) so existing partial
899
+ * CorePrimitives mocks in tests keep compiling — additive rollout of
900
+ * a new primitive shouldn't force every test double to grow a stub. */
901
+ readonly Progress?: ComponentType<ProgressProps>;
891
902
  };
892
903
 
893
904
  /** Offene Extension-Zone für App-eigene Primitives. Devs erweitern