@cosmicdrift/kumiko-renderer 0.385.0 → 0.387.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 (185) hide show
  1. package/LICENSE +21 -57
  2. package/dist/app/action-form-body.d.ts +2 -2
  3. package/dist/app/action-form-body.js +2 -2
  4. package/dist/app/action-form-shim.d.ts +8 -8
  5. package/dist/app/action-form-shim.js +20 -22
  6. package/dist/app/app-features-context.js +4 -4
  7. package/dist/app/column-renderers.d.ts +7 -9
  8. package/dist/app/column-renderers.js +15 -18
  9. package/dist/app/config-edit-body.js +1 -1
  10. package/dist/app/config-edit-shim.d.ts +7 -8
  11. package/dist/app/config-edit-shim.js +18 -20
  12. package/dist/app/custom-screens.d.ts +3 -3
  13. package/dist/app/custom-screens.js +8 -8
  14. package/dist/app/dashboard-body.d.ts +2 -2
  15. package/dist/app/dashboard-body.js +2 -2
  16. package/dist/app/entity-edit-prefill.js +9 -9
  17. package/dist/app/entity-edit-screen.js +4 -4
  18. package/dist/app/entity-list-screen.js +3 -3
  19. package/dist/app/extension-form-submit.d.ts +1 -1
  20. package/dist/app/extension-form-submit.js +14 -15
  21. package/dist/app/extension-sections.d.ts +24 -25
  22. package/dist/app/extension-sections.js +10 -10
  23. package/dist/app/feature-schema.js +11 -11
  24. package/dist/app/form-schema.js +1 -1
  25. package/dist/app/header-actions-bar.js +1 -1
  26. package/dist/app/kumiko-screen.js +8 -10
  27. package/dist/app/layout-fields.js +1 -1
  28. package/dist/app/list-facets.js +3 -3
  29. package/dist/app/nav.d.ts +10 -10
  30. package/dist/app/nav.js +13 -13
  31. package/dist/app/projection-detail-body.js +4 -4
  32. package/dist/app/projection-detail-shim.d.ts +6 -5
  33. package/dist/app/projection-detail-shim.js +23 -22
  34. package/dist/app/projection-list-body.js +10 -10
  35. package/dist/app/projection-list-shim.d.ts +5 -5
  36. package/dist/app/projection-list-shim.js +17 -18
  37. package/dist/app/qn.js +10 -12
  38. package/dist/app/qualify-screen-id.js +1 -1
  39. package/dist/app/screen-access.js +2 -2
  40. package/dist/app/screen-shared.js +1 -1
  41. package/dist/app/secret-mint-body.js +4 -4
  42. package/dist/app/write-failed-error.js +8 -7
  43. package/dist/components/embedded-list-field.js +4 -4
  44. package/dist/components/grid-cell-for-field.d.ts +1 -1
  45. package/dist/components/reference-create-dialog.js +3 -3
  46. package/dist/components/related-list-section.d.ts +1 -1
  47. package/dist/components/related-list-section.js +28 -5
  48. package/dist/components/render-edit-logic.js +1 -1
  49. package/dist/components/render-edit-types.d.ts +6 -6
  50. package/dist/components/render-edit.js +63 -65
  51. package/dist/components/render-field.d.ts +7 -7
  52. package/dist/components/render-field.js +57 -59
  53. package/dist/components/render-list.d.ts +43 -44
  54. package/dist/components/render-list.js +63 -67
  55. package/dist/components/use-wizard-form-draft.js +10 -10
  56. package/dist/context/dispatcher-context.js +5 -5
  57. package/dist/context/user-roles-context.js +2 -2
  58. package/dist/headless/apex/index.d.ts +1 -2
  59. package/dist/headless/apex/index.js +5 -6
  60. package/dist/headless/form/form-controller.js +6 -6
  61. package/dist/headless/form/zod-bridge.js +2 -2
  62. package/dist/headless/format/money.js +1 -1
  63. package/dist/headless/index.js +2 -2
  64. package/dist/headless/view-model/edit.js +2 -2
  65. package/dist/headless/view-model/list.js +3 -4
  66. package/dist/headless/view-model/types.d.ts +3 -3
  67. package/dist/hooks/reference-limits.js +11 -12
  68. package/dist/hooks/use-list-url-state.d.ts +13 -13
  69. package/dist/hooks/use-list-url-state.js +21 -22
  70. package/dist/hooks/use-query.js +3 -3
  71. package/dist/hooks/use-reference-lookup.js +16 -17
  72. package/dist/hooks/use-store.js +18 -22
  73. package/dist/hooks/use-stream-handler.js +4 -2
  74. package/dist/i18n-defaults.js +2 -8
  75. package/dist/i18n.d.ts +10 -10
  76. package/dist/i18n.js +25 -25
  77. package/dist/index.js +5 -4
  78. package/dist/live/dispatcher-live.js +2 -2
  79. package/dist/primitives.d.ts +225 -221
  80. package/dist/primitives.js +1 -1
  81. package/dist/sse/live-events.d.ts +5 -5
  82. package/dist/sse/live-events.js +17 -17
  83. package/dist/tokens.d.ts +8 -8
  84. package/dist/tokens.js +16 -17
  85. package/dist/web/app/browser-locale.d.ts +9 -9
  86. package/dist/web/app/browser-locale.js +19 -19
  87. package/dist/web/app/client-plugin.d.ts +44 -52
  88. package/dist/web/app/client-plugin.js +4 -5
  89. package/dist/web/app/create-app.d.ts +28 -31
  90. package/dist/web/app/create-app.js +21 -22
  91. package/dist/web/app/document-lang-sync.js +1 -1
  92. package/dist/web/app/draft-storage.js +1 -1
  93. package/dist/web/app/nav-providers-context.d.ts +3 -3
  94. package/dist/web/app/nav-providers-context.js +3 -3
  95. package/dist/web/app/nav.d.ts +18 -17
  96. package/dist/web/app/nav.js +66 -64
  97. package/dist/web/app/rich-content-editor.js +1 -1
  98. package/dist/web/components/config-cascade.js +17 -18
  99. package/dist/web/components/config-source-badge.js +4 -4
  100. package/dist/web/layout/app-layout.d.ts +9 -9
  101. package/dist/web/layout/app-layout.js +2 -2
  102. package/dist/web/layout/avatar.d.ts +4 -3
  103. package/dist/web/layout/avatar.js +15 -17
  104. package/dist/web/layout/default-app-shell.d.ts +22 -23
  105. package/dist/web/layout/editor-panel.js +3 -3
  106. package/dist/web/layout/nav-tree.js +86 -84
  107. package/dist/web/layout/profile-menu.d.ts +1 -1
  108. package/dist/web/layout/sidebar-brand.d.ts +7 -7
  109. package/dist/web/layout/sidebar-user.d.ts +1 -1
  110. package/dist/web/layout/sidebar-user.js +4 -4
  111. package/dist/web/layout/sidebar.d.ts +8 -8
  112. package/dist/web/layout/sidebar.js +7 -7
  113. package/dist/web/layout/target-resolver-stub.d.ts +5 -5
  114. package/dist/web/layout/target-resolver-stub.js +20 -20
  115. package/dist/web/layout/target-url.d.ts +7 -7
  116. package/dist/web/layout/target-url.js +22 -22
  117. package/dist/web/layout/theme-toggle.js +4 -5
  118. package/dist/web/layout/workspace-shell.d.ts +21 -21
  119. package/dist/web/layout/workspace-shell.js +29 -30
  120. package/dist/web/lib/cn.d.ts +4 -4
  121. package/dist/web/lib/cn.js +4 -4
  122. package/dist/web/primitives/action-menu.d.ts +8 -8
  123. package/dist/web/primitives/action-menu.js +14 -13
  124. package/dist/web/primitives/calendar-popover.d.ts +1 -1
  125. package/dist/web/primitives/calendar-popover.js +10 -10
  126. package/dist/web/primitives/card-styles.js +1 -1
  127. package/dist/web/primitives/card.js +2 -2
  128. package/dist/web/primitives/combobox.d.ts +12 -12
  129. package/dist/web/primitives/combobox.js +34 -35
  130. package/dist/web/primitives/data-table-cell.js +2 -2
  131. package/dist/web/primitives/data-table.js +2 -3
  132. package/dist/web/primitives/date-field.js +2 -2
  133. package/dist/web/primitives/date-input.js +4 -4
  134. package/dist/web/primitives/default-form.js +2 -2
  135. package/dist/web/primitives/dropdown-menu.js +14 -15
  136. package/dist/web/primitives/embedded-list-input.js +3 -3
  137. package/dist/web/primitives/facet-filter.js +1 -1
  138. package/dist/web/primitives/field.js +9 -8
  139. package/dist/web/primitives/form.d.ts +1 -1
  140. package/dist/web/primitives/form.js +6 -6
  141. package/dist/web/primitives/input.js +2 -2
  142. package/dist/web/primitives/layout-elements.js +1 -1
  143. package/dist/web/primitives/layout.js +2 -2
  144. package/dist/web/primitives/located-timestamp-input.js +4 -4
  145. package/dist/web/primitives/money-input.js +13 -17
  146. package/dist/web/primitives/pager.js +1 -1
  147. package/dist/web/primitives/row-actions.js +3 -4
  148. package/dist/web/primitives/secret-reveal.js +1 -1
  149. package/dist/web/primitives/timestamp-input.d.ts +9 -9
  150. package/dist/web/primitives/timestamp-input.js +21 -21
  151. package/dist/web/primitives/toast.js +12 -13
  152. package/dist/web/sse/live-events.d.ts +6 -6
  153. package/dist/web/sse/live-events.js +3 -3
  154. package/dist/web/tokens.d.ts +11 -12
  155. package/dist/web/tokens.js +35 -37
  156. package/dist/web/version/update-checker.js +14 -13
  157. package/dist/web/widgets/ai-text-field.js +1 -1
  158. package/dist/web/widgets/charts.d.ts +10 -10
  159. package/dist/web/widgets/charts.js +8 -8
  160. package/dist/web/widgets/collapsible-section.d.ts +3 -3
  161. package/dist/web/widgets/collapsible-section.js +6 -6
  162. package/dist/web/widgets/detail-list.d.ts +1 -3
  163. package/dist/web/widgets/detail-list.js +1 -3
  164. package/dist/web/widgets/feed-list.d.ts +2 -2
  165. package/dist/web/widgets/feed-list.js +2 -2
  166. package/dist/web/widgets/form-fields.d.ts +14 -14
  167. package/dist/web/widgets/form-fields.js +14 -13
  168. package/dist/web/widgets/index.js +4 -4
  169. package/dist/web/widgets/infinity-list.js +3 -3
  170. package/dist/web/widgets/mode-switch.d.ts +2 -2
  171. package/dist/web/widgets/mode-switch.js +2 -2
  172. package/dist/web/widgets/progress-list.d.ts +1 -2
  173. package/dist/web/widgets/progress-list.js +1 -2
  174. package/dist/web/widgets/query-table.d.ts +6 -6
  175. package/dist/web/widgets/result-panel.d.ts +16 -16
  176. package/dist/web/widgets/result-panel.js +10 -10
  177. package/dist/web/widgets/section-card.d.ts +3 -3
  178. package/dist/web/widgets/section-card.js +3 -3
  179. package/dist/web/widgets/stat.d.ts +4 -4
  180. package/dist/web/widgets/stat.js +6 -6
  181. package/dist/web/widgets/states.d.ts +4 -4
  182. package/dist/web/widgets/states.js +4 -4
  183. package/dist/web/widgets/use-draft.d.ts +7 -7
  184. package/dist/web/widgets/use-draft.js +7 -7
  185. package/package.json +6 -6
@@ -6,27 +6,26 @@ import { type DataTableDateRangeFacet, type DataTableFacet, type DataTableProps,
6
6
  import { type ListEmptyStateContent } from "./list-empty-state.tsx";
7
7
  export type RenderListProps = {
8
8
  readonly screen: EntityListScreenDefinition;
9
- readonly entity: EntityDefinition;
9
+ readonly entity: Pick<EntityDefinition, "fields" | "derivedFields">;
10
10
  readonly rows: readonly Readonly<Record<string, unknown>>[];
11
11
  readonly featureName: string;
12
12
  readonly translate?: Translate;
13
13
  readonly onRowClick?: (row: ListRowViewModel) => void;
14
- /** Override der Default-Empty-Box. Wenn gesetzt, wird der Auto-CTA
15
- * via `onCreate` ignoriert — Caller-Inhalt gewinnt. */
14
+ /** Overrides the default empty box. When set, the automatic CTA via
15
+ * `onCreate` is ignored; the caller's content takes precedence. */
16
16
  readonly emptyState?: ReactNode;
17
- /** Setzt einen "+ Neu" Button in die Toolbar UND dient als CTA in
18
- * der Default-Empty-Box. Caller liefert die Action selber (z. B.
19
- * navigate auf den Edit-Screen). */
17
+ /** Adds a "+ New" button to the toolbar and uses it as the CTA in the
18
+ * default empty box. The caller provides the action (for example,
19
+ * navigation to the edit screen). */
20
20
  readonly onCreate?: () => void;
21
- /** Label für den + Neu Button. Default kommt aus dem i18n-Bundle
22
- * (`kumiko.actions.create`). Caller kann durch eigenen String
23
- * überschreiben. */
21
+ /** Label for the + New button. Defaults to the i18n bundle key
22
+ * (`kumiko.actions.create`). The caller can override it. */
24
23
  readonly createLabel?: string;
25
- /** Aktiviert ein Search-Input in der Toolbar. Server-side Filter
26
- * via Caller's onSearchChange — RenderList filtert NICHT lokal. */
24
+ /** Enables a search input in the toolbar. Filtering is server-side via
25
+ * the caller's onSearchChange; RenderList does not filter locally. */
27
26
  readonly searchable?: boolean;
28
- /** Placeholder für das Search-Input. Default kommt aus dem i18n-
29
- * Bundle (`kumiko.list.search-placeholder`). */
27
+ /** Placeholder for the search input. Defaults to the i18n bundle key
28
+ * (`kumiko.list.search-placeholder`). */
30
29
  readonly searchPlaceholder?: string;
31
30
  /** Hint at the left of the toolbar (already translated). */
32
31
  readonly description?: string;
@@ -38,35 +37,35 @@ export type RenderListProps = {
38
37
  readonly defaultFilterActive?: boolean;
39
38
  /** Plural-forms key for the footer count noun; default is the entity's `:noun` convention key. */
40
39
  readonly itemNounKey?: string;
41
- /** Aktueller Search-Term (vom URL-State / Parent). RenderList puffert
42
- * Tipps lokal mit 300ms Debounce, bevor onSearchChange gefeuert
43
- * wird — sonst macht jeder Tastendruck einen Server-Roundtrip. */
40
+ /** Current search term (from URL state / the parent). RenderList buffers
41
+ * typing locally with a 300ms debounce before calling onSearchChange;
42
+ * otherwise every keystroke would trigger a server round trip. */
44
43
  readonly searchValue?: string;
45
- /** Wird gerufen wenn der debounced Search-Term sich ändert. Caller
46
- * setzt damit URL-State (?<id>.q=…) und triggert ein refetch. */
44
+ /** Called when the debounced search term changes. The caller uses it to
45
+ * set URL state (?<id>.q=…) and trigger a refetch. */
47
46
  readonly onSearchChange?: (next: string) => void;
48
- /** Aktuelle Sortierung (oder null = Server-Default-Order). */
47
+ /** Current sorting, or null for the server's default order. */
49
48
  readonly sort?: ListSort | null;
50
- /** Wird gerufen mit dem nächsten Sort-State nach einem Header-Klick. */
49
+ /** Called with the next sort state after a header click. */
51
50
  readonly onSortChange?: (next: ListSort | null) => void;
52
- /** Pager-Props für pagination="pages". Wenn undefined, kein Pager-
53
- * UI — Default oder infinite-Mode. KumikoScreen liefert das je nach
54
- * screen.pagination. */
51
+ /** Pager props for pagination="pages". If undefined, no pager UI is
52
+ * rendered; the default or infinite mode is used. KumikoScreen supplies
53
+ * this according to screen.pagination. */
55
54
  readonly pager?: {
56
55
  readonly page: number;
57
56
  readonly limit: number;
58
57
  readonly total: number;
59
58
  readonly onPageChange: (next: number) => void;
60
59
  };
61
- /** Infinite-Scroll-Wiring für pagination="infinite". KumikoScreen
62
- * hält accumulated rows + cursor, RenderList reicht die Callbacks
63
- * einfach durch an DataTable. */
60
+ /** Infinite-scroll wiring for pagination="infinite". KumikoScreen keeps
61
+ * the accumulated rows and cursor; RenderList passes the callbacks
62
+ * through to DataTable. */
64
63
  readonly onReachEnd?: () => void;
65
64
  readonly loadingMore?: boolean;
66
65
  readonly hasMore?: boolean;
67
- /** Pro-Row-Aktionen — Resolved-Form (KumikoScreen baut das aus
68
- * EntityListScreenDefinition.rowActions: handler-QN → dispatcher-Call,
69
- * i18n-Keys → translated Strings). */
66
+ /** Per-row actions in resolved form. KumikoScreen builds them from
67
+ * EntityListScreenDefinition.rowActions: handler-QN becomes a dispatcher
68
+ * call, and i18n keys become translated strings. */
70
69
  readonly rowActions?: readonly DataTableRowAction[];
71
70
  /** How the row-action column renders (see `DataTableProps.rowActionMode`).
72
71
  * KumikoScreen derives it from the actions themselves; without it the
@@ -75,38 +74,38 @@ export type RenderListProps = {
75
74
  /** Forwarded to `DataTableProps.rowGrouping` / `rowTone`. */
76
75
  readonly rowGrouping?: DataTableRowGrouping;
77
76
  readonly rowTone?: DataTableProps["rowTone"];
78
- /** Toolbar-Aktionen im List-Header — Resolved-Form (KumikoScreen baut
79
- * das aus EntityListScreenDefinition.toolbarActions: navigate-target
80
- * → useNav, handler-QN → dispatcher-Call). RenderList rendert die
81
- * Buttons rechts in der Toolbar, vor "+ Neu". */
77
+ /** Toolbar actions in the list header in resolved form. KumikoScreen
78
+ * builds them from EntityListScreenDefinition.toolbarActions:
79
+ * navigate targets use useNav and handler-QN uses a dispatcher call.
80
+ * RenderList renders the buttons on the right side of the toolbar,
81
+ * before "+ New". */
82
82
  readonly toolbarActions?: readonly ToolbarActionButton[];
83
- /** Faceted-Filter-Dropdowns — KumikoScreen baut sie aus den filterable
84
- * select/boolean-Feldern; RenderList reicht sie an die DataTable durch. */
83
+ /** Faceted filter dropdowns. KumikoScreen builds them from filterable
84
+ * select/boolean fields; RenderList passes them through to DataTable. */
85
85
  readonly filterFacets?: readonly DataTableFacet[];
86
- /** Aktuell gewählte Facet-Werte je Feld (vom URL-State). */
86
+ /** Currently selected facet values by field (from URL state). */
87
87
  readonly filterValues?: Readonly<Record<string, readonly string[]>>;
88
- /** Toggle eines Facet-Werts (URL-State setzen + refetch). */
88
+ /** Toggles a facet value (sets URL state and refetches). */
89
89
  readonly onFilterChange?: (field: string, values: readonly string[]) => void;
90
90
  /** Reset aller aktiven Facets. */
91
91
  readonly onFilterReset?: () => void;
92
- /** Time-range filters (fw#3104) — forwarded to the DataTable. */
92
+ /** Time-range filters — forwarded to the DataTable. */
93
93
  readonly dateRangeFacets?: readonly DataTableDateRangeFacet[];
94
94
  /** One bound changed (set URL state + refetch). */
95
95
  readonly onDateRangeChange?: (field: string, bound: "from" | "to", value: string) => void;
96
96
  /** Forwarded to `DataTableProps.chromeless` — drops the table's own card
97
97
  * frame for a host that already provides one (relatedList in a tabs-mode
98
- * section, fw#2722). Default false. */
98
+ * section). Default false. */
99
99
  readonly chromeless?: boolean;
100
100
  /** Forwarded to `DataTableProps.scrollBody` — bounds the table height and
101
101
  * scrolls rows internally instead of growing the page (relatedList in a
102
- * tabs-mode section, fw#2722). Default false. */
102
+ * tabs-mode section). Default false. */
103
103
  readonly scrollBody?: boolean;
104
104
  /** Forwarded to `DataTableProps.screenPadding` — `true`: the table carries
105
105
  * the shared screen padding because it is the screen body (entityList/
106
- * projectionList, fw#2640). Explicit `false`: no padding at all — the
107
- * host already pads its own children (relatedList in a tabs-mode Card,
108
- * fw#3234). Omitted: legacy `p-6` fallback for a standalone table with no
109
- * padded host. */
106
+ * projectionList). Explicit `false`: no padding at all — the host already
107
+ * pads its own children (relatedList in a tabs-mode Card). Omitted: legacy
108
+ * `p-6` fallback for a standalone table with no padded host. */
110
109
  readonly screenPadding?: boolean;
111
110
  /** Forwarded to `DataTableProps.expandedRowIds` / `onToggleRowExpanded` /
112
111
  * `renderExpandedRow` (entityList `expandableRow`). */
@@ -18,8 +18,8 @@ function isHeaderPromotableToolbarAction(action) {
18
18
  }
19
19
  export function RenderList(props) {
20
20
  const { screen, entity, rows, featureName, translate: translateProp, onRowClick, emptyState, onCreate, createLabel, searchable = false, searchPlaceholder, description, emptyContent, narrowed = false, defaultFilterActive: defaultFilterActiveProp = false, 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;
21
- // Wie RenderEdit: Translate-Fallback aus dem i18next-Context, sonst
22
- // wären Column-Header raw i18n-Keys.
21
+ // As in RenderEdit, fall back to the i18next context for translation;
22
+ // otherwise column headers would display raw i18n keys.
23
23
  const t = useTranslation();
24
24
  const translate = translateProp ?? t;
25
25
  const { DataTable, Button, Dialog, Input, Banner, PageHeader } = usePrimitives();
@@ -62,30 +62,28 @@ export function RenderList(props) {
62
62
  return () => clearTimeout(timer);
63
63
  }, [localQ, searchValue, onSearchChange]);
64
64
  const vm = useMemo(() => computeListViewModel({ screen, entity, rows, translate, featureName }), [screen, entity, rows, translate, featureName]);
65
- // Tier 2.7e-4: Reference-Field-Eagerload via Bridge-Component-Pattern.
66
- // Hooks-Rule-Konflikt: pro reference-Spalte muss useReferenceLookup
67
- // gerufen werden, aber die Anzahl varriiert per Schema. Lösung —
68
- // ReferenceLookupBridge-Komponenten pro Spalte gemountet, die ihre
69
- // Map über onMap-Callback in einen lokalen State veröffentlichen.
70
- // Anzahl der Hook-Calls pro Bridge-Component ist konstant (1), und
71
- // React verwaltet die mounted/unmounted Lifecycles beim Schema-
72
- // Wechsel über die key-Property automatisch.
65
+ // Reference-field eager loading through bridge components.
66
+ // The Hooks rule requires useReferenceLookup for each reference column,
67
+ // but the number of columns varies with the schema. Each column therefore
68
+ // mounts a ReferenceLookupBridge that publishes its map to local state
69
+ // through onMap. Each bridge makes a constant number of hook calls (one),
70
+ // and React manages its lifecycle when the schema changes via key.
73
71
  const referenceColumns = useMemo(() => vm.columns
74
72
  .filter((c) => c.type === "reference" && c.refEntity !== undefined)
75
73
  .map((c) => ({
76
74
  field: c.field,
77
75
  refEntity: c.refEntity ?? "",
78
- // Tier 2.7e Cross-Feature: refFeature kommt aus parseRefTarget
79
- // im ViewModel (default = current featureName).
76
+ // refFeature comes from parseRefTarget in the ViewModel
77
+ // (default = the current featureName).
80
78
  refFeature: c.refFeature ?? featureName,
81
79
  labelField: c.refLabelField ?? "id",
82
80
  })), [vm.columns, featureName]);
83
81
  const [referenceLookups, setReferenceLookups] = useState({});
84
82
  const handleLookupMap = useCallback((field, map) => {
85
83
  setReferenceLookups((prev) => {
86
- // skip-update wenn die Map identisch ist (Render-Loop-Schutz —
87
- // Bridge-Component re-rendert sonst und schickt die gleiche Map
88
- // wieder rein bis React in eine Endlosschleife geht).
84
+ // Skip the update when the map is identical. Otherwise the bridge
85
+ // would re-render and send the same map back until React entered
86
+ // an infinite loop.
89
87
  if (prev[field] === map)
90
88
  return prev;
91
89
  return { ...prev, [field]: map };
@@ -98,8 +96,8 @@ export function RenderList(props) {
98
96
  return vm.columns.map((col) => {
99
97
  if (col.type !== "reference")
100
98
  return col;
101
- // Author-deklarierter Renderer übersteuert immer — Default greift
102
- // nur wenn keiner gesetzt ist.
99
+ // An author-declared renderer always takes precedence;
100
+ // the default is used only when none is set.
103
101
  if (col.renderer !== undefined)
104
102
  return col;
105
103
  const map = referenceLookups[col.field];
@@ -145,11 +143,11 @@ export function RenderList(props) {
145
143
  });
146
144
  };
147
145
  const renderer = (value, row) => {
148
- // Tier 2.7e Server-Eagerload: wenn der Server _refs mit-
149
- // schickt, lesen wir den Display-Wert direkt aus der
150
- // resolved Row — kein Roundtrip durch die Bridge-Map nötig
151
- // und keine limit:200-Constraint. EagerloadedRow-Type aus
152
- // @cosmicdrift/kumiko-framework/db pinnt die Form von _refs.
146
+ // Server eager loading: when the server includes _refs, read the display
147
+ // value directly from the resolved row. This avoids a round trip
148
+ // through the bridge map and the limit:200 constraint. The
149
+ // EagerloadedRow type from @cosmicdrift/kumiko-framework/db defines
150
+ // the shape of _refs.
153
151
  const resolved = eagerloadedRef(row, col.field);
154
152
  if (Array.isArray(resolved) && resolved.length > 0) {
155
153
  return resolved.map((r) => String(r[labelField] ?? r["id"] ?? "")).join(", ");
@@ -157,7 +155,7 @@ export function RenderList(props) {
157
155
  if (isPlainRecord(resolved)) {
158
156
  return String(resolved[labelField] ?? resolved["id"] ?? "");
159
157
  }
160
- // Renderer-Side-Fallback (kein Server-Eagerload aktiv).
158
+ // Renderer-side fallback when server eager loading is not active.
161
159
  if (Array.isArray(value)) {
162
160
  if (value.length === 0)
163
161
  return EMPTY_VALUE_PLACEHOLDER;
@@ -182,9 +180,9 @@ export function RenderList(props) {
182
180
  ...vm,
183
181
  columns: enrichedColumns,
184
182
  }), [vm, enrichedColumns]);
185
- // i18n-Defaults für Toolbar/Empty-State Strings — Caller kann jeden
186
- // einzeln per Prop überschreiben, sonst kommen die Framework-Bundles
187
- // (kumiko.actions.create, kumiko.list.search-placeholder, …).
183
+ // i18n defaults for toolbar and empty-state strings. The caller can
184
+ // override each one through a prop; otherwise the framework bundles provide
185
+ // them (kumiko.actions.create, kumiko.list.search-placeholder, …).
188
186
  const effectiveCreateLabel = translate(createLabel ?? screen.createLabel ?? "kumiko.actions.create");
189
187
  const effectiveSearchPlaceholder = translate(searchPlaceholder ?? screen.searchPlaceholder ?? "kumiko.list.search-placeholder");
190
188
  const nounKey = itemNounKey ?? `${featureName}:entity:${screen.entity}:noun`;
@@ -193,12 +191,12 @@ export function RenderList(props) {
193
191
  ? (count) => translate(nounKey, { count })
194
192
  : undefined;
195
193
  const toolbarStart = searchable ? (_jsx(Input, { kind: "text", id: "render-list-search", name: "search", value: localQ, onChange: setLocalQ, placeholder: effectiveSearchPlaceholder })) : undefined;
196
- // Toolbar-End-Slot: optionaler Header-Slot (z.B. Cap-Counter, links im
197
- // rechten Cluster) + Toolbar-Actions (List-Header-Buttons aus dem Schema)
198
- // + optional "+ Neu" am rechten Edge. Der Header-Slot lebt IN der Toolbar-
199
- // Zeile (gleicher Card-Rahmen), nicht als loser Text über dem Screen-Titel
200
- // (Bug-Bash 3 #12). Reihenfolge = Array-Reihenfolge (Schema-deklariert),
201
- // "+ Neu" zuletzt weil das die häufigste/auffälligste CTA ist.
194
+ // Toolbar-end slot: an optional header slot (for example, a cap counter,
195
+ // at the left of the right-hand cluster), toolbar actions from the schema,
196
+ // and an optional "+ New" button at the right edge. The header slot lives
197
+ // inside the toolbar row, with the same card frame, rather than as loose
198
+ // text above the screen title. Items retain array order, and "+ New" comes
199
+ // last because it is the most common and prominent CTA.
202
200
  const hasToolbarActions = toolbarActions !== undefined && toolbarActions.length > 0;
203
201
  const hasHeaderSlot = screen.slots?.header !== undefined;
204
202
  const toolbarIconOnly = hasToolbarActions && shouldRenderActionsIconOnly(toolbarActions);
@@ -218,30 +216,29 @@ export function RenderList(props) {
218
216
  hasToolbarActionsInToolbar ||
219
217
  (onCreate !== undefined && !createInPageHeader) ? (_jsxs(_Fragment, { children: [hasHeaderSlot && _jsx(ListHeaderSlotMount, { screen: screen }), hasToolbarActionsInToolbar &&
220
218
  toolbarActionsInToolbar.map((a) => (_jsx(ToolbarActionView, { action: a, iconOnly: toolbarIconOnly, Button: Button, Dialog: Dialog, Banner: Banner }, a.id))), !createInPageHeader && createButton] })) : undefined;
221
- // Empty-State: Default zeigt Heading + Description + optional CTA-
222
- // Button. Caller kann via emptyState-Prop komplett überschreiben.
223
- // Wenn weder onCreate noch ein Override gegeben ist, fällt die
224
- // DataTable auf den DataTable-Default zurück (`kumiko.list.no-entries`).
219
+ // Empty state: by default, show a heading, description, and optional CTA
220
+ // button. The caller can replace it completely through emptyState.
221
+ // Without onCreate or an override, DataTable uses its default
222
+ // (`kumiko.list.no-entries`).
225
223
  const defaultFilterActive = defaultFilterActiveProp && onFilterReset !== undefined;
226
224
  const composedEmptyState = emptyState ??
227
225
  (onCreate !== undefined || emptyContent !== undefined || narrowed || defaultFilterActive ? (_jsx(ListEmptyStateBody, { content: emptyContent, narrowed: narrowed, defaultFilterActive: defaultFilterActive, ...(onFilterReset !== undefined && { onShowAll: onFilterReset }), translate: translate, createButton: onCreate !== undefined && !narrowed && !defaultFilterActive && !createInPageHeader ? (_jsx(Button, { variant: "primary", icon: "plus", onClick: onCreate, testId: "render-list-empty-create", children: effectiveCreateLabel })) : undefined })) : undefined);
228
- // Title-Resolution: ein konventioneller i18n-Key `screen:<id>.title`.
229
- // Wenn das Bundle den Key kennt, schöner Titel; sonst kommt der
230
- // screenId selber raus (kein "Untitled"-Fallback — der App-Dev sieht
231
- // dann dass die Übersetzung fehlt).
226
+ // Title resolution uses the conventional i18n key `screen:<id>.title`.
227
+ // If the bundle knows the key, use its translated title; otherwise use
228
+ // screenId so the app developer can see that the translation is missing.
232
229
  const titleKey = `screen:${screen.id}.title`;
233
230
  const resolvedTitle = translate(titleKey);
234
231
  const toolbarTitle = resolvedTitle === titleKey ? screen.id : resolvedTitle;
235
- // ListSort = DataTableSort (use-list-url-state aliased) — kein Cast nötig.
232
+ // ListSort = DataTableSort (aliased in use-list-url-state), so no cast is needed.
236
233
  return (_jsxs(_Fragment, { children: [createInPageHeader && PageHeader !== undefined && _jsx(PageHeader, { actions: createButton }), promotedActionButton !== undefined && PageHeader !== undefined && (_jsx(PageHeader, { actions: promotedActionButton })), referenceColumns.map((rc) => (_jsx(ReferenceLookupBridge, { field: rc.field, refEntity: rc.refEntity, labelField: rc.labelField, featureName: rc.refFeature, onMap: handleLookupMap }, rc.field))), _jsx(DataTable, { columns: enrichedVm.columns, rows: enrichedVm.rows, toolbarTitle: toolbarTitle, ...(onRowClick !== undefined && { onRowClick }), ...(composedEmptyState !== undefined && { emptyState: composedEmptyState }), ...(toolbarStart !== undefined && { toolbarStart }), ...(description !== undefined && { toolbarDescription: description }), ...(toolbarEnd !== undefined && { toolbarEnd }), ...(sort !== undefined && { sort }), ...(onSortChange !== undefined && { onSortChange }), ...(pager !== undefined && { pager }), ...(onReachEnd !== undefined && { onReachEnd }), ...(loadingMore !== undefined && { loadingMore }), ...(hasMore !== undefined && { hasMore }), ...(itemNoun !== undefined && { itemNoun }), ...(rowActions !== undefined && { rowActions }), ...(rowActionMode !== undefined && { rowActionMode }), ...(rowGrouping !== undefined && { rowGrouping }), ...(rowTone !== undefined && { rowTone }), ...(filterFacets !== undefined && { filterFacets }), ...(filterValues !== undefined && { filterValues }), ...(onFilterChange !== undefined && { onFilterChange }), ...(onFilterReset !== undefined && { onFilterReset }), ...(dateRangeFacets !== undefined && { dateRangeFacets }), ...(onDateRangeChange !== undefined && { onDateRangeChange }), ...(chromeless !== undefined && { chromeless }), ...(scrollBody !== undefined && { scrollBody }), ...(screenPadding !== undefined && { screenPadding }), ...(expandedRowIds !== undefined && { expandedRowIds }), ...(onToggleRowExpanded !== undefined && { onToggleRowExpanded }), ...(renderExpandedRow !== undefined && { renderExpandedRow }), testId: "render-list-table" })] }));
237
234
  }
238
- // Header-Slot in der Listen-Toolbar (toolbarEnd): `screen.slots.header` ist
239
- // eine PlatformComponent (`{ react: { __component: "X" } }`), aufgelöst über
240
- // dieselbe ExtensionSectionsProvider-Registry wie entityEdit-Sections.
241
- // entityId ist null (Listen-Kontext, keine Row); die Component lädt ihre
242
- // Daten selbst (z.B. ein Cap-Counter aus einer usage-Query). Eigene
243
- // Component wegen Rules-of-Hooks — useExtensionSectionComponent darf nicht
244
- // konditional im RenderList-Body laufen.
235
+ // Header slot in the list toolbar (toolbarEnd): `screen.slots.header` is
236
+ // a PlatformComponent (`{ react: { __component: "X" } }`) resolved through
237
+ // the same ExtensionSectionsProvider registry as entityEdit sections.
238
+ // entityId is null in the list context because there is no row; the component
239
+ // loads its own data (for example, a cap counter from a usage query). A
240
+ // separate component is required by the Rules of Hooks because
241
+ // useExtensionSectionComponent must not run conditionally in RenderList.
245
242
  function ListHeaderSlotMount({ screen, }) {
246
243
  const header = screen.slots?.header;
247
244
  const name = header !== undefined ? extensionSectionName(header) : undefined;
@@ -261,18 +258,18 @@ function ListHeaderSlotMount({ screen, }) {
261
258
  // (useListUrlState) — e.g. a tag filter narrowing the list to an id-set.
262
259
  return _jsx(Component, { entityName: screen.entity, entityId: null, screenId: screen.id });
263
260
  }
264
- // Tier 2.7e-4: Bridge-Component pro reference-Spalte. Mounted für jede
265
- // Spalte einmal, ruft useReferenceLookup unconditional (Hook-Rules
266
- // happy), und published die Map über onMap an den Parent. React
267
- // verwaltet das Mounting/Unmounting beim Schema-Wechsel über die
268
- // key-Prop im map-Loop des Parents.
261
+ // Bridge component for each reference column. It is mounted once per
262
+ // column, calls useReferenceLookup unconditionally, and publishes the map
263
+ // to the parent through onMap. React manages mounting and unmounting when
264
+ // the schema changes through the key prop in the parent's map loop.
269
265
  function ReferenceLookupBridge({ field, refEntity, labelField, featureName, onMap, }) {
270
266
  const lookup = useReferenceLookup(featureName, refEntity, labelField);
271
- // useEffect statt direktem call damit setState außerhalb des Render-
272
- // Pfads passiert (sonst React-Warning "Cannot update a component
273
- // while rendering a different component").
274
- // Unpublished while loading or after a failed lookup: the renderer treats a missing map as
275
- // "not resolved", an empty published map as "nothing found". Only the latter may say "Unknown".
267
+ // Use an effect rather than calling directly so setState runs outside the
268
+ // render path; otherwise React warns about updating one component while
269
+ // rendering another.
270
+ // Do not publish while loading or after a failed lookup: a missing map means
271
+ // "not resolved", whereas an empty published map means "nothing found".
272
+ // Only the latter may display "Unknown".
276
273
  const resolved = !lookup.loading && !lookup.failed;
277
274
  useEffect(() => {
278
275
  if (resolved)
@@ -280,18 +277,17 @@ function ReferenceLookupBridge({ field, refEntity, labelField, featureName, onMa
280
277
  }, [field, lookup.map, resolved, onMap]);
281
278
  return null;
282
279
  }
283
- // ToolbarActionView — pro Toolbar-Action ein Button (+ Confirm-Dialog
284
- // wenn confirm gesetzt). Same Pattern wie RowAction (Inline-Variante)
285
- // aber ohne row-Context. busy-State während async onTrigger; Dialog
286
- // öffnet sich vor dem Trigger wenn confirm/danger gesetzt.
280
+ // ToolbarActionView renders one button per toolbar action, with a confirm
281
+ // dialog when required. It follows the inline RowAction pattern but has no
282
+ // row context. The busy state covers async onTrigger; the dialog opens before
283
+ // the trigger when confirmation is required for confirm/danger actions.
287
284
  function ToolbarActionView({ action, iconOnly = false, Button, Dialog, Banner, }) {
288
285
  const [busy, setBusy] = useState(false);
289
286
  const [confirmOpen, setConfirmOpen] = useState(false);
290
- // Toolbar-Actions haben (anders als rowActions, die über die DataTable-
291
- // Primitive laufen) keine Toast-Primitive zur Verfügung — ein inline
292
- // Error-Banner ist der surfacing-Pfad hier. Gleicher Grund wie bei
293
- // rowActions (Prod-Bug 2026-06-07): ein verschlucktes Failure-Result
294
- // sieht für den User wie "nichts passiert" aus.
287
+ // Toolbar actions do not have access to the toast primitive, unlike
288
+ // rowActions routed through the DataTable primitive, so an inline error
289
+ // banner is used to surface failures. Without it, a swallowed failure result
290
+ // would look to the user as though nothing happened.
295
291
  const [errorText, setErrorText] = useState(null);
296
292
  const trigger = async () => {
297
293
  setBusy(true);
@@ -12,10 +12,10 @@ const FORM_DRAFT_LIST = "form-draft:query:list";
12
12
  // Mirrors bundled-features/src/form-draft/web/constants.ts's FORM_DRAFT_KEY_MAX_LENGTH
13
13
  // — same hardcoding rationale as the QNs above (no dependency on that package).
14
14
  const FORM_DRAFT_KEY_MAX_LENGTH = 256;
15
- // Trailing-edge debounce for patch()-triggered draft saves (#1914). A single
15
+ // Trailing-edge debounce for patch()-triggered draft saves. A single
16
16
  // patch() call (VIN-decode, an extension section) should not save immediately
17
17
  // per call, but patch() can also fire from inside onChange on every keystroke
18
- // (the #1888 derived-field shape) — 500ms collapses a typing burst into one
18
+ // (the derived-field shape) — 500ms collapses a typing burst into one
19
19
  // save instead of one per keystroke, while still saving well before a user
20
20
  // abandons the tab.
21
21
  export const PATCH_DRAFT_SAVE_DEBOUNCE_MS = 500;
@@ -40,7 +40,7 @@ function mintDraftId() {
40
40
  return `draft-${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
41
41
  }
42
42
  export function useWizardFormDraftState({ screenId, entityId, isCreateMode, draftStorage, }) {
43
- // Create-mode draftId (issue #1913) — resumed from `sessionStorage` (web)
43
+ // Create-mode draftId — resumed from `sessionStorage` (web)
44
44
  // on mount so a same-tab reload finds the right one of several parallel
45
45
  // create-sessions on this screen; `null` until the first step change
46
46
  // mints one (saveDraft), or the mount-time `form-draft:query:list`
@@ -55,7 +55,7 @@ export function useWizardFormDraftState({ screenId, entityId, isCreateMode, draf
55
55
  // once for this mount. Without this, a create-mode discard resets `draftId`
56
56
  // to `null` (see discardDraft below) and would re-arm the list effect —
57
57
  // silently adopting an unrelated parallel draft on the same screen right
58
- // after the user just submitted (#1908).
58
+ // after the user just submitted.
59
59
  const didListRef = useRef(false);
60
60
  const [draftCandidates, setDraftCandidates] = useState(null);
61
61
  // Derived from the screen id + the host entity id only — never from
@@ -64,7 +64,7 @@ export function useWizardFormDraftState({ screenId, entityId, isCreateMode, draf
64
64
  // unchanged. Create-mode: `${screenId}:new:${draftId}` once a draftId
65
65
  // exists (resumed, adopted, or minted), `undefined` before that — two
66
66
  // parallel create sessions on the same screen must never collapse onto
67
- // the same key (kumiko-framework#1908).
67
+ // the same key.
68
68
  const draftKey = useMemo(() => isCreateMode
69
69
  ? draftId !== null
70
70
  ? `${screenId}:new:${draftId}`
@@ -133,7 +133,7 @@ export function useWizardFormDraftRestore({ screenId, isCreateMode, draftEnabled
133
133
  hasNavigatedRef,
134
134
  setRawStep,
135
135
  ]);
136
- // Mount-time fallback (issue #1913) for create-mode when no draftId
136
+ // Mount-time fallback for create-mode when no draftId
137
137
  // survived in storage (new tab, cleared storage): ask the server for this
138
138
  // screen's open drafts and let the user choose via a picker (below) —
139
139
  // even for exactly one candidate. Auto-adopting a lone candidate would
@@ -186,7 +186,7 @@ export function buildWizardDraftActions({ screenId, isCreateMode, draftEnabled,
186
186
  // (see draftCandidates below) — adopt it the same way a single
187
187
  // auto-adopted candidate would be.
188
188
  function adoptDraft(candidate) {
189
- // Locked state (#1896/fw#1909): `disabled` means "no write possible" —
189
+ // Locked state: `disabled` means "no write possible" —
190
190
  // repointing draftKey and patching in the candidate's values is exactly
191
191
  // that, so a locked form must not adopt a draft.
192
192
  // skip: a locked form keeps its current draft (reason above).
@@ -202,8 +202,8 @@ export function buildWizardDraftActions({ screenId, isCreateMode, draftEnabled,
202
202
  //
203
203
  // Create-mode, first draft save (step change OR a debounced patch() from
204
204
  // controlled mode / extension sections, see patchAndScheduleDraftSave
205
- // above — both call this on step 0 too): mints the draftId here (issue
206
- // #1913) rather than at mount, so a form nobody ever interacted with
205
+ // above — both call this on step 0 too): mints the draftId here
206
+ // rather than at mount, so a form nobody ever interacted with
207
207
  // never claims a draftId or writes a row. `draftKey`/`draftId` state
208
208
  // won't reflect the mint until the next render, so the just-minted key is
209
209
  // computed inline instead of read from the memoized `draftKey`.
@@ -220,7 +220,7 @@ export function buildWizardDraftActions({ screenId, isCreateMode, draftEnabled,
220
220
  key = `${screenId}:new:${mintedId}`;
221
221
  // A stale picker from the mount-time list fallback must not survive a
222
222
  // mint — picking a candidate afterwards would repoint draftKey at an
223
- // unrelated draft mid-edit and overwrite it (#1908).
223
+ // unrelated draft mid-edit and overwrite it.
224
224
  setDraftCandidates(null);
225
225
  }
226
226
  // skip: create-mode with a draftId that failed to mint — unreachable
@@ -32,11 +32,11 @@ export function useDispatcher() {
32
32
  }
33
33
  return dispatcher;
34
34
  }
35
- // Soft-Variante: null wenn kein Provider mounted statt throw. Für
36
- // Components die einen Dispatcher OPTIONAL brauchen (z.B. KumikoScreen
37
- // rendert mit/ohne rowActions; die Test-Suite mountet kein Dispatcher
38
- // wenn der Screen keine Mutation triggert). Caller ist zuständig dass
39
- // er bei null einen sinnvollen Fallback hat (typisch: kein Action-Wiring).
35
+ // Soft variant: returns null when no provider is mounted instead of throwing. For
36
+ // components that optionally need a Dispatcher (for example, KumikoScreen renders
37
+ // with or without rowActions; the test suite does not mount a Dispatcher when the
38
+ // screen triggers no mutation). Callers must provide a sensible fallback for null
39
+ // (typically, no action wiring).
40
40
  export function useOptionalDispatcher() {
41
41
  return useContext(DispatcherContext) ?? undefined;
42
42
  }
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createContext, useContext } from "react";
3
3
  // Threads the current user's roles through the tree so KumikoScreen can
4
- // gate role-restricted screens at render time (see #1203 — nav filtering
5
- // alone doesn't stop a direct-URL/screenQn hit on a role-gated screen).
4
+ // gate role-restricted screens at render time — nav filtering alone
5
+ // doesn't stop a direct-URL/screenQn hit on a role-gated screen).
6
6
  // Apps wire this from their `shell` render-prop, the same place they
7
7
  // already pass `user` to WorkspaceShell for nav filtering.
8
8
  //
@@ -231,6 +231,5 @@ export declare function injectPageHead(html: string, tagsHtml: string): string;
231
231
  * the shared timeout. Never throws and never changes `html` on a resolver
232
232
  * error, a `null` resolve, or a timeout — the caller's shell always ships
233
233
  * unchanged. runProdApp and runDevApp both call this single function, so a
234
- * future change to timeout or fallback semantics has one place to land
235
- * (kumiko-framework#3026). */
234
+ * future change to timeout or fallback semantics has one place to land. */
236
235
  export declare function resolveAndInjectPageHead(html: string, resolvePageHead: PageHeadResolver, input: PageHeadResolveInput): Promise<string>;
@@ -8,8 +8,8 @@ import { APEX_STRUCTURAL_CSS } from "./css.js";
8
8
  import { APEX_LIGHTBOX_HTML, APEX_LIGHTBOX_SCRIPT } from "./lightbox.js";
9
9
  export { APEX_NAV_MENU_CSS, APEX_NAV_TOGGLE_RESPONSIVE_CSS, APEX_STRUCTURAL_CSS } from "./css.js";
10
10
  export { APEX_LIGHTBOX_HTML, APEX_LIGHTBOX_SCRIPT, APEX_LIGHTBOX_SCRIPT_CSP_HASH, } from "./lightbox.js";
11
- // JSON in <script>-Kontext: `<` als < serialisieren, damit weder
12
- // `</script>` noch `<!--` aus dem Block ausbrechen kann (JSON bleibt valide).
11
+ // JSON in <script> context: serialize `<` as < so neither
12
+ // `</script>` nor `<!--` can escape the block (JSON remains valid).
13
13
  function scriptSafeJsonHtml(value) {
14
14
  return JSON.stringify(value).replace(/</g, "\\u003c");
15
15
  }
@@ -265,7 +265,7 @@ export function renderApexHeadTags(head) {
265
265
  export function renderApexPage(page) {
266
266
  const { head, brand } = page;
267
267
  const theme = page.theme ?? "light";
268
- // Brand-CSS ist app-authored (Trust-Boundary siehe Datei-Header), kein Tenant-Input.
268
+ // Brand CSS is app-authored (see the file header for the trust boundary), not tenant input.
269
269
  const cssHtml = (brand.fontFaceCss ?? "") + brand.tokensCss + APEX_STRUCTURAL_CSS;
270
270
  const sectionsHtml = page.sections.map(renderSection).join("\n\n ");
271
271
  // interactive-widget is ignored by iOS Safari; apex pages have no fixed bottom bars, so
@@ -308,7 +308,7 @@ export function injectPageHead(html, tagsHtml) {
308
308
  // Resolver runs alongside the request, never gates it: a slow, throwing, or
309
309
  // null-returning resolver must never turn a 200 shell into a 500 or a
310
310
  // stall. One constant shared by runProdApp and runDevApp so both time out
311
- // on the same value (kumiko-framework#3026).
311
+ // on the same value.
312
312
  const PAGE_HEAD_TIMEOUT_MS = 300;
313
313
  async function resolvePageHeadWithTimeout(resolvePageHead, input) {
314
314
  let timer;
@@ -367,8 +367,7 @@ function toApexHead(meta) {
367
367
  * the shared timeout. Never throws and never changes `html` on a resolver
368
368
  * error, a `null` resolve, or a timeout — the caller's shell always ships
369
369
  * unchanged. runProdApp and runDevApp both call this single function, so a
370
- * future change to timeout or fallback semantics has one place to land
371
- * (kumiko-framework#3026). */
370
+ * future change to timeout or fallback semantics has one place to land. */
372
371
  export async function resolveAndInjectPageHead(html, resolvePageHead, input) {
373
372
  const meta = await resolvePageHeadWithTimeout(resolvePageHead, input);
374
373
  if (!meta)
@@ -68,7 +68,7 @@ function stripUntouchedEmptyStrings(values, initial, forceIncludeEmpty) {
68
68
  let out;
69
69
  for (const key of Object.keys(v)) {
70
70
  // Value-based seed strip, except keys previously submitted as "" after a
71
- // real clear — those must survive post-rebase submits (#1858).
71
+ // real clear — those must survive post-rebase submits.
72
72
  if (!forceIncludeEmpty.has(key) && v[key] === "" && ini[key] === "") {
73
73
  out ??= { ...v };
74
74
  delete out[key];
@@ -105,7 +105,7 @@ export function createFormController(options) {
105
105
  let initial = { ...options.initial };
106
106
  let errors = Object.freeze({});
107
107
  // Keys last submitted as "" (intentional clear). Survives rebase so a later
108
- // submit of other fields does not drop them (#1858).
108
+ // submit of other fields does not drop them.
109
109
  const forceIncludeEmpty = new Set();
110
110
  // ctx is cast-held as TCtx; `undefined` is valid when callers don't
111
111
  // declare conditional predicates that depend on it.
@@ -166,7 +166,7 @@ export function createFormController(options) {
166
166
  function runValidate(scope, opts) {
167
167
  // See the validate() doc comment in types.ts for the scope contract.
168
168
  // A scope entry matches its exact path and everything below it, but not
169
- // siblings: `address.city` must not pull in `address.zip` (#1898).
169
+ // siblings: `address.city` must not pull in `address.zip`.
170
170
  const isInScope = scope === undefined
171
171
  ? undefined
172
172
  : (path) => scope.some((entry) => path === entry || path.startsWith(`${entry}.`));
@@ -193,8 +193,8 @@ export function createFormController(options) {
193
193
  hiddenFields.add(fieldKey);
194
194
  }
195
195
  // Root-level .refine() issues use path "(root)". Wizard step validate()
196
- // still drops them (#1885); submit() passes includeRoot so RenderEdit
197
- // fields={…} cannot silently skip cross-field rules (#1907).
196
+ // still drops them; submit() passes includeRoot so RenderEdit
197
+ // fields={…} cannot silently skip cross-field rules.
198
198
  const relevantIssues = relevantFieldIssues(zodErrorToFieldIssues(parsed.error), {
199
199
  hiddenFields,
200
200
  ...(scope !== undefined && { scope }),
@@ -382,7 +382,7 @@ export function createFormController(options) {
382
382
  const result = await dispatcher.write(submitCfg.type, payload, { requestId });
383
383
  unansweredAttempt = isTransportFailure(result) ? { requestId, fingerprint } : null;
384
384
  if (result.isSuccess) {
385
- // Remember intentional empty clears so post-rebase submits keep them (#1858).
385
+ // Remember intentional empty clears so post-rebase submits keep them.
386
386
  if (payload && typeof payload === "object" && !Array.isArray(payload)) {
387
387
  for (const [k, v] of Object.entries(payload)) {
388
388
  if (v === "")
@@ -46,8 +46,8 @@ export function zodErrorToFieldIssues(error) {
46
46
  }
47
47
  // The issues that count for validity: hidden fields never block, and a
48
48
  // scope narrows to the listed paths (an entry matches itself and everything
49
- // below it, not siblings — #1898). `includeRoot` keeps "(root)" refine issues
50
- // inside a scope (submit passes it, #1907). Shared by the controller's
49
+ // below it, not siblings). `includeRoot` keeps "(root)" refine issues
50
+ // inside a scope (submit passes it). Shared by the controller's
51
51
  // validate() and RenderEdit's `valid` dry-run so the two cannot drift.
52
52
  export function relevantFieldIssues(issues, opts) {
53
53
  const { hiddenFields, scope, includeRoot } = opts;