@cosmicdrift/kumiko-renderer 0.186.3 → 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.",
@@ -31,6 +37,9 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
31
37
 
32
38
  // Field — aria-Labels der Date/Timestamp-Primitives.
33
39
  "kumiko.field.open-calendar": "Kalender öffnen",
40
+ "kumiko.field.dateField.placeholderYear": "J",
41
+ "kumiko.field.dateField.placeholderMonth": "M",
42
+ "kumiko.field.dateField.placeholderDay": "T",
34
43
  "kumiko.field.time": "Uhrzeit",
35
44
  "kumiko.field.timezone": "Zeitzone",
36
45
  "kumiko.field.locatedTzHint": "Zeit lokal am angegebenen Ort",
@@ -127,6 +136,8 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
127
136
  "kumiko.form.error.version-conflict":
128
137
  "Datensatz wurde zwischenzeitlich geändert. Lade neu und versuche es erneut.",
129
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?",
130
141
 
131
142
  // Validation — Default-Reason-Codes aus dem Framework. App-Code
132
143
  // kann eigene Codes via Validation-Hooks reinwerfen; die hier sind
@@ -189,12 +200,20 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
189
200
  "kumiko.actions.edit": "Edit",
190
201
  "kumiko.actions.copyLink": "Copy link",
191
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}",
192
208
 
193
209
  "kumiko.version.update-available": "A new version is available.",
194
210
 
195
211
  "kumiko.toast.learn-more": "Learn more",
196
212
 
197
213
  "kumiko.field.open-calendar": "Open calendar",
214
+ "kumiko.field.dateField.placeholderYear": "Y",
215
+ "kumiko.field.dateField.placeholderMonth": "M",
216
+ "kumiko.field.dateField.placeholderDay": "D",
198
217
  "kumiko.field.time": "Time",
199
218
  "kumiko.field.timezone": "Time zone",
200
219
  "kumiko.field.locatedTzHint": "Time local to the given location",
@@ -277,6 +296,8 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
277
296
  "kumiko.form.error.version-conflict":
278
297
  "Record was modified in the meantime. Reload and try again.",
279
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?",
280
301
 
281
302
  "kumiko.validation.required": "Required.",
282
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,
@@ -548,6 +548,18 @@ export type DataTableProps = {
548
548
  * "Ende der Liste"-Hinweis statt des Sentinels. Default true. */
549
549
  readonly hasMore?: boolean;
550
550
  readonly testId?: string;
551
+ /** Wires an edit path into a component column-renderer: `DataTableCell`
552
+ * passes this down as `ColumnRendererProps.onChange` for that cell,
553
+ * bound to `(rowId, column.field)`. Renderers without a component
554
+ * renderer (format-spec, type-default) ignore it — read-only stays
555
+ * read-only unless the column has a custom renderer that reads
556
+ * `onChange`. */
557
+ readonly onCellChange?: (rowId: string, field: string, value: unknown) => void;
558
+ /** Overrides the row `data-testid` (default `row-${row.id}`) — for a
559
+ * screen that already has its own DOM-test/e2e naming scheme. */
560
+ readonly getRowTestId?: (row: ListRowViewModel) => string;
561
+ /** Overrides the cell `data-testid` (default `cell-${row.id}-${field}`). */
562
+ readonly getCellTestId?: (row: ListRowViewModel, field: string) => string;
551
563
  };
552
564
 
553
565
  // ---- EmbeddedListInput (createEmbeddedListField widget) ----
@@ -851,6 +863,13 @@ export type CardProps = {
851
863
  readonly testId?: string;
852
864
  };
853
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
+
854
873
  // ---- Core-Registry (Kumiko-eigene Primitives) ----
855
874
 
856
875
  export type CorePrimitives = {
@@ -876,6 +895,10 @@ export type CorePrimitives = {
876
895
  readonly ConfigSourceBadge: ComponentType<ConfigSourceBadgeProps>;
877
896
  readonly ConfigCascadeView: ComponentType<ConfigCascadeViewProps>;
878
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>;
879
902
  };
880
903
 
881
904
  /** Offene Extension-Zone für App-eigene Primitives. Devs erweitern