@cosmicdrift/kumiko-renderer 0.358.0 → 0.360.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.
@@ -4,6 +4,9 @@ import type { ActionFormScreenDefinition, EntityDefinition, EntityEditScreenDefi
4
4
  * iterieren über entity.fields zur Render-Zeit; alle weiteren
5
5
  * EntityDefinition-Felder bleiben undefined. */
6
6
  export declare function synthesizeActionFormEntity(fields: Readonly<Record<string, FieldDefinition>>): EntityDefinition;
7
+ /** Fields the form may seed and submit. An excluded field the layout does not show
8
+ * would otherwise reach the payload through its `default`, and the handler rejects it. */
9
+ export declare function actionFormSubmittableFields(screen: ActionFormScreenDefinition): Readonly<Record<string, FieldDefinition>>;
7
10
  /** Wandelt ein ActionFormScreenDefinition oder SecretMintScreenDefinition in
8
11
  * die EntityEditScreen-Shape die RenderEdit erwartet. type wird auf
9
12
  * "entityEdit" gesetzt damit der Type-Constraint hält; entity wird auf den
@@ -14,6 +14,7 @@
14
14
  // RenderActionForm-Komponente, die `fields` direkt als Input nimmt.
15
15
  // Der Boot-Validator kennt actionForm bereits eigenständig (kein
16
16
  // entity-Lookup), also ist der Server-Pfad davon nicht betroffen.
17
+ import { isFieldsEditSection, normalizeEditField, sectionFieldSpecs, } from "@cosmicdrift/kumiko-framework/ui-types";
17
18
  const ACTION_FORM_PSEUDO_ENTITY = "__action-form__";
18
19
  /** Baut eine minimale EntityDefinition aus den Inline-Fields eines
19
20
  * ActionForm- oder SecretMint-Screens. RenderEdit + computeEditViewModel
@@ -22,6 +23,51 @@ const ACTION_FORM_PSEUDO_ENTITY = "__action-form__";
22
23
  export function synthesizeActionFormEntity(fields) {
23
24
  return { fields };
24
25
  }
26
+ // An actionForm has no create/update mode, so the entityEdit `writeExcludedFields`
27
+ // (which hides the field in create mode) does not fit. The values arrive via prefill
28
+ // (row drawer, handoff, URL), so show them read-only: `submit: false` because the
29
+ // handler does not accept the field, `required: false` so an empty one cannot block submit.
30
+ function lockExcludedFieldSpec(spec, excluded) {
31
+ const normalized = normalizeEditField(spec);
32
+ if (!excluded.has(normalized.field))
33
+ return spec;
34
+ return { ...normalized, readOnly: true, required: false, submit: false };
35
+ }
36
+ function lockExcludedFieldsInSection(section, excluded) {
37
+ if (!isFieldsEditSection(section))
38
+ return section;
39
+ return {
40
+ ...section,
41
+ // kumiko-lint-ignore section-fields-raw rewrites both sources in place — the groups branch below does the same for groups[].fields
42
+ fields: section.fields.map((spec) => lockExcludedFieldSpec(spec, excluded)),
43
+ ...(section.groups !== undefined && {
44
+ groups: section.groups.map((group) => ({
45
+ ...group,
46
+ fields: group.fields.map((spec) => lockExcludedFieldSpec(spec, excluded)),
47
+ })),
48
+ }),
49
+ };
50
+ }
51
+ function layoutWithLockedExcludedFields(screen) {
52
+ if (screen.type !== "actionForm" || !screen.writeExcludedFields?.length)
53
+ return screen.layout;
54
+ const excluded = new Set(screen.writeExcludedFields);
55
+ return {
56
+ ...screen.layout,
57
+ sections: screen.layout.sections.map((section) => lockExcludedFieldsInSection(section, excluded)),
58
+ };
59
+ }
60
+ /** Fields the form may seed and submit. An excluded field the layout does not show
61
+ * would otherwise reach the payload through its `default`, and the handler rejects it. */
62
+ export function actionFormSubmittableFields(screen) {
63
+ const excluded = screen.writeExcludedFields;
64
+ if (!excluded?.length)
65
+ return screen.fields;
66
+ const inLayout = new Set(screen.layout.sections.flatMap((section) => isFieldsEditSection(section)
67
+ ? sectionFieldSpecs(section).map((spec) => normalizeEditField(spec).field)
68
+ : []));
69
+ return Object.fromEntries(Object.entries(screen.fields).filter(([name]) => inLayout.has(name) || !excluded.includes(name)));
70
+ }
25
71
  /** Wandelt ein ActionFormScreenDefinition oder SecretMintScreenDefinition in
26
72
  * die EntityEditScreen-Shape die RenderEdit erwartet. type wird auf
27
73
  * "entityEdit" gesetzt damit der Type-Constraint hält; entity wird auf den
@@ -31,7 +77,7 @@ export function synthesizeActionFormScreen(screen) {
31
77
  id: screen.id,
32
78
  type: "entityEdit",
33
79
  entity: ACTION_FORM_PSEUDO_ENTITY,
34
- layout: screen.layout,
80
+ layout: layoutWithLockedExcludedFields(screen),
35
81
  ...(screen.description !== undefined && { description: screen.description }),
36
82
  ...(screen.access !== undefined && { access: screen.access }),
37
83
  // Only ActionFormScreenDefinition carries slots/fieldLabels; `in` narrows
@@ -23,7 +23,7 @@ import { InsideDrawerProvider, useInsideDrawer } from "../inside-drawer.js";
23
23
  import { PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "../page-header-slot.js";
24
24
  import { statusToneForOptionTone, statusToneForValue, usePrimitives, } from "../primitives.js";
25
25
  import { screenFillsHeight } from "../screen-fills-height.js";
26
- import { synthesizeActionFormEntity, synthesizeActionFormScreen } from "./action-form-shim.js";
26
+ import { actionFormSubmittableFields, synthesizeActionFormEntity, synthesizeActionFormScreen, } from "./action-form-shim.js";
27
27
  import { useAppFeatures } from "./app-features-context.js";
28
28
  import { buildConfigEditSchema, synthesizeConfigEditEntity, synthesizeConfigEditScreen, } from "./config-edit-shim.js";
29
29
  import { useCustomScreenComponent } from "./custom-screens.js";
@@ -2356,7 +2356,8 @@ function ActionFormBody({ schema, screen, translate, initialOverrides, onSuccess
2356
2356
  const { Banner } = usePrimitives();
2357
2357
  // Unused when drawer-hosted — onSuccess/onCancelOverride win below first.
2358
2358
  const returnTarget = useReturnTarget(screen.id);
2359
- const synthEntity = useMemo(() => synthesizeActionFormEntity(screen.fields), [screen.fields]);
2359
+ const submittableFields = useMemo(() => actionFormSubmittableFields(screen), [screen]);
2360
+ const synthEntity = useMemo(() => synthesizeActionFormEntity(submittableFields), [submittableFields]);
2360
2361
  const synthScreen = useMemo(() => synthesizeActionFormScreen(screen), [screen]);
2361
2362
  const pendingHandoff = useInitialValuesHandoff(screen.id);
2362
2363
  // A drawer-hosted form is not a navigation target, so it never takes a handoff.
@@ -2368,7 +2369,7 @@ function ActionFormBody({ schema, screen, translate, initialOverrides, onSuccess
2368
2369
  // submitting a guessed currency would mis-book the amount — block instead.
2369
2370
  const tenantCurrencyFieldNames = useMemo(() => tenantCurrencyMoneyFieldNames(screen.fields), [screen.fields]);
2370
2371
  const { overrides: moneyCurrencyOverrides, loading: currencyLoading, unresolved: currencyUnresolved, } = useMoneyCurrencyOverrides(screen.fields, tenantCurrencyFieldNames, undefined);
2371
- const initial = useMemo(() => mergeSearchParamsIntoInitial(screen.fields, {
2372
+ const initial = useMemo(() => mergeSearchParamsIntoInitial(submittableFields, {
2372
2373
  searchParams: nav.searchParams,
2373
2374
  urlPrefillFields: screen.urlPrefillFields,
2374
2375
  renderableFields: layoutFieldNames(synthScreen),
@@ -2376,7 +2377,7 @@ function ActionFormBody({ schema, screen, translate, initialOverrides, onSuccess
2376
2377
  ...(initialOverrides !== undefined && { drawerOverrides: initialOverrides }),
2377
2378
  ...(handoffValues !== undefined && { handoffValues }),
2378
2379
  }), [
2379
- screen.fields,
2380
+ submittableFields,
2380
2381
  screen.urlPrefillFields,
2381
2382
  nav.searchParams,
2382
2383
  synthScreen,
@@ -263,5 +263,5 @@ function EmbeddedListFieldInner({ field, id, onChange, allIssues, featureName, }
263
263
  const recomputed = nextRows.map((row, i) => touchedIndices.has(i) ? withRecomputedDerived(row, derived, cells) : row);
264
264
  onChange(recomputed);
265
265
  }
266
- return (_jsx(EmbeddedListInput, { id: id, columns: columns, rows: rows, totals: totals, currency: field.embeddedListCurrency, disabled: field.readOnly, minItems: field.embeddedListMinItems, maxItems: field.embeddedListMaxItems, listIssues: combinedListIssues, rowIssues: rowIssues, cellIssues: cellIssues, onCellChange: handleCellChange, onAddRow: handleAddRow, onRemoveRow: handleRemoveRow, onDuplicateRow: handleDuplicateRow, onMoveRow: handleMoveRow, onPasteCells: handlePasteCells, addLabel: t("kumiko.field.embedded-list.add-row"), removeLabel: t("kumiko.field.embedded-list.remove-row"), duplicateLabel: t("kumiko.field.embedded-list.duplicate-row"), moveUpLabel: t("kumiko.field.embedded-list.move-up"), moveDownLabel: t("kumiko.field.embedded-list.move-down"), emptyLabel: t("kumiko.field.embedded-list.empty"), emptyCtaLabel: t("kumiko.field.embedded-list.empty-cta"), testId: id }));
266
+ return (_jsx(EmbeddedListInput, { id: id, columns: columns, rows: rows, totals: totals, currency: field.embeddedListCurrency, disabled: field.readOnly, minItems: field.embeddedListMinItems, maxItems: field.embeddedListMaxItems, listIssues: combinedListIssues, rowIssues: rowIssues, cellIssues: cellIssues, onCellChange: handleCellChange, onAddRow: handleAddRow, onRemoveRow: handleRemoveRow, onDuplicateRow: handleDuplicateRow, onMoveRow: handleMoveRow, onPasteCells: handlePasteCells, addLabel: t("kumiko.field.embedded-list.add-row"), actionsLabel: t("kumiko.field.embedded-list.actions"), removeLabel: t("kumiko.field.embedded-list.remove-row"), duplicateLabel: t("kumiko.field.embedded-list.duplicate-row"), moveUpLabel: t("kumiko.field.embedded-list.move-up"), moveDownLabel: t("kumiko.field.embedded-list.move-down"), emptyLabel: t("kumiko.field.embedded-list.empty"), emptyCtaLabel: t("kumiko.field.embedded-list.empty-cta"), testId: id }));
267
267
  }
@@ -0,0 +1,10 @@
1
+ import type { EditFieldViewModel } from "@cosmicdrift/kumiko-headless";
2
+ import type { ReactNode } from "react";
3
+ export type FileListFieldProps = {
4
+ readonly field: EditFieldViewModel;
5
+ readonly id: string;
6
+ readonly kind: "file" | "image";
7
+ readonly onChange: (value: readonly string[]) => void;
8
+ readonly hasError: boolean;
9
+ };
10
+ export declare function FileListField({ field, id, kind, onChange, hasError, }: FileListFieldProps): ReactNode;
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useTranslation } from "../i18n.js";
3
+ import { usePrimitives } from "../primitives.js";
4
+ function isFileIdList(value) {
5
+ return Array.isArray(value);
6
+ }
7
+ function readFileIds(value) {
8
+ if (!isFileIdList(value))
9
+ return [];
10
+ return value.filter((entry) => typeof entry === "string" && entry !== "");
11
+ }
12
+ export function FileListField({ field, id, kind, onChange, hasError, }) {
13
+ const { Input, Button, Grid, GridCell, Text } = usePrimitives();
14
+ const t = useTranslation();
15
+ const fileIds = readFileIds(field.value);
16
+ const disabled = field.readOnly;
17
+ const canAdd = !disabled && (field.maxCount === undefined || fileIds.length < field.maxCount);
18
+ const inputProps = {
19
+ kind,
20
+ name: field.field,
21
+ disabled,
22
+ hasError,
23
+ ...(field.accept !== undefined && { accept: field.accept }),
24
+ ...(field.maxSize !== undefined && { maxSize: field.maxSize }),
25
+ ...(field.entityType !== undefined && { entityType: field.entityType }),
26
+ ...(field.entityId !== undefined && { entityId: field.entityId }),
27
+ ...(field.fieldName !== undefined && { fieldName: field.fieldName }),
28
+ ...(kind === "image" &&
29
+ field.imageVariant !== undefined && { imageVariant: field.imageVariant }),
30
+ ...(kind === "image" && field.capture !== undefined && { capture: field.capture }),
31
+ };
32
+ function move(fromIndex, toIndex) {
33
+ const next = [...fileIds];
34
+ const [moved] = next.splice(fromIndex, 1);
35
+ if (moved === undefined)
36
+ return;
37
+ next.splice(toIndex, 0, moved);
38
+ onChange(next);
39
+ }
40
+ return (_jsxs(Grid, { columns: 1, testId: `${id}-file-list`, children: [fileIds.length === 0 && _jsx(Text, { variant: "muted", children: t("kumiko.field.file-list.empty") }), fileIds.map((fileId, index) => (_jsxs(GridCell, { children: [_jsx(Input, { ...inputProps, id: `${id}-${index}`, value: fileId, onChange: (next) => {
41
+ if (next === null)
42
+ onChange(fileIds.filter((_, i) => i !== index));
43
+ else
44
+ onChange(fileIds.map((existing, i) => (i === index ? next : existing)));
45
+ } }), !disabled && (_jsxs(Grid, { columns: "auto", children: [index > 0 && (_jsx(Button, { variant: "ghost", size: "icon", icon: "chevron-up", ariaLabel: t("kumiko.field.file-list.move-up"), testId: `${id}-${index}-move-up`, onClick: () => move(index, index - 1) })), index < fileIds.length - 1 && (_jsx(Button, { variant: "ghost", size: "icon", icon: "chevron-down", ariaLabel: t("kumiko.field.file-list.move-down"), testId: `${id}-${index}-move-down`, onClick: () => move(index, index + 1) }))] }))] }, fileId))), canAdd && (_jsxs(GridCell, { children: [_jsx(Text, { variant: "muted", children: t("kumiko.field.file-list.add") }), _jsx(Input, { ...inputProps, id: `${id}-add`, value: null, onChange: (next) => {
46
+ if (next !== null)
47
+ onChange([...fileIds, next]);
48
+ } })] }, "add"))] }));
49
+ }
@@ -631,7 +631,17 @@ export function RenderEdit(props) {
631
631
  translate,
632
632
  featureName,
633
633
  mode: writeVerb,
634
- }), [screen, entity, snapshot.values, translate, featureName, writeVerb]);
634
+ ...(!isCreateMode && entityIdProp != null && { entityId: entityIdProp }),
635
+ }), [
636
+ screen,
637
+ entity,
638
+ snapshot.values,
639
+ translate,
640
+ featureName,
641
+ writeVerb,
642
+ isCreateMode,
643
+ entityIdProp,
644
+ ]);
635
645
  const filteredSections = useMemo(
636
646
  // A fully-hidden "fields" section (every field in it currently
637
647
  // condition-hidden) must not occupy a wizard step; it would render
@@ -15,6 +15,7 @@ import { useLocale, useTranslation } from "../i18n.js";
15
15
  import { usePrimitives } from "../primitives.js";
16
16
  import { AvailabilityOptionsMultiSelect, AvailabilityOptionsSelect, staticSelectOptions, } from "./availability-options-select.js";
17
17
  import { EmbeddedListField } from "./embedded-list-field.js";
18
+ import { FileListField } from "./file-list-field.js";
18
19
  import { MultiSelectCheckboxes } from "./multi-select-checkboxes.js";
19
20
  import { QueryOptionsSelect } from "./query-options-select.js";
20
21
  import { ReferenceCreateDialog } from "./reference-create-dialog.js";
@@ -276,11 +277,11 @@ function ReadOnlyReferenceValue({ field, featureName, }) {
276
277
  return _jsx(Text, { testId: `field-value-${field.field}`, children: labels.join(", ") });
277
278
  }
278
279
  // Field types whose read display already isn't a boxed "disabled input" look
279
- // (embedded/jsonb render as an info Banner, files/images as an unsupported-
280
- // type Banner) — `readOnlyDisplayText` doesn't cover these, they keep going
281
- // through `renderInput`'s existing Banner branch in text-display mode too.
280
+ // (embedded/jsonb render as an info Banner) — `readOnlyDisplayText` doesn't
281
+ // cover these, they keep going through `renderInput`'s existing Banner branch
282
+ // in text-display mode too.
282
283
  function isComplexFieldType(type) {
283
- return type === "embedded" || type === "jsonb" || type === "files" || type === "images";
284
+ return type === "embedded" || type === "jsonb";
284
285
  }
285
286
  // Mirrors ACTION_ICON_BY_ID (kumiko-screen.tsx) for fields: only names with
286
287
  // an unambiguous matching FieldIconKey are listed here, everything else
@@ -383,22 +384,21 @@ function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner,
383
384
  case "image": {
384
385
  const kind = field.type === "image" ? "image" : "file";
385
386
  const fileId = typeof field.value === "string" && field.value !== "" ? field.value : null;
386
- return (_jsx(Input, { kind: kind, ...common, value: fileId, onChange: (v) => onChange(v), ...(field.accept !== undefined && { accept: field.accept }), ...(field.maxSize !== undefined && { maxSize: field.maxSize }), ...(field.entityType !== undefined && { entityType: field.entityType }), ...(field.fieldName !== undefined && { fieldName: field.fieldName }), ...(field.imageVariant !== undefined && { imageVariant: field.imageVariant }), ...(field.capture !== undefined && { capture: field.capture }) }));
387
+ return (_jsx(Input, { kind: kind, ...common, value: fileId, onChange: (v) => onChange(v), ...(field.accept !== undefined && { accept: field.accept }), ...(field.maxSize !== undefined && { maxSize: field.maxSize }), ...(field.entityType !== undefined && { entityType: field.entityType }), ...(field.entityId !== undefined && { entityId: field.entityId }), ...(field.fieldName !== undefined && { fieldName: field.fieldName }), ...(field.imageVariant !== undefined && { imageVariant: field.imageVariant }), ...(field.capture !== undefined && { capture: field.capture }) }));
387
388
  }
389
+ case "files":
390
+ case "images":
391
+ return (_jsx(FileListField, { field: field, id: id, kind: field.type === "images" ? "image" : "file", hasError: hasError, onChange: (ids) => onChange(ids) }));
388
392
  // embedded (without embeddedListCells — that's embeddedList, which has
389
393
  // had its own EmbeddedListField widget since #1838) and jsonb carry
390
- // arbitrary objects; files/images carry a FileRef-UUID array and have
391
- // no multi-upload widget yet (deliberately deferred, #1925). Without a
392
- // dedicated widget these must NOT fall through to a text input:
393
- // stringValue() turns them into "[object Object]" / a comma-joined
394
- // string, and saving that overwrites the real data with the mangled
395
- // string (#1834). A `required: true` on any of these is caught loudly
394
+ // arbitrary objects. Without a dedicated widget these must NOT fall
395
+ // through to a text input: stringValue() turns them into
396
+ // "[object Object]" and saving that overwrites the real data with the
397
+ // mangled string (#1834). A `required: true` on either is caught loudly
396
398
  // at boot (validateNoWidgetRequiredField in the framework package)
397
399
  // instead of silently failing here.
398
400
  case "embedded":
399
- case "jsonb":
400
- case "files":
401
- case "images": {
401
+ case "jsonb": {
402
402
  const hasValue = field.value !== undefined && field.value !== null && field.value !== "";
403
403
  return (_jsxs(Banner, { id: id, variant: "info", children: [t("kumiko.field.unsupported"), hasValue &&
404
404
  (JsonView !== undefined ? (_jsx(JsonView, { value: field.value, testId: `field-value-${field.field}` })) : (_jsx(Text, { variant: "code", children: JSON.stringify(field.value) })))] }));
@@ -559,6 +559,9 @@ function readOnlyDisplayText(field, appLocale, t) {
559
559
  case "file":
560
560
  case "image":
561
561
  return typeof value === "string" ? value : "—";
562
+ case "files":
563
+ case "images":
564
+ return Array.isArray(value) && value.length > 0 ? value.map(stringValue).join(", ") : "—";
562
565
  default:
563
566
  return stringValue(value);
564
567
  }
@@ -45,6 +45,17 @@ export const kumikoDefaultTranslations = {
45
45
  "kumiko.field.writeOnly.setPlaceholder": "Set — leave empty to keep it",
46
46
  "kumiko.field.writeOnly.undo": "Undo",
47
47
  "kumiko.field.writeOnly.willRemove": "Removed on save",
48
+ "kumiko.field.embedded-list.actions": "Actions",
49
+ "kumiko.field.embedded-list.cell-label": "{column}, row {row}",
50
+ "kumiko.field.file-list.add": "Add file",
51
+ "kumiko.field.file-list.move-up": "Move file up",
52
+ "kumiko.field.file-list.move-down": "Move file down",
53
+ "kumiko.field.file-list.empty": "No files yet.",
54
+ "kumiko.field.file-upload.attached": "File attached",
55
+ "kumiko.field.file-upload.change": "Change",
56
+ "kumiko.field.file-upload.failed": "Upload failed",
57
+ "kumiko.field.file-upload.remove": "Remove",
58
+ "kumiko.field.file-upload.upload": "Upload",
48
59
  "kumiko.field.embedded-list.add-row": "Add row",
49
60
  "kumiko.field.embedded-list.remove-row": "Remove row",
50
61
  "kumiko.field.embedded-list.duplicate-row": "Duplicate row",
@@ -295,6 +295,9 @@ export type InputProps = {
295
295
  readonly name: string;
296
296
  /** FileRef-UUID (gespeicherte Datei) oder null (kein Upload). */
297
297
  readonly value: string | null;
298
+ /** Implementations must offer their own remove control that calls
299
+ * `onChange(null)`: FileListField relies on it to drop a file from an
300
+ * `images`/`files` field. */
298
301
  readonly onChange: (fileId: string | null) => void;
299
302
  readonly disabled?: boolean;
300
303
  readonly required?: boolean;
@@ -305,6 +308,9 @@ export type InputProps = {
305
308
  /** Für den Upload-POST `/api/files` — der Endpoint prüft maxSize/accept
306
309
  * gegen die Field-Def von (entityType, fieldName). */
307
310
  readonly entityType?: string;
311
+ /** Must be sent as the `entityId` part of the upload so the FileRef is
312
+ * attached to the record (qr overlays and access guards need it). */
313
+ readonly entityId?: string;
308
314
  readonly fieldName?: string;
309
315
  /** Only for kind:"image" — variant name for the preview
310
316
  * (`/api/files/:id/variant/:name`). Absent → the original. */
@@ -810,6 +816,8 @@ export type EmbeddedListInputProps = {
810
816
  * handling wired up. */
811
817
  readonly onPasteCells?: (rowIndex: number, columnIndex: number, grid: readonly (readonly string[])[]) => void;
812
818
  readonly addLabel: string;
819
+ /** Accessible name of the (otherwise headerless) row-actions column. */
820
+ readonly actionsLabel: string;
813
821
  readonly removeLabel: string;
814
822
  readonly duplicateLabel: string;
815
823
  readonly moveUpLabel: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer",
3
- "version": "0.358.0",
3
+ "version": "0.360.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.358.0",
19
- "@cosmicdrift/kumiko-headless": "0.358.0",
20
- "@cosmicdrift/kumiko-types": "0.358.0",
18
+ "@cosmicdrift/kumiko-framework": "0.360.0",
19
+ "@cosmicdrift/kumiko-headless": "0.360.0",
20
+ "@cosmicdrift/kumiko-types": "0.360.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.358.0"
31
+ "@cosmicdrift/kumiko-locale-de": "0.360.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.360.0",
4
+ "type": "improvement",
5
+ "title": "FileListField edits images and files fields",
6
+ "detail": "RenderField renders type images and files as FileListField instead of the unsupported banner. One tile per stored FileRef id with move up, move down and remove buttons, one trailing empty tile to add a file (hidden at maxCount or when read-only). It is built from Input kind file or image, Button and the layout primitives, passes accept, maxSize, entityType, entityId, fieldName, imageVariant and capture to every tile, and reads only string ids from the stored value. The read-only text display of these fields lists the ids. New i18n keys kumiko.field.file-list.add, move-up, move-down and empty; kumiko.field.file-upload.upload, change, remove, attached and failed; kumiko.field.embedded-list.actions and cell-label. The file and image Input kinds accept entityId. EmbeddedListInput has a new required actionsLabel prop."
7
+ },
2
8
  {
3
9
  "version": "0.357.0",
4
10
  "type": "improvement",