@cosmicdrift/kumiko-renderer 0.351.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
  }
@@ -0,0 +1,22 @@
1
+ import type { EditFieldViewModel } from "@cosmicdrift/kumiko-headless";
2
+ import type { OptionsQueryPayload } from "@cosmicdrift/kumiko-types/fields";
3
+ import type { ReactNode } from "react";
4
+ export type SelectOptionEntry = {
5
+ readonly value: string;
6
+ readonly label: string;
7
+ readonly description?: string;
8
+ readonly disabled?: boolean;
9
+ };
10
+ export declare function staticSelectOptions(field: EditFieldViewModel): readonly SelectOptionEntry[];
11
+ export declare function mergeOptionAvailability(options: readonly SelectOptionEntry[], rows: readonly unknown[], current: string): readonly SelectOptionEntry[];
12
+ type AvailabilityOptionsSelectProps = {
13
+ readonly field: EditFieldViewModel;
14
+ readonly query: string;
15
+ readonly payload: OptionsQueryPayload;
16
+ readonly row: Readonly<Record<string, unknown>>;
17
+ readonly id: string;
18
+ readonly hasError: boolean;
19
+ readonly onChange: (value: unknown) => void;
20
+ };
21
+ export declare function AvailabilityOptionsSelect({ field, query, payload, row, id, hasError, onChange, }: AvailabilityOptionsSelectProps): ReactNode;
22
+ export {};
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useQuery } from "../hooks/use-query.js";
3
+ import { usePrimitives } from "../primitives.js";
4
+ import { resolveOptionsQueryPayload } from "./query-options-select.js";
5
+ // Rows come from a query handler, so they are checked once here instead of trusted.
6
+ function isAvailabilityRow(row) {
7
+ return (typeof row === "object" &&
8
+ row !== null &&
9
+ "value" in row &&
10
+ typeof row.value === "string" &&
11
+ (!("disabled" in row) || row.disabled === undefined || typeof row.disabled === "boolean") &&
12
+ (!("hint" in row) || row.hint === undefined || typeof row.hint === "string"));
13
+ }
14
+ // Static options with the translated labels of the view-model.
15
+ export function staticSelectOptions(field) {
16
+ const labels = field.optionLabels;
17
+ return (field.options ?? []).map((value) => ({ value, label: labels?.[value] ?? value }));
18
+ }
19
+ // Static options stay authoritative: rows for unknown values are ignored, and the
20
+ // stored value is never disabled so a downgraded tenant keeps seeing its choice.
21
+ export function mergeOptionAvailability(options, rows, current) {
22
+ const byValue = new Map();
23
+ for (const row of rows) {
24
+ if (isAvailabilityRow(row))
25
+ byValue.set(row.value, row);
26
+ }
27
+ return options.map((option) => {
28
+ const row = byValue.get(option.value);
29
+ if (row === undefined)
30
+ return option;
31
+ const disabled = row.disabled === true && option.value !== current;
32
+ return {
33
+ ...option,
34
+ ...(row.hint !== undefined && { description: row.hint }),
35
+ ...(disabled && { disabled: true }),
36
+ };
37
+ });
38
+ }
39
+ // Select with static options whose availability (SelectFieldDef.optionsAvailabilityQuery)
40
+ // comes from a query, so it has to be a mounted component for useQuery. Until the
41
+ // result arrives, and when the query fails, every option stays choosable; the write
42
+ // path enforces the rule either way.
43
+ export function AvailabilityOptionsSelect({ field, query, payload, row, id, hasError, onChange, }) {
44
+ const { Input } = usePrimitives();
45
+ const { data } = useQuery(query, resolveOptionsQueryPayload(payload, row));
46
+ const current = typeof field.value === "string" ? field.value : "";
47
+ const display = field.display === "radio" || field.display === "dropdown" ? field.display : undefined;
48
+ return (_jsx(Input, { kind: "select", id: id, name: field.field, disabled: field.readOnly, required: field.required, hasError: hasError, value: current, onChange: (v) => onChange(v), options: mergeOptionAvailability(staticSelectOptions(field), data?.rows ?? [], current), ...(display !== undefined && { display }) }));
49
+ }
@@ -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
  }
@@ -13,6 +13,7 @@ import { useQuery } from "../hooks/use-query.js";
13
13
  import { referenceOptionSource } from "../hooks/use-reference-lookup.js";
14
14
  import { useLocale, useTranslation } from "../i18n.js";
15
15
  import { usePrimitives } from "../primitives.js";
16
+ import { AvailabilityOptionsSelect, staticSelectOptions } from "./availability-options-select.js";
16
17
  import { EmbeddedListField } from "./embedded-list-field.js";
17
18
  import { MultiSelectCheckboxes } from "./multi-select-checkboxes.js";
18
19
  import { QueryOptionsSelect } from "./query-options-select.js";
@@ -42,7 +43,9 @@ export function RenderField({ field, issues, onChange, featureName, labelAppendi
42
43
  // Reference-Field rendert eine eigene Component — sie nutzt
43
44
  // useQuery() für den Live-Lookup, also muss sie als React-
44
45
  // Komponente gemountet werden (nicht als pure render-Call).
45
- const control = field.readOnly && field.renderer !== undefined ? (_jsx(FieldRendererOutput, { field: field, renderer: field.renderer, appLocale: appLocale, ...(row !== undefined && { row }) })) : field.type === "embedded" && field.embeddedListCells !== undefined ? (_jsx(EmbeddedListField, { field: field, id: id, onChange: onChange, allIssues: allIssues ?? {}, featureName: featureName ?? "" })) : field.type === "reference" ? (readOnlyText ? (_jsx(ReadOnlyReferenceValue, { field: field, featureName: featureName ?? "" })) : (_jsx(ReferenceInput, { field: field, id: id, hasError: hasError, onChange: onChange, Input: Input, featureName: featureName ?? "" }))) : field.type === "select" && field.selectOptionsQuery !== undefined && !readOnlyText ? (_jsx(QueryOptionsSelect, { field: field, query: field.selectOptionsQuery.query, payload: field.selectOptionsQuery.payload, row: row ?? { [field.field]: field.value }, id: id, hasError: hasError, onChange: onChange })) : readOnlyText && !isComplexFieldType(field.type) ? (_jsx(Text, { testId: `field-value-${field.field}`, children: readOnlyDisplayText(field, appLocale, t) })) : field.type === "text" && field.writeOnly === true && !readOnlyText ? (_jsx(WriteOnlyTextInput, { field: field, id: id, hasError: hasError, onChange: onChange, changed: changed === true })) : (renderInput({
46
+ const control = field.readOnly && field.renderer !== undefined ? (_jsx(FieldRendererOutput, { field: field, renderer: field.renderer, appLocale: appLocale, ...(row !== undefined && { row }) })) : field.type === "embedded" && field.embeddedListCells !== undefined ? (_jsx(EmbeddedListField, { field: field, id: id, onChange: onChange, allIssues: allIssues ?? {}, featureName: featureName ?? "" })) : field.type === "reference" ? (readOnlyText ? (_jsx(ReadOnlyReferenceValue, { field: field, featureName: featureName ?? "" })) : (_jsx(ReferenceInput, { field: field, id: id, hasError: hasError, onChange: onChange, Input: Input, featureName: featureName ?? "" }))) : field.type === "select" && field.selectOptionsQuery !== undefined && !readOnlyText ? (_jsx(QueryOptionsSelect, { field: field, query: field.selectOptionsQuery.query, payload: field.selectOptionsQuery.payload, row: row ?? { [field.field]: field.value }, id: id, hasError: hasError, onChange: onChange })) : field.type === "select" &&
47
+ field.selectOptionsAvailabilityQuery !== undefined &&
48
+ !readOnlyText ? (_jsx(AvailabilityOptionsSelect, { field: field, query: field.selectOptionsAvailabilityQuery.query, payload: field.selectOptionsAvailabilityQuery.payload, row: row ?? { [field.field]: field.value }, id: id, hasError: hasError, onChange: onChange })) : readOnlyText && !isComplexFieldType(field.type) ? (_jsx(Text, { testId: `field-value-${field.field}`, children: readOnlyDisplayText(field, appLocale, t) })) : field.type === "text" && field.writeOnly === true && !readOnlyText ? (_jsx(WriteOnlyTextInput, { field: field, id: id, hasError: hasError, onChange: onChange, changed: changed === true })) : (renderInput({
46
49
  field,
47
50
  id,
48
51
  hasError,
@@ -374,11 +377,7 @@ function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner,
374
377
  // `<feature>:entity:<entity>:field:<field>:option:<value>`).
375
378
  // Wenn keine Translations registriert sind, fallback auf raw
376
379
  // value als Label — der ComboboxInput zeigt dann unverändert.
377
- const rawOptions = field.options ?? [];
378
- const labels = field.optionLabels;
379
- const selectOptions = labels !== undefined
380
- ? rawOptions.map((value) => ({ value, label: labels[value] ?? value }))
381
- : rawOptions;
380
+ const selectOptions = field.optionLabels !== undefined ? staticSelectOptions(field) : (field.options ?? []);
382
381
  // `display` is shared with multiSelect's "checkboxes", which is not a
383
382
  // select presentation — narrow to the two a select field can request.
384
383
  const display = field.display === "radio" || field.display === "dropdown" ? field.display : undefined;
@@ -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";
@@ -347,6 +347,9 @@ export type InputProps = {
347
347
  * without a group stay ungrouped and come before all groups. Options
348
348
  * with description or group never render as a segmented control. */
349
349
  readonly group?: string;
350
+ /** Not choosable and visibly muted. A disabled option that is the
351
+ * current `value` stays displayed as the selection. */
352
+ readonly disabled?: boolean;
350
353
  }[];
351
354
  readonly disabled?: boolean;
352
355
  readonly required?: boolean;
@@ -1031,8 +1034,9 @@ export type DialogProps = {
1031
1034
  readonly cancelLabel?: string;
1032
1035
  /** `default` = Confirm primary, `danger` = Confirm danger. */
1033
1036
  readonly variant?: "default" | "danger";
1034
- /** Which button takes focus on open. Default: Confirm for a bare dialog,
1035
- * 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
1036
1040
  * dialogs where the safe choice must be the one Enter triggers. */
1037
1041
  readonly initialFocus?: "confirm" | "cancel";
1038
1042
  /** Wird gefeuert wenn der User Confirm drückt. Async-Funktion ist
@@ -1267,7 +1271,7 @@ export type StatusBadgeProps = {
1267
1271
  readonly tone?: StatusTone;
1268
1272
  readonly testId?: string;
1269
1273
  };
1270
- export declare function statusToneForOptionTone(tone: SelectOptionTone): StatusTone;
1274
+ export declare function statusToneForOptionTone(tone: SelectOptionTone): StatusTone | undefined;
1271
1275
  export declare function statusToneForValue(value: string): StatusTone | undefined;
1272
1276
  /** Compact label/value tile (record-detail metrics band). `testId` is the
1273
1277
  * tile's own id — the impl derives `${testId}-label`/`${testId}-value` for
@@ -1520,4 +1524,6 @@ export type PrimitivesProviderProps = {
1520
1524
  readonly value: PrimitivesRegistry;
1521
1525
  };
1522
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;
1523
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.351.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.351.0",
19
- "@cosmicdrift/kumiko-headless": "0.351.0",
20
- "@cosmicdrift/kumiko-types": "0.351.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.351.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,15 @@
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
+ },
8
+ {
9
+ "version": "0.352.0",
10
+ "type": "improvement",
11
+ "title": "Select inputs accept disabled options and load their availability from optionsAvailabilityQuery"
12
+ },
2
13
  {
3
14
  "version": "0.348.0",
4
15
  "type": "improvement",