@eifi1/ui-kit 0.23.0 → 0.24.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 (101) hide show
  1. package/README.md +6 -6
  2. package/dist/chart.d.ts +2 -4
  3. package/dist/components/amount-input.d.ts +16 -9
  4. package/dist/components/amount-input.js +13 -11
  5. package/dist/components/amount-input.js.map +1 -1
  6. package/dist/components/autocomplete.d.ts +5 -0
  7. package/dist/components/autocomplete.js +39 -37
  8. package/dist/components/autocomplete.js.map +1 -1
  9. package/dist/components/button-group.d.ts +2 -4
  10. package/dist/components/calculator.d.ts +2 -4
  11. package/dist/components/column-mapper.d.ts +62 -9
  12. package/dist/components/column-mapper.js +85 -49
  13. package/dist/components/column-mapper.js.map +1 -1
  14. package/dist/components/combobox.d.ts +33 -1
  15. package/dist/components/combobox.js +8 -2
  16. package/dist/components/combobox.js.map +1 -1
  17. package/dist/components/confirm-dialog.d.ts +2 -4
  18. package/dist/components/copy-button.d.ts +2 -4
  19. package/dist/components/danger-confirm.d.ts +2 -4
  20. package/dist/components/danger-confirm.js +52 -5
  21. package/dist/components/danger-confirm.js.map +1 -1
  22. package/dist/components/entity-combobox.d.ts +13 -1
  23. package/dist/components/entity-combobox.js +13 -4
  24. package/dist/components/entity-combobox.js.map +1 -1
  25. package/dist/components/facing-pair.d.ts +2 -4
  26. package/dist/components/file-button.d.ts +2 -4
  27. package/dist/components/file-dropzone.d.ts +2 -4
  28. package/dist/components/form-actions.d.ts +1 -3
  29. package/dist/components/form-actions.js +15 -1
  30. package/dist/components/form-actions.js.map +1 -1
  31. package/dist/components/iban-input.d.ts +2 -4
  32. package/dist/components/language-select.d.ts +2 -4
  33. package/dist/components/money-field.d.ts +2 -4
  34. package/dist/components/multi-entity-combobox.d.ts +5 -1
  35. package/dist/components/multi-entity-combobox.js +11 -3
  36. package/dist/components/multi-entity-combobox.js.map +1 -1
  37. package/dist/components/number-field.d.ts +2 -4
  38. package/dist/components/number-input.d.ts +2 -4
  39. package/dist/components/numpad-sheet.d.ts +2 -4
  40. package/dist/components/phone-input.d.ts +2 -4
  41. package/dist/components/series-chart.d.ts +2 -4
  42. package/dist/components/settings-fields.d.ts +2 -4
  43. package/dist/components/share-card.d.ts +1 -3
  44. package/dist/components/text-link.d.ts +1 -3
  45. package/dist/components/time-input.d.ts +2 -4
  46. package/dist/components/ui.d.ts +2 -4
  47. package/dist/components/ui.js +55 -17
  48. package/dist/components/ui.js.map +1 -1
  49. package/dist/feedback/feedback-attachment.d.ts +66 -1
  50. package/dist/feedback/feedback-attachment.js +32 -12
  51. package/dist/feedback/feedback-attachment.js.map +1 -1
  52. package/dist/feedback/feedback-dialog.d.ts +66 -1
  53. package/dist/feedback/feedback-inbox.d.ts +67 -253
  54. package/dist/feedback/feedback-thread.d.ts +1 -3
  55. package/dist/feedback/feedback-thread.js +8 -2
  56. package/dist/feedback/feedback-thread.js.map +1 -1
  57. package/dist/{feedback-BxeQVzwq.d.ts → feedback-DOwPu-Il.d.ts} +878 -18
  58. package/dist/feedback.d.ts +2 -4
  59. package/dist/hooks/use-file-drop.d.ts +2 -4
  60. package/dist/i18n/defaults.d.ts +2 -4
  61. package/dist/i18n/german.d.ts +2 -4
  62. package/dist/i18n/kit-labels.d.ts +1 -3
  63. package/dist/i18n/languages.d.ts +2 -4
  64. package/dist/i18n/locales/de-CH.d.ts +2 -4
  65. package/dist/i18n/locales/en.d.ts +2 -4
  66. package/dist/i18n/locales/es.d.ts +2 -4
  67. package/dist/i18n/locales/fr.d.ts +2 -4
  68. package/dist/i18n/locales/hu.d.ts +2 -4
  69. package/dist/i18n/locales/it.d.ts +2 -4
  70. package/dist/i18n/locales/zh.d.ts +2 -4
  71. package/dist/i18n/review.d.ts +2 -4
  72. package/dist/index.d.ts +2 -4
  73. package/dist/lib/strip-fade.d.ts +2 -2
  74. package/dist/lib/strip-fade.js.map +1 -1
  75. package/dist/rhf/fields.d.ts +59 -8
  76. package/dist/rhf/fields.js +61 -3
  77. package/dist/rhf/fields.js.map +1 -1
  78. package/dist/rhf/form.d.ts +2 -4
  79. package/dist/rhf.d.ts +3 -5
  80. package/dist/rhf.js.map +1 -1
  81. package/dist/shell/app-shell.d.ts +1 -3
  82. package/dist/shell/top-bar-brand.d.ts +2 -4
  83. package/dist/shell.d.ts +1 -3
  84. package/dist/wizard/stepper-nav.d.ts +2 -4
  85. package/dist/wizard.d.ts +2 -4
  86. package/package.json +2 -2
  87. package/src/components/amount-input.tsx +37 -20
  88. package/src/components/autocomplete.tsx +16 -5
  89. package/src/components/column-mapper.tsx +181 -79
  90. package/src/components/combobox.tsx +41 -3
  91. package/src/components/danger-confirm.tsx +193 -22
  92. package/src/components/entity-combobox.tsx +28 -5
  93. package/src/components/form-actions.tsx +20 -1
  94. package/src/components/multi-entity-combobox.tsx +16 -3
  95. package/src/components/ui.tsx +107 -0
  96. package/src/feedback/feedback-attachment.tsx +65 -12
  97. package/src/feedback/feedback-thread.tsx +66 -6
  98. package/src/lib/strip-fade.ts +2 -2
  99. package/src/rhf/fields.tsx +167 -6
  100. package/src/rhf.ts +1 -1
  101. package/dist/feedback-attachment-fGAzZPf0.d.ts +0 -469
@@ -10,6 +10,7 @@ import {
10
10
  } from "react";
11
11
  import { Camera, FileText, Image as ImageIcon, Paperclip, X } from "lucide-react";
12
12
  import { Button, IconButton, Spinner } from "../components/ui";
13
+ import type { ButtonSize, ButtonVariant } from "../components/ui";
13
14
  import { DEFAULT_FILE_PICKER_LABELS, formatAccept } from "../components/file-button";
14
15
  import { useCommitReason } from "../components/write-lock";
15
16
  import { useKitFileLabels, useKitLabels } from "../i18n/kit-labels";
@@ -186,9 +187,40 @@ interface FeedbackAttachmentFieldBaseProps {
186
187
  * in, whose common parent is where the event bubbles to. Ignored when
187
188
  * `documentPaste` is set. */
188
189
  pasteFrom?: RefObject<HTMLElement | null>;
190
+ /**
191
+ * The add and capture buttons' look (0.24.0, keksdose) — Button's own `variant`.
192
+ * Default `"secondary"`, the look they always had.
193
+ *
194
+ * keksdose's support chat puts this field in ChatComposer's `attachmentSlot`, in the
195
+ * row with Send, where its own picker had been a `ghost` / `sm` FileButton: two
196
+ * bordered full-size buttons under a chat box read as a second form, and the field
197
+ * gave no way to say otherwise. Every mode, wherever the buttons appear; the chips'
198
+ * remove buttons keep their size.
199
+ */
200
+ buttonVariant?: ButtonVariant;
201
+ /** The add and capture buttons' size — Button's own `size`, with `buttonVariant`.
202
+ * Default `"md"`. `"sm"` draws their icons at 14px, as a small button's are. */
203
+ buttonSize?: ButtonSize;
189
204
  className?: string;
190
205
  }
191
206
 
207
+ /**
208
+ * The field's root, in every mode. `relative` for the `sr-only` file input (see the
209
+ * single field). `min-w-0 max-w-full` (0.24.0, keksdose) because the field is often a
210
+ * FLEX ITEM — ChatComposer's `attachmentSlot` row, FormActions' `start` — and a flex item
211
+ * is never narrower than its content's min-content width unless told it may be. A chip
212
+ * truncates its name only inside a box of bounded width, and a screenshot's name
213
+ * ("Screenshot 2026-10-03 at 07.45.12.png") was the min-content: the field grew to it
214
+ * and ran off a 390px phone composer (measured in Chromium: the field's right edge at
215
+ * 592px, the page 614px wide). keksdose passed `className="min-w-0 max-w-full"` at both
216
+ * its callers to stop it; here once, for every host. In normal flow — a dialog, a note
217
+ * editor — both are no-ops.
218
+ */
219
+ const FIELD_ROOT = "relative min-w-0 max-w-full";
220
+
221
+ /** Paperclip and camera at the size the button's text is: 16px at `md`, 14px at `sm`. */
222
+ const buttonIcon = (size: ButtonSize) => (size === "sm" ? "size-3.5" : "size-4");
223
+
192
224
  /** One file: choosing a second means removing the first. The field's only mode until 0.15.5. */
193
225
  export interface FeedbackAttachmentFieldSingleProps extends FeedbackAttachmentFieldBaseProps {
194
226
  multiple?: false;
@@ -354,6 +386,8 @@ function SingleField({
354
386
  documentPaste = false,
355
387
  pasteFrom,
356
388
  disabled = false,
389
+ buttonVariant = "secondary",
390
+ buttonSize = "md",
357
391
  className,
358
392
  }: FeedbackAttachmentFieldSingleProps) {
359
393
  // The object URL is keyed to the file it was made for, so a stale one (from the
@@ -406,8 +440,9 @@ function SingleField({
406
440
  // `relative` for the same reason as FileDropzone: the `sr-only` file input below
407
441
  // is `position: absolute`, and without a positioned ancestor it is laid out
408
442
  // against the initial containing block — extending the DOCUMENT height to its own
409
- // offset and producing a phantom second scrollbar on any long page.
410
- className={cn("relative", className)}
443
+ // offset and producing a phantom second scrollbar on any long page. And
444
+ // `min-w-0 max-w-full`, so a long name truncates in a flex row: see FIELD_ROOT.
445
+ className={cn(FIELD_ROOT, className)}
411
446
  onPaste={onPaste}
412
447
  >
413
448
  <Heading text={labelsProp?.attachment} />
@@ -439,11 +474,12 @@ function SingleField({
439
474
  <div className="flex flex-wrap gap-2">
440
475
  <Button
441
476
  type="button"
442
- variant="secondary"
477
+ variant={buttonVariant}
478
+ size={buttonSize}
443
479
  disabled={disabled}
444
480
  onClick={() => fileInputRef.current?.click()}
445
481
  >
446
- <Paperclip className="size-4" /> {text.attachmentAdd}
482
+ <Paperclip className={buttonIcon(buttonSize)} /> {text.attachmentAdd}
447
483
  </Button>
448
484
  {onCaptureScreenshot && (
449
485
  <CaptureButton
@@ -451,6 +487,8 @@ function SingleField({
451
487
  onFile={pick}
452
488
  label={text.attachmentCapture}
453
489
  disabled={disabled}
490
+ variant={buttonVariant}
491
+ size={buttonSize}
454
492
  />
455
493
  )}
456
494
  </div>
@@ -490,6 +528,8 @@ function MultipleField({
490
528
  documentPaste = false,
491
529
  pasteFrom,
492
530
  disabled = false,
531
+ buttonVariant = "secondary",
532
+ buttonSize = "md",
493
533
  className,
494
534
  }: FeedbackAttachmentFieldMultipleProps) {
495
535
  const rootRef = useRef<HTMLDivElement>(null);
@@ -592,7 +632,7 @@ function MultipleField({
592
632
  ];
593
633
 
594
634
  return (
595
- <div ref={rootRef} className={cn("relative space-y-2", className)} onPaste={onPaste}>
635
+ <div ref={rootRef} className={cn(FIELD_ROOT, "space-y-2", className)} onPaste={onPaste}>
596
636
  <Heading text={labelsProp?.attachment} className="mb-0" />
597
637
  {chips.length > 0 && (
598
638
  // One chip per row, full width: wrapped chips of name-dependent widths made a
@@ -630,12 +670,13 @@ function MultipleField({
630
670
  {room > 0 && (
631
671
  <Button
632
672
  type="button"
633
- variant="secondary"
673
+ variant={buttonVariant}
674
+ size={buttonSize}
634
675
  data-attachment-action=""
635
676
  disabled={disabled}
636
677
  onClick={() => fileInputRef.current?.click()}
637
678
  >
638
- <Paperclip className="size-4" /> {text.attachmentAdd}
679
+ <Paperclip className={buttonIcon(buttonSize)} /> {text.attachmentAdd}
639
680
  </Button>
640
681
  )}
641
682
  {canCapture && onCaptureScreenshot && (
@@ -644,6 +685,8 @@ function MultipleField({
644
685
  onFile={capture}
645
686
  label={text.attachmentCapture}
646
687
  disabled={disabled}
688
+ variant={buttonVariant}
689
+ size={buttonSize}
647
690
  data-attachment-action=""
648
691
  />
649
692
  )}
@@ -697,6 +740,8 @@ function RefsField({
697
740
  disabled = false,
698
741
  commit: commits,
699
742
  disabledReason: ownDisabledReason,
743
+ buttonVariant = "secondary",
744
+ buttonSize = "md",
700
745
  className,
701
746
  }: FeedbackAttachmentFieldRefsProps) {
702
747
  const rootRef = useRef<HTMLDivElement>(null);
@@ -827,7 +872,7 @@ function RefsField({
827
872
  const hasChips = value.length > 0 || uploads.length > 0;
828
873
 
829
874
  return (
830
- <div ref={rootRef} className={cn("relative space-y-2", className)} onPaste={onPaste}>
875
+ <div ref={rootRef} className={cn(FIELD_ROOT, "space-y-2", className)} onPaste={onPaste}>
831
876
  <Heading text={labelsProp?.attachment} className="mb-0" />
832
877
  {hasChips && (
833
878
  <ul aria-label={text.attachmentList} className="flex flex-col gap-2">
@@ -873,13 +918,14 @@ function RefsField({
873
918
  <div className="flex flex-wrap gap-2">
874
919
  <Button
875
920
  type="button"
876
- variant="secondary"
921
+ variant={buttonVariant}
922
+ size={buttonSize}
877
923
  data-attachment-action=""
878
924
  disabled={disabled}
879
925
  disabledReason={reason}
880
926
  onClick={() => fileInputRef.current?.click()}
881
927
  >
882
- <Paperclip className="size-4" /> {text.attachmentAdd}
928
+ <Paperclip className={buttonIcon(buttonSize)} /> {text.attachmentAdd}
883
929
  </Button>
884
930
  {onCaptureScreenshot && (
885
931
  <CaptureButton
@@ -888,6 +934,8 @@ function RefsField({
888
934
  label={text.attachmentCapture}
889
935
  disabled={disabled}
890
936
  disabledReason={reason}
937
+ variant={buttonVariant}
938
+ size={buttonSize}
891
939
  data-attachment-action=""
892
940
  />
893
941
  )}
@@ -945,6 +993,8 @@ function CaptureButton({
945
993
  label,
946
994
  disabled = false,
947
995
  disabledReason,
996
+ variant,
997
+ size,
948
998
  ...rest
949
999
  }: {
950
1000
  capture: () => Promise<File | null>;
@@ -952,13 +1002,16 @@ function CaptureButton({
952
1002
  label: string;
953
1003
  disabled?: boolean;
954
1004
  disabledReason?: ReactNode;
1005
+ variant: ButtonVariant;
1006
+ size: ButtonSize;
955
1007
  "data-attachment-action"?: string;
956
1008
  }) {
957
1009
  const [capturing, setCapturing] = useState(false);
958
1010
  return (
959
1011
  <Button
960
1012
  type="button"
961
- variant="secondary"
1013
+ variant={variant}
1014
+ size={size}
962
1015
  disabled={capturing || disabled}
963
1016
  disabledReason={disabledReason}
964
1017
  {...rest}
@@ -972,7 +1025,7 @@ function CaptureButton({
972
1025
  .finally(() => setCapturing(false));
973
1026
  }}
974
1027
  >
975
- <Camera className="size-4" /> {capturing ? "…" : label}
1028
+ <Camera className={buttonIcon(size)} /> {capturing ? "…" : label}
976
1029
  </Button>
977
1030
  );
978
1031
  }
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
2
- import type { ReactNode, Ref } from "react";
2
+ import type { ReactNode, Ref, RefObject } from "react";
3
3
  import { ImageIcon, ImageOff, Paperclip, Send } from "lucide-react";
4
4
  import { cn } from "../lib/cn";
5
5
  import { formatDate, formatRelativeTime } from "../lib/format";
@@ -467,11 +467,46 @@ function toDate(value: Date | string | number): Date | null {
467
467
  /** What the composer's attach control takes — {@link FeedbackNoteAttachment} with the
468
468
  * labels optional, since the field reads `feedbackAttachment` from the provider, and
469
469
  * `onError` the field's own (0.23.0): it is handed the refused file and the limit too
470
- * (`FeedbackAttachmentErrorInfo`), and a `(kind) => …` handler still fits. */
470
+ * (`FeedbackAttachmentErrorInfo`), and a `(kind) => …` handler still fits. Since
471
+ * 0.24.0 also the field's `buttonVariant` / `buttonSize`, for a composer whose row
472
+ * wants smaller or quieter attach buttons than the bordered `md` default. */
471
473
  export type FeedbackComposerAttachment = Omit<FeedbackNoteAttachment, "labels" | "onError"> & {
472
474
  labels?: Partial<FeedbackAttachmentLabels>;
473
475
  onError?: FeedbackAttachmentFieldSingleProps["onError"];
474
- };
476
+ } & Pick<FeedbackAttachmentFieldSingleProps, "buttonVariant" | "buttonSize">;
477
+
478
+ /**
479
+ * What a render-prop {@link FeedbackComposerProps.attachmentSlot} is handed (0.24.0,
480
+ * keksdose).
481
+ *
482
+ * keksdose's support chat put `<FeedbackAttachmentField refs>` in the slot and a
483
+ * screenshot pasted into the reply box never reached it: the box is the field's
484
+ * sibling, so the paste bubbles to their common parent — the composer's root — and the
485
+ * field hears it only through `pasteFrom`, which needs that root. The composer kept it
486
+ * to itself, so keksdose wrapped the whole composer in a ref'd `<div>` of its own just
487
+ * to have an element to hand over.
488
+ */
489
+ export interface FeedbackComposerSlotContext {
490
+ /**
491
+ * The composer's root, round the text box and this slot — the field's `pasteFrom`,
492
+ * as it is: `pasteFrom={slot.root}`.
493
+ *
494
+ * A ref, not the element, because `pasteFrom` is one: the field reads `.current` in
495
+ * an effect, and the slot renders INSIDE this root, so the two are committed together
496
+ * and React attaches the root before any effect of the slot's runs — the first paste
497
+ * after the first paint already lands. The element itself would be `null` on the
498
+ * first render (it does not exist yet) and could only arrive by a second render
499
+ * through state; the ref is the same object every render, so the field's listener is
500
+ * added once and never re-added.
501
+ *
502
+ * With the built-in `attachment` on as well, it listens on this root too: hand the
503
+ * root to one field only, or an image pasted once is attached twice.
504
+ */
505
+ root: RefObject<HTMLElement | null>;
506
+ /** The composer's `pending` — a send is in flight. For the field's `disabled`
507
+ * (keksdose G5b): the message carries the attachments as they were at the press. */
508
+ pending: boolean;
509
+ }
475
510
 
476
511
  export interface FeedbackComposerProps {
477
512
  /**
@@ -496,8 +531,21 @@ export interface FeedbackComposerProps {
496
531
  * The slot is the host's picker plus the chips of what is uploaded; the upload, the
497
532
  * refs and clearing them once `onSend` resolves stay with the host. Pair it with
498
533
  * `canSend`.
534
+ *
535
+ * A node, or (0.24.0) a function of {@link FeedbackComposerSlotContext} that returns
536
+ * one — for a field that wants the composer's root as its `pasteFrom`, so a
537
+ * screenshot pasted into the box reaches it, and `pending` for its `disabled`:
538
+ *
539
+ * ```tsx
540
+ * attachmentSlot={({ root, pending }) => (
541
+ * <FeedbackAttachmentField refs pasteFrom={root} disabled={pending} … />
542
+ * )}
543
+ * ```
544
+ *
545
+ * Called once per render of the composer, and not at all while `disabledReason`
546
+ * stands in for it.
499
547
  */
500
- attachmentSlot?: ReactNode;
548
+ attachmentSlot?: ReactNode | ((slot: FeedbackComposerSlotContext) => ReactNode);
501
549
  /**
502
550
  * Whether Send is enabled, over the composer's own rule (some text in the box) —
503
551
  * for state the composer cannot see, i.e. `attachmentSlot`'s (keksdose G5): `true`
@@ -718,6 +766,14 @@ export function FeedbackComposer({
718
766
  };
719
767
 
720
768
  const config = attachment === true ? {} : attachment || null;
769
+ // The slot is handed the ref OBJECT, for a field's `pasteFrom`, which reads `.current`
770
+ // in an effect once the root is attached — nothing reads it during this render (see
771
+ // FeedbackComposerSlotContext.root).
772
+ const slot =
773
+ typeof attachmentSlot === "function"
774
+ ? // eslint-disable-next-line react-hooks/refs -- handed on for an effect, not read here
775
+ attachmentSlot({ root, pending })
776
+ : attachmentSlot;
721
777
 
722
778
  return (
723
779
  <div {...rootAttributes} ref={root} className={cn("space-y-2", className)}>
@@ -756,7 +812,7 @@ export function FeedbackComposer({
756
812
  }}
757
813
  />
758
814
  <div className="flex flex-wrap items-end justify-between gap-2">
759
- {config || attachmentSlot != null ? (
815
+ {config || slot != null ? (
760
816
  <div className="flex min-w-0 flex-wrap items-end gap-2">
761
817
  {config && (
762
818
  <FeedbackAttachmentField
@@ -767,11 +823,13 @@ export function FeedbackComposer({
767
823
  maxBytes={config.maxBytes}
768
824
  onError={config.onError}
769
825
  onCaptureScreenshot={config.onCaptureScreenshot}
826
+ buttonVariant={config.buttonVariant}
827
+ buttonSize={config.buttonSize}
770
828
  // The box is this field's sibling; a paste in it bubbles to `root`.
771
829
  pasteFrom={root}
772
830
  />
773
831
  )}
774
- {attachmentSlot}
832
+ {slot}
775
833
  </div>
776
834
  ) : (
777
835
  <span />
@@ -827,3 +885,5 @@ export type ChatComposerHandle = FeedbackComposerHandle;
827
885
  /** {@link FeedbackComposerLabels}, for {@link ChatComposer} — still the `feedbackComposer`
828
886
  * namespace of the provider. */
829
887
  export type ChatComposerLabels = FeedbackComposerLabels;
888
+ /** {@link FeedbackComposerSlotContext}, for {@link ChatComposer}'s `attachmentSlot`. */
889
+ export type ChatComposerSlotContext = FeedbackComposerSlotContext;
@@ -5,8 +5,8 @@ import { useLayoutEffect, useState, type RefObject } from "react";
5
5
  export const TAB_FADE_PX = 24;
6
6
 
7
7
  /**
8
- * Shared by Tabs' strip and AppShell's scrolling phone sub-nav; internal, not part of
9
- * the package's surface.
8
+ * Shared by Tabs' strip, AppShell's scrolling phone sub-nav and (0.24) ColumnRoleTable's
9
+ * preview; internal, not part of the package's surface.
10
10
  *
11
11
  * Which ends of a sideways-scrolling strip have tabs cut off behind them, as the
12
12
  * `mask-image` that fades those ends out — or `undefined` while everything fits.
@@ -9,6 +9,7 @@
9
9
  * <RhfDateField name="start" label="Start" />
10
10
  * <RhfSelect name="type" label="Type" options={TYPES} />
11
11
  * <RhfCombobox name="tenantId" label="Tenant" options={tenants} clearable />
12
+ * <RhfInlineEntityCombobox name="accountId" label="Account" options={accounts} />
12
13
  * <RhfCheckbox name="isDefault" label="Default account" />
13
14
  * <RhfTimeInput name="meetingTime" label="Time" />
14
15
  * <RhfDateRangePicker fromName="periodFrom" toName="periodTo" label="Period" />
@@ -78,7 +79,12 @@ import {
78
79
  } from "../components/toggle-group";
79
80
  import { Checkbox, type CheckboxProps } from "../components/checkbox";
80
81
  import { EntityCombobox, type EntityComboboxProps } from "../components/entity-combobox";
81
- import { Combobox, type ComboboxProps } from "../components/combobox";
82
+ import {
83
+ Combobox,
84
+ InlineEntityCombobox,
85
+ type ComboboxProps,
86
+ type InlineEntityComboboxProps,
87
+ } from "../components/combobox";
82
88
  import type { ComboClearValue } from "../components/combobox-core";
83
89
  import { cn } from "../lib/cn";
84
90
 
@@ -1769,11 +1775,17 @@ export type RhfComboboxProps<
1769
1775
  | "emptyLabel"
1770
1776
  | "clearable"
1771
1777
  | "clearLabel"
1778
+ | "closeLabel"
1772
1779
  | "onCreate"
1773
1780
  | "createLabel"
1781
+ | "createEmptyLabel"
1782
+ | "createCommit"
1783
+ | "commit"
1784
+ | "disabledReason"
1774
1785
  | "filter"
1775
1786
  | "minChars"
1776
1787
  | "debounceMs"
1788
+ | "aria-label"
1777
1789
  > & {
1778
1790
  /** What a clear stores — the picker's `clearValue`. Default `null`; `""` for a
1779
1791
  * schema that spells "no choice" as an empty string. */
@@ -1791,6 +1803,8 @@ function EntityControl<V extends string | number>({
1791
1803
  describedBy,
1792
1804
  clearValue,
1793
1805
  comboClassName,
1806
+ sheetTitle,
1807
+ focusRef,
1794
1808
  comboProps,
1795
1809
  }: {
1796
1810
  field: ControllerRenderProps;
@@ -1801,12 +1815,15 @@ function EntityControl<V extends string | number>({
1801
1815
  describedBy: string | undefined;
1802
1816
  clearValue: ComboClearValue;
1803
1817
  comboClassName?: string;
1818
+ sheetTitle: ReactNode;
1819
+ /** `field.ref`, handed over on its own so the compiler reads `field` as data. */
1820
+ focusRef: ControllerRenderProps["ref"];
1804
1821
  comboProps: Omit<RhfComboboxProps<FieldValues, string, V>, keyof RhfFieldBaseProps | "clearValue" | "comboClassName">;
1805
1822
  }) {
1806
1823
  const box = useRef<HTMLDivElement>(null);
1807
1824
  // EntityCombobox puts `id` and the ARIA below on its trigger, so the label's
1808
- // `htmlFor` names it, the hint describes it and the focus handle finds it by id.
1809
- useFocusHandle(field.ref, () => document.getElementById(id));
1825
+ // `htmlFor` names it and the hint describes it; `field.ref` reaches the trigger
1826
+ // through the picker's own `ref` (0.24), as RhfCountrySelect's does.
1810
1827
  // EntityCombobox reports no blur: focus leaving the picker marks the field touched.
1811
1828
  const onBlur = field.onBlur;
1812
1829
  useLayoutEffect(() => {
@@ -1819,7 +1836,11 @@ function EntityControl<V extends string | number>({
1819
1836
  return (
1820
1837
  <div ref={box}>
1821
1838
  <EntityCombobox<V, ComboClearValue>
1839
+ // The form's label titles the phone sheet: the picker is not handed `label`,
1840
+ // which the FormLabel above already draws.
1841
+ sheetTitle={sheetTitle}
1822
1842
  {...comboProps}
1843
+ ref={focusRef}
1823
1844
  className={comboClassName}
1824
1845
  id={id}
1825
1846
  aria-describedby={describedBy}
@@ -1841,6 +1862,14 @@ function EntityControl<V extends string | number>({
1841
1862
  * The kit's {@link EntityCombobox}: an id-keyed pick from `options` or `loadOptions`.
1842
1863
  * Stores the option's `value`, or `clearValue` on a clear (only offered with
1843
1864
  * `clearable`).
1865
+ *
1866
+ * 0.24: `field.ref` goes through the picker's own `ref` to its trigger (it used to be
1867
+ * a focus handle that found the trigger by id — the same element, now by the route
1868
+ * every other binding takes); the write lock (`commit`, `disabledReason`), the create
1869
+ * row's `createEmptyLabel` / `createCommit`, `closeLabel` and `aria-label` pass
1870
+ * through; and the form's label titles the phone sheet, which fell back to the
1871
+ * placeholder. For the INLINE picker — a text input, the account field typed into —
1872
+ * see {@link RhfInlineEntityCombobox}.
1844
1873
  */
1845
1874
  export function RhfCombobox<
1846
1875
  TFieldValues extends FieldValues = FieldValues,
@@ -1876,6 +1905,8 @@ export function RhfCombobox<
1876
1905
  describedBy={describedBy}
1877
1906
  clearValue={clearValue}
1878
1907
  comboClassName={comboClassName}
1908
+ sheetTitle={hasContent(label) ? label : undefined}
1909
+ focusRef={field.ref}
1879
1910
  comboProps={comboProps}
1880
1911
  />
1881
1912
  )}
@@ -1883,6 +1914,123 @@ export function RhfCombobox<
1883
1914
  );
1884
1915
  }
1885
1916
 
1917
+ type OwnInlineEntityComboboxProps<V extends string | number> = Omit<
1918
+ InlineEntityComboboxProps<V, ComboClearValue>,
1919
+ | "value"
1920
+ | "defaultValue"
1921
+ | "onChange"
1922
+ | "onBlur"
1923
+ | "label"
1924
+ | "hint"
1925
+ | "error"
1926
+ | "invalid"
1927
+ | "disabled"
1928
+ | "className"
1929
+ | "id"
1930
+ | "ref"
1931
+ | "clearValue"
1932
+ >;
1933
+
1934
+ export type RhfInlineEntityComboboxProps<
1935
+ TFieldValues extends FieldValues = FieldValues,
1936
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
1937
+ V extends string | number = string | number,
1938
+ TTransformed = TFieldValues,
1939
+ > = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &
1940
+ OwnInlineEntityComboboxProps<V> & {
1941
+ /** What a clear stores — emptying the text, or the "×" with `clearable`. Default
1942
+ * `null`; `""` for a schema that spells "no choice" as an empty string. The
1943
+ * picker reads either back as empty. */
1944
+ clearValue?: ComboClearValue;
1945
+ /** Classes for the picker — `className` is the item's box. As {@link RhfCombobox}'s. */
1946
+ comboClassName?: string;
1947
+ };
1948
+
1949
+ /**
1950
+ * {@link InlineEntityCombobox} bound to one field, which stores the chosen option's
1951
+ * `value` (an id), or `clearValue` once the text is emptied or the "×" pressed.
1952
+ *
1953
+ * kastlan asked for it (0.24): its account picker IS an `InlineEntityCombobox` — the
1954
+ * account number is typed straight into the field — and the budget line-item form
1955
+ * wired it through {@link RhfField}'s render. The picker took no `ref`, so react-hook-
1956
+ * form's focus-on-error had nothing to focus: a line item saved without an account
1957
+ * failed its submit and left the caret where it was, the one field on the form a
1958
+ * failed submit could not take you to. (The same gap {@link RhfCountrySelect} closed
1959
+ * for the address form's country in 0.23.)
1960
+ *
1961
+ * Bound the way {@link RhfCountrySelect} is: `field.ref` reaches the `<input>` (the
1962
+ * picker's `ref`, 0.24), the form's error paints the input and is the shell's message
1963
+ * under it, and the hint is the shell's description — so neither is ever handed to the
1964
+ * picker, and its box never changes when one comes or goes. The form's label names
1965
+ * the input through `<label for>` and titles the phone sheet (`sheetTitle`); the picker
1966
+ * draws no floating label of its own. Focus leaving the picker marks the field
1967
+ * touched.
1968
+ *
1969
+ * Everything else is the picker's and passes through: `commit` and `disabledReason`
1970
+ * (a picker whose choice saves stays focusable under a lock and says why), `clearable`,
1971
+ * `autoFocus`, `aria-label` for an unlabelled cell, and the create row — `onCreate`,
1972
+ * `createLabel`, `createEmptyLabel`, `createCommit`. Making the record is the
1973
+ * caller's, and so is storing its id once it exists:
1974
+ *
1975
+ * ```tsx
1976
+ * <RhfInlineEntityCombobox<LineItem, "account_id", number>
1977
+ * name="account_id" label="Account" required options={accounts}
1978
+ * rules={{ required: "Choose an account" }}
1979
+ * onCreate={async (name) => form.setValue("account_id", (await createAccount(name)).id)} />
1980
+ * ```
1981
+ *
1982
+ * For the button-shaped picker with an async `loadOptions`, see {@link RhfCombobox}.
1983
+ */
1984
+ export function RhfInlineEntityCombobox<
1985
+ TFieldValues extends FieldValues = FieldValues,
1986
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
1987
+ V extends string | number = string | number,
1988
+ TTransformed = TFieldValues,
1989
+ >({
1990
+ name,
1991
+ control,
1992
+ rules,
1993
+ label,
1994
+ hint,
1995
+ required,
1996
+ disabled,
1997
+ excludeWhenDisabled,
1998
+ className,
1999
+ clearValue = null,
2000
+ comboClassName,
2001
+ ...comboProps
2002
+ }: RhfInlineEntityComboboxProps<TFieldValues, TName, V, TTransformed>) {
2003
+ return (
2004
+ <RhfField
2005
+ {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
2006
+ render={({ field, invalid }) => {
2007
+ const value: unknown = field.value;
2008
+ return (
2009
+ // FormControl clones this element with `id` / `aria-describedby` /
2010
+ // `aria-invalid`, which the picker routes to its <input>.
2011
+ <InlineEntityCombobox<V, ComboClearValue>
2012
+ aria-required={required || undefined}
2013
+ // The form's label is the phone sheet's heading — the picker is not handed
2014
+ // `label`, which the FormLabel above already draws. A caller's own wins.
2015
+ sheetTitle={hasContent(label) ? label : undefined}
2016
+ {...comboProps}
2017
+ className={comboClassName}
2018
+ ref={field.ref}
2019
+ value={(value ?? null) as V | ComboClearValue | null}
2020
+ clearValue={clearValue}
2021
+ onChange={(v) => field.onChange(v)}
2022
+ // The wrapper's blur, which the <input>'s bubbles to after the picker has
2023
+ // judged any loose text — so the value is in before the field is touched.
2024
+ onBlur={field.onBlur}
2025
+ disabled={field.disabled}
2026
+ invalid={invalid}
2027
+ />
2028
+ );
2029
+ }}
2030
+ />
2031
+ );
2032
+ }
2033
+
1886
2034
  export type RhfTextComboboxProps<
1887
2035
  TFieldValues extends FieldValues = FieldValues,
1888
2036
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
@@ -1899,6 +2047,9 @@ export type RhfTextComboboxProps<
1899
2047
  | "optionAdornment"
1900
2048
  | "autoFocus"
1901
2049
  | "aria-label"
2050
+ | "closeLabel"
2051
+ | "commit"
2052
+ | "disabledReason"
1902
2053
  > & {
1903
2054
  /** Classes for the combobox. */
1904
2055
  comboClassName?: string;
@@ -1939,6 +2090,8 @@ export function RhfTextCombobox<
1939
2090
  id={id}
1940
2091
  describedBy={describedBy}
1941
2092
  className={comboClassName}
2093
+ sheetTitle={hasContent(label) ? label : undefined}
2094
+ focusRef={field.ref}
1942
2095
  comboProps={comboProps}
1943
2096
  />
1944
2097
  )}
@@ -1954,6 +2107,8 @@ function TextComboControl({
1954
2107
  id,
1955
2108
  describedBy,
1956
2109
  className,
2110
+ sheetTitle,
2111
+ focusRef,
1957
2112
  comboProps,
1958
2113
  }: {
1959
2114
  field: ControllerRenderProps;
@@ -1963,16 +2118,22 @@ function TextComboControl({
1963
2118
  id: string;
1964
2119
  describedBy: string | undefined;
1965
2120
  className?: string;
2121
+ sheetTitle: ReactNode;
2122
+ /** `field.ref` — see EntityControl. */
2123
+ focusRef: ControllerRenderProps["ref"];
1966
2124
  comboProps: Omit<RhfTextComboboxProps, keyof RhfFieldBaseProps | "comboClassName">;
1967
2125
  }) {
1968
2126
  // The combobox routes `id` and its ARIA to its <input>, so the label's `htmlFor`
1969
- // names it, the hint describes it and the focus handle finds it; the error is its
1970
- // own `error` prop, which it merges in after the hint.
1971
- useFocusHandle(field.ref, () => document.getElementById(id));
2127
+ // names it and the hint describes it; `field.ref` reaches the <input> through the
2128
+ // combobox's own `ref` (0.24). The error is its own `error` prop, which it merges
2129
+ // in after the hint.
1972
2130
  const value: unknown = field.value;
1973
2131
  return (
1974
2132
  <Combobox
2133
+ // The form's label titles the phone sheet (see EntityControl).
2134
+ sheetTitle={sheetTitle}
1975
2135
  {...comboProps}
2136
+ ref={focusRef}
1976
2137
  id={id}
1977
2138
  aria-describedby={describedBy}
1978
2139
  aria-required={required || undefined}
package/src/rhf.ts CHANGED
@@ -12,7 +12,7 @@ export * from "./rhf/use-rhf-wizard-step";
12
12
  // Bound fields — one line per field (RhfTextField, RhfNumberField, RhfIntegerField, RhfMoneyField,
13
13
  // RhfDateField, RhfTextarea, RhfSelect, RhfCheckbox, RhfCombobox, RhfTextCombobox; 0.22: RhfTimeInput,
14
14
  // RhfDateRangePicker, RhfToggleGroup, RhfIbanInput, RhfPhoneInput; 0.23: RhfCountrySelect,
15
- // RhfMonthPicker)
15
+ // RhfMonthPicker; 0.24: RhfInlineEntityCombobox)
16
16
  // and RhfField, the shell they are built on.
17
17
  export * from "./rhf/fields";
18
18
  // LineItems over useFieldArray.