@cosmicdrift/kumiko-renderer 0.333.0 → 0.335.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.
Files changed (52) hide show
  1. package/dist/app/config-edit-shim.d.ts +7 -0
  2. package/dist/app/config-edit-shim.js +39 -0
  3. package/dist/app/content-preview.js +7 -9
  4. package/dist/app/extension-sections.d.ts +6 -0
  5. package/dist/app/form-schema.d.ts +2 -1
  6. package/dist/app/form-schema.js +11 -2
  7. package/dist/app/kumiko-screen.d.ts +5 -0
  8. package/dist/app/kumiko-screen.js +259 -101
  9. package/dist/app/nav.d.ts +5 -0
  10. package/dist/app/nav.js +17 -4
  11. package/dist/app/query-state-banners.d.ts +8 -0
  12. package/dist/app/query-state-banners.js +14 -0
  13. package/dist/app/reference-facet-bridge.js +8 -1
  14. package/dist/app/row-actions.d.ts +12 -0
  15. package/dist/app/row-actions.js +24 -7
  16. package/dist/app/screen-access.js +13 -9
  17. package/dist/app/secret-mint-body.js +41 -7
  18. package/dist/app/secrets-edit-body.js +89 -33
  19. package/dist/components/embedded-list-field.js +2 -4
  20. package/dist/components/grid-cell-for-field.d.ts +7 -2
  21. package/dist/components/grid-cell-for-field.js +2 -2
  22. package/dist/components/multi-select-checkboxes.js +4 -1
  23. package/dist/components/query-options-select.d.ts +12 -3
  24. package/dist/components/query-options-select.js +45 -2
  25. package/dist/components/reference-create-dialog.js +9 -4
  26. package/dist/components/related-list-section.js +8 -10
  27. package/dist/components/render-edit-action-button.d.ts +1 -1
  28. package/dist/components/render-edit-action-button.js +5 -1
  29. package/dist/components/render-edit-logic.d.ts +2 -1
  30. package/dist/components/render-edit-logic.js +8 -5
  31. package/dist/components/render-edit-types.d.ts +30 -1
  32. package/dist/components/render-edit.js +248 -76
  33. package/dist/components/render-field.d.ts +7 -1
  34. package/dist/components/render-field.js +40 -15
  35. package/dist/components/render-list.js +6 -6
  36. package/dist/components/write-form-section.d.ts +5 -1
  37. package/dist/components/write-form-section.js +29 -25
  38. package/dist/context/embedded-screen-context.d.ts +5 -0
  39. package/dist/context/embedded-screen-context.js +12 -0
  40. package/dist/hooks/use-reference-lookup.d.ts +7 -0
  41. package/dist/hooks/use-reference-lookup.js +10 -0
  42. package/dist/i18n-defaults.js +30 -5
  43. package/dist/i18n.js +8 -4
  44. package/dist/index.d.ts +5 -1
  45. package/dist/index.js +3 -0
  46. package/dist/primitives.d.ts +70 -19
  47. package/dist/session/session-ended.d.ts +16 -0
  48. package/dist/session/session-ended.js +73 -0
  49. package/dist/sort-by-accessor.d.ts +6 -2
  50. package/dist/sort-by-accessor.js +13 -3
  51. package/package.json +5 -5
  52. package/src/changes.json +40 -0
@@ -1,6 +1,6 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { evalFieldCondition, isFieldsEditSection, normalizeEditField, PROJECTION_DETAIL_ENTITY, } from "@cosmicdrift/kumiko-framework/ui-types";
3
- import { computeEditViewModel } from "@cosmicdrift/kumiko-headless";
2
+ import { evalFieldCondition, isFieldsEditSection, normalizeEditField, PROJECTION_DETAIL_ENTITY, sectionFieldSpecs, } from "@cosmicdrift/kumiko-framework/ui-types";
3
+ import { computeEditViewModel, relevantFieldIssues, zodErrorToFieldIssues, } from "@cosmicdrift/kumiko-headless";
4
4
  import { RenderEditActionButton } from "./render-edit-action-button.js";
5
5
  import { AllFieldsRequiredProvider } from "./render-field.js";
6
6
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -77,7 +77,7 @@ function deriveFormFields(screen) {
77
77
  // relatedList carries no `fields` for the form-condition map either.
78
78
  if (!isFieldsEditSection(section))
79
79
  continue;
80
- for (const spec of section.fields) {
80
+ for (const spec of sectionFieldSpecs(section)) {
81
81
  const normalized = normalizeEditField(spec);
82
82
  out[normalized.field] = {
83
83
  ...(normalized.visible !== undefined && {
@@ -94,16 +94,67 @@ function deriveFormFields(screen) {
94
94
  }
95
95
  return out;
96
96
  }
97
+ // Dry-run parse against `schema` — NOT controller.validate(), which writes
98
+ // field errors into the snapshot. Issues are filtered exactly like the
99
+ // controller's validate(): hidden fields never count, `scope` narrows.
100
+ function dryRunFieldIssues(schema, snapshot, scope, includeRoot) {
101
+ const parsed = schema.safeParse(snapshot.values);
102
+ if (parsed.success)
103
+ return [];
104
+ const hiddenFields = new Set(Object.entries(snapshot.fields)
105
+ .filter(([, state]) => !state.visible)
106
+ .map(([fieldKey]) => fieldKey));
107
+ return relevantFieldIssues(zodErrorToFieldIssues(parsed.error), {
108
+ hiddenFields,
109
+ ...(scope !== undefined && { scope }),
110
+ includeRoot,
111
+ });
112
+ }
113
+ // Index-independent: indices shift when a fields section hides.
114
+ function wizardStepKey(section, index) {
115
+ const declaredId = section.kind === "fields" || section.kind === "extension" ? section.id : undefined;
116
+ return declaredId ?? section.title ?? `step-${index}`;
117
+ }
118
+ function isMoneyValue(value) {
119
+ return typeof value === "object" && value !== null && "amount" in value;
120
+ }
121
+ // The seeds buildInitialValues writes for NULL columns of an existing record.
122
+ function isEmptyFormValue(value) {
123
+ if (value === null || value === undefined || value === "" || value === 0 || value === false) {
124
+ return true;
125
+ }
126
+ if (Array.isArray(value))
127
+ return value.length === 0;
128
+ return isMoneyValue(value) && value.amount === 0;
129
+ }
130
+ // select/boolean are excluded from the "holds user data" check: they are
131
+ // often pre-filled (defaults, server-set values), so they say nothing about
132
+ // whether the step was filled in. They still count through the validation.
133
+ function isFieldsStepFilled(section, schema, snapshot) {
134
+ const fieldNames = section.fields.map((f) => f.field);
135
+ if (schema !== undefined && dryRunFieldIssues(schema, snapshot, fieldNames, false).length > 0) {
136
+ return false;
137
+ }
138
+ return section.fields.some((f) => f.visible &&
139
+ !f.readOnly &&
140
+ f.type !== "boolean" &&
141
+ f.type !== "select" &&
142
+ !isEmptyFormValue(f.value));
143
+ }
97
144
  // Resolves an extension-section's `{ react: { __component: "X" } }` marker
98
145
  // to a registered React component via ExtensionSectionsProvider (filled in
99
146
  // createKumikoApp from clientFeatures.extensionSectionComponents) and
100
147
  // mounts it with the host entity name + id. Hook lives in its own
101
148
  // component so we don't call `use*` inside vm.sections.map (rules-of-
102
149
  // hooks would punish reordering sections between renders).
103
- function ExtensionSectionMount({ section, entityName: hostEntityName, entityId, initialValues, values, patch, validate, hideTitle, actions, }) {
150
+ function ExtensionSectionMount({ section, entityName: hostEntityName, entityId, initialValues, values, patch, validate, hideTitle, actions, stepKey, onReportStepComplete, }) {
104
151
  const { Banner, FillContainer, Section, Text } = usePrimitives();
105
152
  const name = extensionSectionName(section.component);
106
153
  const Component = useExtensionSectionComponent(name);
154
+ const reportStepComplete = useCallback((complete) => {
155
+ if (stepKey !== undefined)
156
+ onReportStepComplete?.(stepKey, complete);
157
+ }, [stepKey, onReportStepComplete]);
107
158
  const testId = `section-extension-${section.title}`;
108
159
  if (Component === undefined) {
109
160
  const placeholder = (_jsx(Banner, { variant: "info", testId: `section-extension-placeholder-${section.title}`, children: _jsxs(Text, { children: ["Extension section component ", _jsx(Text, { variant: "code", children: name ?? "(no __component name)" }), " ", "not registered in clientFeatures.extensionSectionComponents."] }) }));
@@ -115,7 +166,7 @@ function ExtensionSectionMount({ section, entityName: hostEntityName, entityId,
115
166
  // uses for its own hideTitle branch).
116
167
  return hideTitle === true ? (FillContainer !== undefined ? (_jsx(FillContainer, { testId: testId, children: placeholder })) : (placeholder)) : (_jsx(Section, { title: section.title, testId: testId, children: placeholder }, section.title));
117
168
  }
118
- const mounted = (_jsx(Component, { entityName: section.entityName ?? hostEntityName, entityId: entityId, initialValues: initialValues, values: values, patch: patch, validate: validate }));
169
+ const mounted = (_jsx(Component, { entityName: section.entityName ?? hostEntityName, entityId: entityId, initialValues: initialValues, values: values, patch: patch, validate: validate, ...(onReportStepComplete !== undefined && { reportStepComplete }) }));
119
170
  // Same reasoning as the placeholder branch above: tabs mode's outer Card
120
171
  // is the only frame this content gets, no nested Section divider.
121
172
  return hideTitle === true ? (FillContainer !== undefined ? (_jsx(FillContainer, { testId: testId, children: mounted })) : (mounted)) : (_jsx(Section, { ...(actions !== undefined && { actions }), title: section.title, testId: testId, children: mounted }));
@@ -162,7 +213,8 @@ function resolveTitleTemplate(template, values, translate, fallback) {
162
213
  return translate(template, values);
163
214
  }
164
215
  export function RenderEdit(props) {
165
- const { screen, entity, featureName, initial, writeCommand, translate: translateProp, ctx, schema, onSubmit, payloadMode = "values", buildPayload, serverFieldPathPrefix, onDelete, onCancel, onReload, onDirtyChange, onCopyLink, actions, onRelatedListDrawerAction, submitLabel, submitVariant, footerActions, summary, labelAppendix, fieldAppendix, entityId: entityIdProp, extensionInitialValues, onChange, onControlsReady, fields: fieldsFilter, disabled = false, hideActions, valueDisplay = "form", hideSectionTitles, fillScreenHeight, headerRegion, buildSectionActions, } = props;
216
+ const { screen, entity, featureName, initial, writeCommand, translate: translateProp, ctx, schema, onSubmit, payloadMode = "values", buildPayload, serverFieldPathPrefix, onDelete, onCancel, onReload, onDirtyChange, onCopyLink, actions, onRelatedListDrawerAction, submitLabel, submitVariant, footerActions, summary, labelAppendix, fieldAppendix, fieldAppendixPlacement, fieldDescription, fieldAccent, markChangedFields = false, entityId: entityIdProp, extensionInitialValues, onChange, onControlsReady, fields: fieldsFilter, disabled = false, hideActions, valueDisplay = "form", hideSectionTitles, fillScreenHeight, dirtyFooter, validateOnChange, headerRegion, buildSectionActions, leadContent, } = props;
217
+ const i18nScreenId = props.i18nScreenId ?? screen.id;
166
218
  const { customSubmit } = props;
167
219
  // Translate-Fallback: wenn der Caller keine Translate-Fn übergibt,
168
220
  // konsumieren wir den i18next-Context direkt. Sonst wären Field-
@@ -185,6 +237,9 @@ export function RenderEdit(props) {
185
237
  const [linkCopied, setLinkCopied] = useState(false);
186
238
  const [isSubmitting, setIsSubmitting] = useState(false);
187
239
  const [formError, setFormError] = useState(null);
240
+ // A rejected delete can carry several blocking reasons; the first one is the
241
+ // banner's headline (formError), the rest are listed beneath it.
242
+ const [extraDeleteIssues, setExtraDeleteIssues] = useState([]);
188
243
  // One state for all header actions (actions?), not per-button — only one
189
244
  // action can be in flight at a time, and this keeps the error surfaced in
190
245
  // the shared formError-adjacent banner region instead of inside the
@@ -192,6 +247,14 @@ export function RenderEdit(props) {
192
247
  // flex justify-end action bar).
193
248
  const [actionError, setActionError] = useState(null);
194
249
  const [rawStep, setRawStep] = useState(0);
250
+ const [passedStepKeys, setPassedStepKeys] = useState(() => new Set());
251
+ const [extensionStepComplete, setExtensionStepComplete] = useState({});
252
+ const reportExtensionStepComplete = useCallback((stepKey, complete) => {
253
+ setExtensionStepComplete((prev) => (prev[stepKey] === true) === complete ? prev : { ...prev, [stepKey]: complete });
254
+ }, []);
255
+ // Tabs have stable ids, so the active tab follows its id: a field change
256
+ // that hides an earlier tab must not shift the index onto another tab.
257
+ const [activeTabId, setActiveTabId] = useState(undefined);
195
258
  // Create-mode draftId (issue #1913) — resumed from `sessionStorage` (web)
196
259
  // on mount so a same-tab reload finds the right one of several parallel
197
260
  // create-sessions on this screen; `null` until the first step change
@@ -400,9 +463,12 @@ export function RenderEdit(props) {
400
463
  // on every keystroke, painting error messages while the user is still
401
464
  // typing. `valid` can therefore legitimately diverge from what's
402
465
  // currently rendered under the fields (the last *mutating* validate()
403
- // call, e.g. from controls.validate() or submit()).
466
+ // call, e.g. from controls.validate() or submit()). The issues are
467
+ // filtered exactly like submit()'s validate: hidden fields and fields
468
+ // outside the `fields` scope never make the form invalid.
404
469
  const currentSchema = schemaRef.current;
405
- const valid = currentSchema === undefined ? true : currentSchema.safeParse(snapshot.values).success;
470
+ const valid = currentSchema === undefined ||
471
+ dryRunFieldIssues(currentSchema, snapshot, scopeFieldNamesRef.current, true).length === 0;
406
472
  cb({
407
473
  values: snapshot.values,
408
474
  changes: snapshot.changes,
@@ -569,6 +635,11 @@ export function RenderEdit(props) {
569
635
  // keeps normal document-flow height untouched.
570
636
  const fillHeight = fillScreenHeight === true ||
571
637
  (hideSectionTitles === true && filteredSections[0]?.kind === "relatedList");
638
+ // A lone writeForm tab is the whole page, so its submit joins this form's
639
+ // footer (pinned to the window edge like every other screen's save)
640
+ // instead of sitting in the section's title row.
641
+ const isLoneWriteFormTab = hideSectionTitles === true && filteredSections[0]?.kind === "writeForm";
642
+ const [writeFormFooterAction, setWriteFormFooterAction] = useState(undefined);
572
643
  // Persistiert alle composed Extension-Sections mit der aufgelösten entityId.
573
644
  // false = eine Section schlug fehl (ihr i18n-Key landet im Banner). Ohne
574
645
  // Entity-Kontext (create-mode ohne route-id) gibt es nichts zu schreiben.
@@ -588,12 +659,45 @@ export function RenderEdit(props) {
588
659
  // Clamped on read, not on write: section visibility is value-dependent, so a
589
660
  // stored stepIndex can point past what is currently rendered — that lands on
590
661
  // the last step instead of an empty one.
591
- const currentStep = Math.min(rawStep, lastStepIndex);
662
+ const activeTabIndex = isTabs && activeTabId !== undefined
663
+ ? filteredSections.findIndex((s, i) => tabIdAt(s, i) === activeTabId)
664
+ : -1;
665
+ const currentStep = activeTabIndex !== -1 ? activeTabIndex : Math.min(rawStep, lastStepIndex);
592
666
  // Kept fresh for patchAndScheduleDraftSave's debounce timer above, which
593
667
  // is defined before `currentStep` exists (it depends on `filteredSections`,
594
668
  // computed further up from `vm`) and so cannot close over it directly.
595
669
  currentStepRef.current = currentStep;
596
670
  const isLastWizardStep = currentStep >= lastStepIndex;
671
+ const wizardDoneSteps = useMemo(() => {
672
+ if (!isWizard || isCreateMode)
673
+ return undefined;
674
+ const snapshotForDryRun = { values: snapshot.values, fields: snapshot.fields };
675
+ return filteredSections.map((section, index) => {
676
+ if (passedStepKeys.has(wizardStepKey(section, index)))
677
+ return true;
678
+ if (section.kind === "fields")
679
+ return isFieldsStepFilled(section, schema, snapshotForDryRun);
680
+ return (section.kind === "extension" &&
681
+ extensionStepComplete[wizardStepKey(section, index)] === true);
682
+ });
683
+ }, [
684
+ isWizard,
685
+ isCreateMode,
686
+ filteredSections,
687
+ passedStepKeys,
688
+ extensionStepComplete,
689
+ schema,
690
+ snapshot.values,
691
+ snapshot.fields,
692
+ ]);
693
+ const updateModeStepBarProps = wizardDoneSteps === undefined
694
+ ? {}
695
+ : { doneSteps: wizardDoneSteps, selectableSteps: "all" };
696
+ function jumpToStep(index) {
697
+ setRawStep(index);
698
+ if (isTabs)
699
+ setActiveTabId(tabIdAt(filteredSections[index], index));
700
+ }
597
701
  // Step transitions only — never per keystroke. Deliberately not awaited: a
598
702
  // failed draft save must not block the step change.
599
703
  //
@@ -633,11 +737,13 @@ export function RenderEdit(props) {
633
737
  console.warn(`RenderEdit: draftKey "${key}" is ${key.length} chars, over the server's ${FORM_DRAFT_KEY_MAX_LENGTH}-char limit — skipping this draft save. Shorten screen.id or entity id.`);
634
738
  return;
635
739
  }
636
- const passwordFieldNames = new Set(vm.sections.flatMap((section) => section.kind === "fields"
637
- ? section.fields.filter((f) => f.format === "password").map((f) => f.field)
740
+ const secretFieldNames = new Set(vm.sections.flatMap((section) => section.kind === "fields"
741
+ ? section.fields
742
+ .filter((f) => f.format === "password" || f.writeOnly === true)
743
+ .map((f) => f.field)
638
744
  : []));
639
- // A password field never enters the persisted draft blob — it would be stored in the clear and restored on resume.
640
- const draftValues = Object.fromEntries(Object.entries(controller.getSnapshot().values).filter(([field]) => !passwordFieldNames.has(field)));
745
+ // A password or writeOnly field never enters the persisted draft blob — it would be stored in the clear and restored on resume.
746
+ const draftValues = Object.fromEntries(Object.entries(controller.getSnapshot().values).filter(([field]) => !secretFieldNames.has(field)));
641
747
  void dispatcher
642
748
  .write(FORM_DRAFT_SAVE, {
643
749
  draftKey: key,
@@ -665,29 +771,45 @@ export function RenderEdit(props) {
665
771
  setRawStep(next);
666
772
  return;
667
773
  }
668
- const section = filteredSections[currentStep];
669
- const fieldNames = section?.kind === "fields" ? section.fields.map((f) => f.field) : [];
670
774
  // skip: the current step has field errors — no transition, no draft save.
671
- if (fieldNames.length > 0 && !controller.validate(fieldNames))
775
+ if (currentStepHasFieldErrors())
672
776
  return;
777
+ if (!isCreateMode) {
778
+ const leftSection = filteredSections[currentStep];
779
+ if (leftSection !== undefined) {
780
+ const leftKey = wizardStepKey(leftSection, currentStep);
781
+ setPassedStepKeys((prev) => (prev.has(leftKey) ? prev : new Set(prev).add(leftKey)));
782
+ }
783
+ }
673
784
  setRawStep(next);
674
785
  saveDraft(next);
675
786
  }
787
+ function currentStepHasFieldErrors() {
788
+ const section = filteredSections[currentStep];
789
+ const fieldNames = section?.kind === "fields" ? section.fields.map((f) => f.field) : [];
790
+ return fieldNames.length > 0 && !controller.validate(fieldNames);
791
+ }
676
792
  function handleWizardBack() {
677
- handleWizardJumpBack(currentStep - 1);
793
+ handleWizardStepSelect(currentStep - 1);
678
794
  }
679
- // Only backwards: a forward jump would skip the per-step validate() gate.
680
- function handleWizardJumpBack(targetStep) {
681
- if (targetStep >= currentStep)
795
+ // Update mode: the record already exists and the final Save validates the
796
+ // whole form and jumps to the first erroring step (findFirstErroringStep), so
797
+ // skipping steps forward is safe; leaving the current step still goes through
798
+ // its validate gate.
799
+ function handleWizardStepSelect(targetStep) {
800
+ if (targetStep === currentStep)
682
801
  return;
683
- const previous = Math.max(targetStep, 0);
684
- hasNavigatedRef.current = true;
685
- if (disabled) {
686
- setRawStep(previous);
802
+ const isForward = targetStep > currentStep;
803
+ // Create mode: a forward jump would skip the per-step validate() gate.
804
+ if (isForward && isCreateMode)
687
805
  return;
688
- }
689
- setRawStep(previous);
690
- saveDraft(previous);
806
+ if (isForward && !disabled && currentStepHasFieldErrors())
807
+ return;
808
+ const clampedTarget = Math.min(Math.max(targetStep, 0), lastStepIndex);
809
+ hasNavigatedRef.current = true;
810
+ setRawStep(clampedTarget);
811
+ if (!disabled)
812
+ saveDraft(clampedTarget);
691
813
  }
692
814
  async function discardDraft() {
693
815
  // skip: this screen does not persist a draft.
@@ -818,6 +940,7 @@ export function RenderEdit(props) {
818
940
  let extensionsPersisted = true;
819
941
  if (result.isSuccess) {
820
942
  setFormError(null);
943
+ setExtraDeleteIssues([]);
821
944
  // `isNoOp: true` (payloadMode "changes", pre-filled form submitted
822
945
  // untouched) means controller.submit() never called dispatcher.write
823
946
  // — nothing to discard, and discarding here would delete a draft the
@@ -838,7 +961,7 @@ export function RenderEdit(props) {
838
961
  if (isStepped) {
839
962
  const step = findFirstErroringStep(Object.keys(controller.getSnapshot().errors));
840
963
  if (step !== undefined)
841
- setRawStep(step);
964
+ jumpToStep(step);
842
965
  }
843
966
  }
844
967
  else {
@@ -854,8 +977,9 @@ export function RenderEdit(props) {
854
977
  if (isStepped) {
855
978
  const step = findFirstErroringStep(issuePaths);
856
979
  if (step !== undefined)
857
- setRawStep(step);
980
+ jumpToStep(step);
858
981
  }
982
+ setExtraDeleteIssues([]);
859
983
  setFormError(hasIssueWithoutRenderedField(issuePaths, filteredSections) ? result.error : null);
860
984
  }
861
985
  // skip: on entity-success-but-extension-failure the extension-error
@@ -872,10 +996,7 @@ export function RenderEdit(props) {
872
996
  handleSubmitRef.current = handleSubmit;
873
997
  // Screen forms (entityEdit/actionForm filling the shell height) drop the card;
874
998
  // the title then lives in the shell header when the shell offers a slot.
875
- const isScreenForm = fillScreenHeight === true &&
876
- hideSectionTitles !== true &&
877
- headerRegion === undefined &&
878
- !insideDrawer;
999
+ const isScreenForm = fillScreenHeight === true && hideSectionTitles !== true && !insideDrawer;
879
1000
  const titleInShell = PageHeader !== undefined &&
880
1001
  pageHeaderSlotAvailable &&
881
1002
  hideSectionTitles !== true &&
@@ -953,25 +1074,53 @@ export function RenderEdit(props) {
953
1074
  const isFieldlessSubmitForm = isFieldless && screen.entity !== PROJECTION_DETAIL_ENTITY;
954
1075
  const showsSubmit = isFormEditable || hasExtensionRegistrations || isFieldlessSubmitForm;
955
1076
  const footerSlot = screen.slots?.footer;
1077
+ // An unregistered footer component renders null, so only a resolvable one may
1078
+ // count as an action (else read-only screens get an empty footer strip).
1079
+ const FooterSlotComponent = useExtensionSectionComponent(footerSlot !== undefined ? extensionSectionName(footerSlot) : undefined);
956
1080
  // Mirrors every branch inside formActions below — without this guard
957
1081
  // DefaultForm renders an empty footer strip (border + padding, no content)
958
1082
  // on read-only detail screens, since `actions` would otherwise always be
959
1083
  // a defined (if empty) fragment.
960
- const hasFormActions = (isWizard && currentStep > 0) ||
961
- (isWizard && !isLastWizardStep) ||
962
- (showsSubmit && (!isWizard || isLastWizardStep)) ||
963
- footerSlot !== undefined;
1084
+ const showWizardBack = isWizard && currentStep > 0;
1085
+ const showWizardNext = isWizard && !isLastWizardStep;
1086
+ const showSubmit = showsSubmit && (!isWizard || isLastWizardStep);
1087
+ const hasFormActions = showWizardBack ||
1088
+ showWizardNext ||
1089
+ showSubmit ||
1090
+ FooterSlotComponent !== undefined ||
1091
+ writeFormFooterAction !== undefined;
964
1092
  const nextStepTitle = isWizard ? filteredSections[currentStep + 1]?.title : undefined;
965
- const formActions = (_jsxs(_Fragment, { children: [isWizard && currentStep > 0 && (_jsx(Button, { type: "button", variant: "secondary", icon: "chevron-left", onClick: handleWizardBack, testId: "render-edit-wizard-back", children: translate("kumiko.actions.back") })), footerSlot !== undefined && (_jsx(EditSlotMount, { slot: footerSlot, slotName: "footer", screenId: screen.id, entityName: vm.entityName, entityId: resolveExtensionEntityId(entityIdProp, vm.id), values: snapshot.values, hasUnsavedChanges: snapshot.isDirty || extensionDirty, wizardStep: isWizard ? { index: currentStep, isLast: isLastWizardStep } : undefined, ...(screen.slots?.footerPrimary === true && { [STICKY_PRIMARY_ACTION_PROP]: true }) })), isWizard && !isLastWizardStep && showsSubmit && !isCreateMode && (_jsx(Button, { type: "button", variant: "ghost", disabled: isSubmitting || disabled, onClick: () => void handleSubmit({ saveNow: true }), testId: "render-edit-wizard-save-close", [STICKY_PRIMARY_ACTION_PROP]: true, children: translate("kumiko.wizard.save-and-close") })), isWizard && !isLastWizardStep && (_jsx(Button, { type: "submit", variant: "primary", iconEnd: "chevron-right", testId: "render-edit-wizard-next", children: nextStepTitle !== undefined
1093
+ const isCreate = (() => {
1094
+ const id = resolveExtensionEntityId(entityIdProp, vm.id);
1095
+ return id == null || id === "";
1096
+ })();
1097
+ const changedFieldCount = Object.keys(snapshot.changes).length;
1098
+ // Edit-mode screen forms always carry the dirty status (0 = quiet "No unsaved
1099
+ // changes"); wizards keep the status-only-when-dirty behaviour.
1100
+ const hasDirtyFooter = !isWizard &&
1101
+ (dirtyFooter === true || (isScreenForm && !isCreate && headerRegion === undefined));
1102
+ const unsavedCount = !hasDirtyFooter
1103
+ ? undefined
1104
+ : changedFieldCount > 0
1105
+ ? changedFieldCount
1106
+ : extensionDirty
1107
+ ? 1
1108
+ : 0;
1109
+ const isSettingsList = screen.layout.variant === "settings-list" && hideSectionTitles !== true;
1110
+ const formActions = (_jsxs(_Fragment, { children: [showWizardBack && (_jsx(Button, { type: "button", variant: "secondary", icon: "chevron-left", onClick: handleWizardBack, testId: "render-edit-wizard-back", children: translate("kumiko.actions.back") })), footerSlot !== undefined && (_jsx(EditSlotMount, { slot: footerSlot, slotName: "footer", screenId: screen.id, entityName: vm.entityName, entityId: resolveExtensionEntityId(entityIdProp, vm.id), values: snapshot.values, hasUnsavedChanges: hasUnsavedInput, wizardStep: isWizard ? { index: currentStep, isLast: isLastWizardStep } : undefined, ...(screen.slots?.footerPrimary === true && { [STICKY_PRIMARY_ACTION_PROP]: true }) })), isWizard && !isLastWizardStep && showsSubmit && !isCreateMode && (_jsx(Button, { type: "button", variant: "ghost", disabled: isSubmitting || disabled, onClick: () => void handleSubmit({ saveNow: true }), testId: "render-edit-wizard-save-close", [STICKY_PRIMARY_ACTION_PROP]: true, children: translate("kumiko.wizard.save-and-close") })), showWizardNext && (_jsx(Button, { type: "submit", variant: "primary", iconEnd: "chevron-right", testId: "render-edit-wizard-next", children: nextStepTitle !== undefined
966
1111
  ? translate("kumiko.wizard.next-with-title", { title: nextStepTitle })
967
- : translate("kumiko.actions.next") })), showsSubmit &&
968
- (!isWizard || isLastWizardStep) &&
1112
+ : translate("kumiko.actions.next") })), showSubmit &&
969
1113
  footerActions?.map((action) => (_jsx(Button, { type: "button", variant: action.variant ?? "secondary", ...(action.icon !== undefined && { icon: action.icon }), disabled: isSubmitting || disabled, onClick: () => {
970
1114
  controller.setValues(action.patch); // @cast-boundary form-values
971
1115
  void handleSubmit();
972
- }, testId: `render-edit-footer-action-${action.id}`, children: translate(action.label) }, action.id))), showsSubmit && (!isWizard || isLastWizardStep) && (_jsx(Button, { type: "submit", disabled: (snapshot.isUnchanged && !extensionDirty && !isFieldless && !insideDrawer) ||
1116
+ }, 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) ||
973
1117
  isSubmitting ||
974
- disabled, loading: isSubmitting, variant: submitVariant ?? "primary", testId: "render-edit-submit", children: translate(submitLabel ?? (isWizard ? "kumiko.actions.finish" : "kumiko.actions.save")) }))] }));
1118
+ disabled, loading: isSubmitting, variant: submitVariant ?? "primary", testId: "render-edit-submit", children: translate(submitLabel ??
1119
+ (isWizard
1120
+ ? "kumiko.actions.finish"
1121
+ : hasDirtyFooter
1122
+ ? "kumiko.form.saveChanges"
1123
+ : "kumiko.actions.save")) })), writeFormFooterAction] }));
975
1124
  // Title + Subtitle, create/edit-bewusst. i18n-Keys (mode = "create"|"edit"):
976
1125
  // screen:<id>.<mode>.title / .<mode>.subtitle
977
1126
  // Fallback-Kette: mode-spezifisch → generisch (screen:<id>.title/.subtitle).
@@ -982,18 +1131,14 @@ export function RenderEdit(props) {
982
1131
  // prose, same as it does for an unknown key (fw#2723 review). Same slot
983
1132
  // section.description already fills for a section. Falls back to
984
1133
  // undefined (no subtitle) when neither is set.
985
- const isCreate = (() => {
986
- const id = resolveExtensionEntityId(entityIdProp, vm.id);
987
- return id == null || id === "";
988
- })();
989
1134
  const formMode = isCreate ? "create" : "edit";
990
1135
  const recordTitle = !isCreate && screen.recordTitleField !== undefined
991
1136
  ? resolveRecordTitle(initial[screen.recordTitleField])
992
1137
  : undefined;
993
1138
  const resolveScreenText = (suffix) => {
994
1139
  for (const key of [
995
- `screen:${screen.id}.${formMode}.${suffix}`,
996
- `screen:${screen.id}.${suffix}`,
1140
+ `screen:${i18nScreenId}.${formMode}.${suffix}`,
1141
+ `screen:${i18nScreenId}.${suffix}`,
997
1142
  ]) {
998
1143
  const value = translate(key);
999
1144
  if (value !== key)
@@ -1001,26 +1146,26 @@ export function RenderEdit(props) {
1001
1146
  }
1002
1147
  return undefined;
1003
1148
  };
1004
- const plainFormTitle = resolveScreenText("title") ?? screen.id;
1149
+ const plainFormTitle = resolveScreenText("title") ?? i18nScreenId;
1005
1150
  const formTitle = resolveTitleTemplate(screen.titleTemplate, snapshot.values, translate, plainFormTitle);
1006
- // screen.description is head-card copy, not tab content — in tabs mode
1007
- // (hideSectionTitles) the head card already carries title/subtitle/status.
1151
+ // In tabs mode (hideSectionTitles) screen.description is dropped — the head
1152
+ // card carries it — but an explicit subtitle i18n override has no other
1153
+ // source there, so it stays unless it merely resolves to the description.
1154
+ const explicitSubtitle = resolveScreenText("subtitle");
1155
+ const descriptionSubtitle = screen.description !== undefined ? translate(screen.description) : undefined;
1008
1156
  const formSubtitle = hideSectionTitles !== true
1009
- ? (resolveScreenText("subtitle") ??
1010
- (screen.description !== undefined ? translate(screen.description) : undefined))
1011
- : undefined;
1157
+ ? (explicitSubtitle ?? descriptionSubtitle)
1158
+ : explicitSubtitle !== descriptionSubtitle
1159
+ ? explicitSubtitle
1160
+ : undefined;
1161
+ // Embedded settings-list forms (dashboard panels) have no shell header: the
1162
+ // form title/subtitle move into the first section's header column instead of
1163
+ // floating as a heading above the band.
1164
+ const titleInSettingsHeader = isSettingsList && !titleInShell;
1012
1165
  const usesFieldFlow = isScreenForm || insideDrawer;
1013
1166
  const allRequiredHint = (_jsx(Text, { variant: "muted", testId: "render-edit-all-required-hint", children: translate("kumiko.form.all-required") }));
1014
1167
  const stepRail = isWizard && isScreenForm && StepBar !== undefined;
1015
1168
  const upNextSection = screen.layout.wizard?.aside?.upNext === true ? filteredSections[currentStep + 1] : undefined;
1016
- const changedFieldCount = isCreate ? 0 : Object.keys(snapshot.changes).length;
1017
- const unsavedCount = isCreate || !isScreenForm
1018
- ? 0
1019
- : changedFieldCount > 0
1020
- ? changedFieldCount
1021
- : extensionDirty
1022
- ? 1
1023
- : 0;
1024
1169
  const sectionNavItems = isScreenForm && !isStepped
1025
1170
  ? filteredSections.flatMap((section, index) => section.kind !== "extension" &&
1026
1171
  section.kind !== "relatedList" &&
@@ -1046,8 +1191,13 @@ export function RenderEdit(props) {
1046
1191
  !pageHeaderCompact &&
1047
1192
  ActionOverflowMenu !== undefined && {
1048
1193
  actions: (_jsx(ActionOverflowMenu, { label: translate("kumiko.list.row-actions.more"), items: headerMenuItems, testId: "render-edit-header-menu" })),
1049
- }) })), _jsx(Form, { onSubmit: () => void handleSubmit(), ...(hideSectionTitles !== true && !titleInShell && { title: formTitle }), ...(formSubtitle !== undefined && !stepRail && { subtitle: formSubtitle }), ...(isScreenForm && { screenForm: true }), ...(unsavedCount > 0 && { unsavedCount }), ...(sectionNavItems.length > 0 && { sectionNav: sectionNavItems }), ...(stepRail && {
1050
- sideRail: (_jsx(StepBar, { steps: filteredSections.map((section) => section.title ?? ""), currentIndex: currentStep, compactLabel: compactStepLabel, onStepSelect: handleWizardJumpBack, orientation: "vertical", heading: translate("kumiko.wizard.step", {
1194
+ }) })), _jsx(Form, { onSubmit: () => void handleSubmit(), ...(hideSectionTitles !== true &&
1195
+ !titleInShell &&
1196
+ !titleInSettingsHeader && { title: formTitle }), ...(formSubtitle !== undefined &&
1197
+ !stepRail &&
1198
+ !titleInSettingsHeader && { subtitle: formSubtitle }), ...(isScreenForm && { screenForm: true }), ...(unsavedCount !== undefined &&
1199
+ (unsavedCount > 0 || hasDirtyFooter) && { unsavedCount }), ...(sectionNavItems.length > 0 && { sectionNav: sectionNavItems }), ...(stepRail && {
1200
+ sideRail: (_jsx(StepBar, { steps: filteredSections.map((section) => section.title ?? ""), currentIndex: currentStep, compactLabel: compactStepLabel, onStepSelect: handleWizardStepSelect, ...updateModeStepBarProps, orientation: "vertical", heading: translate("kumiko.wizard.step", {
1051
1201
  current: currentStep + 1,
1052
1202
  total: lastStepIndex + 1,
1053
1203
  }), ...(formSubtitle !== undefined && { description: formSubtitle }), subtitles: filteredSections.map((section) => section.kind === "fields" ? section.subtitle : undefined), ...(upNextSection !== undefined && {
@@ -1059,12 +1209,12 @@ export function RenderEdit(props) {
1059
1209
  },
1060
1210
  }), testId: "render-edit-wizard-steps", compactTestId: "render-edit-wizard-step-label" })),
1061
1211
  }), ...(hideActions !== true && hasFormActions && { actions: formActions }), ...(hideActions !== true &&
1062
- hasSecondaryFormActions && { secondaryActions: secondaryFormActions }), testId: "render-edit-form", stickyActions: isWizard || fillScreenHeight === true, ...(screen.layout.width !== undefined && { width: screen.layout.width }), ...(formHeaderRegion !== undefined && { headerRegion: formHeaderRegion }), ...(titleActionMount !== undefined && { titleAction: titleActionMount }), ...(fillHeight && { fillHeight }), ...(hideSectionTitles === true && { chromeless: true }), ...(summary !== undefined && { summary }), children: _jsxs(AllFieldsRequiredProvider, { value: showsAllRequiredHint, children: [showsAllRequiredHint && !insideDrawer && allRequiredHint, draftCandidates !== null && (_jsx(Banner, { variant: "info", testId: "render-edit-draft-picker", actions: [
1212
+ hasSecondaryFormActions && { secondaryActions: secondaryFormActions }), testId: "render-edit-form", stickyActions: isWizard || fillScreenHeight === true, ...(screen.layout.width !== undefined && { width: screen.layout.width }), ...(formHeaderRegion !== undefined && { headerRegion: formHeaderRegion }), ...(titleActionMount !== undefined && { titleAction: titleActionMount }), ...(fillHeight && { fillHeight }), ...(hideSectionTitles === true && { chromeless: true }), ...(summary !== undefined && { summary }), children: _jsxs(AllFieldsRequiredProvider, { value: showsAllRequiredHint, children: [leadContent, showsAllRequiredHint && !insideDrawer && allRequiredHint, draftCandidates !== null && (_jsx(Banner, { variant: "info", testId: "render-edit-draft-picker", actions: [
1063
1213
  ...draftCandidates.map((candidate) => (_jsx(Button, { type: "button", variant: "link", onClick: () => adoptDraft(candidate), testId: `render-edit-draft-pick-${candidate.id}`, children: formatWhen(candidate.savedAt) }, candidate.id))),
1064
1214
  _jsx(Button, { type: "button", variant: "link", onClick: () => setDraftCandidates(null), testId: "render-edit-draft-start-new", children: translate("kumiko.form.draft.start-new") }, "start-new"),
1065
1215
  ], children: _jsx(Text, { children: translate(draftCandidates.length === 1
1066
1216
  ? "kumiko.form.draft.resume-single"
1067
- : "kumiko.form.draft.resume-multiple") }) })), isWizard && !stepRail && (_jsxs(_Fragment, { children: [Progress !== undefined && (_jsx(Progress, { value: (currentStep + 1) / (lastStepIndex + 1), testId: "render-edit-wizard-progress" })), StepBar === undefined ? (_jsx(Text, { variant: "small", testId: "render-edit-wizard-step-label", children: compactStepLabel })) : (_jsx(StepBar, { steps: filteredSections.map((section) => section.title ?? ""), currentIndex: currentStep, compactLabel: compactStepLabel, onStepSelect: handleWizardJumpBack, testId: "render-edit-wizard-steps", compactTestId: "render-edit-wizard-step-label" }))] })), isTabs &&
1217
+ : "kumiko.form.draft.resume-multiple") }) })), isWizard && !stepRail && (_jsxs(_Fragment, { children: [Progress !== undefined && (_jsx(Progress, { value: (currentStep + 1) / (lastStepIndex + 1), testId: "render-edit-wizard-progress" })), StepBar === undefined ? (_jsx(Text, { variant: "small", testId: "render-edit-wizard-step-label", children: compactStepLabel })) : (_jsx(StepBar, { steps: filteredSections.map((section) => section.title ?? ""), currentIndex: currentStep, compactLabel: compactStepLabel, onStepSelect: handleWizardStepSelect, ...updateModeStepBarProps, testId: "render-edit-wizard-steps", compactTestId: "render-edit-wizard-step-label" }))] })), isTabs &&
1068
1218
  (() => {
1069
1219
  // No Tabs primitive registered → every section renders stacked
1070
1220
  // (stepHidden below is gated on the same condition), which is the
@@ -1080,7 +1230,7 @@ export function RenderEdit(props) {
1080
1230
  // skip: the strip reported an id no section owns.
1081
1231
  if (index === -1)
1082
1232
  return;
1083
- setRawStep(index);
1233
+ jumpToStep(index);
1084
1234
  } }));
1085
1235
  })(), filteredSections.map((section, sectionIndex) => {
1086
1236
  // turns this section's own `actions` (RowAction[]) into
@@ -1120,7 +1270,11 @@ export function RenderEdit(props) {
1120
1270
  const mount = (_jsx(ExtensionSectionMount, { section: section, entityName: vm.entityName, entityId: resolveExtensionEntityId(entityIdProp, vm.id), initialValues: extensionInitialValues, values: snapshot.values,
1121
1271
  // @cast-boundary form-values: ExtensionSectionProps is not generic
1122
1272
  // over TValues; controller is mount-lifetime-stable, see onControlsReady above.
1123
- patch: patchAndScheduleDraftSave, validate: scopedValidate, hideTitle: hideSectionTitles, ...(hideSectionTitles !== true &&
1273
+ patch: patchAndScheduleDraftSave, validate: scopedValidate, hideTitle: hideSectionTitles, ...(isWizard &&
1274
+ !isCreateMode && {
1275
+ stepKey: wizardStepKey(section, sectionIndex),
1276
+ onReportStepComplete: reportExtensionStepComplete,
1277
+ }), ...(hideSectionTitles !== true &&
1124
1278
  sectionActionsEl !== undefined && { actions: sectionActionsEl }) }, section.title));
1125
1279
  // Tabs mode: an unframed padded panel (the tab panel is the
1126
1280
  // surface), so extension content neither sits flush at the shell
@@ -1153,19 +1307,29 @@ export function RenderEdit(props) {
1153
1307
  // Own submit button + dispatcher call, entirely independent of
1154
1308
  // this screen's (nonexistent, on projectionDetail) form submit —
1155
1309
  // rejected at boot in wizard layouts, so no WizardStepGroup here.
1156
- return (_jsx(WriteFormSection, { section: section, featureName: featureName, translate: translate, hideTitle: hideSectionTitles, onSubmitted: () => onReload?.(), ...(sectionActionsEl !== undefined && { actions: sectionActionsEl }) }, section.title ?? `write-form-${sectionIndex}`));
1310
+ return (_jsx(WriteFormSection, { section: section, featureName: featureName, translate: translate, hideTitle: hideSectionTitles, onSubmitted: () => onReload?.(), ...(isLoneWriteFormTab && { onFooterAction: setWriteFormFooterAction }), ...(sectionActionsEl !== undefined && { actions: sectionActionsEl }) }, section.title ?? `write-form-${sectionIndex}`));
1157
1311
  }
1158
1312
  if (!section.visible)
1159
1313
  return null;
1160
1314
  // Titellose Sections kollidieren sonst auf key/testId — Index-Fallback.
1161
1315
  const sectionKey = section.title ?? `section-${sectionIndex}`;
1162
- const renderFieldGrid = (fields, columns, gridKey) => (_jsx(Grid, { columns: columns, ...(usesFieldFlow && { flow: true }), children: fields.map((field) => (_jsx(GridCellForField, { field: disabled ? { ...field, readOnly: true } : field, columns: columns, ...(usesFieldFlow && { flow: true }), issues: snapshot.errors[field.field], onChange: (v) => {
1316
+ const renderFieldGrid = (fields, columns, gridKey) => (_jsx(Grid, { columns: columns, ...(isSettingsList ? { list: true } : usesFieldFlow && { flow: true }), children: fields.map((field) => (_jsx(GridCellForField, { field: disabled ? { ...field, readOnly: true } : field, columns: columns, ...(!isSettingsList && usesFieldFlow && { flow: true }), ...(isSettingsList && {
1317
+ layout: "row",
1318
+ accent: fieldAccent?.(field.field) === true,
1319
+ }), issues: snapshot.errors[field.field], onChange: (v) => {
1163
1320
  controller.setField(field.field, v);
1321
+ if (validateOnChange === true)
1322
+ controller.validate([field.field]);
1164
1323
  }, GridCell: GridCell, featureName: featureName, ...(labelAppendix !== undefined && {
1165
1324
  labelAppendix: labelAppendix(field.field),
1166
1325
  }), ...(fieldAppendix !== undefined && {
1167
1326
  fieldAppendix: fieldAppendix(field.field),
1168
- }), allIssues: snapshot.errors, valueDisplay: valueDisplay, row: snapshot.values, ...(!isCreate && field.field in snapshot.changes && { changed: true }) }, field.field))) }, gridKey));
1327
+ }), ...(fieldAppendixPlacement !== undefined && {
1328
+ appendixPlacement: fieldAppendixPlacement,
1329
+ }), ...(fieldDescription !== undefined && {
1330
+ description: fieldDescription(field.field),
1331
+ }), allIssues: snapshot.errors, valueDisplay: valueDisplay, row: snapshot.values, ...((!isCreate || markChangedFields) &&
1332
+ field.field in snapshot.changes && { changed: true }) }, field.field))) }, gridKey));
1169
1333
  // Tabs mode: the tab panel is the surface, so field sections render
1170
1334
  // as unframed padded panels; group cards inside stay framed.
1171
1335
  // Stays `Card`, not `Section` — Section always flattens to a
@@ -1196,20 +1360,28 @@ export function RenderEdit(props) {
1196
1360
  // tab label right above it. Without a Tabs primitive the sections
1197
1361
  // render stacked, and then the titles are what tells them apart.
1198
1362
  const repeatsTabLabel = isTabs && isStepped;
1199
- const sectionTitle = section.title === formTitle || repeatsTabLabel ? undefined : section.title;
1363
+ const headerFallback = titleInSettingsHeader && sectionIndex === 0;
1364
+ const sectionTitle = headerFallback
1365
+ ? (section.title ?? formTitle)
1366
+ : section.title === formTitle || repeatsTabLabel
1367
+ ? undefined
1368
+ : section.title;
1369
+ const sectionSubtitle = section.description ?? (headerFallback ? formSubtitle : undefined);
1200
1370
  const sectionEl = (_jsxs(Section, { ...(sectionNavItems.some((item) => item.id === formSectionDomId(sectionIndex)) && {
1201
1371
  id: formSectionDomId(sectionIndex),
1202
- }), ...(sectionTitle !== undefined && { title: sectionTitle }), ...(section.description !== undefined && { subtitle: section.description }), ...(section.icon !== undefined && { icon: section.icon }), ...(sectionActionsEl !== undefined && { actions: sectionActionsEl }), testId: `section-${sectionKey}`, children: [showsAllRequiredHint && insideDrawer && sectionIndex === 0 && allRequiredHint, renderFieldGrid(section.fields, section.columns, `${sectionKey}-grid`)] }, sectionKey));
1372
+ }), ...(sectionTitle !== undefined && { title: sectionTitle }), ...(sectionSubtitle !== undefined && { subtitle: sectionSubtitle }), ...(section.icon !== undefined && { icon: section.icon }), ...(sectionActionsEl !== undefined && { actions: sectionActionsEl }), ...(isSettingsList && { layout: "settings-list" }), testId: `section-${sectionKey}`, children: [showsAllRequiredHint && insideDrawer && sectionIndex === 0 && allRequiredHint, renderFieldGrid(section.fields, section.columns, `${sectionKey}-grid`)] }, sectionKey));
1203
1373
  return wrapWizardStep(sectionKey, sectionEl);
1204
- }), formError !== null && (_jsx(Banner, { variant: "error", testId: "render-edit-form-error", actions: formError.code === "version_conflict" && onReload !== undefined ? (_jsx(Button, { variant: "secondary", onClick: () => {
1374
+ }), formError !== null && (_jsxs(Banner, { variant: "error", testId: "render-edit-form-error", actions: formError.code === "version_conflict" && onReload !== undefined ? (_jsx(Button, { variant: "secondary", onClick: () => {
1205
1375
  onReload();
1206
1376
  setFormError(null);
1207
- }, testId: "render-edit-form-error-reload", children: translate("kumiko.actions.reload") })) : undefined, children: _jsx(Text, { testId: "render-edit-form-error-key", children: translate(formError.i18nKey, formError.i18nParams) }) })), extensionErrorKey !== null && (_jsx(Banner, { variant: "error", testId: "render-edit-extension-error", children: _jsx(Text, { testId: "render-edit-extension-error-key", children: translate(extensionErrorKey) }) })), actionError !== null && (_jsx(Banner, { variant: "error", testId: "render-edit-action-error", children: actionError })), onDelete !== undefined && (_jsx(Dialog, { open: confirmDeleteOpen, onOpenChange: setConfirmDeleteOpen, title: translate("kumiko.actions.delete-confirm"), confirmLabel: translate("kumiko.actions.delete"), variant: "danger", onConfirm: async () => {
1377
+ setExtraDeleteIssues([]);
1378
+ }, testId: "render-edit-form-error-reload", children: translate("kumiko.actions.reload") })) : undefined, children: [_jsx(Text, { testId: "render-edit-form-error-key", children: translate(formError.i18nKey, formError.i18nParams) }), extraDeleteIssues.map((issue) => (_jsx(Text, { testId: "render-edit-form-error-extra", children: translate(issue.i18nKey, issue.params) }, `${issue.path}:${issue.i18nKey}`)))] })), (snapshot.errors["(root)"] ?? []).map((issue) => (_jsx(Banner, { variant: "error", testId: "render-edit-root-issue", children: _jsx(Text, { children: translate(issue.i18nKey, issue.params) }) }, issue.i18nKey))), extensionErrorKey !== null && (_jsx(Banner, { variant: "error", testId: "render-edit-extension-error", children: _jsx(Text, { testId: "render-edit-extension-error-key", children: translate(extensionErrorKey) }) })), actionError !== null && (_jsx(Banner, { variant: "error", testId: "render-edit-action-error", children: actionError })), onDelete !== undefined && (_jsx(Dialog, { open: confirmDeleteOpen, onOpenChange: setConfirmDeleteOpen, title: translate("kumiko.actions.delete-confirm"), confirmLabel: translate("kumiko.actions.delete"), variant: "danger", onConfirm: async () => {
1208
1379
  const rejection = await onDelete();
1209
1380
  // A rejected delete (e.g. a preDelete hook) carries its reason in
1210
1381
  // the field issues; the top-level key is only "validation failed".
1211
1382
  if (rejection !== undefined) {
1212
- const issue = rejection.details?.fields?.[0];
1383
+ const [issue, ...otherIssues] = rejection.details?.fields ?? [];
1384
+ setExtraDeleteIssues(otherIssues);
1213
1385
  setFormError(issue === undefined
1214
1386
  ? rejection
1215
1387
  : { ...rejection, i18nKey: issue.i18nKey, i18nParams: issue.params });
@@ -1,5 +1,6 @@
1
1
  import { type EditFieldViewModel, type FieldIssue } from "@cosmicdrift/kumiko-headless";
2
2
  import { type ReactNode } from "react";
3
+ import { type FieldProps } from "../primitives.js";
3
4
  export type RenderFieldProps = {
4
5
  readonly field: EditFieldViewModel;
5
6
  readonly issues?: readonly FieldIssue[];
@@ -14,6 +15,9 @@ export type RenderFieldProps = {
14
15
  /** Optionaler Zusatz-Inhalt der nach dem Input gerendert wird (z.B.
15
16
  * ConfigCascade). */
16
17
  readonly fieldAppendix?: ReactNode;
18
+ readonly appendixPlacement?: FieldProps["appendixPlacement"];
19
+ readonly description?: ReactNode;
20
+ readonly status?: FieldProps["status"];
17
21
  /** Flat issues-by-path map (FormSnapshot.errors) — only relevant for
18
22
  * type:"embedded" with embeddedListCells, to bucket row-/cell-issues
19
23
  * (`${field}.${rowIndex}` / `${field}.${rowIndex}.${cellField}`).
@@ -33,6 +37,8 @@ export type RenderFieldProps = {
33
37
  readonly row?: Readonly<Record<string, unknown>>;
34
38
  /** Marks the field as modified against the loaded record (edit mode only). */
35
39
  readonly changed?: boolean;
40
+ readonly layout?: FieldProps["layout"];
41
+ readonly accent?: boolean;
36
42
  };
37
43
  export declare const AllFieldsRequiredProvider: import("react").Provider<boolean>;
38
- export declare function RenderField({ field, issues, onChange, featureName, labelAppendix, fieldAppendix, allIssues, valueDisplay, row, changed, }: RenderFieldProps): ReactNode;
44
+ export declare function RenderField({ field, issues, onChange, featureName, labelAppendix, fieldAppendix, appendixPlacement, description, status, allIssues, valueDisplay, row, changed, layout, accent, }: RenderFieldProps): ReactNode;