@eifi1/ui-kit 0.5.1 → 0.6.1

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 (136) hide show
  1. package/README.md +50 -1
  2. package/dist/components/amount-input.d.ts +7 -0
  3. package/dist/components/autocomplete.d.ts +103 -0
  4. package/dist/components/autocomplete.js +260 -0
  5. package/dist/components/autocomplete.js.map +1 -0
  6. package/dist/components/calculator.d.ts +7 -0
  7. package/dist/components/checkbox.d.ts +9 -0
  8. package/dist/components/checkbox.js +7 -2
  9. package/dist/components/checkbox.js.map +1 -1
  10. package/dist/components/chip.d.ts +3 -2
  11. package/dist/components/chip.js +14 -1
  12. package/dist/components/chip.js.map +1 -1
  13. package/dist/components/choice-card.d.ts +100 -0
  14. package/dist/components/choice-card.js +170 -0
  15. package/dist/components/choice-card.js.map +1 -0
  16. package/dist/components/combobox-core.d.ts +76 -6
  17. package/dist/components/combobox-core.js +119 -49
  18. package/dist/components/combobox-core.js.map +1 -1
  19. package/dist/components/combobox.d.ts +12 -2
  20. package/dist/components/combobox.js +42 -17
  21. package/dist/components/combobox.js.map +1 -1
  22. package/dist/components/danger-confirm.d.ts +91 -0
  23. package/dist/components/danger-confirm.js +181 -0
  24. package/dist/components/danger-confirm.js.map +1 -0
  25. package/dist/components/dialog-frame.d.ts +84 -0
  26. package/dist/components/dialog-frame.js +86 -0
  27. package/dist/components/dialog-frame.js.map +1 -0
  28. package/dist/components/disclosure.d.ts +108 -0
  29. package/dist/components/disclosure.js +127 -0
  30. package/dist/components/disclosure.js.map +1 -0
  31. package/dist/components/entity-combobox.d.ts +17 -3
  32. package/dist/components/entity-combobox.js +25 -5
  33. package/dist/components/entity-combobox.js.map +1 -1
  34. package/dist/components/file-button.d.ts +161 -0
  35. package/dist/components/file-button.js +243 -0
  36. package/dist/components/file-button.js.map +1 -0
  37. package/dist/components/file-dropzone.d.ts +72 -23
  38. package/dist/components/file-dropzone.js +219 -94
  39. package/dist/components/file-dropzone.js.map +1 -1
  40. package/dist/components/icon-picker.d.ts +72 -0
  41. package/dist/components/icon-picker.js +104 -0
  42. package/dist/components/icon-picker.js.map +1 -0
  43. package/dist/components/mini-calendar.d.ts +3 -0
  44. package/dist/components/mini-calendar.js +4 -3
  45. package/dist/components/mini-calendar.js.map +1 -1
  46. package/dist/components/modal.d.ts +8 -1
  47. package/dist/components/modal.js +4 -2
  48. package/dist/components/modal.js.map +1 -1
  49. package/dist/components/multi-entity-combobox.d.ts +16 -3
  50. package/dist/components/multi-entity-combobox.js +25 -5
  51. package/dist/components/multi-entity-combobox.js.map +1 -1
  52. package/dist/components/number-field.d.ts +41 -1
  53. package/dist/components/number-field.js +42 -10
  54. package/dist/components/number-field.js.map +1 -1
  55. package/dist/components/number-input.d.ts +35 -2
  56. package/dist/components/number-input.js +35 -4
  57. package/dist/components/number-input.js.map +1 -1
  58. package/dist/components/numpad-sheet.d.ts +7 -0
  59. package/dist/components/search-field.d.ts +16 -0
  60. package/dist/components/search-field.js +29 -7
  61. package/dist/components/search-field.js.map +1 -1
  62. package/dist/components/signature-pad.d.ts +43 -1
  63. package/dist/components/signature-pad.js +74 -2
  64. package/dist/components/signature-pad.js.map +1 -1
  65. package/dist/components/swatch-picker.d.ts +69 -0
  66. package/dist/components/swatch-picker.js +75 -0
  67. package/dist/components/swatch-picker.js.map +1 -0
  68. package/dist/components/switch.d.ts +9 -0
  69. package/dist/components/switch.js +7 -2
  70. package/dist/components/switch.js.map +1 -1
  71. package/dist/components/tile-radio.d.ts +50 -0
  72. package/dist/components/tile-radio.js +140 -0
  73. package/dist/components/tile-radio.js.map +1 -0
  74. package/dist/components/toggle-group.d.ts +27 -5
  75. package/dist/components/toggle-group.js +22 -15
  76. package/dist/components/toggle-group.js.map +1 -1
  77. package/dist/components/ui.d.ts +150 -12
  78. package/dist/components/ui.js +196 -22
  79. package/dist/components/ui.js.map +1 -1
  80. package/dist/i18n/defaults.d.ts +7 -0
  81. package/dist/i18n/defaults.js +13 -2
  82. package/dist/i18n/defaults.js.map +1 -1
  83. package/dist/i18n/kit-labels.d.ts +38 -5
  84. package/dist/i18n/kit-labels.js +12 -4
  85. package/dist/i18n/kit-labels.js.map +1 -1
  86. package/dist/index.d.ts +16 -7
  87. package/dist/index.js +12 -0
  88. package/dist/index.js.map +1 -1
  89. package/dist/lib/table-text.d.ts +127 -0
  90. package/dist/lib/table-text.js +82 -0
  91. package/dist/lib/table-text.js.map +1 -0
  92. package/dist/rhf/form.d.ts +79 -0
  93. package/dist/rhf/form.js +143 -0
  94. package/dist/rhf/form.js.map +1 -0
  95. package/dist/rhf.d.ts +4 -0
  96. package/dist/rhf.js +3 -0
  97. package/dist/rhf.js.map +1 -0
  98. package/dist/shell/app-shell.js +3 -1
  99. package/dist/shell/app-shell.js.map +1 -1
  100. package/dist/table-text.d.ts +1 -0
  101. package/dist/table-text.js +3 -0
  102. package/dist/table-text.js.map +1 -0
  103. package/package.json +14 -1
  104. package/src/components/autocomplete.tsx +429 -0
  105. package/src/components/checkbox.tsx +16 -0
  106. package/src/components/chip.tsx +24 -2
  107. package/src/components/choice-card.tsx +305 -0
  108. package/src/components/combobox-core.tsx +228 -58
  109. package/src/components/combobox.tsx +58 -21
  110. package/src/components/danger-confirm.tsx +286 -0
  111. package/src/components/dialog-frame.tsx +179 -0
  112. package/src/components/disclosure.tsx +259 -0
  113. package/src/components/entity-combobox.tsx +41 -6
  114. package/src/components/file-button.tsx +458 -0
  115. package/src/components/file-dropzone.tsx +323 -117
  116. package/src/components/icon-picker.tsx +181 -0
  117. package/src/components/mini-calendar.tsx +7 -3
  118. package/src/components/modal.tsx +10 -2
  119. package/src/components/multi-entity-combobox.tsx +40 -6
  120. package/src/components/number-field.tsx +86 -10
  121. package/src/components/number-input.tsx +79 -2
  122. package/src/components/search-field.tsx +49 -6
  123. package/src/components/signature-pad.tsx +112 -0
  124. package/src/components/swatch-picker.tsx +141 -0
  125. package/src/components/switch.tsx +16 -0
  126. package/src/components/tile-radio.tsx +228 -0
  127. package/src/components/toggle-group.tsx +54 -18
  128. package/src/components/ui.tsx +400 -24
  129. package/src/i18n/defaults.ts +12 -1
  130. package/src/i18n/kit-labels.tsx +45 -5
  131. package/src/index.ts +19 -0
  132. package/src/lib/table-text.ts +265 -0
  133. package/src/rhf/form.tsx +300 -0
  134. package/src/rhf.ts +9 -0
  135. package/src/shell/app-shell.tsx +3 -1
  136. package/src/table-text.ts +8 -0
@@ -0,0 +1,259 @@
1
+ import { useEffect, useId, useState } from "react";
2
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
3
+ import { ChevronDown } from "lucide-react";
4
+
5
+ import { cn } from "../lib/cn";
6
+
7
+ /** How long the fold takes, in ms. The same number as the `duration-200` below and as
8
+ * the unmount timer, because they are the same movement. */
9
+ const COLLAPSE_MS = 200;
10
+
11
+ /** Read at the moment of closing, like `useCloseTransition` does: the setting can
12
+ * change under a long-lived page, and jsdom/SSR have no `matchMedia` — where "no
13
+ * animation" is also the only correct answer, since nothing is painting. */
14
+ function prefersReducedMotion(): boolean {
15
+ return (
16
+ typeof window === "undefined" ||
17
+ typeof window.matchMedia !== "function" ||
18
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches
19
+ );
20
+ }
21
+
22
+ /**
23
+ * `extends` the div's props so an `id` (what a trigger's `aria-controls` points at), a
24
+ * `data-tour` anchor or a test id reaches the element that folds.
25
+ */
26
+ export interface CollapseProps extends ComponentPropsWithoutRef<"div"> {
27
+ open: boolean;
28
+ /**
29
+ * Keep the children mounted while shut (hidden and `inert`) instead of unmounting
30
+ * them once the fold has finished closing.
31
+ *
32
+ * Off by default, and that default is a behaviour contract rather than a
33
+ * performance note: Lenkbank's disclosure bodies FETCH, and a body that only exists
34
+ * while open is what stops a closed card asking — without every caller carrying its
35
+ * own `enabled: open`. On for a body whose state must outlive a close (a half-typed
36
+ * form) or that must be found by the browser's find-in-page.
37
+ */
38
+ keepMounted?: boolean;
39
+ children: ReactNode;
40
+ }
41
+
42
+ /**
43
+ * The fold on its own: content that opens and shuts in place, animated to a height
44
+ * nobody measured.
45
+ *
46
+ * THE TECHNIQUE is `AppShell`'s sidebar group's, lifted out so there is one copy of it.
47
+ * A `grid-template-rows` transition from `0fr` to `1fr` is the one way to animate to a
48
+ * content-sized height without measuring it — so a chart that resizes inside it, or a
49
+ * translated line that wraps, is still exactly as tall as it needs. `visibility` rides
50
+ * the same transition, so a shut body leaves the accessibility tree only once it has
51
+ * finished closing; `inert` takes it out of the tab order immediately.
52
+ *
53
+ * THE CHILDREN are unmounted once closed (see {@link CollapseProps.keepMounted}), but
54
+ * only once the track has finished closing: the track is what animates, so the
55
+ * children have to survive the movement that hides them. Under reduced motion they
56
+ * go at once — there is no movement to wait for — and the opening half is silenced by
57
+ * `motion-reduce:transition-none`, since tokens.css only silences the overlay
58
+ * animations by name.
59
+ *
60
+ * Padding and margins belong on a child: on this element, or the clipping one inside
61
+ * it, they would hold the row open by that much.
62
+ */
63
+ export function Collapse({ open, keepMounted = false, children, className, style, ...rest }: CollapseProps) {
64
+ // Whether the children are still on their way out. Adjusted DURING render when
65
+ // `open` flips (React's "storing information from previous renders"), so a close
66
+ // under reduced motion unmounts in the same render that shut it — a test, and a
67
+ // screen reader, see the body go on the click — and a re-open mid-fold simply
68
+ // cancels the linger.
69
+ const [lingering, setLingering] = useState(false);
70
+ const [prevOpen, setPrevOpen] = useState(open);
71
+ if (open !== prevOpen) {
72
+ setPrevOpen(open);
73
+ setLingering(!open && !prefersReducedMotion());
74
+ }
75
+ useEffect(() => {
76
+ if (!lingering) return;
77
+ const timer = setTimeout(() => setLingering(false), COLLAPSE_MS);
78
+ return () => clearTimeout(timer);
79
+ }, [lingering]);
80
+
81
+ const mounted = open || lingering || keepMounted;
82
+
83
+ return (
84
+ <div
85
+ {...rest}
86
+ inert={!open}
87
+ className={cn(
88
+ "grid transition-[grid-template-rows,visibility] duration-200 ease-out motion-reduce:transition-none",
89
+ className,
90
+ )}
91
+ // Inline rather than `grid-rows-[…]` classes: a caller's `className` must not be
92
+ // able to pin the track open, and the two values ARE the state.
93
+ style={{ ...style, gridTemplateRows: open ? "1fr" : "0fr", visibility: open ? "visible" : "hidden" }}
94
+ >
95
+ <div
96
+ className={cn(
97
+ "min-h-0 overflow-hidden transition-opacity duration-200 ease-out motion-reduce:transition-none",
98
+ open ? "opacity-100" : "opacity-0",
99
+ )}
100
+ >
101
+ {mounted && children}
102
+ </div>
103
+ </div>
104
+ );
105
+ }
106
+
107
+ /**
108
+ * `title` is omitted from the div's own props because this component already owns the
109
+ * name: here it is the header's content (and a ReactNode), not the browser's tooltip.
110
+ * Everything else reaches the outer element.
111
+ */
112
+ export interface DisclosureProps extends Omit<ComponentPropsWithoutRef<"div">, "title"> {
113
+ /** What the header says. A node, so a status chip or a count can sit in it. */
114
+ title: ReactNode;
115
+ /** The smaller line under the title — what is inside, for a reader deciding whether
116
+ * to open it. */
117
+ hint?: ReactNode;
118
+ /**
119
+ * Who decides whether this one is open. Left out, the disclosure decides for itself,
120
+ * which is what one standing on its own wants. Passed, the caller does — a set of
121
+ * which one is open at a time, or an open state kept in the URL so the view can be
122
+ * linked to. There is no accordion component: "one open at a time" is a few lines in
123
+ * the caller and it usually owns a URL parameter, which is app state.
124
+ */
125
+ open?: boolean;
126
+ /** Whether an UNCONTROLLED disclosure starts open. Ignored when `open` is passed. */
127
+ defaultOpen?: boolean;
128
+ /** Called with the next state on every toggle, controlled or not. */
129
+ onOpenChange?: (open: boolean) => void;
130
+ /**
131
+ * `card` (default) draws the kit's `Card` surface round header and body, with the
132
+ * chevron trailing the header and turning over — the "section that opens" of a
133
+ * settings or analysis page. `bare` draws nothing: a leading chevron that turns
134
+ * down, for an inline "Show 3 hidden accounts" inside something that already has
135
+ * its own surface.
136
+ */
137
+ variant?: "card" | "bare";
138
+ /**
139
+ * Wrap the header button in a heading of this level. The WAI-ARIA disclosure pattern
140
+ * puts the button INSIDE the heading when the disclosure titles a section, so the
141
+ * page's heading outline still lists it. Off by default: an inline "show more" is
142
+ * not a section.
143
+ */
144
+ headingAs?: "h2" | "h3" | "h4" | "h5" | "h6";
145
+ /** See {@link CollapseProps.keepMounted}. */
146
+ keepMounted?: boolean;
147
+ disabled?: boolean;
148
+ /** Extra classes for the header button. */
149
+ headerClassName?: string;
150
+ /** Extra classes for the body's wrapper — where its padding and spacing live. */
151
+ bodyClassName?: string;
152
+ children: ReactNode;
153
+ }
154
+
155
+ /**
156
+ * A section that opens in place: a header button with `aria-expanded` and
157
+ * `aria-controls`, and a {@link Collapse} under it.
158
+ *
159
+ * Both apps had written this by hand — Lenkbank as a shared `CollapsibleCard` with ten
160
+ * importers, Keksdose three separate times (accounts, twice; the support panel) — and
161
+ * the copies had drifted: none of Keksdose's animated, and all three swapped a
162
+ * `ChevronRight` for a `ChevronDown` rather than turning one. Here the chevron is one
163
+ * icon that rotates, so the change of state is one element moving rather than two
164
+ * trading places.
165
+ *
166
+ * The body is unmounted while shut; see {@link CollapseProps.keepMounted} for why that
167
+ * is the default and when to opt out.
168
+ */
169
+ export function Disclosure({
170
+ title,
171
+ hint,
172
+ open: controlled,
173
+ defaultOpen = false,
174
+ onOpenChange,
175
+ variant = "card",
176
+ headingAs: Heading,
177
+ keepMounted,
178
+ disabled,
179
+ headerClassName,
180
+ bodyClassName,
181
+ className,
182
+ children,
183
+ ...rest
184
+ }: DisclosureProps) {
185
+ const [own, setOwn] = useState(defaultOpen);
186
+ const open = controlled ?? own;
187
+ const bodyId = useId();
188
+ const card = variant === "card";
189
+
190
+ const toggle = () => {
191
+ const next = !open;
192
+ if (controlled === undefined) setOwn(next);
193
+ onOpenChange?.(next);
194
+ };
195
+
196
+ const button = (
197
+ <button
198
+ type="button"
199
+ aria-expanded={open}
200
+ aria-controls={bodyId}
201
+ disabled={disabled}
202
+ onClick={toggle}
203
+ className={cn(
204
+ "flex w-full gap-2 text-start outline-none disabled:cursor-not-allowed disabled:opacity-50",
205
+ "focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
206
+ card
207
+ ? cn(
208
+ "items-center justify-between rounded-lg p-4 hover:bg-[var(--bg-hover)] focus-visible:ring-inset",
209
+ open && "rounded-b-none",
210
+ )
211
+ : "items-center rounded-sm text-sm font-medium text-[var(--text-secondary)] hover:text-[var(--text-primary)]",
212
+ headerClassName,
213
+ )}
214
+ >
215
+ {!card && <Chevron open={open} leading />}
216
+ <span className="min-w-0">
217
+ <span className={cn("block", card && "text-sm font-semibold text-[var(--text-primary)]")}>{title}</span>
218
+ {hint !== undefined && (
219
+ <span className="mt-0.5 block text-xs font-normal text-[var(--text-muted)]">{hint}</span>
220
+ )}
221
+ </span>
222
+ {card && <Chevron open={open} />}
223
+ </button>
224
+ );
225
+
226
+ return (
227
+ <div
228
+ {...rest}
229
+ className={cn(card && "rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] shadow-sm", className)}
230
+ >
231
+ {/* Preflight already makes h1–h6 inherit size and weight, so the heading adds
232
+ structure and nothing visible. */}
233
+ {Heading ? <Heading>{button}</Heading> : button}
234
+ <Collapse id={bodyId} open={open} keepMounted={keepMounted}>
235
+ <div className={cn(card ? "space-y-3 px-4 pb-4" : "space-y-2 pt-2", bodyClassName)}>{children}</div>
236
+ </Collapse>
237
+ </div>
238
+ );
239
+ }
240
+
241
+ /**
242
+ * One icon for both variants, turned rather than swapped. `leading` (bare) points it
243
+ * along the reading direction while shut — right in LTR, left in RTL — and down when
244
+ * open; trailing (card) points down while shut and up when open, which is the card
245
+ * header's convention. Rotation, not a mirrored glyph, so RTL needs one opposite angle
246
+ * and no `scale` composing with the turn.
247
+ */
248
+ function Chevron({ open, leading = false }: { open: boolean; leading?: boolean }) {
249
+ return (
250
+ <ChevronDown
251
+ aria-hidden
252
+ className={cn(
253
+ "shrink-0 text-[var(--text-muted)] transition-transform duration-200 ease-out motion-reduce:transition-none",
254
+ leading ? "size-3.5" : "size-4",
255
+ leading ? !open && "-rotate-90 rtl:rotate-90" : open && "rotate-180",
256
+ )}
257
+ />
258
+ );
259
+ }
@@ -3,7 +3,12 @@ import type { ComponentPropsWithoutRef, ReactNode } from "react";
3
3
  import { X } from "lucide-react";
4
4
  import { cn } from "../lib/cn";
5
5
  import { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from "./ui";
6
- import { ComboboxPanel, useComboboxCore, type ComboOption } from "./combobox-core";
6
+ import {
7
+ ComboboxPanel,
8
+ useComboboxCore,
9
+ useComboboxFieldError,
10
+ type ComboOption,
11
+ } from "./combobox-core";
7
12
  import { DEFAULT_COMBOBOX_LABELS, DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels";
8
13
 
9
14
  export type { ComboOption } from "./combobox-core";
@@ -19,8 +24,9 @@ export interface EntityComboboxProps<V extends string | number>
19
24
  /** Already-loaded options (client-side filtered). Also used to resolve the
20
25
  * trigger label for the current `value`. */
21
26
  options?: ComboOption<V>[];
22
- /** Async option source, debounced and called on open + as the query changes.
23
- * Stale responses are ignored. When set, `options` is used only for label
27
+ /** Async option source, debounced and called on open + as the query changes
28
+ * (at `minChars` and up). Stale responses are ignored; a rejection empties the
29
+ * list and says `loadErrorLabel` instead of leaving the last query's rows up. When set, `options` is used only for label
24
30
  * resolution, not as the result list. */
25
31
  loadOptions?: (query: string) => Promise<ComboOption<V>[]>;
26
32
  /** External loading flag (OR-ed with the internal async state). */
@@ -42,6 +48,19 @@ export interface EntityComboboxProps<V extends string | number>
42
48
  createLabel?: (query: string) => string;
43
49
  /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */
44
50
  invalid?: boolean;
51
+ /** What is wrong with the value, as {@link Input}'s `error`: rendered under the
52
+ * field, on the trigger's `aria-describedby`, and implies `invalid`. */
53
+ error?: ReactNode;
54
+ /** Narrow `options` client-side by the query. Default `true`; `false` shows them
55
+ * as given (a server-ranked list). */
56
+ filter?: boolean;
57
+ /** Offer nothing, and call no `loadOptions`, below this many characters. Default
58
+ * `0`, i.e. the list loads as the panel opens. */
59
+ minChars?: number;
60
+ /** `loadOptions` debounce. Default 150 ms. */
61
+ debounceMs?: number;
62
+ /** Shown when `loadOptions` rejects. Default: `combobox.loadError`. */
63
+ loadErrorLabel?: string;
45
64
  }
46
65
 
47
66
  /**
@@ -69,10 +88,23 @@ export function EntityCombobox<V extends string | number>({
69
88
  createLabel,
70
89
  className,
71
90
  invalid,
91
+ error,
92
+ filter,
93
+ minChars,
94
+ debounceMs,
95
+ loadErrorLabel,
72
96
  "aria-label": ariaLabel,
73
97
  ...rest
74
98
  }: EntityComboboxProps<V>) {
75
- const core = useComboboxCore<V>({ options, loadOptions, loading });
99
+ const core = useComboboxCore<V>({
100
+ options,
101
+ loadOptions,
102
+ loading,
103
+ filter,
104
+ minChars,
105
+ debounceMs,
106
+ });
107
+ const field = useComboboxFieldError(error, invalid);
76
108
  // The props are the per-instance overrides, the provider the app-wide ones; a
77
109
  // prop left `undefined` falls through to the provider rather than masking it.
78
110
  const labels = useKitLabels("combobox", DEFAULT_COMBOBOX_LABELS, {
@@ -138,7 +170,8 @@ export function EntityCombobox<V extends string | number>({
138
170
  (typeof label === "string" ? common.fieldValue(label, triggerText) : undefined)
139
171
  }
140
172
  disabled={disabled}
141
- aria-invalid={invalid || undefined}
173
+ aria-invalid={field.isInvalid || undefined}
174
+ aria-describedby={field.describedBy}
142
175
  onClick={() => !disabled && setOpen((o) => !o)}
143
176
  // Down/Up opens the list from the closed trigger, per the APG. Enter and
144
177
  // Space already do it through the button's own click.
@@ -154,7 +187,7 @@ export function EntityCombobox<V extends string | number>({
154
187
  "pr-9",
155
188
  label !== undefined && FIELD_FLOATING_PAD,
156
189
  disabled && "cursor-not-allowed opacity-50",
157
- invalid && FIELD_INVALID,
190
+ field.isInvalid && FIELD_INVALID,
158
191
  )}
159
192
  >
160
193
  <span className="flex min-w-0 items-center gap-2">
@@ -201,6 +234,7 @@ export function EntityCombobox<V extends string | number>({
201
234
  searchPlaceholder={labels.search}
202
235
  emptyLabel={labels.noResults}
203
236
  closeLabel={closeLabel}
237
+ loadErrorLabel={loadErrorLabel}
204
238
  isSelected={(v) => v === value}
205
239
  onChoose={choose}
206
240
  showCreate={showCreate}
@@ -210,6 +244,7 @@ export function EntityCombobox<V extends string | number>({
210
244
  }}
211
245
  createContent={labels.create(q)}
212
246
  />
247
+ {field.errorEl}
213
248
  </div>
214
249
  );
215
250
  }