@cosmicdrift/kumiko-renderer 0.352.0 → 0.353.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.
@@ -319,7 +319,7 @@ export function literalCurrencyOverrides(fields) {
319
319
  // names themselves because an edit form only needs the fetch for fields the
320
320
  // record has no stored value for.
321
321
  export function useMoneyCurrencyOverrides(fields, tenantFieldNames, fallback) {
322
- const query = useQuery("config:query:values", {}, { enabled: tenantFieldNames.length > 0 });
322
+ const query = useQuery("config:query:values", { keys: [TENANT_CURRENCY_CONFIG_KEY] }, { enabled: tenantFieldNames.length > 0 });
323
323
  const resolution = resolveTenantCurrency(tenantFieldNames, query, fallback);
324
324
  const resolvedTenantCurrency = resolution.status === "ready" ? resolution.currency : undefined;
325
325
  const overrides = useMemo(() => {
@@ -1830,7 +1830,8 @@ function headerStatusTone(header, record) {
1830
1830
  const declared = header.statusTones !== undefined && Object.hasOwn(header.statusTones, value)
1831
1831
  ? header.statusTones[value]
1832
1832
  : undefined;
1833
- return declared !== undefined ? statusToneForOptionTone(declared) : statusToneForValue(value);
1833
+ const declaredTone = declared !== undefined ? statusToneForOptionTone(declared) : undefined;
1834
+ return declaredTone ?? statusToneForValue(value);
1834
1835
  }
1835
1836
  function resolveSubtitleHref(header, record) {
1836
1837
  if (header?.subtitleHref === undefined)
@@ -1,8 +1,9 @@
1
1
  import type { EntityDefinition, EntityListScreenDefinition, ListColumnSpec, ProjectionListScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
2
  /** Minimal EntityDefinition from the column list: every field is a text
3
- * field. `sortable` applies uniformly to all columns — buildAppSchema
4
- * derives it from the query's Zod schema (`screen.sortable`, fw#2165); the
5
- * query itself has no per-field server-sort guarantee.
3
+ * field. `sortable` applies to all columns — buildAppSchema derives it from
4
+ * the query's Zod schema (`screen.sortable`, fw#2165); the query itself has
5
+ * no per-field server-sort guarantee, so a column opts out with
6
+ * `sortable: false` when the handler cannot order by it.
6
7
  * computeListViewModel only reads `fields[<col>].type` — text is enough,
7
8
  * presentation comes from the column renderer + explicit label. */
8
9
  export declare function synthesizeProjectionEntity(columns: readonly ListColumnSpec[], sortable: boolean): EntityDefinition;
@@ -16,15 +16,20 @@
16
16
  import { normalizeListColumn } from "@cosmicdrift/kumiko-framework/ui-types";
17
17
  const PROJECTION_PSEUDO_ENTITY = "__projection__";
18
18
  /** Minimal EntityDefinition from the column list: every field is a text
19
- * field. `sortable` applies uniformly to all columns — buildAppSchema
20
- * derives it from the query's Zod schema (`screen.sortable`, fw#2165); the
21
- * query itself has no per-field server-sort guarantee.
19
+ * field. `sortable` applies to all columns — buildAppSchema derives it from
20
+ * the query's Zod schema (`screen.sortable`, fw#2165); the query itself has
21
+ * no per-field server-sort guarantee, so a column opts out with
22
+ * `sortable: false` when the handler cannot order by it.
22
23
  * computeListViewModel only reads `fields[<col>].type` — text is enough,
23
24
  * presentation comes from the column renderer + explicit label. */
24
25
  export function synthesizeProjectionEntity(columns, sortable) {
25
26
  const fields = {};
26
27
  for (const col of columns) {
27
- fields[normalizeListColumn(col).field] = { type: "text", sortable };
28
+ const normalized = normalizeListColumn(col);
29
+ fields[normalized.field] = {
30
+ type: "text",
31
+ sortable: sortable && normalized.sortable !== false,
32
+ };
28
33
  }
29
34
  return { fields };
30
35
  }
@@ -4,7 +4,7 @@ import { RenderEdit } from "../components/render-edit.js";
4
4
  import { useTranslation } from "../i18n.js";
5
5
  import { usePrimitives } from "../primitives.js";
6
6
  import { synthesizeActionFormEntity, synthesizeActionFormScreen, synthesizeSecretMintConfirmScreen, } from "./action-form-shim.js";
7
- import { buildInitialValues, literalCurrencyOverrides, mergeSearchParamsIntoInitial, } from "./kumiko-screen.js";
7
+ import { buildInitialValues, mergeSearchParamsIntoInitial, tenantCurrencyMoneyFieldNames, useMoneyCurrencyOverrides, } from "./kumiko-screen.js";
8
8
  import { layoutFieldNames } from "./layout-fields.js";
9
9
  import { useInitialValuesHandoff, useNav } from "./nav.js";
10
10
  import { lastSegment } from "./qn.js";
@@ -26,6 +26,9 @@ function extractCarriedValues(data, carry) {
26
26
  }
27
27
  return carried;
28
28
  }
29
+ // Entity-less forms have no `entity.defaultCurrency`; same fallback as actionForm.
30
+ const SECRET_MINT_CURRENCY_FALLBACK = "EUR";
31
+ const NO_FIELDS = {};
29
32
  function isBlank(value) {
30
33
  if (value === undefined || value === null || value === "")
31
34
  return true;
@@ -50,17 +53,26 @@ export function SecretMintBody({ schema, screen, translate }) {
50
53
  const synthEntity = useMemo(() => synthesizeActionFormEntity(screen.fields), [screen.fields]);
51
54
  const synthScreen = useMemo(() => synthesizeActionFormScreen(screen), [screen]);
52
55
  const handoffValues = useInitialValuesHandoff(screen.id);
56
+ const mintTenantCurrencyFields = useMemo(() => tenantCurrencyMoneyFieldNames(screen.fields), [screen.fields]);
57
+ const { overrides: mintCurrencyOverrides, loading: mintCurrencyLoading } = useMoneyCurrencyOverrides(screen.fields, mintTenantCurrencyFields, SECRET_MINT_CURRENCY_FALLBACK);
53
58
  const initial = useMemo(() => mergeSearchParamsIntoInitial(screen.fields, {
54
59
  searchParams: nav.searchParams,
55
60
  urlPrefillFields: screen.urlPrefillFields,
56
61
  renderableFields: layoutFieldNames(synthScreen),
57
62
  // A mint form has no entity either, so its money fields name their own
58
- // currency source (fw#2839). Only the literal form is resolved here:
59
- // `{ kind: "tenant" }` would need the config query this screen never
60
- // runs, and a one-time secret mint has no money use case to justify it.
61
- moneyCurrencyOverrides: literalCurrencyOverrides(screen.fields),
63
+ // currency source (fw#2839, fw#2933).
64
+ ...(mintCurrencyOverrides !== undefined && {
65
+ moneyCurrencyOverrides: mintCurrencyOverrides,
66
+ }),
62
67
  ...(handoffValues !== undefined && { handoffValues }),
63
- }), [screen.fields, screen.urlPrefillFields, nav.searchParams, synthScreen, handoffValues]);
68
+ }), [
69
+ screen.fields,
70
+ screen.urlPrefillFields,
71
+ nav.searchParams,
72
+ synthScreen,
73
+ handoffValues,
74
+ mintCurrencyOverrides,
75
+ ]);
64
76
  const [revealed, setRevealed] = useState(null);
65
77
  const [done, setDone] = useState(false);
66
78
  // The secret was minted server-side but none of the declared reveal fields
@@ -72,9 +84,12 @@ export function SecretMintBody({ schema, screen, translate }) {
72
84
  const confirm = screen.confirm;
73
85
  const confirmEntity = useMemo(() => (confirm !== undefined ? synthesizeActionFormEntity(confirm.fields) : undefined), [confirm]);
74
86
  const confirmScreen = useMemo(() => (confirm !== undefined ? synthesizeSecretMintConfirmScreen(screen, confirm) : undefined), [screen, confirm]);
87
+ const confirmFields = confirm?.fields ?? NO_FIELDS;
88
+ const confirmTenantCurrencyFields = useMemo(() => tenantCurrencyMoneyFieldNames(confirmFields), [confirmFields]);
89
+ const { overrides: confirmCurrencyOverrides, loading: confirmCurrencyLoading } = useMoneyCurrencyOverrides(confirmFields, confirmTenantCurrencyFields, SECRET_MINT_CURRENCY_FALLBACK);
75
90
  const confirmInitial = useMemo(() => confirm !== undefined
76
- ? buildInitialValues(confirm.fields, undefined, literalCurrencyOverrides(confirm.fields))
77
- : undefined, [confirm]);
91
+ ? buildInitialValues(confirm.fields, undefined, confirmCurrencyOverrides)
92
+ : undefined, [confirm, confirmCurrencyOverrides]);
78
93
  const handleSubmitted = useCallback((result) => {
79
94
  if (!result.isSuccess)
80
95
  return;
@@ -166,9 +181,17 @@ export function SecretMintBody({ schema, screen, translate }) {
166
181
  // keys stop the mint and confirm forms sharing one instance (and its
167
182
  // submit state) across phases.
168
183
  if (confirm !== undefined && confirmEntity !== undefined && confirmScreen !== undefined) {
184
+ // Hold until a tenant-declared currency lands, else the form would seed (and
185
+ // on a fast click submit) the fallback currency (fw#2933).
186
+ if (confirmCurrencyLoading) {
187
+ return (_jsx(Banner, { padded: true, variant: "loading", testId: "kumiko-screen-loading", children: "Loading\u2026" }));
188
+ }
169
189
  return (_jsx(RenderEdit, { screen: confirmScreen, i18nScreenId: screen.id, entity: confirmEntity, featureName: schema.featureName, initial: confirmInitial ?? {}, writeCommand: confirm.handler, payloadMode: "values", buildPayload: (snapshot) => ({ ...snapshot.values, ...carriedRef.current }), onSubmit: handleConfirmSubmitted, onCancel: handleCancel ?? restartMint, fillScreenHeight: true, leadContent: _jsx(Section, { testId: "kumiko-screen-secret-mint-card", children: revealContent }), ...(translate !== undefined && { translate }), ...(confirm.submitLabel !== undefined && { submitLabel: confirm.submitLabel }) }, "confirm"));
170
190
  }
171
191
  return (_jsxs(Card, { options: { screenBody: true }, testId: "kumiko-screen-secret-mint-card", children: [revealContent, _jsx(Button, { type: "button", variant: "primary", onClick: finishMint, testId: "kumiko-screen-secret-mint-confirm", children: effectiveTranslate(screen.reveal.confirmLabel ?? "kumiko.secretMint.confirm") })] }));
172
192
  }
193
+ if (mintCurrencyLoading) {
194
+ return (_jsx(Banner, { padded: true, variant: "loading", testId: "kumiko-screen-loading", children: "Loading\u2026" }));
195
+ }
173
196
  return (_jsx(RenderEdit, { screen: synthScreen, entity: synthEntity, featureName: schema.featureName, initial: initial, extensionInitialValues: initial, writeCommand: screen.handler, payloadMode: "values", onSubmit: handleSubmitted, fillScreenHeight: true, ...(handleCancel !== undefined && { onCancel: handleCancel }), ...(translate !== undefined && { translate }), ...(screen.submitLabel !== undefined && { submitLabel: screen.submitLabel }) }, "mint"));
174
197
  }
@@ -752,10 +752,10 @@ export function RenderEdit(props) {
752
752
  }
753
753
  const secretFieldNames = new Set(vm.sections.flatMap((section) => section.kind === "fields"
754
754
  ? section.fields
755
- .filter((f) => f.format === "password" || f.writeOnly === true)
755
+ .filter((f) => f.format === "password" || f.writeOnly === true || f.sensitive === true)
756
756
  .map((f) => f.field)
757
757
  : []));
758
- // A password or writeOnly field never enters the persisted draft blob — it would be stored in the clear and restored on resume.
758
+ // A password, writeOnly or sensitive field never enters the persisted draft blob — it would be stored in the clear and restored on resume.
759
759
  const draftValues = Object.fromEntries(Object.entries(controller.getSnapshot().values).filter(([field]) => !secretFieldNames.has(field)));
760
760
  void dispatcher
761
761
  .write(FORM_DRAFT_SAVE, {
@@ -1131,7 +1131,7 @@ export function RenderEdit(props) {
1131
1131
  footerActions?.map((action) => (_jsx(Button, { type: "button", variant: action.variant ?? "secondary", ...(action.icon !== undefined && { icon: action.icon }), disabled: isSubmitting || disabled, onClick: () => {
1132
1132
  controller.setValues(action.patch); // @cast-boundary form-values
1133
1133
  void handleSubmit();
1134
- }, testId: `render-edit-footer-action-${action.id}`, children: translate(action.label) }, action.id))), hasDirtyFooter && showsSubmit && unsavedCount !== undefined && unsavedCount > 0 && (_jsx(Button, { type: "button", variant: "ghost", disabled: isSubmitting || disabled, onClick: () => controller.reset(), testId: "render-edit-discard", [STICKY_PRIMARY_ACTION_PROP]: true, children: translate("kumiko.form.discard") })), showSubmit && (_jsx(Button, { type: "submit", disabled: (snapshot.isUnchanged && !extensionDirty && !isFieldless && !insideDrawer) ||
1134
+ }, testId: `render-edit-footer-action-${action.id}`, children: translate(action.label) }, action.id))), hasDirtyFooter && showsSubmit && unsavedCount !== undefined && unsavedCount > 0 && (_jsx(Button, { type: "button", variant: "ghost", disabled: isSubmitting || disabled, onClick: () => controller.reset(), testId: "render-edit-discard", [STICKY_PRIMARY_ACTION_PROP]: true, children: translate("kumiko.form.discard") })), showSubmit && (_jsx(Button, { type: "submit", disabled: (snapshot.isUnchanged && !extensionDirty && !isFieldlessSubmitForm && !insideDrawer) ||
1135
1135
  isSubmitting ||
1136
1136
  disabled, loading: isSubmitting, variant: submitVariant ?? "primary", testId: "render-edit-submit", children: translate(submitLabel ??
1137
1137
  (isWizard
@@ -1367,10 +1367,13 @@ export function RenderEdit(props) {
1367
1367
  }
1368
1368
  // No section title here (fw#3218) — the Tab strip right above
1369
1369
  // already names this panel, so a repeated Card title would just
1370
- // duplicate it. The title row only exists to carry actions, and
1371
- // then it carries only the actions, never the title text.
1372
- const cardEl = (_jsx(Card, { options: { framed: false }, ...(sectionActionsEl !== undefined && {
1373
- slots: { headerActions: sectionActionsEl },
1370
+ // duplicate it. The header row only exists to carry the section
1371
+ // description and actions, never the title text.
1372
+ const cardEl = (_jsx(Card, { options: { framed: false }, ...((sectionActionsEl !== undefined || section.description !== undefined) && {
1373
+ slots: {
1374
+ ...(section.description !== undefined && { subtitle: section.description }),
1375
+ ...(sectionActionsEl !== undefined && { headerActions: sectionActionsEl }),
1376
+ },
1374
1377
  }), testId: `section-${sectionKey}`, children: renderFieldGrid(section.fields, section.columns, `${sectionKey}-grid`) }, sectionKey));
1375
1378
  return wrapWizardStep(sectionKey, cardEl);
1376
1379
  }
@@ -17,6 +17,7 @@
17
17
  // internen Cache shared zwischen List + Edit-Form für die gleiche
18
18
  // Entity, Live-Updates kommen via SSE (use-query-live).
19
19
  import { REFERENCE_LOOKUP_SOURCES, SYSTEM_REFERENCE_LABELS, } from "@cosmicdrift/kumiko-framework/ui-types";
20
+ import { PII_ERASED_SENTINEL } from "@cosmicdrift/kumiko-types/kms-adapter-types";
20
21
  import { useMemo } from "react";
21
22
  import { toKebab } from "../app/qn.js";
22
23
  import { useTranslation } from "../i18n.js";
@@ -61,7 +62,7 @@ export function useReferenceLookup(featureName, refEntity, labelField) {
61
62
  continue;
62
63
  const idStr = String(id);
63
64
  const label = row[labelKey] ?? id;
64
- out.set(idStr, String(label));
65
+ out.set(idStr, label === PII_ERASED_SENTINEL ? translate("kumiko.pii.erased") : String(label));
65
66
  }
66
67
  // System-scope ids (e.g. SYSTEM_TENANT_ID) never have a backing row, so
67
68
  // the bulk lookup above never covers them — inject the label directly.
@@ -41,6 +41,7 @@ export const kumikoDefaultTranslations = {
41
41
  "kumiko.field.unsupported": "This field type can't be edited here yet.",
42
42
  "kumiko.field.writeOnly.remove": "Remove stored value",
43
43
  "kumiko.field.writeOnly.set": "Set",
44
+ "kumiko.pii.erased": "Deleted",
44
45
  "kumiko.field.writeOnly.setPlaceholder": "Set — leave empty to keep it",
45
46
  "kumiko.field.writeOnly.undo": "Undo",
46
47
  "kumiko.field.writeOnly.willRemove": "Removed on save",
@@ -107,6 +108,8 @@ export const kumikoDefaultTranslations = {
107
108
  "kumiko.dashboard.panel.error.title": "{label} could not be loaded",
108
109
  "kumiko.dashboard.time-range": "Time range",
109
110
  "kumiko.screen.unavailable": "This page is not available right now.",
111
+ "kumiko.dashboard.brush-end": "Window end",
112
+ "kumiko.dashboard.brush-start": "Window start",
110
113
  "kumiko.dashboard.today": "Today",
111
114
  "kumiko.dashboard.updated-at": "As of {time}",
112
115
  "kumiko.app-boot.loading": "Loading…",
package/dist/index.d.ts CHANGED
@@ -72,7 +72,7 @@ export { kumikoDefaultTranslations } from "./i18n-defaults.js";
72
72
  export { InsideDrawerProvider, useInsideDrawer } from "./inside-drawer.js";
73
73
  export { PageHeaderCompactProvider, PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "./page-header-slot.js";
74
74
  export type { ActionMenuItemSpec, ActionOverflowMenuProps, AppPrimitives, BannerProps, ButtonProps, CardOptions, CardProps, CardSlots, CopyButtonProps, CorePrimitives, DataTableDateRangeFacet, DataTableFacet, DataTableFacetChip, DataTableProps, DataTableRowAction, DataTableRowActionMode, DataTableRowGrouping, DataTableSort, DataTableSortDir, DialogProps, DrawerProps, EmbeddedListCellType, EmbeddedListColumn, EmbeddedListInputProps, EmbeddedListTotal, FieldCellWidth, FieldProps, FillContainerProps, FooterActionMarker, FooterActionRole, FormProps, FormSectionNavItem, FormWidth, GridCellProps, GridProps, HeadingProps, InputProps, JsonViewProps, LightboxImage, LightboxProps, LinkProps, MetricBandProps, MetricProps, ModalProps, PageHeaderProps, PrimitivesProviderProps, PrimitivesRegistry, ProgressProps, ProgressTone, PromoPanelAction, PromoPanelProps, RuntimeRenderer, SecretRevealProps, SecretRevealValue, SectionProps, ShareButtonProps, StatusBadgeProps, StatusTone, StepBarProps, StickyActionBarProps, StickyPrimaryActionMarker, TabsProps, TextProps, TrailingInputAction, WizardStepGroupProps, } from "./primitives.js";
75
- export { FOOTER_ACTION_ROLE_PROP, NARROW_LABEL_PROP, PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, usePrimitives, } from "./primitives.js";
75
+ export { FOOTER_ACTION_ROLE_PROP, NARROW_LABEL_PROP, PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, useOptionalPrimitives, usePrimitives, } from "./primitives.js";
76
76
  export type { SessionEndedSignal, SessionEndedSignalProviderProps, } from "./session/session-ended.js";
77
77
  export { createSessionEndedSignal, isSessionEndedError, SESSION_ENDED_ERROR_CODES, SessionEndedSignalProvider, useSessionEndedSignal, withSessionEndedDetection, } from "./session/session-ended.js";
78
78
  export { buildWhatsAppShareUrl } from "./share-links.js";
package/dist/index.js CHANGED
@@ -52,7 +52,7 @@ export { createStaticLocaleResolver, FormalityProvider, LocaleProvider, mergeTra
52
52
  export { kumikoDefaultTranslations } from "./i18n-defaults.js";
53
53
  export { InsideDrawerProvider, useInsideDrawer } from "./inside-drawer.js";
54
54
  export { PageHeaderCompactProvider, PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "./page-header-slot.js";
55
- export { FOOTER_ACTION_ROLE_PROP, NARROW_LABEL_PROP, PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, usePrimitives, } from "./primitives.js";
55
+ export { FOOTER_ACTION_ROLE_PROP, NARROW_LABEL_PROP, PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, useOptionalPrimitives, usePrimitives, } from "./primitives.js";
56
56
  export { createSessionEndedSignal, isSessionEndedError, SESSION_ENDED_ERROR_CODES, SessionEndedSignalProvider, useSessionEndedSignal, withSessionEndedDetection, } from "./session/session-ended.js";
57
57
  export { buildWhatsAppShareUrl } from "./share-links.js";
58
58
  export { sortByAccessor } from "./sort-by-accessor.js";
@@ -1034,8 +1034,9 @@ export type DialogProps = {
1034
1034
  readonly cancelLabel?: string;
1035
1035
  /** `default` = Confirm primary, `danger` = Confirm danger. */
1036
1036
  readonly variant?: "default" | "danger";
1037
- /** Which button takes focus on open. Default: Confirm for a bare dialog,
1038
- * the browser/Radix default when `children` are present. `"cancel"` for
1037
+ /** Which button takes focus on open. Default: Confirm for a bare dialog
1038
+ * (Cancel when `variant` is `danger`), the browser/Radix default when
1039
+ * `children` are present. `"cancel"` for
1039
1040
  * dialogs where the safe choice must be the one Enter triggers. */
1040
1041
  readonly initialFocus?: "confirm" | "cancel";
1041
1042
  /** Wird gefeuert wenn der User Confirm drückt. Async-Funktion ist
@@ -1270,7 +1271,7 @@ export type StatusBadgeProps = {
1270
1271
  readonly tone?: StatusTone;
1271
1272
  readonly testId?: string;
1272
1273
  };
1273
- export declare function statusToneForOptionTone(tone: SelectOptionTone): StatusTone;
1274
+ export declare function statusToneForOptionTone(tone: SelectOptionTone): StatusTone | undefined;
1274
1275
  export declare function statusToneForValue(value: string): StatusTone | undefined;
1275
1276
  /** Compact label/value tile (record-detail metrics band). `testId` is the
1276
1277
  * tile's own id — the impl derives `${testId}-label`/`${testId}-value` for
@@ -1523,4 +1524,6 @@ export type PrimitivesProviderProps = {
1523
1524
  readonly value: PrimitivesRegistry;
1524
1525
  };
1525
1526
  export declare function PrimitivesProvider({ children, value }: PrimitivesProviderProps): ReactNode;
1527
+ /** Like `usePrimitives`, but `undefined` outside a provider — for components that also render standalone. */
1528
+ export declare function useOptionalPrimitives(): PrimitivesRegistry | undefined;
1526
1529
  export declare function usePrimitives(): PrimitivesRegistry;
@@ -52,8 +52,11 @@ const STATUS_TONE_BY_OPTION_TONE = {
52
52
  bad: "bad",
53
53
  neutral: "muted",
54
54
  };
55
+ // Own-key check: an unknown tone from an untyped definition must yield undefined so callers fall back to the value heuristic.
55
56
  export function statusToneForOptionTone(tone) {
56
- return STATUS_TONE_BY_OPTION_TONE[tone];
57
+ return Object.hasOwn(STATUS_TONE_BY_OPTION_TONE, tone)
58
+ ? STATUS_TONE_BY_OPTION_TONE[tone]
59
+ : undefined;
57
60
  }
58
61
  export function statusToneForValue(value) {
59
62
  const slug = value
@@ -69,6 +72,10 @@ const PrimitivesContext = createContext(undefined);
69
72
  export function PrimitivesProvider({ children, value }) {
70
73
  return _jsx(PrimitivesContext.Provider, { value: value, children: children });
71
74
  }
75
+ /** Like `usePrimitives`, but `undefined` outside a provider — for components that also render standalone. */
76
+ export function useOptionalPrimitives() {
77
+ return useContext(PrimitivesContext);
78
+ }
72
79
  export function usePrimitives() {
73
80
  const registry = useContext(PrimitivesContext);
74
81
  if (registry === undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer",
3
- "version": "0.352.0",
3
+ "version": "0.353.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>",
@@ -15,9 +15,9 @@
15
15
  }
16
16
  },
17
17
  "dependencies": {
18
- "@cosmicdrift/kumiko-framework": "0.352.0",
19
- "@cosmicdrift/kumiko-headless": "0.352.0",
20
- "@cosmicdrift/kumiko-types": "0.352.0",
18
+ "@cosmicdrift/kumiko-framework": "0.353.0",
19
+ "@cosmicdrift/kumiko-headless": "0.353.0",
20
+ "@cosmicdrift/kumiko-types": "0.353.0",
21
21
  "react": "^19.2.6",
22
22
  "temporal-polyfill": "^0.3.2",
23
23
  "zod": "^4.4.3"
@@ -28,7 +28,7 @@
28
28
  "@types/react-dom": "^19.2.3",
29
29
  "jsdom": "^29.1.1",
30
30
  "react-dom": "^19.2.6",
31
- "@cosmicdrift/kumiko-locale-de": "0.352.0"
31
+ "@cosmicdrift/kumiko-locale-de": "0.353.0"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",
package/src/changes.json CHANGED
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.353.0",
4
+ "type": "fix",
5
+ "title": "secretMint with tenant currency waits for the tenant currency in mint and confirm step",
6
+ "detail": "Number inputs no longer keep a typed prefix, secretMint honours tenant currency, refEntity respects real field types\nA `NumberInput` draft that does not parse (\"12abc\", \"12,5\" on an integer field) now clears the value instead of leaving the last parseable prefix (\"12\") to be submitted. A `secretMint` screen with `currency: { kind: \"tenant\" }` money fields now waits for the tenant currency in both the mint and the confirm step, like `actionForm`, instead of seeding a bare `0`. `refEntity` on an `entityList` column or `entityEdit` field no longer overrides a real non-text entity field (a multi-reference stays multi, a sortable column stays sortable). The Tailwind scan skips compiled `__tests__` in `dist`."
7
+ },
2
8
  {
3
9
  "version": "0.352.0",
4
10
  "type": "improvement",