@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
@@ -20,27 +20,27 @@ type ComboboxBaseProps = {
20
20
  readonly placeholder?: string;
21
21
  readonly searchPlaceholder?: string;
22
22
  readonly emptyText?: string;
23
- /** Tier 2.7e Remote-Search: wenn gesetzt, wechselt der Combobox in
24
- * Remote-Mode. cmdk's client-side Filter wird deaktiviert (Server
25
- * hat schon gefiltert), und der Search-Input ruft onSearchChange
26
- * debounced (300ms). Ohne diesen Callback bleibt die Local-Filter-
27
- * Variante (cmdk fuzzy-match auf den geladenen options). */
23
+ /** When set, enables remote search. cmdk's client-side filter is disabled
24
+ * because the server has already filtered the results, and the search input
25
+ * invokes onSearchChange with a 300ms debounce. Without this callback,
26
+ * filtering remains local using cmdk's fuzzy matching on the loaded options.
27
+ */
28
28
  readonly onSearchChange?: (q: string) => void;
29
- /** Spinner im Trigger + Popover-Footer wenn remote search läuft. */
29
+ /** Shows a spinner in the trigger and popover footer while remote search is
30
+ * in progress. */
30
31
  readonly loading?: boolean;
31
32
  /** Fixed footer row below the option list ("+ createLabel"). Rendered
32
33
  * unfiltered — stays visible even when the search matches nothing,
33
34
  * which is the most common case for reference-lookups on a freshly
34
- * seeded system (kumiko-framework#1681). Omit for the plain
35
+ * seeded system. Omit for the plain
35
36
  * select-existing combobox. */
36
37
  readonly onCreate?: () => void;
37
38
  /** Label for the create-footer row. Default: i18n `kumiko.actions.create`. */
38
39
  readonly createLabel?: string;
39
- /** Test-Hook: forciert den initial open-state des Popovers. In
40
- * jsdom + Radix-Popover triggert userEvent.click auf den Trigger
41
- * nicht zuverlässig PointerEvents — Tests setzen defaultOpen=true,
42
- * damit der Popover sofort gerendert ist. Production-Code lässt
43
- * den Default (false). */
40
+ /** Test hook that controls the popover's initial open state. In jsdom with
41
+ * Radix Popover, userEvent.click does not reliably trigger PointerEvents;
42
+ * tests set defaultOpen=true so the popover is rendered immediately.
43
+ * Production code leaves the default as false. */
44
44
  readonly defaultOpen?: boolean;
45
45
  };
46
46
  export type ComboboxInputProps = ComboboxBaseProps & ({
@@ -1,18 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // Tier 2.1c: Searchable Select / Combobox.
2
+ // Searchable Select / Combobox.
3
3
  //
4
- // Pattern ist das shadcn-Standard-Combobox: Popover-Trigger als Button
5
- // (zeigt aktuellen Wert oder Placeholder), Popover-Content enthält
6
- // cmdk-Command mit Search-Input + filterable Item-List.
4
+ // The pattern follows the shadcn standard: a popover trigger rendered as a
5
+ // button showing the current value or placeholder, with a popover containing
6
+ // a cmdk command with a search input and filterable item list.
7
7
  //
8
- // cmdk ist Radix's Standard-Combobox (Library für Command-K + Searchable
9
- // Selects), client-side Fuzzy-Match via `Command.Filter` mit Default-
10
- // Behavior. Server-side Remote-Search (debounced query) ist nicht im
11
- // MVP — kommt später als zweiter Mode-Schalter.
8
+ // cmdk provides client-side fuzzy matching through `Command.Filter` by
9
+ // default. Server-side remote search with a debounced query is not included
10
+ // in the MVP and can be enabled later as a separate mode.
12
11
  //
13
- // Multi-Mode: `multiple: true` schaltet auf Tag-Anzeige + Mehrfach-
14
- // Auswahl. Selected-Tags rendern als kleine entfernbare Chips, das
15
- // Search-Input bleibt offen für weitere Auswahl.
12
+ // Multi-mode: `multiple: true` switches to tag display and multi-selection.
13
+ // Selected values render as small chips, while the search input remains open
14
+ // for additional selections.
16
15
  import * as PopoverPrimitive from "@radix-ui/react-popover";
17
16
  import { Command } from "cmdk";
18
17
  import { Check, ChevronDown, Loader2, Plus } from "lucide-react";
@@ -31,23 +30,24 @@ const popoverContentClass = "z-50 w-[var(--radix-popover-trigger-width)] rounded
31
30
  "data-[state=open]:fade-in-0";
32
31
  const tagClass = "inline-flex items-center gap-1 rounded-sm bg-muted px-2 py-0.5 text-xs " +
33
32
  "font-medium text-muted-foreground";
34
- // Debounce-Default lebt zentral (hooks/reference-limits.ts) damit
35
- // app-weit ein consistentes Tipp-Window gilt.
36
- // Substring-Filter für cmdk's Local-Mode. cmdk's Default ist
37
- // `command-score` (fuzzy), das matcht "Ad" auch gegen "Backend" weil 'a'
38
- // und 'd' irgendwo enthalten sind — für Reference-Lookups irreführend
39
- // (User erwartet Prefix/Substring-Verhalten, nicht Subsequence-Match).
40
- // Returns 1 wenn search im value enthalten, 0 sonst — cmdk hidet Items
41
- // mit Score 0.
33
+ // The debounce default is centralized in hooks/reference-limits.ts so the
34
+ // same typing interval applies throughout the app.
35
+ // Substring filter for cmdk's local mode. cmdk's default is
36
+ // `command-score` (fuzzy), which matches "Ad" against "Backend" because 'a'
37
+ // and 'd' occur somewhere in the text. That is misleading for reference
38
+ // lookups, where users expect prefix or substring matching rather than
39
+ // subsequence matching.
40
+ // Returns 1 when search occurs in value or keywords, and 0 otherwise;
41
+ // cmdk hides items with a score of 0.
42
42
  function substringFilter(value, search, keywords) {
43
43
  const needle = search.toLowerCase();
44
44
  return [value, ...(keywords ?? [])].some((text) => text.toLowerCase().includes(needle)) ? 1 : 0;
45
45
  }
46
46
  export function ComboboxInput(props) {
47
47
  const { id, name, options, disabled, required, hasError, ariaLabel, placeholder, searchPlaceholder, emptyText, onSearchChange, loading, defaultOpen, onCreate, createLabel, } = props;
48
- // i18n-Defaults aus dem Framework-Bundle (kumikoDefaultTranslations).
49
- // Caller-Override gewinnt; Bundle-Override greift wenn der Caller
50
- // den Prop nicht setzt; raw-Key-Fallback wenn weder noch.
48
+ // i18n defaults come from the framework bundle (kumikoDefaultTranslations).
49
+ // A caller override takes precedence, followed by the bundle override;
50
+ // the raw key is used only when neither is available.
51
51
  const t = useTranslation();
52
52
  const effectivePlaceholder = placeholder ?? t("kumiko.combobox.placeholder");
53
53
  const effectiveSearchPlaceholder = searchPlaceholder ?? t("kumiko.combobox.search-placeholder");
@@ -56,10 +56,10 @@ export function ComboboxInput(props) {
56
56
  const effectiveCreateLabel = createLabel ?? t("kumiko.actions.create");
57
57
  const multiple = props.multiple === true;
58
58
  const [open, setOpen] = useState(defaultOpen === true);
59
- // Local Search-Buffer für Remote-Mode. Tipps werden mit 300ms
60
- // Debounce an onSearchChange weitergereicht, damit pro Tastendruck
61
- // nicht ein Server-Roundtrip fliegt. Im Local-Mode ist das State
62
- // ungenutzt (cmdk steuert sein Search-Input intern).
59
+ // Local search buffer for remote mode. Keystrokes are passed to
60
+ // onSearchChange after a 300ms debounce, avoiding a server round-trip for
61
+ // every keystroke. In local mode, this state is unused because cmdk
62
+ // controls its search input internally.
63
63
  const [searchTerm, setSearchTerm] = useState("");
64
64
  const isRemote = onSearchChange !== undefined;
65
65
  useEffect(() => {
@@ -68,15 +68,14 @@ export function ComboboxInput(props) {
68
68
  const timer = setTimeout(() => onSearchChange(searchTerm), REFERENCE_SEARCH_DEBOUNCE_MS);
69
69
  return () => clearTimeout(timer);
70
70
  }, [isRemote, searchTerm, onSearchChange]);
71
- // Beim Schließen des Popovers den Suchbegriff zurücksetzen damit
72
- // beim nächsten Öffnen nicht der vorherige term hängt. Würde sonst
73
- // den Server-State stale halten zwischen Aufrufen.
71
+ // Reset the search term when the popover closes so the next opening does not
72
+ // retain the previous term and leave the server state stale between uses.
74
73
  useEffect(() => {
75
74
  if (!open && searchTerm !== "")
76
75
  setSearchTerm("");
77
76
  }, [open, searchTerm]);
78
- // Discriminated-Union per `multiple`: TS narrowt props.value/onChange
79
- // automatisch — Runtime-Cast entfällt.
77
+ // The discriminated union lets TypeScript narrow props.value and onChange
78
+ // automatically, so no runtime cast is needed.
80
79
  const selectedValues = multiple ? new Set(props.value) : new Set();
81
80
  const singleValue = !multiple ? props.value : "";
82
81
  const singleLabel = options.find((o) => o.value === singleValue)?.label ?? "";
@@ -93,10 +92,10 @@ export function ComboboxInput(props) {
93
92
  };
94
93
  const triggerLabel = multiple ? null : singleLabel === "" ? placeholder : singleLabel;
95
94
  return (_jsxs(PopoverPrimitive.Root, { open: open, onOpenChange: setOpen, children: [_jsx("input", { type: "hidden", name: name, value: multiple ? props.value.join(",") : props.value }), _jsxs(PopoverPrimitive.Trigger, { id: id, "data-testid": `combobox-${id}`, type: "button", disabled: disabled, "aria-required": required, "aria-invalid": hasError === true ? true : undefined, "aria-label": ariaLabel, "aria-haspopup": "listbox", "aria-expanded": open, title: !multiple && singleLabel !== "" ? singleLabel : undefined, className: cn(triggerClass, hasError === true && "border-destructive focus-visible:ring-destructive"), children: [multiple ? (_jsx("span", { className: "flex flex-wrap items-center gap-1", children: selectedValues.size === 0 ? (_jsx("span", { className: "text-muted-foreground", children: effectivePlaceholder })) : (
96
- // Tags sind read-only Anzeige (kein nested <button>
97
- // möglich im Trigger-<button>). Entfernen via Re-Click
98
- // im Combobox-Popup — der Item-Toggle deselektiert die
99
- // entsprechende Option (analog shadcn-Standard-Combobox).
95
+ // Tags are read-only because the trigger is a <button> and cannot contain a
96
+ // nested <button>. Removing a value therefore requires selecting
97
+ // it again in the combobox popup, where the item toggle deselects
98
+ // the corresponding option.
100
99
  [...selectedValues].map((v) => {
101
100
  const opt = options.find((o) => o.value === v);
102
101
  return (_jsx("span", { className: tagClass, children: opt?.label ?? v }, v));
@@ -152,7 +152,7 @@ function isMoneyValue(value) {
152
152
  //
153
153
  // - boolean -> check mark / empty
154
154
  // - timestamp/date -> locale-formatted (no raw ISO string)
155
- // - number/decimal/bigInt → locale-formatted via Intl.NumberFormat (fw#2160)
155
+ // - number/decimal/bigInt → locale-formatted via Intl.NumberFormat
156
156
  // - select/multiSelect → human-readable (kebab-case → Title Case), multiSelect values joined with ", "
157
157
  // - money → { amount, currency } formatted via Intl (not "[object Object]")
158
158
  // - text/else -> toString
@@ -390,7 +390,7 @@ export function DataTableCell({ value, row, field, type, renderer, optionLabels,
390
390
  // A select value renders as a pill instead of plain text. When the raw
391
391
  // value is a known status word, it carries the same tone as the
392
392
  // projectionDetail header badge — a status column stayed grey while the
393
- // detail view of the same value was coloured (fw#2579). Unknown values
393
+ // detail view of the same value was coloured. Unknown values
394
394
  // keep the neutral outline pill.
395
395
  if (type === "select" && value !== null && value !== undefined && value !== "") {
396
396
  return selectPill(value, optionLabels, optionTones, locale, RegisteredStatusBadge);
@@ -58,13 +58,12 @@ function showFullTextOnHover(event) {
58
58
  element.title = element.scrollWidth > element.clientWidth ? (element.textContent ?? "") : "";
59
59
  }
60
60
  export function DefaultDataTable({ columns, rows, onRowClick, sort, onSortChange, emptyState, toolbarStart, toolbarDescription, toolbarEnd, pager, onReachEnd, loadingMore, hasMore, itemNoun, rowActions, rowActionMode, rowGrouping, rowTone, filterFacets, filterValues, onFilterChange, onFilterReset, dateRangeFacets, onDateRangeChange, testId, onCellChange, getRowTestId, getCellTestId, chromeless, scrollBody, screenPadding, expandedRowIds, onToggleRowExpanded, renderExpandedRow, }) {
61
- // One locale/translate subscription per table — not per cell (fw#2345).
61
+ // One locale/translate subscription per table — not per cell.
62
62
  // Optional hooks: a bare DataTable outside LocaleProvider must not crash.
63
63
  const tableTranslate = useOptionalTranslation();
64
64
  const tableLocale = useOptionalLocale();
65
65
  // Below 768px a table scrolls its columns out of reach with no visible
66
- // affordance (fw#2159 fixed the desktop case; narrow viewports never had
67
- // one). Cards replace the table entirely below the breakpoint — same
66
+ // affordance. Cards replace the table entirely below the breakpoint — same
68
67
  // single-mount pattern as EmbeddedListInput/embedded-list-input.tsx.
69
68
  const isNarrow = useIsNarrowViewport();
70
69
  const [facetsOpenNarrow, setFacetsOpenNarrow] = useState(false);
@@ -6,7 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  // One source for both date primitives: DateInput (kind:"date") is a thin
7
7
  // re-export of this, TimestampInput (kind:"timestamp") uses it as the
8
8
  // date part next to the time input. So `date` and `timestamp` share the
9
- // same typing/navigation UX instead of two diverging primitives (#369).
9
+ // same typing/navigation UX instead of two diverging primitives.
10
10
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
11
11
  import { useState } from "react";
12
12
  import { useTranslation } from "../../index.js";
@@ -57,7 +57,7 @@ export function DateField({ id, name, value, onChange, disabled, required, hasEr
57
57
  // the app's UI locale via t() — a caller rendering a date in a
58
58
  // locale that differs from the UI locale gets a mixed-locale
59
59
  // placeholder (e.g. German UI + locale="en-US" → "MM/TT/JJJJ").
60
- // Accepted for now (fw#1879): a static per-locale letter lookup
60
+ // Accepted for now: a static per-locale letter lookup
61
61
  // would need its own locale-root resolution and tests for a
62
62
  // placeholder-only, Low-severity mismatch.
63
63
  placeholder: formatDatePlaceholder(resolvedLocale, {
@@ -1,5 +1,5 @@
1
- // DateInput (kind:"date") — die tippbare Datums-Eingabe lebt in DateField,
2
- // damit `date` und `timestamp` dieselbe Tipp-/Kalender-UX teilen (#369).
3
- // Diese Datei hält nur das öffentliche Mapping; die Logik ist in
4
- // date-field.tsx, die Date-Parse-Utils in date-parse.ts.
1
+ // DateInput (kind:"date") — the typeable date input lives in DateField,
2
+ // so `date` and `timestamp` share the same typing/calendar UX.
3
+ // This file only provides the public mapping; the logic is in
4
+ // date-field.tsx, and the date parsing utilities are in date-parse.ts.
5
5
  export { DateField as DateInput } from "./date-field.js";
@@ -49,7 +49,7 @@ export function DefaultForm({ onSubmit, focusFirstErrorKey, children, title, sub
49
49
  // it above the viewport edge; the shell shrinks (`flex-1`) to make room.
50
50
  const pinnedFooter = isPinnedFooter(stickyActions, fillHeight);
51
51
  const pinnedShellClassName = pinnedFooter ? "h-auto flex-1 pb-6" : undefined;
52
- // Tab content (bedienkonzept A1): the head card is the ONLY card on the
52
+ // Tab content: the head card is the ONLY card on the
53
53
  // screen, so a tabbed projectionDetail's tab content must render as a
54
54
  // sibling of that card, not nest another one around itself — chromeless
55
55
  // skips the card wrapper and its border/padding so sections and footer
@@ -71,7 +71,7 @@ export function DefaultForm({ onSubmit, focusFirstErrorKey, children, title, sub
71
71
  // FormScreenShell ancestor is itself height-constrained and
72
72
  // shrinks it back down via min-h-0 — flex-1 would instead force
73
73
  // it to always fill the remaining height, stretching a short
74
- // relatedList tab to the bottom of the panel (fw#2778).
74
+ // relatedList tab to the bottom of the panel.
75
75
  // The min-height floor keeps the card from collapsing to ~0 on short
76
76
  // viewports; the shell then overflows and <main> scrolls instead of
77
77
  // the overflow-hidden card clipping the table.
@@ -1,16 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // shadcn-style DropdownMenu auf Radix-Basis. Wrappers um
3
- // @radix-ui/react-dropdown-menu mit den Tailwind-Token-Klassen die
4
- // die anderen Primitives auch nutzen — Trigger-Button-Look kommt vom
5
- // Caller (asChild-Pattern), wir liefern nur Content/Item/Label/
6
- // Separator/CheckboxItem mit konsistenter Optik.
2
+ // shadcn-style DropdownMenu built on Radix. Wrappers around
3
+ // @radix-ui/react-dropdown-menu use the same Tailwind token classes as
4
+ // the other primitives. The caller provides the trigger-button styling
5
+ // through the asChild pattern; this module supplies only Content, Item,
6
+ // Label, Separator, and CheckboxItem with consistent styling.
7
7
  //
8
- // Vorteile gegenüber dem self-rolled useDropdownMenu-Hook:
9
- // - Click-outside, Escape, Focus-Trap, Roving-Tabindex, ARIA-Roles,
10
- // Pointer-vs-Keyboard-Subtleties — alles geschenkt von Radix.
11
- // - Portal'd Content rendert über Stacking-Context-Grenzen (nützlich
12
- // in Dialogs/Popovers).
13
- // - Keyboard-Nav (↑↓ + Home/End) funktioniert von Haus aus.
8
+ // Advantages over the custom useDropdownMenu hook:
9
+ // - Click-outside, Escape, focus trapping, roving tabindex, ARIA roles,
10
+ // and pointer-versus-keyboard subtleties are handled by Radix.
11
+ // - Portaled content renders across stacking-context boundaries, which is
12
+ // useful in dialogs and popovers.
13
+ // - Keyboard navigation (↑↓ + Home/End) works by default.
14
14
  import * as Primitive from "@radix-ui/react-dropdown-menu";
15
15
  import { Check } from "lucide-react";
16
16
  import { cn } from "../lib/cn.js";
@@ -33,10 +33,9 @@ const itemClass = "relative flex cursor-default select-none items-center gap-2 r
33
33
  export function DropdownMenuItem({ className, ...props }) {
34
34
  return _jsx(Primitive.Item, { className: cn(itemClass, className), ...props });
35
35
  }
36
- // CheckboxItem mit Check-Icon links — für "ausgewählter Eintrag in
37
- // einer Liste"-Patterns (TenantSwitcher, LanguageSwitcher). Radix
38
- // rendert <ItemIndicator> nur wenn checked=true, kein eigener Branch
39
- // nötig.
36
+ // CheckboxItem with a check icon on the left for "selected item in a list"
37
+ // patterns (TenantSwitcher, LanguageSwitcher). Radix renders
38
+ // <ItemIndicator> only when checked=true, so no separate branch is needed.
40
39
  export function DropdownMenuCheckboxItem({ className, children, checked, ...props }) {
41
40
  return (_jsxs(Primitive.CheckboxItem, { checked: checked, className: cn(itemClass, "pl-8 pr-2", className), ...props, children: [_jsx("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: _jsx(Primitive.ItemIndicator, { children: _jsx(Check, { className: "h-3.5 w-3.5" }) }) }), children] }));
42
41
  }
@@ -8,7 +8,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  // Only one of the two layouts (table for md+, cards below md) is mounted
9
9
  // at a time, picked via useIsNarrowViewport — mounting both and toggling
10
10
  // with `hidden`/`md:hidden` left two live inputs per cell sharing one DOM
11
- // id (#1854). The `hidden md:block` / `md:hidden` classes stay on the
11
+ // id. The `hidden md:block` / `md:hidden` classes stay on the
12
12
  // wrapper divs as a defensive fallback (e.g. SSR/no-JS, where
13
13
  // getServerSnapshot forces the desktop layout) even though
14
14
  // useIsNarrowViewport — unlike the vendored useIsMobile it replaced —
@@ -211,7 +211,7 @@ function RowActions({ rowIndex, rowsLength, minItems, maxItems, onDuplicateRow,
211
211
  }
212
212
  export function EmbeddedListInput({ id, columns, rows, totals, currency, disabled, fixedRows, minItems, maxItems, listIssues, rowIssues, cellIssues, onCellChange, onAddRow, onRemoveRow, onDuplicateRow, onMoveRow, onPasteCells, addLabel, actionsLabel, removeLabel, duplicateLabel, moveUpLabel, moveDownLabel, emptyLabel, emptyCtaLabel, testId, }) {
213
213
  // Callers that don't wire up the field-currency plumbing (or direct
214
- // callers/tests that predate #1839) keep getting the same "EUR" this
214
+ // callers/tests) keep getting the same "EUR" this
215
215
  // component always hardcoded.
216
216
  const effectiveCurrency = currency ?? "EUR";
217
217
  const t = useTranslation();
@@ -231,7 +231,7 @@ export function EmbeddedListInput({ id, columns, rows, totals, currency, disable
231
231
  // notice the desktop table remounting with a fresh DOM node.
232
232
  const [scrollContainerEl, setScrollContainerEl] = useState(null);
233
233
  // Resetting scrollLeft here is what guarantees the table always opens
234
- // scrolled fully left (fw#2159): browsers still auto-scroll a focused
234
+ // scrolled fully left: browsers still auto-scroll a focused
235
235
  // descendant into view even inside an overflow-x-auto container, but
236
236
  // that's now a real, user-drivable scroll instead of an invisible one.
237
237
  useEffect(() => {
@@ -28,7 +28,7 @@ export function FacetFilter({ facet, selected, onChange, dense = false, }) {
28
28
  }
29
29
  return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(UiButton, { variant: "outline", size: "sm", className: cn("h-9", dense && "h-8 border-border-strong px-2.5 max-md:h-11"), "data-testid": `facet-${facet.field}`, children: [facet.label, selected.length > 0 && (_jsx(Badge, { variant: "secondary", className: "ml-1 rounded-sm px-1 font-normal tabular-nums", children: selected.length })), _jsx(ChevronDown, { className: "text-muted-foreground" })] }) }), _jsx(DropdownMenuContent, { align: "start", className: "max-h-[var(--radix-dropdown-menu-content-available-height)] w-44 overflow-y-auto", children: facet.options.map((opt) => (_jsx(DropdownMenuCheckboxItem, { checked: selected.includes(opt.value), onCheckedChange: (checked) => toggle(opt.value, checked), onSelect: (e) => e.preventDefault(), "data-testid": `facet-${facet.field}-${opt.value}`, children: opt.label }, opt.value))) })] }));
30
30
  }
31
- // Time-range filter (fw#3104): two native <input type="date"> instead of a
31
+ // Time-range filter: two native <input type="date"> instead of a
32
32
  // dropdown — the browser supplies the calendar, the locale and the keyboard
33
33
  // handling, so no date dependency is needed. Each bound constrains the other
34
34
  // through min/max so the picker can't offer an inverted span; the caller
@@ -81,10 +81,11 @@ const FIELD_ICONS = {
81
81
  key: KeyRound,
82
82
  "map-pin": MapPin,
83
83
  };
84
+ function isFieldIconKey(key) {
85
+ return Object.hasOwn(FIELD_ICONS, key);
86
+ }
84
87
  export function fieldIconFor(icon) {
85
- return icon !== undefined && Object.hasOwn(FIELD_ICONS, icon)
86
- ? FIELD_ICONS[icon]
87
- : undefined;
88
+ return icon !== undefined && isFieldIconKey(icon) ? FIELD_ICONS[icon] : undefined;
88
89
  }
89
90
  // Wraps a text/number input with a left-positioned prefix icon when
90
91
  // `icon` carries a known FIELD_ICONS key. `pl-8` overrides (via
@@ -127,7 +128,7 @@ export function withTrailingAction(action, input, keepWrapper = false) {
127
128
  // Default presentation for `kind: "select"` without a `display` of its own
128
129
  // (board rule): up to 3 short options read as a segmented control, up to 3
129
130
  // longer ones as a vertical radio list, everything else as a dropdown. Labels
130
- // arrive translated, so the widget can differ between UI languages (#2606).
131
+ // arrive translated, so the widget can differ between UI languages.
131
132
  const SEGMENTED_SELECT_MAX_OPTIONS = 3;
132
133
  const SEGMENTED_SELECT_MAX_LABEL_LENGTH = 16;
133
134
  export function defaultSelectPresentation(options) {
@@ -138,9 +139,9 @@ export function defaultSelectPresentation(options) {
138
139
  : "radioList";
139
140
  }
140
141
  // A "" value is the unselected placeholder, not a real choice — a radio
141
- // group can't render an unchecked-everything state as its own segment
142
- // (#2606 follow-up). Filtered once so the eligibility count and the
143
- // rendered segments never drift apart.
142
+ // group can't render an unchecked-everything state as its own segment.
143
+ // Filtered once so the eligibility count and the rendered segments never drift
144
+ // apart.
144
145
  export function withoutSegmentedSelectPlaceholder(options) {
145
146
  return options.filter((option) => option.value !== "");
146
147
  }
@@ -162,7 +163,7 @@ export function RadioListSelect({ id, name, value, onChange, options, variant =
162
163
  : "flex items-start gap-2 text-sm font-normal text-foreground has-[:disabled]:opacity-50", children: [_jsx("input", { type: "radio", name: name, value: opt.value, checked: opt.value === value, disabled: disabled === true || opt.disabled === true, "aria-describedby": opt.description !== undefined ? descriptionIdOf(opt) : undefined, "data-testid": `radio-list-${id}-${opt.value}`, onChange: () => onChange(opt.value), className: cn("size-4 accent-primary", card ? "mt-0.5 shrink-0" : "mt-0.5") }), _jsxs("span", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: cn(card && "font-semibold"), children: opt.label }), opt.description !== undefined && (_jsx("span", { id: descriptionIdOf(opt), className: "text-[13px] text-foreground-secondary", children: opt.description }))] })] }, opt.value)))] }, section.heading ?? ""))) }));
163
164
  }
164
165
  // The vendored shadcn Textarea carries `field-sizing: content`, which derives
165
- // the box height from the content and makes the `rows` attribute inert (#2677).
166
+ // the box height from the content and makes the `rows` attribute inert.
166
167
  // A declared row count therefore only survives as a min-height floor: the field
167
168
  // starts at `rows` lines tall and still grows with its content. The addend is
168
169
  // the textarea frame — py-2 top+bottom plus the 1px borders.
@@ -47,7 +47,7 @@ export declare function FormScreenShell({ children, className, testId, maxWidth,
47
47
  readonly maxWidth?: FormScreenShellWidth;
48
48
  /** Stacks children in a `h-full` flex column instead of normal document
49
49
  * flow, so a `flex-1 min-h-0` child can claim the remaining height below
50
- * the others (fw#2722 — DefaultForm's tabs+relatedList case). Default
50
+ * the others (DefaultForm's tabs+relatedList case). Default
51
51
  * false: unchanged, content-sized height. */
52
52
  readonly fillHeight?: boolean;
53
53
  }): ReactNode;
@@ -11,11 +11,11 @@ import "../react-css-custom-properties.js";
11
11
  import { cardFooterBorder, cardHeaderBorder } from "./card-styles.js";
12
12
  import { InsideFormContext, ScreenFormContext, ScreenWidthContext } from "./contexts.js";
13
13
  // Extension sections that render their own <form> via BareFormProvider
14
- // (legacy custom-form pattern, e.g. ChangeEmailSection before fw#2703) land
14
+ // (legacy custom-form pattern, e.g. ChangeEmailSection) land
15
15
  // inside RenderEdit's host <form> (ExtensionSectionMount, render-edit.tsx) —
16
16
  // nested <form> elements are invalid DOM, and real browsers can silently
17
17
  // fall back to a native GET submit of the OUTER form, leaking field values
18
- // into the URL (fw#2312, fw#2705). Degrading to a <div> when already inside
18
+ // into the URL. Degrading to a <div> when already inside
19
19
  // a form avoids the nesting; the captured click still routes to THIS form's
20
20
  // onSubmit instead of activating the real ancestor <form>.
21
21
  const SUBMIT_CONTROL_SELECTOR = [
@@ -95,7 +95,7 @@ export function FormSections({ children, chromeless, stickyActions, fillHeight,
95
95
  // Margins, not padding: padding would land inside a child that has its own border/height.
96
96
  !chromeless && "[&>:not(section)]:mx-6 [&>:not(section)]:my-3", !chromeless && "[&>:not(section):first-child]:mt-6 [&>:not(section):last-child]:mb-6",
97
97
  // ponytail: fixed footer now pins only the primary action (single
98
- // button row + its own p-4, fw#2606) instead of the whole footer —
98
+ // button row + its own p-4) instead of the whole footer —
99
99
  // shrunk from pb-32 accordingly. Widen again if a wizard's primary
100
100
  // action ever wraps to two rows.
101
101
  stickyActions === true && fillHeight !== true && STICKY_FOOTER_SPACER_CLASS,
@@ -117,7 +117,7 @@ export function FormTitleBlock({ title, subtitle, titleAction, testId, bordered,
117
117
  }
118
118
  // `formActions` (render-edit.tsx) mixes a non-submit wizard Back button in
119
119
  // with the submit-type Next/Finish button inside one Fragment — only the
120
- // submit one is the "primary" action that fw#1918 needs pinned above a
120
+ // submit one is the "primary" action that needs pinned above a
121
121
  // virtual keyboard. Back reads as `type="button"`, same as `secondaryActions`
122
122
  // (Cancel/Delete/…), so it can safely join that group on mobile. A footer-slot
123
123
  // mount can't expose `type="submit"` (its button is opaque app code), so it
@@ -162,7 +162,7 @@ export function FormFooter({ actions, secondaryActions, testId, chromeless, stic
162
162
  const hasPrimaryAction = primaryActionNodes !== undefined && primaryActionNodes.length > 0;
163
163
  // No primary node found among `actions` (e.g. a wizard step with only
164
164
  // secondary buttons): fall back to pinning the whole, unpartitioned
165
- // `actions` node instead of splitting it — matches the pre-split fw#1918
165
+ // `actions` node instead of splitting it — matches the previous
166
166
  // behaviour where sticky footers always kept `actions` fixed.
167
167
  const nonPrimaryActionNodes = hasPrimaryAction
168
168
  ? actionNodes?.filter((node) => !isPrimaryStickyAction(node))
@@ -196,7 +196,7 @@ export function FormFooter({ actions, secondaryActions, testId, chromeless, stic
196
196
  : "flex-wrap max-sm:w-full max-sm:[&>button]:flex-1 max-sm:[&>button]:min-h-11 sm:ml-auto",
197
197
  // Below sm (640px): pin only the primary action to the viewport
198
198
  // bottom instead of normal flow, so a virtual keyboard shrinking
199
- // the viewport can't push it out of reach (fw#1918). `fixed`
199
+ // the viewport can't push it out of reach. `fixed`
200
200
  // escapes the card's `overflow-hidden` (only transform/filter/
201
201
  // contain ancestors trap it, confirmed against
202
202
  // AppLayout/SidebarInset — neither sets those).
@@ -86,7 +86,7 @@ export function DefaultInput(props) {
86
86
  const radioGroupOptions = props.display === "radio" ? comboOptions : withoutSegmentedSelectPlaceholder(comboOptions);
87
87
  // An explicit `display` is an author decision and outranks the
88
88
  // heuristic in both directions — a requested radio group renders as
89
- // one even when the options outnumber the heuristic's threshold (#2711).
89
+ // one even when the options outnumber the heuristic's threshold.
90
90
  const requestedPresentation = props.radioVariant === "card"
91
91
  ? "radioList"
92
92
  : props.display === "radio"
@@ -107,7 +107,7 @@ export function DefaultInput(props) {
107
107
  return (_jsx(ComboboxInput, { id: props.id, name: props.name, value: props.value, onChange: props.onChange, options: dropdownOptions, ...(props.disabled !== undefined && { disabled: props.disabled }), ...(props.required !== undefined && { required: props.required }), ...(props.hasError !== undefined && { hasError: props.hasError }) }));
108
108
  }
109
109
  case "combobox": {
110
- // Tier 2.1c + tier 2.7e: discriminated union per `multiple`; we split
110
+ // Discriminated union per `multiple`; we split
111
111
  // TS-side into two branches so ComboboxInput's single/multi variants
112
112
  // render with the right types.
113
113
  const baseProps = {
@@ -48,7 +48,7 @@ export function DefaultSection({ id, title, subtitle, children, actions, variant
48
48
  export function DefaultFillContainer({ children, testId, grow }) {
49
49
  // flex-1 only on request (`grow`, set under fillScreenHeight): without it the
50
50
  // container sizes to its content and only shrinks (min-h-0), so a two-row
51
- // table in a non-fixed-height screen does not stretch (fw#2778).
51
+ // table in a non-fixed-height screen does not stretch.
52
52
  return (_jsx("div", { "data-testid": testId, className: cn("flex min-h-0 flex-col", grow === true && "flex-1"), children: children }));
53
53
  }
54
54
  const DRAWER_FIELD_CELL_WIDTH_CLASS = {
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cn } from "../lib/cn.js";
3
3
  // Shared 3xl/4xl/full → max-w-*/mx-auto map. FormScreenShell
4
4
  // (primitives/index.tsx) reuses this instead of keeping its own copy —
5
- // same width tokens must render the same class everywhere (fw#2640).
5
+ // same width tokens must render the same class everywhere.
6
6
  export const screenWidthClassName = {
7
7
  "3xl": "max-w-3xl mx-auto",
8
8
  "4xl": "max-w-4xl mx-auto",
@@ -19,7 +19,7 @@ export const screenFormColumnWidthClassName = {
19
19
  // FormScreenShell, list screens and the pinned-footer screen form). The inset
20
20
  // is the one the settings screens use, so a list, a dashboard and a form start
21
21
  // at the same edge. The bottom inset keeps the last field or table row off the
22
- // viewport edge (fw#2640).
22
+ // viewport edge.
23
23
  export const screenPaddingClassName = "px-4 pb-6 pt-5 md:px-10 md:pb-10 md:pt-7";
24
24
  const STACK_GAP = { sm: "gap-2", md: "gap-4", lg: "gap-6" };
25
25
  export function Stack({ gap = "md", className, children, testId }) {
@@ -7,10 +7,10 @@ export function LocatedTimestampInput({ id, name, value, onChange, locale, min,
7
7
  const t = useTranslation();
8
8
  const at = value === "" ? "" : value.at;
9
9
  const tz = value === "" ? "" : value.tz;
10
- // `at` ist Minuten-Wallclock (z.iso.datetime({local:true}) akzeptiert das);
11
- // tz ein IANA-Name. Solange beides leer ist gilt das Feld als leer; sobald
12
- // eines gesetzt ist, fließt das Paar — die Vollständigkeit erzwingt die
13
- // Required-/Zod-Validierung beim Submit (so bleibt eine halbe Eingabe sichtbar).
10
+ // `at` is a minute-precision wall-clock value (z.iso.datetime({local:true}) accepts it);
11
+ // tz is an IANA name. The field is empty while both are empty; once either
12
+ // is set, the pair is emitted — Required/Zod validation on submit enforces
13
+ // completeness (so a partial input remains visible).
14
14
  function emit(nextAt, nextTz) {
15
15
  if (nextAt === "" && nextTz === "") {
16
16
  onChange(undefined);
@@ -1,21 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // MoneyInput — type=text mit focus-aware Locale-Format. Canonical-Wert
3
- // bleiben Minor-Units (Cents) wie auf der Wire; während Focus zeigt das
4
- // Input einen rohen, editierbaren Decimal-String. Bei Blur formatiert
5
- // Intl.NumberFormat mit `style: "currency"` — das liefert Currency-
6
- // Symbol (€/$/¥) UND Tausender-Trenner UND korrekte Decimals in einem
7
- // Aufruf.
2
+ // MoneyInput uses locale-aware formatting while preserving minor units (cents) as the canonical value.
3
+ // On focus, it shows a raw editable decimal string; on blur, Intl.NumberFormat with
4
+ // `style: "currency"` supplies the currency symbol (€/$/¥), grouping separators, and
5
+ // correct decimal places in one call.
8
6
  //
9
- // Warum nicht type=number: number-Inputs lehnen formatierte Strings
10
- // ("1.234,56 €") ab — kein Browser akzeptiert Locale-Decimals (Komma)
11
- // in number-Inputs. inputMode="decimal" gibt mobiles Numpad-Keyboard
12
- // trotzdem.
7
+ // `type=number` cannot accept formatted strings such as ("1.234,56 €") or locale-specific
8
+ // decimal separators such as commas. `inputMode="decimal"` still provides a mobile numeric
9
+ // keypad.
13
10
  import { useLayoutEffect, useMemo, useRef, useState } from "react";
14
11
  import { currencyDecimals, formatMoney as formatMoneyForLocale } from "../../headless/index.js";
15
12
  import { cn } from "../lib/cn.js";
16
13
  import { parseLocaleNumber, resolveSafeLocale } from "./locale-number.js";
17
14
  // Re-exported for backward compat — callers used to import this from here
18
- // before it moved to headless (shared with RenderField, kumiko-framework#1923).
15
+ // before it moved to headless (shared with RenderField).
19
16
  export { currencyDecimals, parseLocaleNumber };
20
17
  // The border and focus ring live on the wrapper so the currency symbol can be
21
18
  // a flex sibling of the input: a symbol of any width ("€", "CHF", "R$") takes
@@ -24,22 +21,21 @@ const wrapperClass = "flex h-9 w-full items-center rounded-md border border-inpu
24
21
  "transition-colors focus-within:ring-1 focus-within:ring-ring";
25
22
  const inputClass = "h-full min-w-0 flex-1 bg-transparent px-3 placeholder:text-muted-foreground " +
26
23
  "focus-visible:outline-none disabled:cursor-not-allowed " +
27
- // Numerische Inputs rechtsbündig — wie native type=number — damit
28
- // Beträge unter Listen-Spalten an den Tausender-Stellen alignen.
24
+ // Right-aligns the value like a native type=number input so amounts align at the thousands
25
+ // separators in list columns.
29
26
  "text-right tabular-nums";
30
27
  export function MoneyInput({ id, name, value, onChange, currency, locale, disabled, required, hasError, ariaLabel, }) {
31
28
  const decimals = currencyDecimals(currency);
32
29
  const factor = 10 ** decimals;
33
30
  const resolvedLocale = resolveSafeLocale(locale);
34
31
  const [focused, setFocused] = useState(false);
35
- // Raw-Edit-Buffer während Focus. Sonst würde jeder Tipp-Step durch
36
- // Math.round → format-Roundtrip jagen und der Cursor würde springen.
32
+ // Keeps edits raw while focused; otherwise each keystroke would round-trip through
33
+ // Math.round and formatting, causing the cursor to jump.
37
34
  const [draft, setDraft] = useState("");
38
35
  const inputRef = useRef(null);
39
36
  const major = value === "" ? null : value / factor;
40
37
  const { symbol, symbolPosition, formatNumber } = useMemo(() => moneyFormatParts(currency, resolvedLocale), [currency, resolvedLocale]);
41
38
  const formatted = value === "" ? "" : formatNumber(value / factor);
42
- // Edit-Mode: Decimal-String ohne Tausender-Trenner.
43
39
  const toEditable = (m) => m === null
44
40
  ? ""
45
41
  : m.toLocaleString(resolvedLocale, {
@@ -58,7 +54,7 @@ export function MoneyInput({ id, name, value, onChange, currency, locale, disabl
58
54
  // to the *end* of the new value rather than preserving position — so
59
55
  // without an explicit re-select, typing right after focus appends
60
56
  // instead of replacing. That's what silently corrupted values set via
61
- // Playwright's `.fill()` (framework#1856). It also matches standard
57
+ // Playwright's `.fill()`. It also matches standard
62
58
  // money-input UX (immediate overtype on click), and sidesteps mapping a
63
59
  // click position from the formatted view to the editable one, which has
64
60
  // no well-defined equivalent once separators and the currency symbol
@@ -20,7 +20,7 @@ export const TABLE_FOOTER_BAR_CLASS = cn("flex h-15 shrink-0 items-center gap-4
20
20
  // `screen.pageSize ?? 50`) rather than the real per-screen pageSize, which isn't
21
21
  // threaded down to this component. A screen with a custom pageSize can still
22
22
  // under- or over-show the marker; thread pageSize through KumikoScreen →
23
- // RenderList → DataTable → InfiniteSentinel if that's needed (fw#1713).
23
+ // RenderList → DataTable → InfiniteSentinel if that's needed.
24
24
  export const END_LABEL_MIN_ROWS = 50;
25
25
  export function InfiniteSentinel({ onReachEnd, loadingMore, hasMore, rowsCount, testId, }) {
26
26
  const t = useTranslation();