@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
@@ -10,7 +10,7 @@ import { screenAccessAllows } from "../app/screen-access.js";
10
10
  import { useUserRoles } from "../context/user-roles-context.js";
11
11
  import { REFERENCE_COMBOBOX_LIMIT } from "../hooks/reference-limits.js";
12
12
  import { useQuery } from "../hooks/use-query.js";
13
- import { referenceLookupSource } from "../hooks/use-reference-lookup.js";
13
+ import { referenceOptionSource } from "../hooks/use-reference-lookup.js";
14
14
  import { useLocale, useTranslation } from "../i18n.js";
15
15
  import { usePrimitives } from "../primitives.js";
16
16
  import { EmbeddedListField } from "./embedded-list-field.js";
@@ -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,17 +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
59
  }
60
- // Where a reference field's option rows come from, and which of their keys
61
- // holds the display text — the entity's own list handler plus `refLabelField`
62
- // by default, or an author-supplied handler whose rows carry `label`
63
- // (fw#2780), for targets with no readable column of their own.
64
- function referenceOptionSource(field, refFeature, refEntity) {
65
- if (field.refOptionsQuery !== undefined) {
66
- return { queryQn: field.refOptionsQuery, labelKey: "label" };
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
+ } }));
67
75
  }
68
- return referenceLookupSource(refFeature, refEntity, field.refLabelField ?? "id");
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
+ }) }));
69
85
  }
70
86
  // Tier 2.7e-3 + 2.1c: Reference-Input rendert eine Searchable Combobox
71
87
  // gefüllt aus einer Live-Query auf die referenced Entity. Default-
@@ -438,15 +454,22 @@ function resolveMoneyCurrency(value, fieldCurrency) {
438
454
  }
439
455
  return fieldCurrency ?? "EUR";
440
456
  }
457
+ // The suffix span is absolutely positioned inside a fixed right padding sized
458
+ // for short symbols; a free-text sibling value longer than any real unit
459
+ // symbol would overflow the number.
460
+ const MAX_SIBLING_UNIT_LENGTH = 8;
441
461
  // Static unit → used as-is. Sibling-field reference → read the live row
442
- // value; missing/empty/non-string sibling means no suffix, never a guess.
462
+ // value; missing/empty/non-string/overlong sibling means no suffix, never a guess.
443
463
  function resolveNumberUnit(unit, row) {
444
464
  if (unit === undefined)
445
465
  return undefined;
446
466
  if (typeof unit === "string")
447
467
  return unit;
448
468
  const sibling = row?.[unit.field];
449
- return typeof sibling === "string" && sibling.length > 0 ? sibling : undefined;
469
+ if (typeof sibling !== "string")
470
+ return undefined;
471
+ const trimmed = sibling.trim();
472
+ return trimmed.length > 0 && trimmed.length <= MAX_SIBLING_UNIT_LENGTH ? trimmed : undefined;
450
473
  }
451
474
  function moneyMinorValue(v, currency) {
452
475
  if (v === undefined || v === null || v === "")
@@ -488,10 +511,12 @@ function locatedValue(v) {
488
511
  // covered here — it needs a live label lookup, see ReadOnlyReferenceValue.
489
512
  // `isComplexFieldType` types aren't covered either — callers keep those on
490
513
  // `renderInput`'s existing Banner fallback.
491
- function readOnlyDisplayText(field, appLocale) {
514
+ function readOnlyDisplayText(field, appLocale, t) {
492
515
  const { type, value } = field;
493
516
  if (value === undefined || value === null || value === "")
494
517
  return "—";
518
+ if (type === "text" && field.writeOnly === true)
519
+ return t("kumiko.field.writeOnly.set");
495
520
  if (type === "text" && field.format === "password")
496
521
  return "••••••••";
497
522
  switch (type) {
@@ -6,11 +6,11 @@ import { useReferenceLookup } from "../hooks/use-reference-lookup.js";
6
6
  import { useTranslation } from "../i18n.js";
7
7
  import { usePageHeaderCompact, usePageHeaderSlotAvailable } from "../page-header-slot.js";
8
8
  import { shouldRenderActionsIconOnly, usePrimitives, } from "../primitives.js";
9
+ import { needsActionConfirm } from "./render-edit-action-button.js";
9
10
  const SEARCH_DEBOUNCE_MS = 300;
10
- // Fire-and-forget actions (schema navigate/drawer) can leave the toolbar for the
11
- // page header without losing the confirm dialog or busy/error surfacing.
11
+ // Icon-less actions are promoted too (with a "plus" fallback at the call site).
12
12
  function isHeaderPromotableToolbarAction(action) {
13
- return (action.style === "primary" && action.confirm === undefined && action.confirmRequired === false);
13
+ return action.style === "primary" && !needsActionConfirm(action);
14
14
  }
15
15
  export function RenderList(props) {
16
16
  const { screen, entity, rows, featureName, translate: translateProp, onRowClick, emptyState, onCreate, createLabel, searchable = false, searchPlaceholder, description, itemNounKey, searchValue, onSearchChange, sort, onSortChange, pager, onReachEnd, loadingMore, hasMore, rowActions, rowActionMode, rowGrouping, rowTone, toolbarActions, filterFacets, filterValues, onFilterChange, onFilterReset, dateRangeFacets, onDateRangeChange, chromeless, scrollBody, screenPadding, expandedRowIds, onToggleRowExpanded, renderExpandedRow, } = props;
@@ -147,7 +147,7 @@ export function RenderList(props) {
147
147
  pageHeaderCompact
148
148
  ? toolbarActions?.find(isHeaderPromotableToolbarAction)
149
149
  : undefined;
150
- const promotedActionButton = promotedToolbarAction !== undefined ? (_jsx(Button, { variant: "primary", icon: promotedToolbarAction.icon ?? "plus", size: "icon", ariaLabel: promotedToolbarAction.label, title: promotedToolbarAction.label, onClick: () => void promotedToolbarAction.onTrigger(), testId: `render-list-toolbar-action-${promotedToolbarAction.id}` })) : undefined;
150
+ const promotedActionButton = promotedToolbarAction !== undefined ? (_jsx(ToolbarActionView, { action: { ...promotedToolbarAction, icon: promotedToolbarAction.icon ?? "plus" }, iconOnly: true, Button: Button, Dialog: Dialog, Banner: Banner })) : undefined;
151
151
  const toolbarActionsInToolbar = toolbarActions?.filter((a) => a !== promotedToolbarAction) ?? [];
152
152
  const hasToolbarActionsInToolbar = toolbarActionsInToolbar.length > 0;
153
153
  const toolbarEnd = hasHeaderSlot ||
@@ -239,9 +239,9 @@ function ToolbarActionView({ action, iconOnly = false, Button, Dialog, Banner, }
239
239
  }
240
240
  };
241
241
  const variant = action.style ?? "secondary";
242
- const needsConfirm = action.confirm !== undefined || (action.confirmRequired ?? action.style === "danger");
242
+ const needsConfirm = needsActionConfirm(action);
243
243
  const showIconOnly = iconOnly && action.icon !== undefined;
244
- return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: variant, loading: busy, ...(action.icon !== undefined && { icon: action.icon }), size: showIconOnly ? "icon" : "sm", ...(showIconOnly && { ariaLabel: action.label }), onClick: () => {
244
+ return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: variant, loading: busy, ...(action.icon !== undefined && { icon: action.icon }), size: showIconOnly ? "icon" : "sm", ...(showIconOnly && { ariaLabel: action.label, title: action.label }), onClick: () => {
245
245
  if (needsConfirm) {
246
246
  setConfirmOpen(true);
247
247
  }
@@ -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,25 +1,14 @@
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
- import { REQUIRED_FIELD_I18N_KEY } from "../app/form-schema.js";
5
+ import { isPresent, 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
- // Same "has a value" rule as buildFormSchema's isPresent (app/form-schema.ts)
12
- // — duplicated because that helper walks raw EditFieldSpec + EntityDefinition,
13
- // unavailable here: this section only ever sees computeEditViewModel's
14
- // already-resolved EditFieldViewModel[] (required/readOnly/visible are plain
15
- // booleans by the time render-edit.tsx hands the section to this component).
16
- function isPresent(value) {
17
- if (value === undefined || value === null || value === "")
18
- return false;
19
- if (Array.isArray(value) && value.length === 0)
20
- return false;
21
- return true;
22
- }
11
+ import { hasIssueWithoutVisibleField } from "./render-edit-logic.js";
23
12
  function buildWriteFormSchema(fields) {
24
13
  return z
25
14
  .object({})
@@ -45,7 +34,7 @@ function buildWriteFormSchema(fields) {
45
34
  // would render a second, invalid nested <form> inside the host's own form
46
35
  // element — so this mounts its own independent useForm() controller and
47
36
  // submits via a plain button click instead of native form submission.
48
- export function WriteFormSection({ section, featureName, translate, hideTitle, onSubmitted, actions, }) {
37
+ export function WriteFormSection({ section, featureName, translate, hideTitle, onSubmitted, actions, onFooterAction, }) {
49
38
  const { Section, Card, Grid, GridCell, Button, Banner } = usePrimitives();
50
39
  const t = useTranslation();
51
40
  const effectiveTranslate = translate ?? t;
@@ -71,11 +60,14 @@ export function WriteFormSection({ section, featureName, translate, hideTitle, o
71
60
  }
72
61
  if (result.validationBlocked)
73
62
  return;
74
- // Field-level issues already surface inline via snapshot.errors (same
75
- // GridCellForField/RenderField path RenderEdit uses) — the banner is
76
- // only for form-level errors nothing else would show (fw#1901 pattern).
77
- const fieldIssues = result.error.details?.fields ?? [];
78
- setError(fieldIssues.length === 0 ? dispatcherErrorText(result.error, effectiveTranslate) : null);
63
+ // Field-level issues surface inline via snapshot.errors (same
64
+ // GridCellForField/RenderField path RenderEdit uses) — the banner stays
65
+ // for anything no rendered field can show (`version`, `id`, a root
66
+ // refine, a hidden field).
67
+ const issuePaths = (result.error.details?.fields ?? []).map((i) => i.path);
68
+ setError(hasIssueWithoutVisibleField(issuePaths, section.fields)
69
+ ? dispatcherErrorText(result.error, effectiveTranslate)
70
+ : null);
79
71
  }
80
72
  finally {
81
73
  setIsSubmitting(false);
@@ -85,8 +77,20 @@ export function WriteFormSection({ section, featureName, translate, hideTitle, o
85
77
  // type="button" (not "submit") is load-bearing: this section is deliberately
86
78
  // NOT a nested <form> (see the component doc above), so a "submit" type
87
79
  // would instead trigger the host RenderEdit's own form submit.
88
- const submitButton = (_jsx(Button, { type: "button", variant: "primary", disabled: isSubmitting, loading: isSubmitting, onClick: () => void handleSubmit(), testId: "write-form-section-submit", children: section.submitLabel ?? effectiveTranslate("kumiko.actions.save") }));
89
- const titleRowActions = actions !== undefined ? (_jsxs(_Fragment, { children: [actions, submitButton] })) : (submitButton);
80
+ // The footer copy of the button outlives this render, so it calls the
81
+ // latest handleSubmit through a ref instead of capturing a stale closure.
82
+ const handleSubmitRef = useRef(handleSubmit);
83
+ handleSubmitRef.current = handleSubmit;
84
+ const submitLabel = section.submitLabel ?? effectiveTranslate("kumiko.actions.save");
85
+ 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 }));
86
+ // biome-ignore lint/correctness/useExhaustiveDependencies: submitButton is rebuilt every render; isSubmitting and submitLabel are everything it shows.
87
+ useEffect(() => {
88
+ if (onFooterAction === undefined)
89
+ return;
90
+ onFooterAction(submitButton);
91
+ return () => onFooterAction(undefined);
92
+ }, [onFooterAction, isSubmitting, submitLabel]);
93
+ const titleRowActions = onFooterAction !== undefined ? (actions) : actions !== undefined ? (_jsxs(_Fragment, { children: [actions, submitButton] })) : (submitButton);
90
94
  // Section always flattens to a borderless divider when rendered inside
91
95
  // RenderEdit's own <Form> in tabs mode (hideTitle) — an unframed Card panel
92
96
  // stands in, like render-edit.tsx's tabs-mode fields/extension branches.
@@ -94,10 +98,10 @@ export function WriteFormSection({ section, featureName, translate, hideTitle, o
94
98
  return (_jsx(Card, { options: { framed: false }, slots: {
95
99
  ...(section.description !== undefined && { subtitle: section.description }),
96
100
  headerActions: titleRowActions,
97
- }, testId: `write-form-${section.title ?? "section"}`, children: content }));
101
+ }, testId: `write-form-${section.title ?? section.handler}`, children: content }));
98
102
  }
99
103
  // Routed through Section's `actions` slot (same mechanism render-edit.tsx
100
104
  // uses via Form's `actions`) so the button gets the established right-
101
105
  // aligned footer treatment instead of stretching full-width inline.
102
- return (_jsx(Section, { ...(section.title !== undefined && { title: section.title }), ...(section.description !== undefined && { subtitle: section.description }), ...(section.icon !== undefined && { icon: section.icon }), actions: titleRowActions, testId: `write-form-${section.title ?? "section"}`, children: content }));
106
+ return (_jsx(Section, { ...(section.title !== undefined && { title: section.title }), ...(section.description !== undefined && { subtitle: section.description }), ...(section.icon !== undefined && { icon: section.icon }), actions: titleRowActions, testId: `write-form-${section.title ?? section.handler}`, children: content }));
103
107
  }
@@ -0,0 +1,5 @@
1
+ import { type ReactNode } from "react";
2
+ export declare function EmbeddedScreenProvider({ children }: {
3
+ readonly children: ReactNode;
4
+ }): ReactNode;
5
+ export declare function useIsEmbeddedScreen(): boolean;
@@ -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
+ }
@@ -6,6 +6,13 @@ export declare function referenceLookupSource(refFeature: string, refEntity: str
6
6
  readonly queryQn: string;
7
7
  readonly labelKey: string;
8
8
  };
9
+ export declare function referenceOptionSource(field: {
10
+ readonly refLabelField?: string;
11
+ readonly refOptionsQuery?: string;
12
+ }, refFeature: string, refEntity: string): {
13
+ readonly queryQn: string;
14
+ readonly labelKey: string;
15
+ };
9
16
  /** Empty while the query loads (or fails), so callers fall back to the raw id.
10
17
  * `featureName` is the reference's target feature, not the screen's. */
11
18
  export declare function useReferenceLookup(featureName: string, refEntity: string, labelField: string): {
@@ -34,6 +34,16 @@ export function referenceLookupSource(refFeature, refEntity, labelField) {
34
34
  labelKey: labelField,
35
35
  };
36
36
  }
37
+ // Where a reference field's option rows come from, and which of their keys
38
+ // holds the display text — the entity's own list handler plus `refLabelField`
39
+ // by default, or an author-supplied handler whose rows carry `label`
40
+ // (fw#2780), for targets with no readable column of their own.
41
+ export function referenceOptionSource(field, refFeature, refEntity) {
42
+ if (field.refOptionsQuery !== undefined) {
43
+ return { queryQn: field.refOptionsQuery, labelKey: "label" };
44
+ }
45
+ return referenceLookupSource(refFeature, refEntity, field.refLabelField ?? "id");
46
+ }
37
47
  /** Empty while the query loads (or fails), so callers fall back to the raw id.
38
48
  * `featureName` is the reference's target feature, not the screen's. */
39
49
  export function useReferenceLookup(featureName, refEntity, labelField) {
@@ -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": "My value",
165
+ "kumiko.config.source.user": "Your value",
157
166
  "kumiko.config.source.tenant": "Tenant",
158
- "kumiko.config.source.system": "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": "No value set",
164
- "kumiko.config.cascade.activeMarker": "active",
165
- "kumiko.config.cascade.resetTo": "Reset override ({scope})",
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/i18n.js CHANGED
@@ -65,7 +65,8 @@ export function useLocale() {
65
65
  }
66
66
  // Subscribe + current locale-snapshot. Wir brauchen den Rückgabewert
67
67
  // selbst nicht — wichtig ist nur der re-render-trigger.
68
- useSyncExternalStore(ctx.resolver.subscribe, () => ctx.resolver.locale(), () => ctx.resolver.locale());
68
+ const getLocaleSnapshot = () => ctx.resolver.locale();
69
+ useSyncExternalStore(ctx.resolver.subscribe, getLocaleSnapshot, getLocaleSnapshot);
69
70
  return ctx.resolver;
70
71
  }
71
72
  /** `t("key", params)` resolves in this order:
@@ -83,7 +84,8 @@ export function useTranslation() {
83
84
  // Re-Render bei Sprach-Wechsel. `ctx.resolver.subscribe` ist bereits
84
85
  // eine stable-reference aus dem Resolver, daher hier keine eigene
85
86
  // Memoization der Subscribe-Callback nötig.
86
- const locale = useSyncExternalStore(ctx.resolver.subscribe, () => ctx.resolver.locale(), () => ctx.resolver.locale());
87
+ const getLocaleSnapshot = () => ctx.resolver.locale();
88
+ const locale = useSyncExternalStore(ctx.resolver.subscribe, getLocaleSnapshot, getLocaleSnapshot);
87
89
  // `t` MUSS referenz-stabil sein solange sich Resolver/Bundles/Locale
88
90
  // nicht ändern — Consumer nutzen `t` regelmäßig in useEffect-Deps
89
91
  // (z.B. um Queries neu zu laden wenn sich die Sprache ändert). Ein neu
@@ -132,7 +134,8 @@ function bundleLocaleTiers(locale, fallbackLocale, formality) {
132
134
  * Used by DataTable FormatCell so plain tables without a provider do not crash. */
133
135
  export function useOptionalLocale() {
134
136
  const ctx = useContext(LocaleContext);
135
- useSyncExternalStore((onStoreChange) => (ctx ? ctx.resolver.subscribe(onStoreChange) : () => { }), () => (ctx ? ctx.resolver.locale() : "en"), () => (ctx ? ctx.resolver.locale() : "en"));
137
+ const getLocaleSnapshot = () => (ctx ? ctx.resolver.locale() : "en");
138
+ useSyncExternalStore((onStoreChange) => (ctx ? ctx.resolver.subscribe(onStoreChange) : () => { }), getLocaleSnapshot, getLocaleSnapshot);
136
139
  return ctx === undefined ? undefined : ctx.resolver.locale();
137
140
  }
138
141
  /** Non-throwing time-zone read — "UTC" outside LocaleProvider, matching
@@ -144,7 +147,8 @@ export function useOptionalTimeZone() {
144
147
  /** Non-throwing translate — undefined outside LocaleProvider. */
145
148
  export function useOptionalTranslation() {
146
149
  const ctx = useContext(LocaleContext);
147
- const locale = useSyncExternalStore((onStoreChange) => (ctx ? ctx.resolver.subscribe(onStoreChange) : () => { }), () => (ctx ? ctx.resolver.locale() : "en"), () => (ctx ? ctx.resolver.locale() : "en"));
150
+ const getLocaleSnapshot = () => (ctx ? ctx.resolver.locale() : "en");
151
+ const locale = useSyncExternalStore((onStoreChange) => (ctx ? ctx.resolver.subscribe(onStoreChange) : () => { }), getLocaleSnapshot, getLocaleSnapshot);
148
152
  const formality = useContext(FormalityContext);
149
153
  const t = useCallback((key, params) => ctx === undefined ? key : translateWithFallbacks(ctx, locale, formality, key, params), [ctx, locale, formality]);
150
154
  return ctx === undefined ? undefined : t;
package/dist/index.d.ts CHANGED
@@ -35,6 +35,7 @@ export { dispatcherErrorText, WriteFailedError } from "./app/write-failed-error.
35
35
  export { RelatedListSection } from "./components/related-list-section.js";
36
36
  export type { RenderEditAction, RenderEditChangeState, RenderEditControls, RenderEditProps, } from "./components/render-edit.js";
37
37
  export { RenderEdit } from "./components/render-edit.js";
38
+ export { needsActionConfirm } from "./components/render-edit-action-button.js";
38
39
  export type { RenderFieldProps } from "./components/render-field.js";
39
40
  export { RenderField } from "./components/render-field.js";
40
41
  export type { RenderListProps } from "./components/render-list.js";
@@ -43,6 +44,7 @@ export type { DispatcherProviderProps } from "./context/dispatcher-context.js";
43
44
  export { DispatcherProvider, useDispatcher, useDispatcherStatus, useOptionalDispatcher, } from "./context/dispatcher-context.js";
44
45
  export type { DraftStorage, DraftStorageProviderProps } from "./context/draft-storage-context.js";
45
46
  export { DraftStorageProvider, useDraftStorage } from "./context/draft-storage-context.js";
47
+ export { EmbeddedScreenProvider, useIsEmbeddedScreen } from "./context/embedded-screen-context.js";
46
48
  export type { UserRolesProviderProps } from "./context/user-roles-context.js";
47
49
  export { UserRolesProvider, useUserRoles } from "./context/user-roles-context.js";
48
50
  export { formatWhen } from "./format-when.js";
@@ -67,8 +69,10 @@ export { createStaticLocaleResolver, FormalityProvider, LocaleProvider, mergeTra
67
69
  export { kumikoDefaultTranslations } from "./i18n-defaults.js";
68
70
  export { InsideDrawerProvider, useInsideDrawer } from "./inside-drawer.js";
69
71
  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";
72
+ 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
73
  export { PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, usePrimitives, } from "./primitives.js";
74
+ export type { SessionEndedSignal, SessionEndedSignalProviderProps, } from "./session/session-ended.js";
75
+ export { createSessionEndedSignal, isSessionEndedError, SESSION_ENDED_ERROR_CODES, SessionEndedSignalProvider, useSessionEndedSignal, withSessionEndedDetection, } from "./session/session-ended.js";
72
76
  export { buildWhatsAppShareUrl } from "./share-links.js";
73
77
  export { sortByAccessor } from "./sort-by-accessor.js";
74
78
  export type { LiveEvent, LiveEventSubscriber, LiveEventsProviderProps } from "./sse/live-events.js";
package/dist/index.js CHANGED
@@ -29,10 +29,12 @@ export { VariableChips } from "./app/variable-chips.js";
29
29
  export { dispatcherErrorText, WriteFailedError } from "./app/write-failed-error.js";
30
30
  export { RelatedListSection } from "./components/related-list-section.js";
31
31
  export { RenderEdit } from "./components/render-edit.js";
32
+ export { needsActionConfirm } from "./components/render-edit-action-button.js";
32
33
  export { RenderField } from "./components/render-field.js";
33
34
  export { RenderList } from "./components/render-list.js";
34
35
  export { DispatcherProvider, useDispatcher, useDispatcherStatus, useOptionalDispatcher, } from "./context/dispatcher-context.js";
35
36
  export { DraftStorageProvider, useDraftStorage } from "./context/draft-storage-context.js";
37
+ export { EmbeddedScreenProvider, useIsEmbeddedScreen } from "./context/embedded-screen-context.js";
36
38
  export { UserRolesProvider, useUserRoles } from "./context/user-roles-context.js";
37
39
  export { formatWhen } from "./format-when.js";
38
40
  export { REFERENCE_COMBOBOX_LIMIT, REFERENCE_LIST_LOOKUP_LIMIT, REFERENCE_SEARCH_DEBOUNCE_MS, } from "./hooks/reference-limits.js";
@@ -49,6 +51,7 @@ export { kumikoDefaultTranslations } from "./i18n-defaults.js";
49
51
  export { InsideDrawerProvider, useInsideDrawer } from "./inside-drawer.js";
50
52
  export { PageHeaderCompactProvider, PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "./page-header-slot.js";
51
53
  export { PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, usePrimitives, } from "./primitives.js";
54
+ export { createSessionEndedSignal, isSessionEndedError, SESSION_ENDED_ERROR_CODES, SessionEndedSignalProvider, useSessionEndedSignal, withSessionEndedDetection, } from "./session/session-ended.js";
52
55
  export { buildWhatsAppShareUrl } from "./share-links.js";
53
56
  export { sortByAccessor } from "./sort-by-accessor.js";
54
57
  export { LiveEventsProvider, useLiveEvents } from "./sse/live-events.js";