@cosmicdrift/kumiko-renderer 0.333.0 → 0.334.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.
- package/dist/app/config-edit-shim.d.ts +7 -0
- package/dist/app/config-edit-shim.js +39 -0
- package/dist/app/kumiko-screen.js +63 -36
- package/dist/app/query-state-banners.d.ts +8 -0
- package/dist/app/query-state-banners.js +14 -0
- package/dist/app/secret-mint-body.js +3 -3
- package/dist/app/secrets-edit-body.js +46 -17
- package/dist/components/grid-cell-for-field.d.ts +7 -2
- package/dist/components/grid-cell-for-field.js +2 -2
- package/dist/components/query-options-select.d.ts +12 -3
- package/dist/components/query-options-select.js +45 -2
- package/dist/components/related-list-section.js +1 -5
- package/dist/components/render-edit-types.d.ts +20 -0
- package/dist/components/render-edit.js +64 -26
- package/dist/components/render-field.d.ts +7 -1
- package/dist/components/render-field.js +32 -4
- package/dist/components/write-form-section.d.ts +5 -1
- package/dist/components/write-form-section.js +17 -5
- package/dist/context/embedded-screen-context.d.ts +5 -0
- package/dist/context/embedded-screen-context.js +12 -0
- package/dist/i18n-defaults.js +30 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/primitives.d.ts +54 -12
- package/package.json +5 -5
|
@@ -162,7 +162,7 @@ function resolveTitleTemplate(template, values, translate, fallback) {
|
|
|
162
162
|
return translate(template, values);
|
|
163
163
|
}
|
|
164
164
|
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;
|
|
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, fieldAppendixPlacement, fieldDescription, fieldAccent, markChangedFields = false, entityId: entityIdProp, extensionInitialValues, onChange, onControlsReady, fields: fieldsFilter, disabled = false, hideActions, valueDisplay = "form", hideSectionTitles, fillScreenHeight, dirtyFooter, validateOnChange, headerRegion, buildSectionActions, } = props;
|
|
166
166
|
const { customSubmit } = props;
|
|
167
167
|
// Translate-Fallback: wenn der Caller keine Translate-Fn übergibt,
|
|
168
168
|
// konsumieren wir den i18next-Context direkt. Sonst wären Field-
|
|
@@ -569,6 +569,11 @@ export function RenderEdit(props) {
|
|
|
569
569
|
// keeps normal document-flow height untouched.
|
|
570
570
|
const fillHeight = fillScreenHeight === true ||
|
|
571
571
|
(hideSectionTitles === true && filteredSections[0]?.kind === "relatedList");
|
|
572
|
+
// A lone writeForm tab is the whole page, so its submit joins this form's
|
|
573
|
+
// footer (pinned to the window edge like every other screen's save)
|
|
574
|
+
// instead of sitting in the section's title row.
|
|
575
|
+
const isLoneWriteFormTab = hideSectionTitles === true && filteredSections[0]?.kind === "writeForm";
|
|
576
|
+
const [writeFormFooterAction, setWriteFormFooterAction] = useState(undefined);
|
|
572
577
|
// Persistiert alle composed Extension-Sections mit der aufgelösten entityId.
|
|
573
578
|
// false = eine Section schlug fehl (ihr i18n-Key landet im Banner). Ohne
|
|
574
579
|
// Entity-Kontext (create-mode ohne route-id) gibt es nichts zu schreiben.
|
|
@@ -872,10 +877,7 @@ export function RenderEdit(props) {
|
|
|
872
877
|
handleSubmitRef.current = handleSubmit;
|
|
873
878
|
// Screen forms (entityEdit/actionForm filling the shell height) drop the card;
|
|
874
879
|
// 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;
|
|
880
|
+
const isScreenForm = fillScreenHeight === true && hideSectionTitles !== true && !insideDrawer;
|
|
879
881
|
const titleInShell = PageHeader !== undefined &&
|
|
880
882
|
pageHeaderSlotAvailable &&
|
|
881
883
|
hideSectionTitles !== true &&
|
|
@@ -960,8 +962,26 @@ export function RenderEdit(props) {
|
|
|
960
962
|
const hasFormActions = (isWizard && currentStep > 0) ||
|
|
961
963
|
(isWizard && !isLastWizardStep) ||
|
|
962
964
|
(showsSubmit && (!isWizard || isLastWizardStep)) ||
|
|
963
|
-
footerSlot !== undefined
|
|
965
|
+
footerSlot !== undefined ||
|
|
966
|
+
writeFormFooterAction !== undefined;
|
|
964
967
|
const nextStepTitle = isWizard ? filteredSections[currentStep + 1]?.title : undefined;
|
|
968
|
+
const isCreate = (() => {
|
|
969
|
+
const id = resolveExtensionEntityId(entityIdProp, vm.id);
|
|
970
|
+
return id == null || id === "";
|
|
971
|
+
})();
|
|
972
|
+
const changedFieldCount = Object.keys(snapshot.changes).length;
|
|
973
|
+
// Edit-mode screen forms always carry the dirty status (0 = quiet "No unsaved
|
|
974
|
+
// changes"); wizards keep the status-only-when-dirty behaviour.
|
|
975
|
+
const hasDirtyFooter = !isWizard &&
|
|
976
|
+
(dirtyFooter === true || (isScreenForm && !isCreate && headerRegion === undefined));
|
|
977
|
+
const unsavedCount = !hasDirtyFooter
|
|
978
|
+
? undefined
|
|
979
|
+
: changedFieldCount > 0
|
|
980
|
+
? changedFieldCount
|
|
981
|
+
: extensionDirty
|
|
982
|
+
? 1
|
|
983
|
+
: 0;
|
|
984
|
+
const isSettingsList = screen.layout.variant === "settings-list" && hideSectionTitles !== true;
|
|
965
985
|
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
|
|
966
986
|
? translate("kumiko.wizard.next-with-title", { title: nextStepTitle })
|
|
967
987
|
: translate("kumiko.actions.next") })), showsSubmit &&
|
|
@@ -969,9 +989,14 @@ export function RenderEdit(props) {
|
|
|
969
989
|
footerActions?.map((action) => (_jsx(Button, { type: "button", variant: action.variant ?? "secondary", ...(action.icon !== undefined && { icon: action.icon }), disabled: isSubmitting || disabled, onClick: () => {
|
|
970
990
|
controller.setValues(action.patch); // @cast-boundary form-values
|
|
971
991
|
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) ||
|
|
992
|
+
}, 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") })), showsSubmit && (!isWizard || isLastWizardStep) && (_jsx(Button, { type: "submit", disabled: (snapshot.isUnchanged && !extensionDirty && !isFieldless && !insideDrawer) ||
|
|
973
993
|
isSubmitting ||
|
|
974
|
-
disabled, loading: isSubmitting, variant: submitVariant ?? "primary", testId: "render-edit-submit", children: translate(submitLabel ??
|
|
994
|
+
disabled, loading: isSubmitting, variant: submitVariant ?? "primary", testId: "render-edit-submit", children: translate(submitLabel ??
|
|
995
|
+
(isWizard
|
|
996
|
+
? "kumiko.actions.finish"
|
|
997
|
+
: hasDirtyFooter
|
|
998
|
+
? "kumiko.form.saveChanges"
|
|
999
|
+
: "kumiko.actions.save")) })), writeFormFooterAction] }));
|
|
975
1000
|
// Title + Subtitle, create/edit-bewusst. i18n-Keys (mode = "create"|"edit"):
|
|
976
1001
|
// screen:<id>.<mode>.title / .<mode>.subtitle
|
|
977
1002
|
// Fallback-Kette: mode-spezifisch → generisch (screen:<id>.title/.subtitle).
|
|
@@ -982,10 +1007,6 @@ export function RenderEdit(props) {
|
|
|
982
1007
|
// prose, same as it does for an unknown key (fw#2723 review). Same slot
|
|
983
1008
|
// section.description already fills for a section. Falls back to
|
|
984
1009
|
// undefined (no subtitle) when neither is set.
|
|
985
|
-
const isCreate = (() => {
|
|
986
|
-
const id = resolveExtensionEntityId(entityIdProp, vm.id);
|
|
987
|
-
return id == null || id === "";
|
|
988
|
-
})();
|
|
989
1010
|
const formMode = isCreate ? "create" : "edit";
|
|
990
1011
|
const recordTitle = !isCreate && screen.recordTitleField !== undefined
|
|
991
1012
|
? resolveRecordTitle(initial[screen.recordTitleField])
|
|
@@ -1009,18 +1030,14 @@ export function RenderEdit(props) {
|
|
|
1009
1030
|
? (resolveScreenText("subtitle") ??
|
|
1010
1031
|
(screen.description !== undefined ? translate(screen.description) : undefined))
|
|
1011
1032
|
: undefined;
|
|
1033
|
+
// Embedded settings-list forms (dashboard panels) have no shell header: the
|
|
1034
|
+
// form title/subtitle move into the first section's header column instead of
|
|
1035
|
+
// floating as a heading above the band.
|
|
1036
|
+
const titleInSettingsHeader = isSettingsList && !titleInShell;
|
|
1012
1037
|
const usesFieldFlow = isScreenForm || insideDrawer;
|
|
1013
1038
|
const allRequiredHint = (_jsx(Text, { variant: "muted", testId: "render-edit-all-required-hint", children: translate("kumiko.form.all-required") }));
|
|
1014
1039
|
const stepRail = isWizard && isScreenForm && StepBar !== undefined;
|
|
1015
1040
|
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
1041
|
const sectionNavItems = isScreenForm && !isStepped
|
|
1025
1042
|
? filteredSections.flatMap((section, index) => section.kind !== "extension" &&
|
|
1026
1043
|
section.kind !== "relatedList" &&
|
|
@@ -1046,7 +1063,12 @@ export function RenderEdit(props) {
|
|
|
1046
1063
|
!pageHeaderCompact &&
|
|
1047
1064
|
ActionOverflowMenu !== undefined && {
|
|
1048
1065
|
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 &&
|
|
1066
|
+
}) })), _jsx(Form, { onSubmit: () => void handleSubmit(), ...(hideSectionTitles !== true &&
|
|
1067
|
+
!titleInShell &&
|
|
1068
|
+
!titleInSettingsHeader && { title: formTitle }), ...(formSubtitle !== undefined &&
|
|
1069
|
+
!stepRail &&
|
|
1070
|
+
!titleInSettingsHeader && { subtitle: formSubtitle }), ...(isScreenForm && { screenForm: true }), ...(unsavedCount !== undefined &&
|
|
1071
|
+
(unsavedCount > 0 || hasDirtyFooter) && { unsavedCount }), ...(sectionNavItems.length > 0 && { sectionNav: sectionNavItems }), ...(stepRail && {
|
|
1050
1072
|
sideRail: (_jsx(StepBar, { steps: filteredSections.map((section) => section.title ?? ""), currentIndex: currentStep, compactLabel: compactStepLabel, onStepSelect: handleWizardJumpBack, orientation: "vertical", heading: translate("kumiko.wizard.step", {
|
|
1051
1073
|
current: currentStep + 1,
|
|
1052
1074
|
total: lastStepIndex + 1,
|
|
@@ -1153,19 +1175,29 @@ export function RenderEdit(props) {
|
|
|
1153
1175
|
// Own submit button + dispatcher call, entirely independent of
|
|
1154
1176
|
// this screen's (nonexistent, on projectionDetail) form submit —
|
|
1155
1177
|
// 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}`));
|
|
1178
|
+
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
1179
|
}
|
|
1158
1180
|
if (!section.visible)
|
|
1159
1181
|
return null;
|
|
1160
1182
|
// Titellose Sections kollidieren sonst auf key/testId — Index-Fallback.
|
|
1161
1183
|
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 }),
|
|
1184
|
+
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 && {
|
|
1185
|
+
layout: "row",
|
|
1186
|
+
accent: fieldAccent?.(field.field) === true,
|
|
1187
|
+
}), issues: snapshot.errors[field.field], onChange: (v) => {
|
|
1163
1188
|
controller.setField(field.field, v);
|
|
1189
|
+
if (validateOnChange === true)
|
|
1190
|
+
controller.validate([field.field]);
|
|
1164
1191
|
}, GridCell: GridCell, featureName: featureName, ...(labelAppendix !== undefined && {
|
|
1165
1192
|
labelAppendix: labelAppendix(field.field),
|
|
1166
1193
|
}), ...(fieldAppendix !== undefined && {
|
|
1167
1194
|
fieldAppendix: fieldAppendix(field.field),
|
|
1168
|
-
}),
|
|
1195
|
+
}), ...(fieldAppendixPlacement !== undefined && {
|
|
1196
|
+
appendixPlacement: fieldAppendixPlacement,
|
|
1197
|
+
}), ...(fieldDescription !== undefined && {
|
|
1198
|
+
description: fieldDescription(field.field),
|
|
1199
|
+
}), allIssues: snapshot.errors, valueDisplay: valueDisplay, row: snapshot.values, ...((!isCreate || markChangedFields) &&
|
|
1200
|
+
field.field in snapshot.changes && { changed: true }) }, field.field))) }, gridKey));
|
|
1169
1201
|
// Tabs mode: the tab panel is the surface, so field sections render
|
|
1170
1202
|
// as unframed padded panels; group cards inside stay framed.
|
|
1171
1203
|
// Stays `Card`, not `Section` — Section always flattens to a
|
|
@@ -1196,10 +1228,16 @@ export function RenderEdit(props) {
|
|
|
1196
1228
|
// tab label right above it. Without a Tabs primitive the sections
|
|
1197
1229
|
// render stacked, and then the titles are what tells them apart.
|
|
1198
1230
|
const repeatsTabLabel = isTabs && isStepped;
|
|
1199
|
-
const
|
|
1231
|
+
const headerFallback = titleInSettingsHeader && sectionIndex === 0;
|
|
1232
|
+
const sectionTitle = headerFallback
|
|
1233
|
+
? (section.title ?? formTitle)
|
|
1234
|
+
: section.title === formTitle || repeatsTabLabel
|
|
1235
|
+
? undefined
|
|
1236
|
+
: section.title;
|
|
1237
|
+
const sectionSubtitle = section.description ?? (headerFallback ? formSubtitle : undefined);
|
|
1200
1238
|
const sectionEl = (_jsxs(Section, { ...(sectionNavItems.some((item) => item.id === formSectionDomId(sectionIndex)) && {
|
|
1201
1239
|
id: formSectionDomId(sectionIndex),
|
|
1202
|
-
}), ...(sectionTitle !== undefined && { title: sectionTitle }), ...(
|
|
1240
|
+
}), ...(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
1241
|
return wrapWizardStep(sectionKey, sectionEl);
|
|
1204
1242
|
}), formError !== null && (_jsx(Banner, { variant: "error", testId: "render-edit-form-error", actions: formError.code === "version_conflict" && onReload !== undefined ? (_jsx(Button, { variant: "secondary", onClick: () => {
|
|
1205
1243
|
onReload();
|
|
@@ -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;
|
|
@@ -19,7 +19,7 @@ import { QueryOptionsSelect } from "./query-options-select.js";
|
|
|
19
19
|
import { ReferenceCreateDialog } from "./reference-create-dialog.js";
|
|
20
20
|
const AllFieldsRequiredContext = createContext(false);
|
|
21
21
|
export const AllFieldsRequiredProvider = AllFieldsRequiredContext.Provider;
|
|
22
|
-
export function RenderField({ field, issues, onChange, featureName, labelAppendix, fieldAppendix, allIssues, valueDisplay = "form", row, changed, }) {
|
|
22
|
+
export function RenderField({ field, issues, onChange, featureName, labelAppendix, fieldAppendix, appendixPlacement, description, status, allIssues, valueDisplay = "form", row, changed, layout, accent, }) {
|
|
23
23
|
const { Field, Input, Banner, Text, JsonView } = usePrimitives();
|
|
24
24
|
// App-Locale (i18n) für money/date-Inputs — sonst fielen sie auf
|
|
25
25
|
// navigator.language (Browser-Sprache) zurück statt der gewählten
|
|
@@ -42,7 +42,7 @@ export function RenderField({ field, issues, onChange, featureName, labelAppendi
|
|
|
42
42
|
// Reference-Field rendert eine eigene Component — sie nutzt
|
|
43
43
|
// useQuery() für den Live-Lookup, also muss sie als React-
|
|
44
44
|
// Komponente gemountet werden (nicht als pure render-Call).
|
|
45
|
-
const control = field.readOnly && field.renderer !== undefined ? (_jsx(FieldRendererOutput, { field: field, renderer: field.renderer, appLocale: appLocale, ...(row !== undefined && { row }) })) : field.type === "embedded" && field.embeddedListCells !== undefined ? (_jsx(EmbeddedListField, { field: field, id: id, onChange: onChange, allIssues: allIssues ?? {}, featureName: featureName ?? "" })) : field.type === "reference" ? (readOnlyText ? (_jsx(ReadOnlyReferenceValue, { field: field, featureName: featureName ?? "" })) : (_jsx(ReferenceInput, { field: field, id: id, hasError: hasError, onChange: onChange, Input: Input, featureName: featureName ?? "" }))) : field.type === "select" && field.selectOptionsQuery !== undefined && !readOnlyText ? (_jsx(QueryOptionsSelect, { field: field, query: field.selectOptionsQuery.query, payload: field.selectOptionsQuery.payload, id: id, hasError: hasError, onChange: onChange })) : readOnlyText && !isComplexFieldType(field.type) ? (_jsx(Text, { testId: `field-value-${field.field}`, children: readOnlyDisplayText(field, appLocale) })) : (renderInput({
|
|
45
|
+
const control = field.readOnly && field.renderer !== undefined ? (_jsx(FieldRendererOutput, { field: field, renderer: field.renderer, appLocale: appLocale, ...(row !== undefined && { row }) })) : field.type === "embedded" && field.embeddedListCells !== undefined ? (_jsx(EmbeddedListField, { field: field, id: id, onChange: onChange, allIssues: allIssues ?? {}, featureName: featureName ?? "" })) : field.type === "reference" ? (readOnlyText ? (_jsx(ReadOnlyReferenceValue, { field: field, featureName: featureName ?? "" })) : (_jsx(ReferenceInput, { field: field, id: id, hasError: hasError, onChange: onChange, Input: Input, featureName: featureName ?? "" }))) : field.type === "select" && field.selectOptionsQuery !== undefined && !readOnlyText ? (_jsx(QueryOptionsSelect, { field: field, query: field.selectOptionsQuery.query, payload: field.selectOptionsQuery.payload, row: row ?? { [field.field]: field.value }, id: id, hasError: hasError, onChange: onChange })) : readOnlyText && !isComplexFieldType(field.type) ? (_jsx(Text, { testId: `field-value-${field.field}`, children: readOnlyDisplayText(field, appLocale, t) })) : field.type === "text" && field.writeOnly === true && !readOnlyText ? (_jsx(WriteOnlyTextInput, { field: field, id: id, hasError: hasError, onChange: onChange, changed: changed === true })) : (renderInput({
|
|
46
46
|
field,
|
|
47
47
|
id,
|
|
48
48
|
hasError,
|
|
@@ -55,7 +55,33 @@ export function RenderField({ field, issues, onChange, featureName, labelAppendi
|
|
|
55
55
|
t,
|
|
56
56
|
row,
|
|
57
57
|
}));
|
|
58
|
-
return (_jsx(Field, { id: id, label: field.label, required: field.required && field.type !== "boolean" && !allFieldsRequired, ...(issues !== undefined && { issues }), ...(labelAppendix !== undefined && { labelAppendix }), ...(fieldAppendix !== undefined && { fieldAppendix }), ...(changed === true && { changed }), testId: `field-${field.field}`, children: control }));
|
|
58
|
+
return (_jsx(Field, { id: id, label: field.label, required: field.required && field.type !== "boolean" && !allFieldsRequired, ...(issues !== undefined && { issues }), ...(labelAppendix !== undefined && { labelAppendix }), ...(fieldAppendix !== undefined && { fieldAppendix }), ...(appendixPlacement !== undefined && { appendixPlacement }), ...(description !== undefined && { description }), ...(status !== undefined && { status }), ...(changed === true && { changed }), ...(layout !== undefined && { layout }), ...(accent === true && { accent }), testId: `field-${field.field}`, children: control }));
|
|
59
|
+
}
|
|
60
|
+
// Wire value of a writeOnly field is `true` (set) / `null` (empty), never the
|
|
61
|
+
// secret. Typing sets a new value; an untouched or emptied input submits
|
|
62
|
+
// nothing / "" which the server reads as "keep". `null` after a loaded `true`
|
|
63
|
+
// (changed) means "remove on save".
|
|
64
|
+
function WriteOnlyTextInput({ field, id, hasError, onChange, changed, }) {
|
|
65
|
+
const { Input } = usePrimitives();
|
|
66
|
+
const t = useTranslation();
|
|
67
|
+
const isSet = field.value === true;
|
|
68
|
+
const isMarkedForRemoval = field.value === null && changed;
|
|
69
|
+
if (isMarkedForRemoval) {
|
|
70
|
+
return (_jsx(Input, { kind: "password", id: id, name: field.field, disabled: true, hasError: hasError, value: "", onChange: () => { }, autoComplete: "new-password", placeholder: t("kumiko.field.writeOnly.willRemove"), testId: `field-write-only-removal-${field.field}`, trailingAction: {
|
|
71
|
+
icon: "undo",
|
|
72
|
+
label: t("kumiko.field.writeOnly.undo"),
|
|
73
|
+
onPress: () => onChange(true),
|
|
74
|
+
} }));
|
|
75
|
+
}
|
|
76
|
+
return (_jsx(Input, { kind: "password", id: id, name: field.field, disabled: field.readOnly, required: field.required && !isSet, hasError: hasError, value: typeof field.value === "string" ? field.value : "", onChange: (v) => onChange(v), autoComplete: "new-password", ...(isSet && { placeholder: t("kumiko.field.writeOnly.setPlaceholder") }), ...(isSet &&
|
|
77
|
+
!field.required &&
|
|
78
|
+
!field.readOnly && {
|
|
79
|
+
trailingAction: {
|
|
80
|
+
icon: "clear",
|
|
81
|
+
label: t("kumiko.field.writeOnly.remove"),
|
|
82
|
+
onPress: () => onChange(null),
|
|
83
|
+
},
|
|
84
|
+
}) }));
|
|
59
85
|
}
|
|
60
86
|
// Where a reference field's option rows come from, and which of their keys
|
|
61
87
|
// holds the display text — the entity's own list handler plus `refLabelField`
|
|
@@ -488,10 +514,12 @@ function locatedValue(v) {
|
|
|
488
514
|
// covered here — it needs a live label lookup, see ReadOnlyReferenceValue.
|
|
489
515
|
// `isComplexFieldType` types aren't covered either — callers keep those on
|
|
490
516
|
// `renderInput`'s existing Banner fallback.
|
|
491
|
-
function readOnlyDisplayText(field, appLocale) {
|
|
517
|
+
function readOnlyDisplayText(field, appLocale, t) {
|
|
492
518
|
const { type, value } = field;
|
|
493
519
|
if (value === undefined || value === null || value === "")
|
|
494
520
|
return "—";
|
|
521
|
+
if (type === "text" && field.writeOnly === true)
|
|
522
|
+
return t("kumiko.field.writeOnly.set");
|
|
495
523
|
if (type === "text" && field.format === "password")
|
|
496
524
|
return "••••••••";
|
|
497
525
|
switch (type) {
|
|
@@ -13,5 +13,9 @@ export type WriteFormSectionProps = {
|
|
|
13
13
|
* rendered alongside (before) the section's own submit button in the
|
|
14
14
|
* title-row actions slot. */
|
|
15
15
|
readonly actions?: ReactNode;
|
|
16
|
+
/** Set when this section is a whole tab: the submit button is handed to
|
|
17
|
+
* the host form's footer (pinned like every other screen's save) instead
|
|
18
|
+
* of the section's title row. Called with undefined on unmount. */
|
|
19
|
+
readonly onFooterAction?: (action: ReactNode | undefined) => void;
|
|
16
20
|
};
|
|
17
|
-
export declare function WriteFormSection({ section, featureName, translate, hideTitle, onSubmitted, actions, }: WriteFormSectionProps): ReactNode;
|
|
21
|
+
export declare function WriteFormSection({ section, featureName, translate, hideTitle, onSubmitted, actions, onFooterAction, }: WriteFormSectionProps): ReactNode;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { I18N_KEY_PARAM } from "@cosmicdrift/kumiko-headless";
|
|
3
|
-
import { useMemo, useState } from "react";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import * as z from "zod";
|
|
5
5
|
import { REQUIRED_FIELD_I18N_KEY } from "../app/form-schema.js";
|
|
6
6
|
import { dispatcherErrorText } from "../app/write-failed-error.js";
|
|
7
7
|
import { useForm } from "../hooks/use-form.js";
|
|
8
8
|
import { useTranslation } from "../i18n.js";
|
|
9
|
-
import { usePrimitives } from "../primitives.js";
|
|
9
|
+
import { STICKY_PRIMARY_ACTION_PROP, usePrimitives } from "../primitives.js";
|
|
10
10
|
import { GridCellForField } from "./grid-cell-for-field.js";
|
|
11
11
|
// Same "has a value" rule as buildFormSchema's isPresent (app/form-schema.ts)
|
|
12
12
|
// — duplicated because that helper walks raw EditFieldSpec + EntityDefinition,
|
|
@@ -45,7 +45,7 @@ function buildWriteFormSchema(fields) {
|
|
|
45
45
|
// would render a second, invalid nested <form> inside the host's own form
|
|
46
46
|
// element — so this mounts its own independent useForm() controller and
|
|
47
47
|
// submits via a plain button click instead of native form submission.
|
|
48
|
-
export function WriteFormSection({ section, featureName, translate, hideTitle, onSubmitted, actions, }) {
|
|
48
|
+
export function WriteFormSection({ section, featureName, translate, hideTitle, onSubmitted, actions, onFooterAction, }) {
|
|
49
49
|
const { Section, Card, Grid, GridCell, Button, Banner } = usePrimitives();
|
|
50
50
|
const t = useTranslation();
|
|
51
51
|
const effectiveTranslate = translate ?? t;
|
|
@@ -85,8 +85,20 @@ export function WriteFormSection({ section, featureName, translate, hideTitle, o
|
|
|
85
85
|
// type="button" (not "submit") is load-bearing: this section is deliberately
|
|
86
86
|
// NOT a nested <form> (see the component doc above), so a "submit" type
|
|
87
87
|
// would instead trigger the host RenderEdit's own form submit.
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
// The footer copy of the button outlives this render, so it calls the
|
|
89
|
+
// latest handleSubmit through a ref instead of capturing a stale closure.
|
|
90
|
+
const handleSubmitRef = useRef(handleSubmit);
|
|
91
|
+
handleSubmitRef.current = handleSubmit;
|
|
92
|
+
const submitLabel = section.submitLabel ?? effectiveTranslate("kumiko.actions.save");
|
|
93
|
+
const submitButton = (_jsx(Button, { type: "button", variant: "primary", disabled: isSubmitting, loading: isSubmitting, onClick: () => void handleSubmitRef.current(), testId: "write-form-section-submit", [STICKY_PRIMARY_ACTION_PROP]: true, children: submitLabel }));
|
|
94
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: submitButton is rebuilt every render; isSubmitting and submitLabel are everything it shows.
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (onFooterAction === undefined)
|
|
97
|
+
return;
|
|
98
|
+
onFooterAction(submitButton);
|
|
99
|
+
return () => onFooterAction(undefined);
|
|
100
|
+
}, [onFooterAction, isSubmitting, submitLabel]);
|
|
101
|
+
const titleRowActions = onFooterAction !== undefined ? (actions) : actions !== undefined ? (_jsxs(_Fragment, { children: [actions, submitButton] })) : (submitButton);
|
|
90
102
|
// Section always flattens to a borderless divider when rendered inside
|
|
91
103
|
// RenderEdit's own <Form> in tabs mode (hideTitle) — an unframed Card panel
|
|
92
104
|
// stands in, like render-edit.tsx's tabs-mode fields/extension branches.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
// A host that already frames and pads the embedded screen (dashboard panel
|
|
4
|
+
// with `chromeless`) marks the subtree, so list screens drop their own
|
|
5
|
+
// screen padding and card frame instead of stacking it on the host's.
|
|
6
|
+
const EmbeddedScreenContext = createContext(false);
|
|
7
|
+
export function EmbeddedScreenProvider({ children }) {
|
|
8
|
+
return _jsx(EmbeddedScreenContext, { value: true, children: children });
|
|
9
|
+
}
|
|
10
|
+
export function useIsEmbeddedScreen() {
|
|
11
|
+
return useContext(EmbeddedScreenContext);
|
|
12
|
+
}
|
package/dist/i18n-defaults.js
CHANGED
|
@@ -14,6 +14,7 @@ export const kumikoDefaultTranslations = {
|
|
|
14
14
|
"kumiko.actions.delete": "Delete",
|
|
15
15
|
"kumiko.actions.delete-confirm": "Confirm delete?",
|
|
16
16
|
"kumiko.actions.reload": "Reload",
|
|
17
|
+
"kumiko.actions.retry": "Try again",
|
|
17
18
|
"kumiko.actions.create": "New",
|
|
18
19
|
"kumiko.actions.edit": "Edit",
|
|
19
20
|
"kumiko.actions.view": "View",
|
|
@@ -36,6 +37,11 @@ export const kumikoDefaultTranslations = {
|
|
|
36
37
|
"kumiko.field.locatedTzHint": "Time local to the given location",
|
|
37
38
|
"kumiko.field.reference-created-no-id": "Record was created but could not be selected automatically. Please select it manually.",
|
|
38
39
|
"kumiko.field.unsupported": "This field type can't be edited here yet.",
|
|
40
|
+
"kumiko.field.writeOnly.remove": "Remove stored value",
|
|
41
|
+
"kumiko.field.writeOnly.set": "Set",
|
|
42
|
+
"kumiko.field.writeOnly.setPlaceholder": "Set — leave empty to keep it",
|
|
43
|
+
"kumiko.field.writeOnly.undo": "Undo",
|
|
44
|
+
"kumiko.field.writeOnly.willRemove": "Removed on save",
|
|
39
45
|
"kumiko.field.embedded-list.add-row": "Add row",
|
|
40
46
|
"kumiko.field.embedded-list.remove-row": "Remove row",
|
|
41
47
|
"kumiko.field.embedded-list.duplicate-row": "Duplicate row",
|
|
@@ -77,6 +83,9 @@ export const kumikoDefaultTranslations = {
|
|
|
77
83
|
"kumiko.list.sort.by": "Sorted by {column}",
|
|
78
84
|
"kumiko.form.changed": "changed",
|
|
79
85
|
"kumiko.form.on-this-page": "On this page",
|
|
86
|
+
"kumiko.form.noUnsavedChanges": "No unsaved changes",
|
|
87
|
+
"kumiko.form.discard": "Discard",
|
|
88
|
+
"kumiko.form.saveChanges": "Save changes",
|
|
80
89
|
"kumiko.form.unsaved.one": "1 unsaved change",
|
|
81
90
|
"kumiko.form.unsaved.other": "{count} unsaved changes",
|
|
82
91
|
"kumiko.wizard.next-with-title": "Next: {title}",
|
|
@@ -153,16 +162,30 @@ export const kumikoDefaultTranslations = {
|
|
|
153
162
|
"kumiko.contentEditor.heading2": "Heading 2",
|
|
154
163
|
"kumiko.contentEditor.bulletList": "Bullet list",
|
|
155
164
|
"kumiko.contentEditor.orderedList": "Numbered list",
|
|
156
|
-
"kumiko.config.source.user": "
|
|
165
|
+
"kumiko.config.source.user": "Your value",
|
|
157
166
|
"kumiko.config.source.tenant": "Tenant",
|
|
158
|
-
"kumiko.config.source.system": "
|
|
167
|
+
"kumiko.config.source.system": "Platform",
|
|
159
168
|
"kumiko.config.source.appOverride": "App override",
|
|
160
169
|
"kumiko.config.source.computed": "Computed",
|
|
161
170
|
"kumiko.config.source.default": "Default",
|
|
162
171
|
"kumiko.config.source.missing": "Missing",
|
|
163
|
-
"kumiko.config.cascade.noValue": "
|
|
164
|
-
"kumiko.config.cascade.
|
|
165
|
-
"kumiko.config.cascade.
|
|
172
|
+
"kumiko.config.cascade.noValue": "Not set",
|
|
173
|
+
"kumiko.config.cascade.noDefault": "not set",
|
|
174
|
+
"kumiko.config.cascade.notSetRequired": "Not set. Required.",
|
|
175
|
+
"kumiko.config.cascade.activeMarker": "in use",
|
|
176
|
+
"kumiko.config.cascade.resetTo": "Reset to default",
|
|
177
|
+
"kumiko.config.cascade.resetToApp": "Reset to app value",
|
|
178
|
+
"kumiko.config.cascade.showLevels": "Show all levels",
|
|
179
|
+
"kumiko.config.cascade.hideLevels": "Hide levels",
|
|
180
|
+
"kumiko.config.cascade.on": "On",
|
|
181
|
+
"kumiko.config.cascade.off": "Off",
|
|
182
|
+
"kumiko.config.cascade.origin.tenant": "Set for this tenant. Default is {value}.",
|
|
183
|
+
"kumiko.config.cascade.origin.user": "Set by you. Default is {value}.",
|
|
184
|
+
"kumiko.config.cascade.originNoDefault.tenant": "Set for this tenant.",
|
|
185
|
+
"kumiko.config.cascade.originNoDefault.user": "Set by you.",
|
|
186
|
+
"kumiko.config.cascade.originNoDefault.system": "Set for the platform.",
|
|
187
|
+
"kumiko.config.cascade.origin.system": "Set for the platform. App default is {value}.",
|
|
188
|
+
"kumiko.config.cascade.origin.default": "Uses the default",
|
|
166
189
|
"kumiko.form.error.generic": "Something went wrong.",
|
|
167
190
|
"kumiko.form.error.version-conflict": "Record was modified in the meantime. Reload and try again.",
|
|
168
191
|
"kumiko.form.extension.save-failed": "A custom field could not be saved.",
|
|
@@ -183,6 +206,8 @@ export const kumikoDefaultTranslations = {
|
|
|
183
206
|
"errors.validation.invalid_type": "Invalid value.",
|
|
184
207
|
"errors.validation.too_small": "Too small or too short (minimum: {minimum}).",
|
|
185
208
|
"errors.validation.too_big": "Too big or too long (maximum: {maximum}).",
|
|
209
|
+
"kumiko.validation.number.min": "Must be at least {minimum}.",
|
|
210
|
+
"kumiko.validation.number.max": "Must be {maximum} or less.",
|
|
186
211
|
"errors.validation.invalid_format": "Invalid format.",
|
|
187
212
|
"errors.validation.not_multiple_of": "Must be a multiple of {divisor}.",
|
|
188
213
|
"errors.validation.unrecognized_keys": "Unknown fields.",
|
package/dist/index.d.ts
CHANGED
|
@@ -43,6 +43,7 @@ export type { DispatcherProviderProps } from "./context/dispatcher-context.js";
|
|
|
43
43
|
export { DispatcherProvider, useDispatcher, useDispatcherStatus, useOptionalDispatcher, } from "./context/dispatcher-context.js";
|
|
44
44
|
export type { DraftStorage, DraftStorageProviderProps } from "./context/draft-storage-context.js";
|
|
45
45
|
export { DraftStorageProvider, useDraftStorage } from "./context/draft-storage-context.js";
|
|
46
|
+
export { EmbeddedScreenProvider, useIsEmbeddedScreen } from "./context/embedded-screen-context.js";
|
|
46
47
|
export type { UserRolesProviderProps } from "./context/user-roles-context.js";
|
|
47
48
|
export { UserRolesProvider, useUserRoles } from "./context/user-roles-context.js";
|
|
48
49
|
export { formatWhen } from "./format-when.js";
|
|
@@ -67,7 +68,7 @@ export { createStaticLocaleResolver, FormalityProvider, LocaleProvider, mergeTra
|
|
|
67
68
|
export { kumikoDefaultTranslations } from "./i18n-defaults.js";
|
|
68
69
|
export { InsideDrawerProvider, useInsideDrawer } from "./inside-drawer.js";
|
|
69
70
|
export { PageHeaderCompactProvider, PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "./page-header-slot.js";
|
|
70
|
-
export type { ActionMenuItemSpec, ActionOverflowMenuProps, AppPrimitives, BannerProps, ButtonProps, CardOptions, CardProps, CardSlots, CopyButtonProps, CorePrimitives, DataTableDateRangeFacet, DataTableFacet, DataTableFacetChip, DataTableProps, DataTableRowAction, DataTableRowActionMode, DataTableRowGrouping, DataTableSort, DataTableSortDir, DialogProps, DrawerProps, EmbeddedListCellType, EmbeddedListColumn, EmbeddedListInputProps, EmbeddedListTotal, FieldCellWidth, FieldProps, FillContainerProps, FormProps, FormSectionNavItem, FormWidth, GridCellProps, GridProps, HeadingProps, InputProps, JsonViewProps, LightboxProps, LinkProps, MetricBandProps, MetricProps, ModalProps, PageHeaderProps, PrimitivesProviderProps, PrimitivesRegistry, ProgressProps, ProgressTone, PromoPanelAction, PromoPanelProps, RuntimeRenderer, SecretRevealProps, SecretRevealValue, SectionProps, ShareButtonProps, StatusBadgeProps, StatusTone, StepBarProps, StickyActionBarProps, StickyPrimaryActionMarker, TabsProps, TextProps, WizardStepGroupProps, } from "./primitives.js";
|
|
71
|
+
export type { ActionMenuItemSpec, ActionOverflowMenuProps, AppPrimitives, BannerProps, ButtonProps, CardOptions, CardProps, CardSlots, CopyButtonProps, CorePrimitives, DataTableDateRangeFacet, DataTableFacet, DataTableFacetChip, DataTableProps, DataTableRowAction, DataTableRowActionMode, DataTableRowGrouping, DataTableSort, DataTableSortDir, DialogProps, DrawerProps, EmbeddedListCellType, EmbeddedListColumn, EmbeddedListInputProps, EmbeddedListTotal, FieldCellWidth, FieldProps, FillContainerProps, FormProps, FormSectionNavItem, FormWidth, GridCellProps, GridProps, HeadingProps, InputProps, JsonViewProps, LightboxProps, LinkProps, MetricBandProps, MetricProps, ModalProps, PageHeaderProps, PrimitivesProviderProps, PrimitivesRegistry, ProgressProps, ProgressTone, PromoPanelAction, PromoPanelProps, RuntimeRenderer, SecretRevealProps, SecretRevealValue, SectionProps, ShareButtonProps, StatusBadgeProps, StatusTone, StepBarProps, StickyActionBarProps, StickyPrimaryActionMarker, TabsProps, TextProps, TrailingInputAction, WizardStepGroupProps, } from "./primitives.js";
|
|
71
72
|
export { PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, usePrimitives, } from "./primitives.js";
|
|
72
73
|
export { buildWhatsAppShareUrl } from "./share-links.js";
|
|
73
74
|
export { sortByAccessor } from "./sort-by-accessor.js";
|
package/dist/index.js
CHANGED
|
@@ -33,6 +33,7 @@ export { RenderField } from "./components/render-field.js";
|
|
|
33
33
|
export { RenderList } from "./components/render-list.js";
|
|
34
34
|
export { DispatcherProvider, useDispatcher, useDispatcherStatus, useOptionalDispatcher, } from "./context/dispatcher-context.js";
|
|
35
35
|
export { DraftStorageProvider, useDraftStorage } from "./context/draft-storage-context.js";
|
|
36
|
+
export { EmbeddedScreenProvider, useIsEmbeddedScreen } from "./context/embedded-screen-context.js";
|
|
36
37
|
export { UserRolesProvider, useUserRoles } from "./context/user-roles-context.js";
|
|
37
38
|
export { formatWhen } from "./format-when.js";
|
|
38
39
|
export { REFERENCE_COMBOBOX_LIMIT, REFERENCE_LIST_LOOKUP_LIMIT, REFERENCE_SEARCH_DEBOUNCE_MS, } from "./hooks/reference-limits.js";
|
package/dist/primitives.d.ts
CHANGED
|
@@ -123,11 +123,28 @@ export type FieldProps = {
|
|
|
123
123
|
readonly issues?: readonly FieldIssue[];
|
|
124
124
|
readonly labelAppendix?: ReactNode;
|
|
125
125
|
readonly fieldAppendix?: ReactNode;
|
|
126
|
+
/** Where `fieldAppendix` sits relative to the control. Default
|
|
127
|
+
* `above-control` (a disclosure panel belongs under its label-row
|
|
128
|
+
* trigger); `below-control` keeps sibling inputs on one line when the
|
|
129
|
+
* appendix is a status line or an expanded detail. */
|
|
130
|
+
readonly appendixPlacement?: "above-control" | "below-control";
|
|
131
|
+
/** Help text directly under the label, above the control. */
|
|
132
|
+
readonly description?: ReactNode;
|
|
133
|
+
/** Status pill at the right end of the label row (e.g. "Saved", "Not set"). */
|
|
134
|
+
readonly status?: {
|
|
135
|
+
readonly tone: StatusTone;
|
|
136
|
+
readonly label: string;
|
|
137
|
+
readonly testId?: string;
|
|
138
|
+
};
|
|
126
139
|
readonly children: ReactNode;
|
|
127
140
|
/** Label-Layout. `stacked` (Default): Label über dem Control. `inline`:
|
|
128
141
|
* Control + Label nebeneinander (Box links, Label rechts) — für
|
|
129
142
|
* Checkbox/Switch-Felder (boolean), wie das shadcn-Muster. */
|
|
130
|
-
readonly layout?: "stacked" | "inline";
|
|
143
|
+
readonly layout?: "stacked" | "inline" | "row";
|
|
144
|
+
/** Marks a field whose value is set at this level (differs from the
|
|
145
|
+
* inherited one). Only `layout="row"` draws it: a 2px primary line on the
|
|
146
|
+
* row's left edge. Ignored by the other layouts. */
|
|
147
|
+
readonly accent?: boolean;
|
|
131
148
|
/** Collapses the visible label to `sr-only` — stays associated via
|
|
132
149
|
* `htmlFor` (no separate `aria-label` needed). For tables/grids whose
|
|
133
150
|
* column header already carries the label for each row. */
|
|
@@ -141,6 +158,12 @@ export type FieldProps = {
|
|
|
141
158
|
/** Discriminated union — jede Input-Sorte hat ihre eigene value/onChange
|
|
142
159
|
* Signatur. Custom-Impls dispatchen intern, rendern anders (Toggle
|
|
143
160
|
* statt Checkbox), oder nur einzelne kinds unterschiedlich. */
|
|
161
|
+
export type TrailingInputAction = {
|
|
162
|
+
readonly icon: "clear" | "undo";
|
|
163
|
+
readonly label: string;
|
|
164
|
+
readonly onPress: () => void;
|
|
165
|
+
readonly testId?: string;
|
|
166
|
+
};
|
|
144
167
|
export type InputProps = {
|
|
145
168
|
readonly kind: "text";
|
|
146
169
|
readonly id: string;
|
|
@@ -198,6 +221,9 @@ export type InputProps = {
|
|
|
198
221
|
* Browser-Password-Manager nutzen das für die Speicherentscheidung.
|
|
199
222
|
* Native: textContentType="password" / "newPassword". */
|
|
200
223
|
readonly autoComplete?: "current-password" | "new-password";
|
|
224
|
+
/** Icon button inside the field's trailing edge (clear a stored value,
|
|
225
|
+
* undo a pending removal). `label` is its accessible name + tooltip. */
|
|
226
|
+
readonly trailingAction?: TrailingInputAction;
|
|
201
227
|
readonly testId?: string;
|
|
202
228
|
} | {
|
|
203
229
|
readonly kind: "number";
|
|
@@ -299,8 +325,13 @@ export type InputProps = {
|
|
|
299
325
|
readonly options: readonly string[] | readonly {
|
|
300
326
|
readonly value: string;
|
|
301
327
|
readonly label: string;
|
|
302
|
-
/**
|
|
328
|
+
/** Muted second line under the label (radio list and dropdown items;
|
|
329
|
+
* the dropdown search matches it too). */
|
|
303
330
|
readonly description?: string;
|
|
331
|
+
/** Group heading. Groups form in order of first occurrence; options
|
|
332
|
+
* without a group stay ungrouped and come before all groups. Options
|
|
333
|
+
* with description or group never render as a segmented control. */
|
|
334
|
+
readonly group?: string;
|
|
304
335
|
}[];
|
|
305
336
|
readonly disabled?: boolean;
|
|
306
337
|
readonly required?: boolean;
|
|
@@ -837,8 +868,8 @@ export type FormProps = {
|
|
|
837
868
|
* detail screens (which also fill the height) do not set it. Native impls
|
|
838
869
|
* may ignore this prop. */
|
|
839
870
|
readonly screenForm?: boolean;
|
|
840
|
-
/** Number of unsaved changes, shown in the pinned footer
|
|
841
|
-
*
|
|
871
|
+
/** Number of unsaved changes, shown in the pinned footer; 0 shows the quiet
|
|
872
|
+
* "No unsaved changes" status. Omitted (create mode) — no status at all. */
|
|
842
873
|
readonly unsavedCount?: number;
|
|
843
874
|
/** Drawer-hosted forms: context box above the fields. */
|
|
844
875
|
readonly summary?: {
|
|
@@ -870,6 +901,9 @@ export type SectionProps = {
|
|
|
870
901
|
* title-less/subtitle-less Section still draws the row when `actions`
|
|
871
902
|
* is set, so a hideTitle tabs-Section with actions isn't stranded. */
|
|
872
903
|
readonly actions?: ReactNode;
|
|
904
|
+
/** "settings-list": title + subtitle form a left header column and the
|
|
905
|
+
* children (field rows) the right one on `md:` and up. Default "stacked". */
|
|
906
|
+
readonly layout?: "stacked" | "settings-list";
|
|
873
907
|
/** "destructive" marks the Section as a warning/danger area (e.g. account
|
|
874
908
|
* deletion, restrict processing) — border color only, no content change.
|
|
875
909
|
* Default "default" (normal card border). */
|
|
@@ -920,6 +954,9 @@ export type GridProps = {
|
|
|
920
954
|
/** Fields flow left to right in wrapping rows sized by `GridCell.width`
|
|
921
955
|
* instead of equal-width tracks (screen forms and drawers). */
|
|
922
956
|
readonly flow?: boolean;
|
|
957
|
+
/** One full-width row per child, separated by hairlines — the field rows of
|
|
958
|
+
* a `settings-list` section. Wins over `columns`/`flow`. */
|
|
959
|
+
readonly list?: boolean;
|
|
923
960
|
};
|
|
924
961
|
/** Width class of a field cell inside a `flow` grid: sized by what the field holds. */
|
|
925
962
|
export type FieldCellWidth = "text" | "number" | "money" | "date" | "timestamp" | "select" | "full" | "toggle";
|
|
@@ -1036,19 +1073,19 @@ export type ConfigSourceBadgeProps = {
|
|
|
1036
1073
|
* rule as ConfigCascadeView's toDisplayLevels. */
|
|
1037
1074
|
readonly screenScope?: ConfigScope;
|
|
1038
1075
|
};
|
|
1039
|
-
/**
|
|
1040
|
-
*
|
|
1041
|
-
*
|
|
1042
|
-
* split mode the screen owns the `expanded` state (both slots share it);
|
|
1043
|
-
* without `slot` the component renders both with its own state. */
|
|
1076
|
+
/** Cascade view for a config-edit field: one origin line ("Set for this
|
|
1077
|
+
* tenant", "Uses the default", …), a reset action for an own value, and a
|
|
1078
|
+
* disclosure listing every level. Owns its disclosure state. */
|
|
1044
1079
|
export type ConfigCascadeViewProps = {
|
|
1045
1080
|
readonly cascade: ConfigCascade;
|
|
1046
1081
|
readonly screenScope: ConfigScope;
|
|
1047
1082
|
readonly onReset?: (key: string, scope: ConfigScope) => void;
|
|
1048
1083
|
readonly qualifiedKey?: string;
|
|
1049
|
-
|
|
1050
|
-
readonly
|
|
1051
|
-
|
|
1084
|
+
/** The key declares `required: true` — a missing value reads "Not set. Required.". */
|
|
1085
|
+
readonly required?: boolean;
|
|
1086
|
+
/** Display form of a level value (e.g. a select's label instead of its raw
|
|
1087
|
+
* value). Absent = the raw value; a missing value keeps the no-value text. */
|
|
1088
|
+
readonly renderValue?: (value: string | number | boolean) => ReactNode;
|
|
1052
1089
|
};
|
|
1053
1090
|
/** Card-Slots — benannte Inhalts-Regionen, alle optional. Neue Slots später =
|
|
1054
1091
|
* rein additiv → Consumer müssen NIE migriert werden. `header` ist der Escape-
|
|
@@ -1082,6 +1119,11 @@ export type CardOptions = {
|
|
|
1082
1119
|
/** false drops border, background, shadow and radius but keeps padding and
|
|
1083
1120
|
* header slots — a panel inside a host surface such as a tab panel. Default true. */
|
|
1084
1121
|
readonly framed?: boolean;
|
|
1122
|
+
/** The card is the whole screen body (secretMint reveal and done phase):
|
|
1123
|
+
* no frame, the shared screen insets and width of form screens, children
|
|
1124
|
+
* stacked with a gap. Without it such a screen sits raw against the
|
|
1125
|
+
* shell edge. Default false. */
|
|
1126
|
+
readonly screenBody?: boolean;
|
|
1085
1127
|
};
|
|
1086
1128
|
/** Generische Card-Chrome (border/bg/radius/shadow + Header/Body/Footer) als
|
|
1087
1129
|
* Primitive — eine Quelle für alle Card-Flächen, statt pro App nachgebaut.
|