@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.
- package/README.md +6 -6
- package/dist/chart.d.ts +2 -4
- package/dist/components/amount-input.d.ts +16 -9
- package/dist/components/amount-input.js +13 -11
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +5 -0
- package/dist/components/autocomplete.js +39 -37
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +2 -4
- package/dist/components/calculator.d.ts +2 -4
- package/dist/components/column-mapper.d.ts +62 -9
- package/dist/components/column-mapper.js +85 -49
- package/dist/components/column-mapper.js.map +1 -1
- package/dist/components/combobox.d.ts +33 -1
- package/dist/components/combobox.js +8 -2
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +2 -4
- package/dist/components/copy-button.d.ts +2 -4
- package/dist/components/danger-confirm.d.ts +2 -4
- package/dist/components/danger-confirm.js +52 -5
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +13 -1
- package/dist/components/entity-combobox.js +13 -4
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +2 -4
- package/dist/components/file-button.d.ts +2 -4
- package/dist/components/file-dropzone.d.ts +2 -4
- package/dist/components/form-actions.d.ts +1 -3
- package/dist/components/form-actions.js +15 -1
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +2 -4
- package/dist/components/language-select.d.ts +2 -4
- package/dist/components/money-field.d.ts +2 -4
- package/dist/components/multi-entity-combobox.d.ts +5 -1
- package/dist/components/multi-entity-combobox.js +11 -3
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/number-field.d.ts +2 -4
- package/dist/components/number-input.d.ts +2 -4
- package/dist/components/numpad-sheet.d.ts +2 -4
- package/dist/components/phone-input.d.ts +2 -4
- package/dist/components/series-chart.d.ts +2 -4
- package/dist/components/settings-fields.d.ts +2 -4
- package/dist/components/share-card.d.ts +1 -3
- package/dist/components/text-link.d.ts +1 -3
- package/dist/components/time-input.d.ts +2 -4
- package/dist/components/ui.d.ts +2 -4
- package/dist/components/ui.js +55 -17
- package/dist/components/ui.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +66 -1
- package/dist/feedback/feedback-attachment.js +32 -12
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +66 -1
- package/dist/feedback/feedback-inbox.d.ts +67 -253
- package/dist/feedback/feedback-thread.d.ts +1 -3
- package/dist/feedback/feedback-thread.js +8 -2
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{feedback-BxeQVzwq.d.ts → feedback-DOwPu-Il.d.ts} +878 -18
- package/dist/feedback.d.ts +2 -4
- package/dist/hooks/use-file-drop.d.ts +2 -4
- package/dist/i18n/defaults.d.ts +2 -4
- package/dist/i18n/german.d.ts +2 -4
- package/dist/i18n/kit-labels.d.ts +1 -3
- package/dist/i18n/languages.d.ts +2 -4
- package/dist/i18n/locales/de-CH.d.ts +2 -4
- package/dist/i18n/locales/en.d.ts +2 -4
- package/dist/i18n/locales/es.d.ts +2 -4
- package/dist/i18n/locales/fr.d.ts +2 -4
- package/dist/i18n/locales/hu.d.ts +2 -4
- package/dist/i18n/locales/it.d.ts +2 -4
- package/dist/i18n/locales/zh.d.ts +2 -4
- package/dist/i18n/review.d.ts +2 -4
- package/dist/index.d.ts +2 -4
- package/dist/lib/strip-fade.d.ts +2 -2
- package/dist/lib/strip-fade.js.map +1 -1
- package/dist/rhf/fields.d.ts +59 -8
- package/dist/rhf/fields.js +61 -3
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +2 -4
- package/dist/rhf.d.ts +3 -5
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +1 -3
- package/dist/shell/top-bar-brand.d.ts +2 -4
- package/dist/shell.d.ts +1 -3
- package/dist/wizard/stepper-nav.d.ts +2 -4
- package/dist/wizard.d.ts +2 -4
- package/package.json +2 -2
- package/src/components/amount-input.tsx +37 -20
- package/src/components/autocomplete.tsx +16 -5
- package/src/components/column-mapper.tsx +181 -79
- package/src/components/combobox.tsx +41 -3
- package/src/components/danger-confirm.tsx +193 -22
- package/src/components/entity-combobox.tsx +28 -5
- package/src/components/form-actions.tsx +20 -1
- package/src/components/multi-entity-combobox.tsx +16 -3
- package/src/components/ui.tsx +107 -0
- package/src/feedback/feedback-attachment.tsx +65 -12
- package/src/feedback/feedback-thread.tsx +66 -6
- package/src/lib/strip-fade.ts +2 -2
- package/src/rhf/fields.tsx +167 -6
- package/src/rhf.ts +1 -1
- 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
|
-
|
|
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=
|
|
477
|
+
variant={buttonVariant}
|
|
478
|
+
size={buttonSize}
|
|
443
479
|
disabled={disabled}
|
|
444
480
|
onClick={() => fileInputRef.current?.click()}
|
|
445
481
|
>
|
|
446
|
-
<Paperclip className=
|
|
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("
|
|
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=
|
|
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=
|
|
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("
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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 ||
|
|
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
|
-
{
|
|
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;
|
package/src/lib/strip-fade.ts
CHANGED
|
@@ -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
|
|
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.
|
package/src/rhf/fields.tsx
CHANGED
|
@@ -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 {
|
|
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
|
|
1809
|
-
|
|
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
|
|
1970
|
-
// own `error` prop, which it merges
|
|
1971
|
-
|
|
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.
|