@cosmicdrift/kumiko-renderer 0.361.0 → 0.363.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.
@@ -24,5 +24,8 @@ export function ContentPreview({ content, variables, contentFormat, }) {
24
24
  const safeVariables = contentFormat === "rich"
25
25
  ? Object.fromEntries(Object.entries(variables).map(([name, value]) => [name, escapeHtmlAttr(value)]))
26
26
  : variables;
27
- return (_jsx(Editor, { id: id, value: substituteVariables(content, safeVariables), onChange: noop, variables: [], readOnly: true }));
27
+ // "document" keeps `{{x}}` as-is: substituting it into a button/link href would make the
28
+ // schema drop that node; variable chips stay visible.
29
+ const value = contentFormat === "document" ? content : substituteVariables(content, safeVariables);
30
+ return _jsx(Editor, { id: id, value: value, onChange: noop, variables: [], readOnly: true });
28
31
  }
@@ -9,19 +9,19 @@ export function primaryHeaderAction(actions, preferredId) {
9
9
  actions.find((a) => a.style === "primary") ??
10
10
  actions[0]);
11
11
  }
12
- // A bare icon for a destructive or confirm-gated action (e.g. "x" for
13
- // terminate) reads as "close", so only plain icon actions may stand alone.
12
+ // The undeclared fallback picks an action nobody chose for the header, so a
13
+ // destructive or confirm-gated one (a bare "x" reads as "close") may not stand alone.
14
14
  function canStandAsIconButton(action) {
15
15
  return action?.icon !== undefined && action.style !== "danger" && !needsActionConfirm(action);
16
16
  }
17
- // A declared, visible `primaryAction` is always the phone header's button
18
- // (labelled when a bare icon would mislead), so it matches the desktop header
19
- // and `edit` moves into the menu. Without one the pencil-style icon action
20
- // stays visible, falling back to the precedence-only primary.
17
+ // A declared, visible `primaryAction` is always the phone header's button, icon
18
+ // only when it has an icon (the author chose the icon, the label is its aria-label),
19
+ // so it matches the desktop header and `edit` moves into the menu. Without one the
20
+ // pencil-style icon action stays visible, falling back to the precedence-only primary.
21
21
  export function compactHeaderPrimary(actions, preferredId) {
22
22
  const declared = preferredId !== undefined ? actions.find((a) => a.id === preferredId) : undefined;
23
23
  if (declared !== undefined)
24
- return { action: declared, iconOnly: canStandAsIconButton(declared) };
24
+ return { action: declared, iconOnly: declared.icon !== undefined };
25
25
  const iconAction = primaryHeaderAction(actions);
26
26
  return canStandAsIconButton(iconAction) ? { action: iconAction, iconOnly: true } : undefined;
27
27
  }
@@ -1,5 +1,5 @@
1
1
  import type { ScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
- import type { ListRowViewModel, Translate } from "@cosmicdrift/kumiko-headless";
2
+ import type { FormValues, ListRowViewModel, Translate } from "@cosmicdrift/kumiko-headless";
3
3
  import { type ReactNode } from "react";
4
4
  import { type ListSort } from "../hooks/use-list-url-state.js";
5
5
  import type { FeatureSchema } from "./feature-schema.js";
@@ -19,7 +19,7 @@ export { qualifyScreenId };
19
19
  export declare function qualifyNavId(featureName: string, navId: string): string;
20
20
  export { screenAccessAllows };
21
21
  export declare function KumikoScreen({ schema, qn, translate, entityId, onRowClick, onCopyLink, }: KumikoScreenProps): ReactNode;
22
- export declare function buildInitialValues(fields: Readonly<Record<string, unknown>>, defaultCurrency?: string, moneyCurrencyOverrides?: Readonly<Record<string, string>>): Readonly<Record<string, unknown>>;
22
+ export declare function buildInitialValues(fields: Readonly<Record<string, unknown>>, defaultCurrency?: string, moneyCurrencyOverrides?: Readonly<Record<string, string>>): FormValues;
23
23
  export declare function tenantCurrencyMoneyFieldNames(fields: Readonly<Record<string, unknown>>): readonly string[];
24
24
  export declare function literalCurrencyOverrides(fields: Readonly<Record<string, unknown>>): Readonly<Record<string, string>>;
25
25
  export declare function useMoneyCurrencyOverrides(fields: Readonly<Record<string, unknown>>, tenantFieldNames: readonly string[], fallback: string | undefined): {
@@ -4,6 +4,7 @@ import { applyFormatSpec, buildOptionLabels, computeRelatedListSectionViewModel,
4
4
  import { resolveActionIcon } from "@cosmicdrift/kumiko-types/action-icon";
5
5
  import { TENANT_CURRENCY_CONFIG_KEY } from "@cosmicdrift/kumiko-types/fields";
6
6
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
7
+ import { isPlainObject, propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
7
8
  import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
8
9
  import { QueryOptionLabel, resolveOptionsQueryPayload, } from "../components/query-options-select.js";
9
10
  import { extractCreatedId, extractIdField } from "../components/reference-create-dialog.js";
@@ -246,19 +247,29 @@ function useNavigateToCreateFor(schema, entityName, createScreen, createUnavaila
246
247
  // — used for a `money` field that declares `currency: { kind: "tenant" }`, so
247
248
  // its empty initial value carries the tenant's own currency instead of the
248
249
  // entity-wide default.
250
+ function versionOf(record) {
251
+ const version = record["version"];
252
+ return typeof version === "number" ? version : 1;
253
+ }
249
254
  export function buildInitialValues(fields, defaultCurrency, moneyCurrencyOverrides) {
250
255
  const out = {};
251
256
  for (const [name, def] of Object.entries(fields)) {
252
- const shape = def;
253
- if (shape.default !== undefined) {
254
- out[name] = shape.default;
257
+ const type = propertyOf(def, "type");
258
+ const defaultValue = propertyOf(def, "default");
259
+ if (defaultValue !== undefined) {
260
+ out[name] = defaultValue;
261
+ continue;
262
+ }
263
+ // An untouched optional number must not persist as a deliberate 0.
264
+ if (type === "number" && propertyOf(def, "required") !== true) {
265
+ out[name] = undefined;
255
266
  continue;
256
267
  }
257
- if (shape.type === "embedded" && shape.multiple === true) {
268
+ if (type === "embedded" && propertyOf(def, "multiple") === true) {
258
269
  out[name] = [];
259
270
  continue;
260
271
  }
261
- if (shape.type === "money") {
272
+ if (type === "money") {
262
273
  const currency = moneyCurrencyOverrides?.[name] ?? defaultCurrency;
263
274
  if (currency !== undefined) {
264
275
  out[name] = { amount: 0, currency };
@@ -266,11 +277,11 @@ export function buildInitialValues(fields, defaultCurrency, moneyCurrencyOverrid
266
277
  }
267
278
  }
268
279
  out[name] =
269
- shape.type === "boolean"
280
+ type === "boolean"
270
281
  ? false
271
- : shape.type === "number" || shape.type === "money"
282
+ : type === "number" || type === "money"
272
283
  ? 0
273
- : shape.type === "multiSelect"
284
+ : type === "multiSelect"
274
285
  ? []
275
286
  : "";
276
287
  }
@@ -285,9 +296,10 @@ export function buildInitialValues(fields, defaultCurrency, moneyCurrencyOverrid
285
296
  export function tenantCurrencyMoneyFieldNames(fields) {
286
297
  const names = [];
287
298
  for (const [name, def] of Object.entries(fields)) {
288
- const shape = def;
289
- if (shape.type === "money" && shape.currency?.kind === "tenant")
299
+ if (propertyOf(def, "type") === "money" &&
300
+ propertyOf(propertyOf(def, "currency"), "kind") === "tenant") {
290
301
  names.push(name);
302
+ }
291
303
  }
292
304
  return names;
293
305
  }
@@ -322,9 +334,11 @@ const UPDATE_CHANGES_KEY = "changes";
322
334
  export function literalCurrencyOverrides(fields) {
323
335
  const out = {};
324
336
  for (const [name, def] of Object.entries(fields)) {
325
- const shape = def;
326
- const code = shape.currency?.code;
327
- if (shape.type === "money" && shape.currency?.kind === "literal" && code !== undefined) {
337
+ const currency = propertyOf(def, "currency");
338
+ const code = propertyOf(currency, "code");
339
+ if (propertyOf(def, "type") === "money" &&
340
+ propertyOf(currency, "kind") === "literal" &&
341
+ typeof code === "string") {
328
342
  out[name] = code;
329
343
  }
330
344
  }
@@ -370,7 +384,8 @@ const CURRENCY_CODE_PATTERN = /^[A-Za-z]{3}$/;
370
384
  function isMoneyValue(value) {
371
385
  if (typeof value !== "object" || value === null)
372
386
  return false;
373
- const { amount, currency } = value;
387
+ const amount = propertyOf(value, "amount");
388
+ const currency = propertyOf(value, "currency");
374
389
  return (typeof amount === "number" &&
375
390
  Number.isFinite(amount) &&
376
391
  typeof currency === "string" &&
@@ -521,7 +536,7 @@ function coerceEmbeddedListRows(raw, fieldName, schema, maxItems) {
521
536
  }
522
537
  const cellEntries = [];
523
538
  for (const [cellName, cell] of Object.entries(schema)) {
524
- const cellValue = candidate[cellName];
539
+ const cellValue = propertyOf(candidate, cellName);
525
540
  if (cellValue === undefined || cellValue === null)
526
541
  continue;
527
542
  const coerced = coerceEmbeddedCell(cellValue, cell);
@@ -757,7 +772,7 @@ function EntityEditUpdateBody({ schema, screen, entity, entityId, translate, onC
757
772
  // der ursprünglichen version sitzen — der buildPayload würde
758
773
  // weiter die stale version stampen und der Konflikt-Recovery wäre
759
774
  // kaputt.
760
- const recordKey = record.version ?? 1;
775
+ const recordKey = versionOf(record);
761
776
  return (_jsx(EntityEditUpdateForm, { schema: schema, screen: screen, entity: entity, entityId: entityId, record: record, onReload: detailQuery.refetch, ...(translate !== undefined && { translate }), ...(onCopyLink !== undefined && { onCopyLink }), ...(autoReloadOnSave === true && { onSaved: () => void detailQuery.refetch() }), ...(onDeleted !== undefined && { onDeleted }) }, `${entityId}:${recordKey}`));
762
777
  }
763
778
  function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onReload, translate, onCopyLink, onSaved, onDeleted, }) {
@@ -768,7 +783,7 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
768
783
  // captured once and stamped into every update payload — if a
769
784
  // concurrent writer bumps it, the server returns a version-conflict
770
785
  // error and the user reloads.
771
- const recordVersion = record.version ?? 1;
786
+ const recordVersion = versionOf(record);
772
787
  const entityDefaultCurrency = entity.defaultCurrency ?? "EUR";
773
788
  // A money field declaring `currency: { kind: "tenant" }` (fw#2933) resolves
774
789
  // its empty-value currency from the tenant-settings config key instead of
@@ -795,9 +810,7 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
795
810
  // damit sie beim Edit den Bestand zeigen statt write-only zu sein.
796
811
  const extensionInitialValues = useMemo(() => {
797
812
  const cf = record["customFields"];
798
- return cf !== null && typeof cf === "object" && !Array.isArray(cf)
799
- ? cf
800
- : undefined;
813
+ return isPlainObject(cf) ? cf : undefined;
801
814
  }, [record]);
802
815
  const writeCommand = entityWriteCommand(schema.featureName, screen.entity, "update");
803
816
  const deleteCommand = entityWriteCommand(schema.featureName, screen.entity, "delete");
@@ -1161,7 +1174,8 @@ function EntityListScreen({ schema, screen, translate, onRowClick, }) {
1161
1174
  function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
1162
1175
  const isEmbeddedScreen = useIsEmbeddedScreen();
1163
1176
  const featureName = schema.featureName;
1164
- const onCreate = useNavigateToCreateFor(schema, screen.entity, screen.createScreen, screen.createUnavailable);
1177
+ const resolvedCreate = useNavigateToCreateFor(schema, screen.entity, screen.createScreen, screen.createUnavailable);
1178
+ const onCreate = screen.allowCreate === false ? undefined : resolvedCreate;
1165
1179
  const { Banner } = usePrimitives();
1166
1180
  const queryType = entityQueryCommand(featureName, screen.entity, "list");
1167
1181
  const nav = useNav();
@@ -1211,8 +1225,8 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
1211
1225
  const filterPayload = useMemo(() => buildFilterPayload(urlState.filters, (field) => {
1212
1226
  // entity.fields ist am Renderer-Layer schwach getypt (vom Schema
1213
1227
  // deserialisiert) — Boundary-Cast wie buildInitialValues.
1214
- const def = entity.fields[field];
1215
- return def?.type;
1228
+ const type = propertyOf(entity.fields[field], "type");
1229
+ return typeof type === "string" ? type : undefined;
1216
1230
  }), [urlState.filters, entity.fields]);
1217
1231
  // Entity-only additions (screen.filter, faceted filters) layer on top of
1218
1232
  // the shared buildListQueryPayload — projectionList has neither.
@@ -2373,7 +2387,7 @@ function navigateAfterRedirect(options) {
2373
2387
  function overlayUpdatedProjection(record, resultData) {
2374
2388
  if (typeof resultData !== "object" || resultData === null)
2375
2389
  return record;
2376
- const projection = resultData.data;
2390
+ const projection = propertyOf(resultData, "data");
2377
2391
  if (typeof projection !== "object" || projection === null)
2378
2392
  return record;
2379
2393
  return { ...record, ...projection };
@@ -2559,8 +2573,9 @@ function mapFailedConfigCommandIssuesToField(error, failedIndex, commandFieldNam
2559
2573
  },
2560
2574
  };
2561
2575
  }
2562
- // A number/money field without a declared default starts empty: buildInitialValues
2563
- // maps it to 0, which a save would persist as a deliberate "0" override.
2576
+ // A money field without a declared default starts empty too (buildInitialValues
2577
+ // maps it to 0, which a save would persist as a deliberate "0" override); numbers
2578
+ // are handled there, except a required one, which config fields never are.
2564
2579
  function buildConfigEditDefaults(fields) {
2565
2580
  const defaults = buildInitialValues(fields); // @cast-boundary render-helper
2566
2581
  for (const [shortName, fieldDef] of Object.entries(fields)) {
@@ -2621,7 +2636,7 @@ function ConfigEditBody({ schema, screen, translate, }) {
2621
2636
  out[shortName] = defaults[shortName];
2622
2637
  continue;
2623
2638
  }
2624
- const ftype = fieldDef.type;
2639
+ const ftype = propertyOf(fieldDef, "type");
2625
2640
  // Field-Type-Coercion: config-Werte sind string|number|boolean,
2626
2641
  // aber der Form-State erwartet das passende Field-Native-Type.
2627
2642
  if (ftype === "number" || ftype === "money") {
@@ -1,4 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
2
3
  import { useCallback, useMemo, useRef, useState } from "react";
3
4
  import { RenderEdit } from "../components/render-edit.js";
4
5
  import { useTranslation } from "../i18n.js";
@@ -14,7 +15,7 @@ function extractRevealValue(data, field) {
14
15
  return undefined;
15
16
  if (!Object.hasOwn(data, field))
16
17
  return undefined;
17
- return data[field];
18
+ return propertyOf(data, field);
18
19
  }
19
20
  function extractCarriedValues(data, carry) {
20
21
  const carried = {};
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
2
3
  import { useMemo } from "react";
3
4
  import { buildInitialValues, tenantCurrencyMoneyFieldNames, useMoneyCurrencyOverrides, } from "../app/kumiko-screen.js";
4
5
  import { toKebab } from "../app/qn.js";
@@ -24,7 +25,7 @@ export function extractIdField(data, field) {
24
25
  return undefined;
25
26
  if (!Object.hasOwn(data, field))
26
27
  return undefined;
27
- const value = data[field];
28
+ const value = propertyOf(data, field);
28
29
  return typeof value === "string" ? value : undefined;
29
30
  }
30
31
  export function extractCreatedId(data) {
@@ -1,6 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { evalFieldCondition, isFieldsEditSection, normalizeEditField, PROJECTION_DETAIL_ENTITY, sectionFieldSpecs, } from "@cosmicdrift/kumiko-framework/ui-types";
3
3
  import { computeEditViewModel, groupIssuesByPath, relevantFieldIssues, zodErrorToFieldIssues, } from "@cosmicdrift/kumiko-headless";
4
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
4
5
  import { RenderEditActionButton } from "./render-edit-action-button.js";
5
6
  import { AllFieldsRequiredProvider } from "./render-field.js";
6
7
  import { useActionMenuItems } from "./use-action-menu-items.js";
@@ -49,9 +50,13 @@ function newDraftPrefix(screenId) {
49
50
  // `crypto.randomUUID` is missing in non-secure contexts and React Native/Hermes
50
51
  // without a polyfill — guard like dispatcher-live.ts's generateRequestId.
51
52
  function mintDraftId() {
52
- const c = globalThis.crypto;
53
- if (typeof c?.randomUUID === "function")
54
- return c.randomUUID();
53
+ const webCrypto = propertyOf(globalThis, "crypto");
54
+ const randomUUID = propertyOf(webCrypto, "randomUUID");
55
+ if (typeof randomUUID === "function") {
56
+ const id = Reflect.apply(randomUUID, webCrypto, []);
57
+ if (typeof id === "string")
58
+ return id;
59
+ }
55
60
  return `draft-${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
56
61
  }
57
62
  // End-to-end renderer für einen entityEdit screen. Rendert aus-
@@ -551,7 +556,7 @@ export function RenderEdit(props) {
551
556
  // was in flight — their input wins over the stored draft.
552
557
  if (controller.getSnapshot().isDirty || hasNavigatedRef.current)
553
558
  return;
554
- // @cast-boundary form-draft blob: `values` round-trips through an opaque
559
+ // @cast-boundary form-values draft blob: `values` round-trips through an opaque
555
560
  // jsonb column and comes back untyped.
556
561
  controller.setValues(draft.values);
557
562
  setRawStep(Math.max(draft.stepIndex, 0));
@@ -1,6 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { isFormatSpec, SYSTEM_REFERENCE_LABELS, } from "@cosmicdrift/kumiko-framework/ui-types";
3
3
  import { applyFormatSpec, currencyDecimals, } from "@cosmicdrift/kumiko-headless";
4
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
4
5
  import { createContext, useCallback, useContext, useMemo, useState } from "react";
5
6
  import { useAppFeatures } from "../app/app-features-context.js";
6
7
  import { useColumnRenderer } from "../app/column-renderers.js";
@@ -159,7 +160,7 @@ function ReferenceInput({ field, id, hasError, onChange, Input, featureName, })
159
160
  }
160
161
  setCreatedWithoutIdWarning(false);
161
162
  if (isMultiple) {
162
- const current = Array.isArray(field.value) ? field.value : [];
163
+ const current = stringArrayOf(field.value);
163
164
  onChange([...current, newId]);
164
165
  }
165
166
  else {
@@ -194,9 +195,7 @@ function ReferenceInput({ field, id, hasError, onChange, Input, featureName, })
194
195
  };
195
196
  const createDialog = (_jsxs(_Fragment, { children: [canCreate && refCreateScreen && refEntityDef && (_jsx(ReferenceCreateDialog, { open: createOpen, onClose: () => setCreateOpen(false), onCreated: handleCreated, featureName: refFeature, screen: refCreateScreen, entity: refEntityDef })), createdWithoutIdWarning && (_jsx(Banner, { variant: "error", children: t("kumiko.field.reference-created-no-id") }))] }));
196
197
  if (isMultiple) {
197
- const arrayValue = Array.isArray(field.value)
198
- ? field.value
199
- : [];
198
+ const arrayValue = stringArrayOf(field.value);
200
199
  return (_jsxs(_Fragment, { children: [_jsx(Input, { kind: "combobox", ...baseInputProps, multiple: true, value: arrayValue, onChange: (v) => onChange(v) }), createDialog] }));
201
200
  }
202
201
  const stringValue = field.value === undefined || field.value === null ? "" : String(field.value);
@@ -228,7 +227,7 @@ function FieldRendererOutput({ field, renderer, row, appLocale, }) {
228
227
  // when set; coalesce undefined (spread override) back to appLocale (#2332).
229
228
  return (_jsx(Text, { testId: `field-value-${field.field}`, children: applyFormatSpec({
230
229
  ...renderer,
231
- locale: renderer.locale ?? appLocale,
230
+ locale: stringPropertyOf(renderer, "locale") ?? appLocale,
232
231
  }, field.value, t) }));
233
232
  }
234
233
  if (componentName !== undefined) {
@@ -254,9 +253,7 @@ function ReadOnlyReferenceValue({ field, featureName, }) {
254
253
  limit: REFERENCE_COMBOBOX_LIMIT,
255
254
  });
256
255
  const ids = isMultiple
257
- ? Array.isArray(field.value)
258
- ? field.value
259
- : []
256
+ ? stringArrayOf(field.value)
260
257
  : typeof field.value === "string" && field.value !== ""
261
258
  ? [field.value]
262
259
  : [];
@@ -354,7 +351,7 @@ function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner,
354
351
  case "tz":
355
352
  return (_jsx(Input, { kind: "tz", ...common, value: stringValue(field.value), onChange: (v) => onChange(v) }));
356
353
  case "multiSelect": {
357
- const arrayValue = Array.isArray(field.value) ? field.value : [];
354
+ const arrayValue = stringArrayOf(field.value);
358
355
  const renderMultiSelect = (options) => field.display === "checkboxes" ? (_jsx(MultiSelectCheckboxes, { field: field, id: id, options: options, value: arrayValue, onChange: (v) => onChange(v) })) : (_jsx(Input, { kind: "combobox", ...common, multiple: true, value: arrayValue, onChange: (v) => onChange(v), options: options }));
359
356
  if (field.selectOptionsAvailabilityQuery !== undefined && field.readOnly !== true) {
360
357
  return (_jsx(AvailabilityOptionsMultiSelect, { field: field, query: field.selectOptionsAvailabilityQuery.query, currentValues: arrayValue, children: renderMultiSelect }));
@@ -474,13 +471,20 @@ function resolveNumberUnit(unit, row) {
474
471
  const trimmed = sibling.trim();
475
472
  return trimmed.length > 0 && trimmed.length <= MAX_SIBLING_UNIT_LENGTH ? trimmed : undefined;
476
473
  }
474
+ function stringArrayOf(value) {
475
+ return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : [];
476
+ }
477
+ function stringPropertyOf(value, key) {
478
+ const property = propertyOf(value, key);
479
+ return typeof property === "string" ? property : undefined;
480
+ }
477
481
  function moneyMinorValue(v, currency) {
478
482
  if (v === undefined || v === null || v === "")
479
483
  return "";
480
484
  if (typeof v === "number")
481
485
  return Math.round(v * 10 ** currencyDecimals(currency));
482
486
  if (typeof v === "object") {
483
- const amount = v.amount;
487
+ const amount = propertyOf(v, "amount");
484
488
  if (typeof amount === "number")
485
489
  return Math.round(amount * 10 ** currencyDecimals(currency));
486
490
  }
@@ -498,11 +502,11 @@ function moneyPayload(minorUnits, currency) {
498
502
  // (noch nicht gesetzt) → "" als Empty-Sentinel, analog money/timestamp.
499
503
  function locatedValue(v) {
500
504
  if (v !== null && typeof v === "object" && "at" in v && "tz" in v) {
501
- const o = v;
505
+ const utc = propertyOf(v, "utc");
502
506
  return {
503
- at: typeof o.at === "string" ? o.at : "",
504
- tz: typeof o.tz === "string" ? o.tz : "",
505
- ...(typeof o.utc === "string" && { utc: o.utc }),
507
+ at: typeof v.at === "string" ? v.at : "",
508
+ tz: typeof v.tz === "string" ? v.tz : "",
509
+ ...(typeof utc === "string" && { utc }),
506
510
  };
507
511
  }
508
512
  return "";
@@ -14,11 +14,10 @@ function buildWriteFormSchema(fields) {
14
14
  .object({})
15
15
  .passthrough()
16
16
  .superRefine((values, ctx) => {
17
- const record = values;
18
17
  for (const field of fields) {
19
18
  if (field.readOnly || !field.visible || !field.required)
20
19
  continue;
21
- if (isPresent(record[field.field]))
20
+ if (isPresent(values[field.field]))
22
21
  continue;
23
22
  ctx.addIssue({
24
23
  code: "custom",
@@ -261,6 +261,7 @@ export const kumikoDefaultTranslations = {
261
261
  "errors.internal": "Something went wrong. Please try again later.",
262
262
  "errors.rate_limited": "Too many requests. Please try again shortly.",
263
263
  "errors.rate_limit_unavailable": "Rate limiting is temporarily unavailable. Please try again shortly.",
264
+ "errors.puppeteer_unavailable": "The PDF service is not reachable. PDFs cannot be created right now.",
264
265
  "errors.cap.exceeded": "Limit reached. Upgrade your plan or wait for the next period.",
265
266
  "errors.download.urlMissing": "Download unavailable — please try again.",
266
267
  "auth.errors.originNotAllowed": "Requests from this origin are not allowed.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer",
3
- "version": "0.361.0",
3
+ "version": "0.363.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.361.0",
19
- "@cosmicdrift/kumiko-headless": "0.361.0",
20
- "@cosmicdrift/kumiko-types": "0.361.0",
18
+ "@cosmicdrift/kumiko-framework": "0.363.0",
19
+ "@cosmicdrift/kumiko-headless": "0.363.0",
20
+ "@cosmicdrift/kumiko-types": "0.363.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.361.0"
31
+ "@cosmicdrift/kumiko-locale-de": "0.363.0"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",
package/src/changes.json CHANGED
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "version": "0.363.0",
4
+ "type": "improvement",
5
+ "title": "propertyOf type guard and fewer unchecked casts in the UI packages",
6
+ "detail": "\"@cosmicdrift/kumiko-types/type-guards\" exports propertyOf, which reads one property off an unknown value. Headless, renderer, renderer-web and dispatcher-live replace unchecked as-casts and globalThis probes with it and with narrowing. Behavior stays the same for well-formed input; a format spec, upload response or stream event with a wrongly typed property is now ignored instead of passed through."
7
+ },
8
+ {
9
+ "version": "0.362.0",
10
+ "type": "improvement",
11
+ "title": "entityList honours allowCreate false",
12
+ "detail": "EntityListBody passes no onCreate when the list sets allowCreate false."
13
+ },
14
+ {
15
+ "version": "0.362.0",
16
+ "type": "fix",
17
+ "title": "A declared primaryAction with an icon is icon-only in the phone header",
18
+ "detail": "Regardless of confirm or style; the label is the button's aria-label and title. Without an icon it stays a text button. The ui-walkthrough terminate sample uses the circle-stop icon, since a bare x reads as close."
19
+ },
2
20
  {
3
21
  "version": "0.361.0",
4
22
  "type": "improvement",