@inui/design-system 0.0.0-stage → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +360 -2
- package/dist/components/ui/accordion.d.ts +44 -0
- package/dist/components/ui/accordion.d.ts.map +1 -0
- package/dist/components/ui/accordion.js +86 -0
- package/dist/components/ui/accordion.js.map +1 -0
- package/dist/components/ui/ai-avatar.d.ts +46 -0
- package/dist/components/ui/ai-avatar.d.ts.map +1 -0
- package/dist/components/ui/ai-avatar.js +43 -0
- package/dist/components/ui/ai-avatar.js.map +1 -0
- package/dist/components/ui/ai-label.d.ts +118 -0
- package/dist/components/ui/ai-label.d.ts.map +1 -0
- package/dist/components/ui/ai-label.js +184 -0
- package/dist/components/ui/ai-label.js.map +1 -0
- package/dist/components/ui/ai-sparkle.d.ts +91 -0
- package/dist/components/ui/ai-sparkle.d.ts.map +1 -0
- package/dist/components/ui/ai-sparkle.js +200 -0
- package/dist/components/ui/ai-sparkle.js.map +1 -0
- package/dist/components/ui/alert-dialog.d.ts +79 -0
- package/dist/components/ui/alert-dialog.d.ts.map +1 -0
- package/dist/components/ui/alert-dialog.js +147 -0
- package/dist/components/ui/alert-dialog.js.map +1 -0
- package/dist/components/ui/alert.d.ts +71 -0
- package/dist/components/ui/alert.d.ts.map +1 -0
- package/dist/components/ui/alert.js +108 -0
- package/dist/components/ui/alert.js.map +1 -0
- package/dist/components/ui/aspect-ratio.d.ts +30 -0
- package/dist/components/ui/aspect-ratio.d.ts.map +1 -0
- package/dist/components/ui/aspect-ratio.js +40 -0
- package/dist/components/ui/aspect-ratio.js.map +1 -0
- package/dist/components/ui/attachment.d.ts +74 -0
- package/dist/components/ui/attachment.d.ts.map +1 -0
- package/dist/components/ui/attachment.js +142 -0
- package/dist/components/ui/attachment.js.map +1 -0
- package/dist/components/ui/avatar.d.ts +82 -0
- package/dist/components/ui/avatar.d.ts.map +1 -0
- package/dist/components/ui/avatar.js +123 -0
- package/dist/components/ui/avatar.js.map +1 -0
- package/dist/components/ui/badge.d.ts +130 -0
- package/dist/components/ui/badge.d.ts.map +1 -0
- package/dist/components/ui/badge.js +226 -0
- package/dist/components/ui/badge.js.map +1 -0
- package/dist/components/ui/breadcrumb.d.ts +43 -0
- package/dist/components/ui/breadcrumb.d.ts.map +1 -0
- package/dist/components/ui/breadcrumb.js +98 -0
- package/dist/components/ui/breadcrumb.js.map +1 -0
- package/dist/components/ui/browser-tabs.d.ts +218 -0
- package/dist/components/ui/browser-tabs.d.ts.map +1 -0
- package/dist/components/ui/browser-tabs.js +528 -0
- package/dist/components/ui/browser-tabs.js.map +1 -0
- package/dist/components/ui/bubble.d.ts +80 -0
- package/dist/components/ui/bubble.d.ts.map +1 -0
- package/dist/components/ui/bubble.js +235 -0
- package/dist/components/ui/bubble.js.map +1 -0
- package/dist/components/ui/button-group.d.ts +60 -0
- package/dist/components/ui/button-group.d.ts.map +1 -0
- package/dist/components/ui/button-group.js +89 -0
- package/dist/components/ui/button-group.js.map +1 -0
- package/dist/components/ui/button.d.ts +157 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/button.js +144 -0
- package/dist/components/ui/button.js.map +1 -0
- package/dist/components/ui/calendar.d.ts +57 -0
- package/dist/components/ui/calendar.d.ts.map +1 -0
- package/dist/components/ui/calendar.js +163 -0
- package/dist/components/ui/calendar.js.map +1 -0
- package/dist/components/ui/card.d.ts +96 -0
- package/dist/components/ui/card.d.ts.map +1 -0
- package/dist/components/ui/card.js +131 -0
- package/dist/components/ui/card.js.map +1 -0
- package/dist/components/ui/carousel.d.ts +86 -0
- package/dist/components/ui/carousel.d.ts.map +1 -0
- package/dist/components/ui/carousel.js +207 -0
- package/dist/components/ui/carousel.js.map +1 -0
- package/dist/components/ui/checkbox.d.ts +54 -0
- package/dist/components/ui/checkbox.d.ts.map +1 -0
- package/dist/components/ui/checkbox.js +78 -0
- package/dist/components/ui/checkbox.js.map +1 -0
- package/dist/components/ui/collapsible.d.ts +45 -0
- package/dist/components/ui/collapsible.d.ts.map +1 -0
- package/dist/components/ui/collapsible.js +65 -0
- package/dist/components/ui/collapsible.js.map +1 -0
- package/dist/components/ui/combobox.d.ts +111 -0
- package/dist/components/ui/combobox.d.ts.map +1 -0
- package/dist/components/ui/combobox.js +264 -0
- package/dist/components/ui/combobox.js.map +1 -0
- package/dist/components/ui/command.d.ts +71 -0
- package/dist/components/ui/command.d.ts.map +1 -0
- package/dist/components/ui/command.js +133 -0
- package/dist/components/ui/command.js.map +1 -0
- package/dist/components/ui/context-menu.d.ts +70 -0
- package/dist/components/ui/context-menu.d.ts.map +1 -0
- package/dist/components/ui/context-menu.js +163 -0
- package/dist/components/ui/context-menu.js.map +1 -0
- package/dist/components/ui/count-badge.d.ts +40 -0
- package/dist/components/ui/count-badge.d.ts.map +1 -0
- package/dist/components/ui/count-badge.js +43 -0
- package/dist/components/ui/count-badge.js.map +1 -0
- package/dist/components/ui/data-table.d.ts +1026 -0
- package/dist/components/ui/data-table.d.ts.map +1 -0
- package/dist/components/ui/data-table.js +3423 -0
- package/dist/components/ui/data-table.js.map +1 -0
- package/dist/components/ui/dialog.d.ts +73 -0
- package/dist/components/ui/dialog.d.ts.map +1 -0
- package/dist/components/ui/dialog.js +145 -0
- package/dist/components/ui/dialog.js.map +1 -0
- package/dist/components/ui/direction.d.ts +35 -0
- package/dist/components/ui/direction.d.ts.map +1 -0
- package/dist/components/ui/direction.js +35 -0
- package/dist/components/ui/direction.js.map +1 -0
- package/dist/components/ui/drawer.d.ts +53 -0
- package/dist/components/ui/drawer.d.ts.map +1 -0
- package/dist/components/ui/drawer.js +115 -0
- package/dist/components/ui/drawer.js.map +1 -0
- package/dist/components/ui/dropdown-menu.d.ts +75 -0
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
- package/dist/components/ui/dropdown-menu.js +173 -0
- package/dist/components/ui/dropdown-menu.js.map +1 -0
- package/dist/components/ui/dropzone.d.ts +141 -0
- package/dist/components/ui/dropzone.d.ts.map +1 -0
- package/dist/components/ui/dropzone.js +257 -0
- package/dist/components/ui/dropzone.js.map +1 -0
- package/dist/components/ui/empty.d.ts +94 -0
- package/dist/components/ui/empty.d.ts.map +1 -0
- package/dist/components/ui/empty.js +149 -0
- package/dist/components/ui/empty.js.map +1 -0
- package/dist/components/ui/field.d.ts +122 -0
- package/dist/components/ui/field.d.ts.map +1 -0
- package/dist/components/ui/field.js +254 -0
- package/dist/components/ui/field.js.map +1 -0
- package/dist/components/ui/hover-card.d.ts +51 -0
- package/dist/components/ui/hover-card.d.ts.map +1 -0
- package/dist/components/ui/hover-card.js +101 -0
- package/dist/components/ui/hover-card.js.map +1 -0
- package/dist/components/ui/input-group.d.ts +62 -0
- package/dist/components/ui/input-group.d.ts.map +1 -0
- package/dist/components/ui/input-group.js +115 -0
- package/dist/components/ui/input-group.js.map +1 -0
- package/dist/components/ui/input-otp.d.ts +52 -0
- package/dist/components/ui/input-otp.d.ts.map +1 -0
- package/dist/components/ui/input-otp.js +89 -0
- package/dist/components/ui/input-otp.js.map +1 -0
- package/dist/components/ui/input.d.ts +47 -0
- package/dist/components/ui/input.d.ts.map +1 -0
- package/dist/components/ui/input.js +47 -0
- package/dist/components/ui/input.js.map +1 -0
- package/dist/components/ui/item.d.ts +74 -0
- package/dist/components/ui/item.d.ts.map +1 -0
- package/dist/components/ui/item.js +176 -0
- package/dist/components/ui/item.js.map +1 -0
- package/dist/components/ui/kbd.d.ts +41 -0
- package/dist/components/ui/kbd.d.ts.map +1 -0
- package/dist/components/ui/kbd.js +58 -0
- package/dist/components/ui/kbd.js.map +1 -0
- package/dist/components/ui/key-value-pair.d.ts +56 -0
- package/dist/components/ui/key-value-pair.d.ts.map +1 -0
- package/dist/components/ui/key-value-pair.js +110 -0
- package/dist/components/ui/key-value-pair.js.map +1 -0
- package/dist/components/ui/label.d.ts +83 -0
- package/dist/components/ui/label.d.ts.map +1 -0
- package/dist/components/ui/label.js +85 -0
- package/dist/components/ui/label.js.map +1 -0
- package/dist/components/ui/marker.d.ts +54 -0
- package/dist/components/ui/marker.d.ts.map +1 -0
- package/dist/components/ui/marker.js +79 -0
- package/dist/components/ui/marker.js.map +1 -0
- package/dist/components/ui/menubar.d.ts +72 -0
- package/dist/components/ui/menubar.d.ts.map +1 -0
- package/dist/components/ui/menubar.js +174 -0
- package/dist/components/ui/menubar.js.map +1 -0
- package/dist/components/ui/message-scroller.d.ts +53 -0
- package/dist/components/ui/message-scroller.d.ts.map +1 -0
- package/dist/components/ui/message-scroller.js +95 -0
- package/dist/components/ui/message-scroller.js.map +1 -0
- package/dist/components/ui/message.d.ts +63 -0
- package/dist/components/ui/message.d.ts.map +1 -0
- package/dist/components/ui/message.js +92 -0
- package/dist/components/ui/message.js.map +1 -0
- package/dist/components/ui/meta-list.d.ts +55 -0
- package/dist/components/ui/meta-list.d.ts.map +1 -0
- package/dist/components/ui/meta-list.js +75 -0
- package/dist/components/ui/meta-list.js.map +1 -0
- package/dist/components/ui/native-select.d.ts +51 -0
- package/dist/components/ui/native-select.d.ts.map +1 -0
- package/dist/components/ui/native-select.js +75 -0
- package/dist/components/ui/native-select.js.map +1 -0
- package/dist/components/ui/navigation-menu.d.ts +56 -0
- package/dist/components/ui/navigation-menu.d.ts.map +1 -0
- package/dist/components/ui/navigation-menu.js +116 -0
- package/dist/components/ui/navigation-menu.js.map +1 -0
- package/dist/components/ui/page-header.d.ts +223 -0
- package/dist/components/ui/page-header.d.ts.map +1 -0
- package/dist/components/ui/page-header.js +233 -0
- package/dist/components/ui/page-header.js.map +1 -0
- package/dist/components/ui/pagination.d.ts +68 -0
- package/dist/components/ui/pagination.d.ts.map +1 -0
- package/dist/components/ui/pagination.js +145 -0
- package/dist/components/ui/pagination.js.map +1 -0
- package/dist/components/ui/popover.d.ts +61 -0
- package/dist/components/ui/popover.d.ts.map +1 -0
- package/dist/components/ui/popover.js +144 -0
- package/dist/components/ui/popover.js.map +1 -0
- package/dist/components/ui/progress.d.ts +35 -0
- package/dist/components/ui/progress.d.ts.map +1 -0
- package/dist/components/ui/progress.js +46 -0
- package/dist/components/ui/progress.js.map +1 -0
- package/dist/components/ui/questionnaire.d.ts +92 -0
- package/dist/components/ui/questionnaire.d.ts.map +1 -0
- package/dist/components/ui/questionnaire.js +277 -0
- package/dist/components/ui/questionnaire.js.map +1 -0
- package/dist/components/ui/radio-group.d.ts +50 -0
- package/dist/components/ui/radio-group.d.ts.map +1 -0
- package/dist/components/ui/radio-group.js +65 -0
- package/dist/components/ui/radio-group.js.map +1 -0
- package/dist/components/ui/resizable.d.ts +44 -0
- package/dist/components/ui/resizable.d.ts.map +1 -0
- package/dist/components/ui/resizable.js +73 -0
- package/dist/components/ui/resizable.js.map +1 -0
- package/dist/components/ui/scroll-area.d.ts +55 -0
- package/dist/components/ui/scroll-area.d.ts.map +1 -0
- package/dist/components/ui/scroll-area.js +72 -0
- package/dist/components/ui/scroll-area.js.map +1 -0
- package/dist/components/ui/select.d.ts +274 -0
- package/dist/components/ui/select.d.ts.map +1 -0
- package/dist/components/ui/select.js +609 -0
- package/dist/components/ui/select.js.map +1 -0
- package/dist/components/ui/separator.d.ts +31 -0
- package/dist/components/ui/separator.d.ts.map +1 -0
- package/dist/components/ui/separator.js +38 -0
- package/dist/components/ui/separator.js.map +1 -0
- package/dist/components/ui/sheet.d.ts +59 -0
- package/dist/components/ui/sheet.d.ts.map +1 -0
- package/dist/components/ui/sheet.js +128 -0
- package/dist/components/ui/sheet.js.map +1 -0
- package/dist/components/ui/sidebar.d.ts +166 -0
- package/dist/components/ui/sidebar.d.ts.map +1 -0
- package/dist/components/ui/sidebar.js +512 -0
- package/dist/components/ui/sidebar.js.map +1 -0
- package/dist/components/ui/skeleton.d.ts +35 -0
- package/dist/components/ui/skeleton.d.ts.map +1 -0
- package/dist/components/ui/skeleton.js +46 -0
- package/dist/components/ui/skeleton.js.map +1 -0
- package/dist/components/ui/slider.d.ts +39 -0
- package/dist/components/ui/slider.d.ts.map +1 -0
- package/dist/components/ui/slider.js +75 -0
- package/dist/components/ui/slider.js.map +1 -0
- package/dist/components/ui/sonner.d.ts +56 -0
- package/dist/components/ui/sonner.d.ts.map +1 -0
- package/dist/components/ui/sonner.js +96 -0
- package/dist/components/ui/sonner.js.map +1 -0
- package/dist/components/ui/spinner.d.ts +37 -0
- package/dist/components/ui/spinner.d.ts.map +1 -0
- package/dist/components/ui/spinner.js +51 -0
- package/dist/components/ui/spinner.js.map +1 -0
- package/dist/components/ui/stepper.d.ts +84 -0
- package/dist/components/ui/stepper.d.ts.map +1 -0
- package/dist/components/ui/stepper.js +145 -0
- package/dist/components/ui/stepper.js.map +1 -0
- package/dist/components/ui/switch.d.ts +36 -0
- package/dist/components/ui/switch.d.ts.map +1 -0
- package/dist/components/ui/switch.js +44 -0
- package/dist/components/ui/switch.js.map +1 -0
- package/dist/components/ui/table.d.ts +80 -0
- package/dist/components/ui/table.d.ts.map +1 -0
- package/dist/components/ui/table.js +130 -0
- package/dist/components/ui/table.js.map +1 -0
- package/dist/components/ui/tabs.d.ts +87 -0
- package/dist/components/ui/tabs.d.ts.map +1 -0
- package/dist/components/ui/tabs.js +128 -0
- package/dist/components/ui/tabs.js.map +1 -0
- package/dist/components/ui/textarea.d.ts +35 -0
- package/dist/components/ui/textarea.d.ts.map +1 -0
- package/dist/components/ui/textarea.js +38 -0
- package/dist/components/ui/textarea.js.map +1 -0
- package/dist/components/ui/time-picker.d.ts +58 -0
- package/dist/components/ui/time-picker.d.ts.map +1 -0
- package/dist/components/ui/time-picker.js +188 -0
- package/dist/components/ui/time-picker.js.map +1 -0
- package/dist/components/ui/toggle-group.d.ts +52 -0
- package/dist/components/ui/toggle-group.d.ts.map +1 -0
- package/dist/components/ui/toggle-group.js +94 -0
- package/dist/components/ui/toggle-group.js.map +1 -0
- package/dist/components/ui/toggle.d.ts +50 -0
- package/dist/components/ui/toggle.d.ts.map +1 -0
- package/dist/components/ui/toggle.js +70 -0
- package/dist/components/ui/toggle.js.map +1 -0
- package/dist/components/ui/tooltip.d.ts +51 -0
- package/dist/components/ui/tooltip.d.ts.map +1 -0
- package/dist/components/ui/tooltip.js +78 -0
- package/dist/components/ui/tooltip.js.map +1 -0
- package/dist/hooks/use-dark-mode.d.ts +2 -0
- package/dist/hooks/use-dark-mode.d.ts.map +1 -0
- package/dist/hooks/use-dark-mode.js +36 -0
- package/dist/hooks/use-dark-mode.js.map +1 -0
- package/dist/hooks/use-mobile.d.ts +2 -0
- package/dist/hooks/use-mobile.d.ts.map +1 -0
- package/dist/hooks/use-mobile.js +20 -0
- package/dist/hooks/use-mobile.js.map +1 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +77 -0
- package/dist/lib/scope.d.ts +64 -0
- package/dist/lib/scope.d.ts.map +1 -0
- package/dist/lib/scope.js +74 -0
- package/dist/lib/scope.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +11 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/styles.css +2 -0
- package/dist/theme-tokens.css +537 -0
- package/dist/theme.css +417 -0
- package/package.json +103 -3
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
import { Separator } from "./separator.js";
|
|
4
|
+
import { Label, OptionalHint, RequiredDot } from "./label.js";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { useMemo } from "react";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { cva } from "class-variance-authority";
|
|
9
|
+
//#region src/components/ui/field.tsx
|
|
10
|
+
/**
|
|
11
|
+
* Field — form field wrapper (label + control + description + error).
|
|
12
|
+
*
|
|
13
|
+
* A composite family, not a single component: Field lays out one control
|
|
14
|
+
* with its label/description/error slots; FieldSet+FieldLegend group
|
|
15
|
+
* several Fields for assistive tech; FieldGroup stacks Fields together;
|
|
16
|
+
* FieldSeparator divides sections. Invalid state is driven by
|
|
17
|
+
* `data-invalid` on Field plus `aria-invalid` on the control.
|
|
18
|
+
*/
|
|
19
|
+
var FieldIdContext = React.createContext(null);
|
|
20
|
+
var FieldSetIdContext = React.createContext(null);
|
|
21
|
+
function FieldSet({ className, "aria-labelledby": ariaLabelledby, ...props }) {
|
|
22
|
+
const [legendId, setLegendId] = React.useState(null);
|
|
23
|
+
const idContext = React.useMemo(() => ({
|
|
24
|
+
legendId,
|
|
25
|
+
setLegendId
|
|
26
|
+
}), [legendId]);
|
|
27
|
+
return /* @__PURE__ */ jsx(FieldSetIdContext.Provider, {
|
|
28
|
+
value: idContext,
|
|
29
|
+
children: /* @__PURE__ */ jsx("fieldset", {
|
|
30
|
+
"data-slot": "field-set",
|
|
31
|
+
"aria-labelledby": ariaLabelledby ?? legendId ?? void 0,
|
|
32
|
+
className: cn("flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3 has-[>[data-slot=field-legend-row]]:!gap-0", className),
|
|
33
|
+
...props
|
|
34
|
+
})
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function FieldLegend({ className, id, variant = "legend", required = false, optional = false, children, aiLabel, aiLabelAlign = "inline", ...props }) {
|
|
38
|
+
const generatedId = React.useId();
|
|
39
|
+
const legendId = id ?? generatedId;
|
|
40
|
+
const idContext = React.useContext(FieldSetIdContext);
|
|
41
|
+
React.useLayoutEffect(() => {
|
|
42
|
+
idContext?.setLegendId(legendId);
|
|
43
|
+
return () => idContext?.setLegendId(null);
|
|
44
|
+
}, [legendId]);
|
|
45
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
46
|
+
"data-slot": "field-legend-row",
|
|
47
|
+
className: cn("group/field-legend flex h-6 w-full items-start gap-2", aiLabel && aiLabelAlign === "end" && "justify-between"),
|
|
48
|
+
children: [/* @__PURE__ */ jsxs("legend", {
|
|
49
|
+
id: legendId,
|
|
50
|
+
"data-slot": "field-legend",
|
|
51
|
+
"data-variant": variant,
|
|
52
|
+
"data-required": required || void 0,
|
|
53
|
+
"data-optional": optional && !required || void 0,
|
|
54
|
+
className: cn("font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base", className),
|
|
55
|
+
...props,
|
|
56
|
+
children: [children, required ? /* @__PURE__ */ jsx(RequiredDot, { className: "ml-1 align-top" }) : optional ? /* @__PURE__ */ jsx(OptionalHint, { className: "ml-1" }) : null]
|
|
57
|
+
}), aiLabel]
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function FieldGroup({ className, ...props }) {
|
|
61
|
+
return /* @__PURE__ */ jsx("div", {
|
|
62
|
+
"data-slot": "field-group",
|
|
63
|
+
className: cn("group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4", className),
|
|
64
|
+
...props
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
var fieldVariants = cva("group/field flex w-full gap-2 data-[invalid=true]:text-destructive", {
|
|
68
|
+
variants: { orientation: {
|
|
69
|
+
vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
|
|
70
|
+
horizontal: "flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
|
71
|
+
responsive: "flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px"
|
|
72
|
+
} },
|
|
73
|
+
defaultVariants: { orientation: "vertical" }
|
|
74
|
+
});
|
|
75
|
+
function Field({ className, orientation = "vertical", ...props }) {
|
|
76
|
+
const [titleId, setTitleId] = React.useState(null);
|
|
77
|
+
const [descriptionId, setDescriptionId] = React.useState(null);
|
|
78
|
+
const [errorId, setErrorId] = React.useState(null);
|
|
79
|
+
const idContext = React.useMemo(() => ({
|
|
80
|
+
titleId,
|
|
81
|
+
setTitleId,
|
|
82
|
+
descriptionId,
|
|
83
|
+
setDescriptionId,
|
|
84
|
+
errorId,
|
|
85
|
+
setErrorId
|
|
86
|
+
}), [
|
|
87
|
+
titleId,
|
|
88
|
+
descriptionId,
|
|
89
|
+
errorId
|
|
90
|
+
]);
|
|
91
|
+
return /* @__PURE__ */ jsx(FieldIdContext.Provider, {
|
|
92
|
+
value: idContext,
|
|
93
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
94
|
+
role: "group",
|
|
95
|
+
"data-slot": "field",
|
|
96
|
+
"data-orientation": orientation,
|
|
97
|
+
className: cn(fieldVariants({ orientation }), className),
|
|
98
|
+
...props
|
|
99
|
+
})
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function FieldContent({ className, ...props }) {
|
|
103
|
+
return /* @__PURE__ */ jsx("div", {
|
|
104
|
+
"data-slot": "field-content",
|
|
105
|
+
className: cn("group/field-content flex flex-1 flex-col gap-0.5 leading-snug", className),
|
|
106
|
+
...props
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function FieldLabel({ className, aiLabel, aiLabelAlign = "end", ...props }) {
|
|
110
|
+
const label = /* @__PURE__ */ jsx(Label, {
|
|
111
|
+
"data-slot": aiLabel ? void 0 : "field-label",
|
|
112
|
+
className: cn("group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:text-disabled-foreground has-data-[state=checked]:border-primary/30 has-data-[state=checked]:bg-primary/5 has-[>[data-slot=field]]:rounded-xl has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5 dark:has-data-[state=checked]:border-primary/20 dark:has-data-[state=checked]:bg-primary/10", "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col", !aiLabel && className),
|
|
113
|
+
...props
|
|
114
|
+
});
|
|
115
|
+
if (!aiLabel) return label;
|
|
116
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
117
|
+
"data-slot": "field-label",
|
|
118
|
+
className: cn("group/field-label flex h-6 w-full items-start gap-2 -mb-2", aiLabelAlign === "end" && "justify-between", className),
|
|
119
|
+
children: [label, aiLabel]
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function FieldTitle({ className, id, ...props }) {
|
|
123
|
+
const generatedId = React.useId();
|
|
124
|
+
const titleId = id ?? generatedId;
|
|
125
|
+
const idContext = React.useContext(FieldIdContext);
|
|
126
|
+
React.useLayoutEffect(() => {
|
|
127
|
+
idContext?.setTitleId(titleId);
|
|
128
|
+
return () => idContext?.setTitleId(null);
|
|
129
|
+
}, [titleId]);
|
|
130
|
+
return /* @__PURE__ */ jsx("div", {
|
|
131
|
+
"data-slot": "field-label",
|
|
132
|
+
id: titleId,
|
|
133
|
+
className: cn("flex w-fit items-center gap-2 text-sm font-medium group-data-[disabled=true]/field:text-disabled-foreground", className),
|
|
134
|
+
...props
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function FieldDescription({ className, id, ...props }) {
|
|
138
|
+
const generatedId = React.useId();
|
|
139
|
+
const descriptionId = id ?? generatedId;
|
|
140
|
+
const idContext = React.useContext(FieldIdContext);
|
|
141
|
+
React.useLayoutEffect(() => {
|
|
142
|
+
idContext?.setDescriptionId(descriptionId);
|
|
143
|
+
return () => idContext?.setDescriptionId(null);
|
|
144
|
+
}, [descriptionId]);
|
|
145
|
+
return /* @__PURE__ */ jsx("p", {
|
|
146
|
+
"data-slot": "field-description",
|
|
147
|
+
id: descriptionId,
|
|
148
|
+
className: cn("text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5", "last:mt-0 nth-last-2:-mt-1", "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary", className),
|
|
149
|
+
...props
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function FieldSeparator({ children, className, ...props }) {
|
|
153
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
154
|
+
"data-slot": "field-separator",
|
|
155
|
+
"data-content": !!children,
|
|
156
|
+
className: cn("relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2", className),
|
|
157
|
+
...props,
|
|
158
|
+
children: [/* @__PURE__ */ jsx(Separator, { className: "absolute inset-0 top-1/2" }), children && /* @__PURE__ */ jsx("span", {
|
|
159
|
+
className: "relative mx-auto block w-fit bg-background px-2 text-muted-foreground",
|
|
160
|
+
"data-slot": "field-separator-content",
|
|
161
|
+
children
|
|
162
|
+
})]
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function FieldError({ className, children, errors, id, ...props }) {
|
|
166
|
+
const generatedId = React.useId();
|
|
167
|
+
const errorId = id ?? generatedId;
|
|
168
|
+
const idContext = React.useContext(FieldIdContext);
|
|
169
|
+
const content = useMemo(() => {
|
|
170
|
+
if (children) return children;
|
|
171
|
+
if (!errors?.length) return null;
|
|
172
|
+
const uniqueErrors = [...new Map(errors.map((error) => [error?.message, error])).values()];
|
|
173
|
+
if (uniqueErrors?.length == 1) return uniqueErrors[0]?.message;
|
|
174
|
+
return /* @__PURE__ */ jsx("ul", {
|
|
175
|
+
className: "ml-4 flex list-disc flex-col gap-1",
|
|
176
|
+
children: uniqueErrors.map((error, index) => error?.message && /* @__PURE__ */ jsx("li", { children: error.message }, index))
|
|
177
|
+
});
|
|
178
|
+
}, [children, errors]);
|
|
179
|
+
React.useLayoutEffect(() => {
|
|
180
|
+
idContext?.setErrorId(content ? errorId : null);
|
|
181
|
+
return () => idContext?.setErrorId(null);
|
|
182
|
+
}, [errorId, !!content]);
|
|
183
|
+
if (!content) return null;
|
|
184
|
+
return /* @__PURE__ */ jsx("div", {
|
|
185
|
+
role: "alert",
|
|
186
|
+
"data-slot": "field-error",
|
|
187
|
+
id: errorId,
|
|
188
|
+
className: cn("text-sm font-normal text-destructive", className),
|
|
189
|
+
...props,
|
|
190
|
+
children: content
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* WHEN TO USE
|
|
195
|
+
* - Any labelled form field that needs help text or validation messaging — wraps any input primitive (Input, Textarea, Checkbox, ...)
|
|
196
|
+
* - `orientation="horizontal"` for label-beside-control rows (e.g. a Checkbox with title/description via FieldContent + FieldTitle)
|
|
197
|
+
* - FieldSet + FieldLegend to group related Fields for assistive tech; FieldGroup to stack them with consistent spacing
|
|
198
|
+
* - FieldSeparator to divide sections within a FieldGroup, optionally with a label (e.g. "OR")
|
|
199
|
+
*
|
|
200
|
+
* NOT WHEN
|
|
201
|
+
* - A bare label with no description or error text — use Label directly, it's lighter
|
|
202
|
+
* - Display-only key/value pairs with no input — this is a form-control wrapper, not a data display
|
|
203
|
+
*
|
|
204
|
+
* KEY RULES
|
|
205
|
+
* - orientation: vertical (default), horizontal, responsive (stacks on narrow, horizontal at @md container width)
|
|
206
|
+
* - Required/optional are marked on the label, nowhere else: `<FieldLabel required>` (red dot) or `<FieldLabel optional>` (muted "(optional)"), and the same two props on FieldLegend for a whole group. Visual only — the control still carries `required`/`aria-required` for assistive tech. Pick one convention per form: mark the required fields or the optional ones, not both.
|
|
207
|
+
* - Mark invalid state with `data-invalid` on Field AND `aria-invalid` on the control — Field's red text styling depends on `data-invalid`, the control's border/ring styling depends on `aria-invalid`
|
|
208
|
+
* - FieldError accepts either `children` (a fixed message) or an `errors` array of `{ message }` objects (react-hook-form shape) — duplicates are deduped by message, renders a single line for one error or a bulleted list for multiple; renders nothing if there's no content
|
|
209
|
+
* - FieldDescription/FieldError generate an id and register it on Field's context; Checkbox reads that context and defaults its own `aria-describedby`/`aria-labelledby` (to FieldTitle's id) whenever the consumer hasn't already set one — for every other control, still give each element an explicit `id` and set the control's `aria-describedby` to match (see USAGE below), or a screen reader user who tabs to the control never hears the hint/error
|
|
210
|
+
* - FieldLabel wraps Label — use it (not FieldTitle) when the row has an actual form control; use FieldTitle for the horizontal FieldContent pattern where the "label" is really a heading beside a standalone control. FieldTitle is a plain heading-styled `<div>`, not a `<label>` — for controls other than Checkbox, give the control its own `aria-label`/`aria-labelledby` matching FieldTitle's text (see USAGE below) or it ships with no accessible name at all.
|
|
211
|
+
*
|
|
212
|
+
* USAGE
|
|
213
|
+
* <Field>
|
|
214
|
+
* <FieldLabel htmlFor="email">Email</FieldLabel>
|
|
215
|
+
* <Input id="email" type="email" aria-describedby="email-hint" />
|
|
216
|
+
* <FieldDescription id="email-hint">We'll never share your address.</FieldDescription>
|
|
217
|
+
* </Field>
|
|
218
|
+
*
|
|
219
|
+
* // With validation — aria-describedby lists both ids so an invalid field
|
|
220
|
+
* // still announces its hint alongside the error, not just one or the other
|
|
221
|
+
* <Field data-invalid={!!error}>
|
|
222
|
+
* <FieldLabel htmlFor="email">Email</FieldLabel>
|
|
223
|
+
* <Input id="email" type="email" aria-invalid={!!error} aria-describedby="email-hint email-error" />
|
|
224
|
+
* <FieldDescription id="email-hint">We'll never share your address.</FieldDescription>
|
|
225
|
+
* <FieldError id="email-error">{error}</FieldError>
|
|
226
|
+
* </Field>
|
|
227
|
+
*
|
|
228
|
+
* <Field data-invalid orientation="horizontal">
|
|
229
|
+
* <FieldContent>
|
|
230
|
+
* <FieldTitle>Marketing emails</FieldTitle>
|
|
231
|
+
* <FieldDescription>Receive product news and updates.</FieldDescription>
|
|
232
|
+
* </FieldContent>
|
|
233
|
+
* <Checkbox />
|
|
234
|
+
* </Field>
|
|
235
|
+
* // Checkbox picks up FieldTitle as its aria-labelledby and
|
|
236
|
+
* // FieldDescription/FieldError as its aria-describedby automatically —
|
|
237
|
+
* // pass an explicit aria-label/aria-labelledby/aria-describedby yourself
|
|
238
|
+
* // only to override that default. Any other control in this position
|
|
239
|
+
* // (there's no `<label>` wrapping it) still needs its own aria-label/
|
|
240
|
+
* // aria-labelledby matching FieldTitle's text.
|
|
241
|
+
*
|
|
242
|
+
* <FieldSet>
|
|
243
|
+
* <FieldLegend>Project details</FieldLegend>
|
|
244
|
+
* <FieldGroup>
|
|
245
|
+
* <Field>...</Field>
|
|
246
|
+
* <FieldSeparator />
|
|
247
|
+
* <Field>...</Field>
|
|
248
|
+
* </FieldGroup>
|
|
249
|
+
* </FieldSet>
|
|
250
|
+
*/
|
|
251
|
+
//#endregion
|
|
252
|
+
export { Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldIdContext, FieldLabel, FieldLegend, FieldSeparator, FieldSet, FieldTitle };
|
|
253
|
+
|
|
254
|
+
//# sourceMappingURL=field.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field.js","names":[],"sources":["../../../src/components/ui/field.tsx"],"sourcesContent":["\"use client\"\n\n/**\n * Field — form field wrapper (label + control + description + error).\n *\n * A composite family, not a single component: Field lays out one control\n * with its label/description/error slots; FieldSet+FieldLegend group\n * several Fields for assistive tech; FieldGroup stacks Fields together;\n * FieldSeparator divides sections. Invalid state is driven by\n * `data-invalid` on Field plus `aria-invalid` on the control.\n */\n\nimport * as React from \"react\"\nimport { useMemo } from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Label, OptionalHint, RequiredDot } from \"@/components/ui/label\"\nimport { Separator } from \"@/components/ui/separator\"\n\n// FieldTitle (unlike FieldLabel) is a plain heading `<div>`, not a `<label>`,\n// for the horizontal FieldContent pattern where the control is a standalone\n// sibling rather than something a `<label>` wraps — so it carries no\n// `htmlFor`/id wiring of its own. Without this context, that documented\n// pattern shipped controls (Checkbox, ...) with no accessible name at all.\n// Consumer-supplied `aria-label`/`aria-labelledby`/`aria-describedby` on the\n// control always wins; this only fills the gap when none was given.\nconst FieldIdContext = React.createContext<{\n titleId: string | null\n setTitleId: (id: string | null) => void\n descriptionId: string | null\n setDescriptionId: (id: string | null) => void\n errorId: string | null\n setErrorId: (id: string | null) => void\n} | null>(null)\n\n// FieldLegend now always renders `<legend>` inside a row `<div>` (so it can\n// sit next to an optional `aiLabel` at a fixed height — see FieldLegend\n// below), which means `<legend>` is no longer FieldSet's literal direct\n// child. That breaks the native fieldset/legend accessible-name link (the\n// browser only picks up `<legend>` as the fieldset's name when it's a\n// direct child), so FieldSet has to restore it explicitly via\n// `aria-labelledby` — this context is how FieldLegend's id reaches it.\nconst FieldSetIdContext = React.createContext<{\n legendId: string | null\n setLegendId: (id: string | null) => void\n} | null>(null)\n\nfunction FieldSet({\n className,\n \"aria-labelledby\": ariaLabelledby,\n ...props\n}: React.ComponentProps<\"fieldset\">) {\n const [legendId, setLegendId] = React.useState<string | null>(null)\n const idContext = React.useMemo(\n () => ({ legendId, setLegendId }),\n [legendId]\n )\n\n return (\n <FieldSetIdContext.Provider value={idContext}>\n <fieldset\n data-slot=\"field-set\"\n aria-labelledby={ariaLabelledby ?? legendId ?? undefined}\n className={cn(\n // `field-legend-row` (FieldLegend's own wrapper) forces gap to 0:\n // FieldLegend's row is a fixed h-6 (24px) and that height is the\n // whole label-to-control gap by design, so FieldSet must not\n // stack its own gap on top. `!` because this has to win over the\n // checkbox-group/radio-group gap-3 rules below when a FieldSet\n // has both a legend row AND one of those as its two children —\n // same `:has()` specificity, so source order alone can't be\n // trusted (see RadioGroup's AI story, which hit exactly this).\n \"flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3 has-[>[data-slot=field-legend-row]]:!gap-0\",\n className\n )}\n {...props}\n />\n </FieldSetIdContext.Provider>\n )\n}\n\nfunction FieldLegend({\n className,\n id,\n variant = \"legend\",\n required = false,\n optional = false,\n children,\n aiLabel,\n aiLabelAlign = \"inline\",\n ...props\n}: React.ComponentProps<\"legend\"> & {\n variant?: \"legend\" | \"label\"\n /** Red dot after the legend — the group (checkboxes, radios) is mandatory. Visual only: put `required`/`aria-required` on the group's control(s) too. */\n required?: boolean\n /** Muted \"(optional)\" after the legend. Mutually exclusive with `required`. */\n optional?: boolean\n /** An `AILabel` rendered in the same fixed-height row as the legend — a sibling, never nested inside `<legend>` (AILabelTrigger is a button; a button inside a legend/label is invalid HTML). */\n aiLabel?: React.ReactNode\n /** \"inline\" (default) places `aiLabel` immediately after the legend text — the usual placement for a per-group AI marker (Checkbox/RadioGroup's own AI-flavored stories). \"end\" pushes it to the row's far end instead. */\n aiLabelAlign?: \"inline\" | \"end\"\n}) {\n const generatedId = React.useId()\n const legendId = id ?? generatedId\n const idContext = React.useContext(FieldSetIdContext)\n\n React.useLayoutEffect(() => {\n idContext?.setLegendId(legendId)\n return () => idContext?.setLegendId(null)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [legendId])\n\n return (\n <div\n data-slot=\"field-legend-row\"\n className={cn(\n \"group/field-legend flex h-6 w-full items-start gap-2\",\n aiLabel && aiLabelAlign === \"end\" && \"justify-between\"\n )}\n >\n <legend\n id={legendId}\n data-slot=\"field-legend\"\n data-variant={variant}\n data-required={required || undefined}\n data-optional={(optional && !required) || undefined}\n className={cn(\n \"font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base\",\n className\n )}\n {...props}\n >\n {children}\n {required ? (\n <RequiredDot className=\"ml-1 align-top\" />\n ) : optional ? (\n <OptionalHint className=\"ml-1\" />\n ) : null}\n </legend>\n {aiLabel}\n </div>\n )\n}\n\nfunction FieldGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"field-group\"\n className={cn(\n \"group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nconst fieldVariants = cva(\n \"group/field flex w-full gap-2 data-[invalid=true]:text-destructive\",\n {\n variants: {\n orientation: {\n vertical: \"flex-col *:w-full [&>.sr-only]:w-auto\",\n horizontal:\n \"flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px\",\n responsive:\n \"flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px\",\n },\n },\n defaultVariants: {\n orientation: \"vertical\",\n },\n }\n)\n\nfunction Field({\n className,\n orientation = \"vertical\",\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof fieldVariants>) {\n const [titleId, setTitleId] = React.useState<string | null>(null)\n const [descriptionId, setDescriptionId] = React.useState<string | null>(\n null\n )\n const [errorId, setErrorId] = React.useState<string | null>(null)\n const idContext = React.useMemo(\n () => ({\n titleId,\n setTitleId,\n descriptionId,\n setDescriptionId,\n errorId,\n setErrorId,\n }),\n [titleId, descriptionId, errorId]\n )\n\n return (\n <FieldIdContext.Provider value={idContext}>\n <div\n role=\"group\"\n data-slot=\"field\"\n data-orientation={orientation}\n className={cn(fieldVariants({ orientation }), className)}\n {...props}\n />\n </FieldIdContext.Provider>\n )\n}\n\nfunction FieldContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"field-content\"\n className={cn(\n \"group/field-content flex flex-1 flex-col gap-0.5 leading-snug\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction FieldLabel({\n className,\n aiLabel,\n aiLabelAlign = \"end\",\n ...props\n}: React.ComponentProps<typeof Label> & {\n /** An `AILabel` rendered in the same row as the label — a sibling, never\n * nested inside `<label>` (AILabelTrigger is a button; a button inside a\n * label is invalid HTML). Passing this switches FieldLabel to a fixed\n * `h-6` (24px) row, so a grid mixing AI and non-AI fields still aligns —\n * plain FieldLabel (no `aiLabel`) keeps its normal auto height, since\n * forcing every label to a fixed row would also force the unrelated\n * \"FieldLabel wrapping a nested Field\" option-card pattern (see\n * `has-[>[data-slot=field]]` below) into that same fixed height, which\n * needs to stay auto/flex-col instead. */\n aiLabel?: React.ReactNode\n /** \"end\" (default) pushes `aiLabel` to the row's far end — Input/Textarea/Select's own AI-flavored stories. \"inline\" places it immediately after the label text instead. */\n aiLabelAlign?: \"inline\" | \"end\"\n}) {\n const label = (\n <Label\n data-slot={aiLabel ? undefined : \"field-label\"}\n className={cn(\n \"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:text-disabled-foreground has-data-[state=checked]:border-primary/30 has-data-[state=checked]:bg-primary/5 has-[>[data-slot=field]]:rounded-xl has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5 dark:has-data-[state=checked]:border-primary/20 dark:has-data-[state=checked]:bg-primary/10\",\n \"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col\",\n !aiLabel && className\n )}\n {...props}\n />\n )\n\n if (!aiLabel) {\n return label\n }\n\n return (\n <div\n data-slot=\"field-label\"\n className={cn(\n // `-mb-2` cancels Field's own `gap-2` after this row, so the whole\n // gap to the control is this row's own height (24px) — one source\n // of spacing, not two stacked ones. Field's `gap-2` itself stays\n // untouched (it still spaces the control from FieldDescription/\n // FieldError below), so nothing else using Field regresses.\n \"group/field-label flex h-6 w-full items-start gap-2 -mb-2\",\n aiLabelAlign === \"end\" && \"justify-between\",\n className\n )}\n >\n {label}\n {aiLabel}\n </div>\n )\n}\n\nfunction FieldTitle({\n className,\n id,\n ...props\n}: React.ComponentProps<\"div\">) {\n const generatedId = React.useId()\n const titleId = id ?? generatedId\n const idContext = React.useContext(FieldIdContext)\n\n React.useLayoutEffect(() => {\n idContext?.setTitleId(titleId)\n return () => idContext?.setTitleId(null)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [titleId])\n\n return (\n <div\n data-slot=\"field-label\"\n id={titleId}\n className={cn(\n \"flex w-fit items-center gap-2 text-sm font-medium group-data-[disabled=true]/field:text-disabled-foreground\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction FieldDescription({\n className,\n id,\n ...props\n}: React.ComponentProps<\"p\">) {\n const generatedId = React.useId()\n const descriptionId = id ?? generatedId\n const idContext = React.useContext(FieldIdContext)\n\n React.useLayoutEffect(() => {\n idContext?.setDescriptionId(descriptionId)\n return () => idContext?.setDescriptionId(null)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [descriptionId])\n\n return (\n <p\n data-slot=\"field-description\"\n id={descriptionId}\n className={cn(\n \"text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5\",\n \"last:mt-0 nth-last-2:-mt-1\",\n \"[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction FieldSeparator({\n children,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n children?: React.ReactNode\n}) {\n return (\n <div\n data-slot=\"field-separator\"\n data-content={!!children}\n className={cn(\n \"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2\",\n className\n )}\n {...props}\n >\n <Separator className=\"absolute inset-0 top-1/2\" />\n {children && (\n <span\n className=\"relative mx-auto block w-fit bg-background px-2 text-muted-foreground\"\n data-slot=\"field-separator-content\"\n >\n {children}\n </span>\n )}\n </div>\n )\n}\n\nfunction FieldError({\n className,\n children,\n errors,\n id,\n ...props\n}: React.ComponentProps<\"div\"> & {\n errors?: Array<{ message?: string } | undefined>\n}) {\n const generatedId = React.useId()\n const errorId = id ?? generatedId\n const idContext = React.useContext(FieldIdContext)\n\n const content = useMemo(() => {\n if (children) {\n return children\n }\n\n if (!errors?.length) {\n return null\n }\n\n const uniqueErrors = [\n ...new Map(errors.map((error) => [error?.message, error])).values(),\n ]\n\n if (uniqueErrors?.length == 1) {\n return uniqueErrors[0]?.message\n }\n\n return (\n <ul className=\"ml-4 flex list-disc flex-col gap-1\">\n {uniqueErrors.map(\n (error, index) =>\n error?.message && <li key={index}>{error.message}</li>\n )}\n </ul>\n )\n }, [children, errors])\n\n React.useLayoutEffect(() => {\n idContext?.setErrorId(content ? errorId : null)\n return () => idContext?.setErrorId(null)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [errorId, !!content])\n\n if (!content) {\n return null\n }\n\n return (\n <div\n role=\"alert\"\n data-slot=\"field-error\"\n id={errorId}\n className={cn(\"text-sm font-normal text-destructive\", className)}\n {...props}\n >\n {content}\n </div>\n )\n}\n\nexport {\n Field,\n FieldLabel,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLegend,\n FieldSeparator,\n FieldSet,\n FieldContent,\n FieldTitle,\n FieldIdContext,\n}\n\n/**\n * WHEN TO USE\n * - Any labelled form field that needs help text or validation messaging — wraps any input primitive (Input, Textarea, Checkbox, ...)\n * - `orientation=\"horizontal\"` for label-beside-control rows (e.g. a Checkbox with title/description via FieldContent + FieldTitle)\n * - FieldSet + FieldLegend to group related Fields for assistive tech; FieldGroup to stack them with consistent spacing\n * - FieldSeparator to divide sections within a FieldGroup, optionally with a label (e.g. \"OR\")\n *\n * NOT WHEN\n * - A bare label with no description or error text — use Label directly, it's lighter\n * - Display-only key/value pairs with no input — this is a form-control wrapper, not a data display\n *\n * KEY RULES\n * - orientation: vertical (default), horizontal, responsive (stacks on narrow, horizontal at @md container width)\n * - Required/optional are marked on the label, nowhere else: `<FieldLabel required>` (red dot) or `<FieldLabel optional>` (muted \"(optional)\"), and the same two props on FieldLegend for a whole group. Visual only — the control still carries `required`/`aria-required` for assistive tech. Pick one convention per form: mark the required fields or the optional ones, not both.\n * - Mark invalid state with `data-invalid` on Field AND `aria-invalid` on the control — Field's red text styling depends on `data-invalid`, the control's border/ring styling depends on `aria-invalid`\n * - FieldError accepts either `children` (a fixed message) or an `errors` array of `{ message }` objects (react-hook-form shape) — duplicates are deduped by message, renders a single line for one error or a bulleted list for multiple; renders nothing if there's no content\n * - FieldDescription/FieldError generate an id and register it on Field's context; Checkbox reads that context and defaults its own `aria-describedby`/`aria-labelledby` (to FieldTitle's id) whenever the consumer hasn't already set one — for every other control, still give each element an explicit `id` and set the control's `aria-describedby` to match (see USAGE below), or a screen reader user who tabs to the control never hears the hint/error\n * - FieldLabel wraps Label — use it (not FieldTitle) when the row has an actual form control; use FieldTitle for the horizontal FieldContent pattern where the \"label\" is really a heading beside a standalone control. FieldTitle is a plain heading-styled `<div>`, not a `<label>` — for controls other than Checkbox, give the control its own `aria-label`/`aria-labelledby` matching FieldTitle's text (see USAGE below) or it ships with no accessible name at all.\n *\n * USAGE\n * <Field>\n * <FieldLabel htmlFor=\"email\">Email</FieldLabel>\n * <Input id=\"email\" type=\"email\" aria-describedby=\"email-hint\" />\n * <FieldDescription id=\"email-hint\">We'll never share your address.</FieldDescription>\n * </Field>\n *\n * // With validation — aria-describedby lists both ids so an invalid field\n * // still announces its hint alongside the error, not just one or the other\n * <Field data-invalid={!!error}>\n * <FieldLabel htmlFor=\"email\">Email</FieldLabel>\n * <Input id=\"email\" type=\"email\" aria-invalid={!!error} aria-describedby=\"email-hint email-error\" />\n * <FieldDescription id=\"email-hint\">We'll never share your address.</FieldDescription>\n * <FieldError id=\"email-error\">{error}</FieldError>\n * </Field>\n *\n * <Field data-invalid orientation=\"horizontal\">\n * <FieldContent>\n * <FieldTitle>Marketing emails</FieldTitle>\n * <FieldDescription>Receive product news and updates.</FieldDescription>\n * </FieldContent>\n * <Checkbox />\n * </Field>\n * // Checkbox picks up FieldTitle as its aria-labelledby and\n * // FieldDescription/FieldError as its aria-describedby automatically —\n * // pass an explicit aria-label/aria-labelledby/aria-describedby yourself\n * // only to override that default. Any other control in this position\n * // (there's no `<label>` wrapping it) still needs its own aria-label/\n * // aria-labelledby matching FieldTitle's text.\n *\n * <FieldSet>\n * <FieldLegend>Project details</FieldLegend>\n * <FieldGroup>\n * <Field>...</Field>\n * <FieldSeparator />\n * <Field>...</Field>\n * </FieldGroup>\n * </FieldSet>\n */\n"],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HoverCard — rich preview panel that opens on hover, with open/close delays.
|
|
3
|
+
*
|
|
4
|
+
* Pointer-driven — there is no click or
|
|
5
|
+
* keyboard-only path to the same content, so treat it as a progressive
|
|
6
|
+
* enhancement, not the only route to the information it shows.
|
|
7
|
+
*/
|
|
8
|
+
import * as React from "react";
|
|
9
|
+
import { HoverCard as HoverCardPrimitive } from "radix-ui";
|
|
10
|
+
declare function HoverCard({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Root>): React.JSX.Element;
|
|
11
|
+
declare function HoverCardTrigger({ asChild, href, tabIndex, ...props }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>): React.JSX.Element;
|
|
12
|
+
declare function HoverCardContent({ className, align, sideOffset, ...props }: React.ComponentProps<typeof HoverCardPrimitive.Content>): React.JSX.Element;
|
|
13
|
+
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
|
14
|
+
/**
|
|
15
|
+
* WHEN TO USE
|
|
16
|
+
* - Previewing an entity on hover without navigating away — a user profile,
|
|
17
|
+
* a linked record's summary, a card behind a link
|
|
18
|
+
*
|
|
19
|
+
* NOT WHEN
|
|
20
|
+
* - Touch-primary interfaces — there's no hover event, so the content
|
|
21
|
+
* becomes unreachable; don't gate essential info behind it
|
|
22
|
+
* - A short, plain-text label — use Tooltip
|
|
23
|
+
* - The panel must open on click or hold interactive controls the user
|
|
24
|
+
* must act on — use Popover
|
|
25
|
+
*
|
|
26
|
+
* KEY RULES
|
|
27
|
+
* - Opens on hover (or focus), not click — this is the trigger mechanism
|
|
28
|
+
* that separates it from Popover
|
|
29
|
+
* - `openDelay`/`closeDelay` default to radix's built-in delays; set both
|
|
30
|
+
* to 0 for instant preview
|
|
31
|
+
* - HoverCardContent is 16rem (w-64) wide by default; override via className
|
|
32
|
+
* - `align` defaults to "center", `sideOffset` defaults to 4
|
|
33
|
+
*
|
|
34
|
+
* USAGE
|
|
35
|
+
* <HoverCard>
|
|
36
|
+
* <HoverCardTrigger asChild><Button variant="link">@priya</Button></HoverCardTrigger>
|
|
37
|
+
* <HoverCardContent className="w-72">
|
|
38
|
+
* <div className="flex gap-3">
|
|
39
|
+
* <Avatar><AvatarImage src="..." /><AvatarFallback>CN</AvatarFallback></Avatar>
|
|
40
|
+
* <div className="grid gap-1">
|
|
41
|
+
* <p className="text-sm font-medium">Priya Nair</p>
|
|
42
|
+
* <p className="text-sm text-muted-foreground">Bio text.</p>
|
|
43
|
+
* </div>
|
|
44
|
+
* </div>
|
|
45
|
+
* </HoverCardContent>
|
|
46
|
+
* </HoverCard>
|
|
47
|
+
*
|
|
48
|
+
* // Instant, for reviewing content without waiting on hover delay
|
|
49
|
+
* <HoverCard openDelay={0} closeDelay={0}>...</HoverCard>
|
|
50
|
+
*/
|
|
51
|
+
//# sourceMappingURL=hover-card.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hover-card.d.ts","sourceRoot":"","sources":["../../../src/components/ui/hover-card.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAE,SAAS,IAAI,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAU1D,iBAAS,SAAS,CAAC,EACjB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,kBAAkB,CAAC,IAAI,CAAC,qBAOtD;AAED,iBAAS,gBAAgB,CAAC,EACxB,OAAO,EACP,IAAI,EACJ,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,kBAAkB,CAAC,OAAO,CAAC,qBA6BzD;AAED,iBAAS,gBAAgB,CAAC,EACxB,SAAS,EACT,KAAgB,EAChB,UAAc,EACd,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,kBAAkB,CAAC,OAAO,CAAC,qBA0CzD;AAED,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,CAAA;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { cn } from "../../lib/utils.js";
|
|
2
|
+
import { getPortalContainer } from "../../lib/scope.js";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { HoverCard as HoverCard$1 } from "radix-ui";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
//#region src/components/ui/hover-card.tsx
|
|
7
|
+
/**
|
|
8
|
+
* HoverCard — rich preview panel that opens on hover, with open/close delays.
|
|
9
|
+
*
|
|
10
|
+
* Pointer-driven — there is no click or
|
|
11
|
+
* keyboard-only path to the same content, so treat it as a progressive
|
|
12
|
+
* enhancement, not the only route to the information it shows.
|
|
13
|
+
*/
|
|
14
|
+
var HoverCardIdContext = React.createContext(null);
|
|
15
|
+
function HoverCard({ ...props }) {
|
|
16
|
+
const contentId = React.useId();
|
|
17
|
+
return /* @__PURE__ */ jsx(HoverCardIdContext.Provider, {
|
|
18
|
+
value: contentId,
|
|
19
|
+
children: /* @__PURE__ */ jsx(HoverCard$1.Root, {
|
|
20
|
+
"data-slot": "hover-card",
|
|
21
|
+
...props
|
|
22
|
+
})
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function HoverCardTrigger({ asChild, href, tabIndex, ...props }) {
|
|
26
|
+
const contentId = React.useContext(HoverCardIdContext);
|
|
27
|
+
const needsFallbackSemantics = !asChild && !href;
|
|
28
|
+
return /* @__PURE__ */ jsx(HoverCard$1.Trigger, {
|
|
29
|
+
"data-slot": "hover-card-trigger",
|
|
30
|
+
asChild,
|
|
31
|
+
href,
|
|
32
|
+
tabIndex: tabIndex ?? (needsFallbackSemantics ? 0 : void 0),
|
|
33
|
+
role: needsFallbackSemantics ? "button" : void 0,
|
|
34
|
+
"aria-describedby": contentId ?? void 0,
|
|
35
|
+
...props
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function HoverCardContent({ className, align = "center", sideOffset = 4, ...props }) {
|
|
39
|
+
const contentId = React.useContext(HoverCardIdContext);
|
|
40
|
+
const contentRef = React.useRef(null);
|
|
41
|
+
React.useEffect(() => {
|
|
42
|
+
if (process.env.NODE_ENV === "production") return;
|
|
43
|
+
const el = contentRef.current;
|
|
44
|
+
if (!el) return;
|
|
45
|
+
if (el.querySelectorAll("a[href], button, input, select, textarea, [tabindex]").length > 0) console.warn("HoverCardContent contains interactive/focusable content — HoverCard's content is never part of the tab order (it's dismissed on trigger blur), so these controls are unreachable by keyboard. Use Popover instead if the content needs to hold interactive controls.");
|
|
46
|
+
}, []);
|
|
47
|
+
return /* @__PURE__ */ jsx(HoverCard$1.Portal, {
|
|
48
|
+
"data-slot": "hover-card-portal",
|
|
49
|
+
container: getPortalContainer(),
|
|
50
|
+
children: /* @__PURE__ */ jsx(HoverCard$1.Content, {
|
|
51
|
+
ref: contentRef,
|
|
52
|
+
id: contentId ?? void 0,
|
|
53
|
+
"data-slot": "hover-card-content",
|
|
54
|
+
align,
|
|
55
|
+
sideOffset,
|
|
56
|
+
className: cn("z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-popover ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className),
|
|
57
|
+
...props
|
|
58
|
+
})
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* WHEN TO USE
|
|
63
|
+
* - Previewing an entity on hover without navigating away — a user profile,
|
|
64
|
+
* a linked record's summary, a card behind a link
|
|
65
|
+
*
|
|
66
|
+
* NOT WHEN
|
|
67
|
+
* - Touch-primary interfaces — there's no hover event, so the content
|
|
68
|
+
* becomes unreachable; don't gate essential info behind it
|
|
69
|
+
* - A short, plain-text label — use Tooltip
|
|
70
|
+
* - The panel must open on click or hold interactive controls the user
|
|
71
|
+
* must act on — use Popover
|
|
72
|
+
*
|
|
73
|
+
* KEY RULES
|
|
74
|
+
* - Opens on hover (or focus), not click — this is the trigger mechanism
|
|
75
|
+
* that separates it from Popover
|
|
76
|
+
* - `openDelay`/`closeDelay` default to radix's built-in delays; set both
|
|
77
|
+
* to 0 for instant preview
|
|
78
|
+
* - HoverCardContent is 16rem (w-64) wide by default; override via className
|
|
79
|
+
* - `align` defaults to "center", `sideOffset` defaults to 4
|
|
80
|
+
*
|
|
81
|
+
* USAGE
|
|
82
|
+
* <HoverCard>
|
|
83
|
+
* <HoverCardTrigger asChild><Button variant="link">@priya</Button></HoverCardTrigger>
|
|
84
|
+
* <HoverCardContent className="w-72">
|
|
85
|
+
* <div className="flex gap-3">
|
|
86
|
+
* <Avatar><AvatarImage src="..." /><AvatarFallback>CN</AvatarFallback></Avatar>
|
|
87
|
+
* <div className="grid gap-1">
|
|
88
|
+
* <p className="text-sm font-medium">Priya Nair</p>
|
|
89
|
+
* <p className="text-sm text-muted-foreground">Bio text.</p>
|
|
90
|
+
* </div>
|
|
91
|
+
* </div>
|
|
92
|
+
* </HoverCardContent>
|
|
93
|
+
* </HoverCard>
|
|
94
|
+
*
|
|
95
|
+
* // Instant, for reviewing content without waiting on hover delay
|
|
96
|
+
* <HoverCard openDelay={0} closeDelay={0}>...</HoverCard>
|
|
97
|
+
*/
|
|
98
|
+
//#endregion
|
|
99
|
+
export { HoverCard, HoverCardContent, HoverCardTrigger };
|
|
100
|
+
|
|
101
|
+
//# sourceMappingURL=hover-card.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hover-card.js","names":[],"sources":["../../../src/components/ui/hover-card.tsx"],"sourcesContent":["/**\n * HoverCard — rich preview panel that opens on hover, with open/close delays.\n *\n * Pointer-driven — there is no click or\n * keyboard-only path to the same content, so treat it as a progressive\n * enhancement, not the only route to the information it shows.\n */\nimport * as React from \"react\"\nimport { HoverCard as HoverCardPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\nimport { getPortalContainer } from \"@/lib/scope\"\n\n// Radix's HoverCard has no Title/Description parts of its own (unlike\n// Dialog/Popover) and generates no id — this is the one thing that lets\n// Trigger and Content reference each other at all.\nconst HoverCardIdContext = React.createContext<string | null>(null)\n\nfunction HoverCard({\n ...props\n}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {\n const contentId = React.useId()\n return (\n <HoverCardIdContext.Provider value={contentId}>\n <HoverCardPrimitive.Root data-slot=\"hover-card\" {...props} />\n </HoverCardIdContext.Provider>\n )\n}\n\nfunction HoverCardTrigger({\n asChild,\n href,\n tabIndex,\n ...props\n}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {\n const contentId = React.useContext(HoverCardIdContext)\n // Radix's Trigger renders a bare `Primitive.a` with pointer/focus\n // handlers wired for the open/close timing, but no `href`, `role`, or\n // `tabIndex` of its own — a plain, non-`asChild`, no-`href` trigger (the\n // one thing the doc-comment's own USAGE example avoids by always\n // wrapping a Button) is otherwise excluded from the tab order entirely,\n // so keyboard/AT users never trigger the open. Only apply the fallback\n // when `asChild` isn't wrapping something that's presumably already\n // interactive.\n const needsFallbackSemantics = !asChild && !href\n return (\n <HoverCardPrimitive.Trigger\n data-slot=\"hover-card-trigger\"\n asChild={asChild}\n href={href}\n tabIndex={tabIndex ?? (needsFallbackSemantics ? 0 : undefined)}\n role={needsFallbackSemantics ? \"button\" : undefined}\n // Nothing else ties the trigger to its preview content — Radix\n // generates no id for either side to reference (confirmed: no\n // Title/Description parts exist here, unlike Dialog/Popover).\n // aria-describedby (not aria-labelledby: this is supplementary\n // preview content, not the trigger's own name) is the only realistic\n // non-visual channel, since HoverCardContentImpl deliberately pulls\n // its interior out of the tab order.\n aria-describedby={contentId ?? undefined}\n {...props}\n />\n )\n}\n\nfunction HoverCardContent({\n className,\n align = \"center\",\n sideOffset = 4,\n ...props\n}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {\n const contentId = React.useContext(HoverCardIdContext)\n const contentRef = React.useRef<HTMLDivElement>(null)\n\n React.useEffect(() => {\n if (process.env.NODE_ENV === \"production\") return\n const el = contentRef.current\n if (!el) return\n // Radix force-sets tabindex=\"-1\" on every tabbable descendant here\n // (hover-only content can't safely hold focus) — the doc comment\n // already warns against putting interactive controls inside, but\n // nothing enforces it; this just surfaces the mistake in dev instead\n // of only failing silently under keyboard-only testing.\n const focusable = el.querySelectorAll(\n \"a[href], button, input, select, textarea, [tabindex]\"\n )\n if (focusable.length > 0) {\n console.warn(\n \"HoverCardContent contains interactive/focusable content — HoverCard's content is never part of the tab order (it's dismissed on trigger blur), so these controls are unreachable by keyboard. Use Popover instead if the content needs to hold interactive controls.\"\n )\n }\n }, [])\n\n return (\n <HoverCardPrimitive.Portal\n data-slot=\"hover-card-portal\"\n container={getPortalContainer()}\n >\n <HoverCardPrimitive.Content\n ref={contentRef}\n id={contentId ?? undefined}\n data-slot=\"hover-card-content\"\n align={align}\n sideOffset={sideOffset}\n className={cn(\n \"z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-popover ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95\",\n className\n )}\n {...props}\n />\n </HoverCardPrimitive.Portal>\n )\n}\n\nexport { HoverCard, HoverCardTrigger, HoverCardContent }\n\n/**\n * WHEN TO USE\n * - Previewing an entity on hover without navigating away — a user profile,\n * a linked record's summary, a card behind a link\n *\n * NOT WHEN\n * - Touch-primary interfaces — there's no hover event, so the content\n * becomes unreachable; don't gate essential info behind it\n * - A short, plain-text label — use Tooltip\n * - The panel must open on click or hold interactive controls the user\n * must act on — use Popover\n *\n * KEY RULES\n * - Opens on hover (or focus), not click — this is the trigger mechanism\n * that separates it from Popover\n * - `openDelay`/`closeDelay` default to radix's built-in delays; set both\n * to 0 for instant preview\n * - HoverCardContent is 16rem (w-64) wide by default; override via className\n * - `align` defaults to \"center\", `sideOffset` defaults to 4\n *\n * USAGE\n * <HoverCard>\n * <HoverCardTrigger asChild><Button variant=\"link\">@priya</Button></HoverCardTrigger>\n * <HoverCardContent className=\"w-72\">\n * <div className=\"flex gap-3\">\n * <Avatar><AvatarImage src=\"...\" /><AvatarFallback>CN</AvatarFallback></Avatar>\n * <div className=\"grid gap-1\">\n * <p className=\"text-sm font-medium\">Priya Nair</p>\n * <p className=\"text-sm text-muted-foreground\">Bio text.</p>\n * </div>\n * </div>\n * </HoverCardContent>\n * </HoverCard>\n *\n * // Instant, for reviewing content without waiting on hover delay\n * <HoverCard openDelay={0} closeDelay={0}>...</HoverCard>\n */\n"],"mappings":";;;;;;;;;;;;;AAgBA,IAAM,qBAAqB,MAAM,cAA6B,IAAI;AAElE,SAAS,UAAU,EACjB,GAAG,SACoD;CACvD,MAAM,YAAY,MAAM,MAAM;CAC9B,OACE,oBAAC,mBAAmB,UAApB;EAA6B,OAAO;EAClC,UAAA,oBAAC,YAAmB,MAApB;GAAyB,aAAU;GAAa,GAAI;EAAQ,CAAA;CACjC,CAAA;AAEjC;AAEA,SAAS,iBAAiB,EACxB,SACA,MACA,UACA,GAAG,SACuD;CAC1D,MAAM,YAAY,MAAM,WAAW,kBAAkB;CASrD,MAAM,yBAAyB,CAAC,WAAW,CAAC;CAC5C,OACE,oBAAC,YAAmB,SAApB;EACE,aAAU;EACD;EACH;EACN,UAAU,aAAa,yBAAyB,IAAI,KAAA;EACpD,MAAM,yBAAyB,WAAW,KAAA;EAQ1C,oBAAkB,aAAa,KAAA;EAC/B,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,iBAAiB,EACxB,WACA,QAAQ,UACR,aAAa,GACb,GAAG,SACuD;CAC1D,MAAM,YAAY,MAAM,WAAW,kBAAkB;CACrD,MAAM,aAAa,MAAM,OAAuB,IAAI;CAEpD,MAAM,gBAAgB;EACpB,IAAA,QAAA,IAAA,aAA6B,cAAc;EAC3C,MAAM,KAAK,WAAW;EACtB,IAAI,CAAC,IAAI;EAST,IAHkB,GAAG,iBACnB,sDAEE,CAAA,CAAU,SAAS,GACrB,QAAQ,KACN,sQACF;CAEJ,GAAG,CAAC,CAAC;CAEL,OACE,oBAAC,YAAmB,QAApB;EACE,aAAU;EACV,WAAW,mBAAmB;EAE9B,UAAA,oBAAC,YAAmB,SAApB;GACE,KAAK;GACL,IAAI,aAAa,KAAA;GACjB,aAAU;GACH;GACK;GACZ,WAAW,GACT,ohBACA,SACF;GACA,GAAI;EACL,CAAA;CACwB,CAAA;AAE/B"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InputGroup — Input/Textarea with attached icons, affixes, or buttons.
|
|
3
|
+
*
|
|
4
|
+
* A composite family: InputGroup is the bordered shell; InputGroupInput /
|
|
5
|
+
* InputGroupTextarea render the actual control borderless inside it;
|
|
6
|
+
* InputGroupAddon positions icons/text/buttons at any edge.
|
|
7
|
+
* The group's focus/error rings live on the shell (InputGroup), not the
|
|
8
|
+
* inner control.
|
|
9
|
+
*/
|
|
10
|
+
import * as React from "react";
|
|
11
|
+
import { type VariantProps } from "class-variance-authority";
|
|
12
|
+
import { Button } from "../../components/ui/button";
|
|
13
|
+
declare function InputGroup({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
14
|
+
declare const inputGroupAddonVariants: (props?: ({
|
|
15
|
+
align?: "inline-start" | "inline-end" | "block-end" | "block-start" | null | undefined;
|
|
16
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
17
|
+
declare function InputGroupAddon({ className, align, ...props }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>): React.JSX.Element;
|
|
18
|
+
type InputGroupButtonSize = "xs" | "sm";
|
|
19
|
+
/** @deprecated use `iconOnly` with "xs" | "sm" — same two-axis split as Button. */
|
|
20
|
+
type InputGroupButtonIconSize = "icon-xs" | "icon-sm";
|
|
21
|
+
declare function InputGroupButton({ className, type, variant, size, iconOnly, ...props }: React.ComponentProps<"button"> & Pick<React.ComponentProps<typeof Button>, "variant"> & {
|
|
22
|
+
asChild?: boolean;
|
|
23
|
+
/** Height step inside the group: "xs" (default) or "sm". Icon-only is a separate axis — `iconOnly`. */
|
|
24
|
+
size?: InputGroupButtonSize | InputGroupButtonIconSize;
|
|
25
|
+
/** Square, icon-only button at the chosen `size` step (the same split Button uses). Pass `aria-label`. */
|
|
26
|
+
iconOnly?: boolean;
|
|
27
|
+
}): React.JSX.Element;
|
|
28
|
+
declare function InputGroupText({ className, ...props }: React.ComponentProps<"span">): React.JSX.Element;
|
|
29
|
+
declare function InputGroupInput({ className, ...props }: Omit<React.ComponentProps<"input">, "size">): React.JSX.Element;
|
|
30
|
+
declare function InputGroupTextarea({ className, ...props }: React.ComponentProps<"textarea">): React.JSX.Element;
|
|
31
|
+
export { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea, };
|
|
32
|
+
/**
|
|
33
|
+
* WHEN TO USE
|
|
34
|
+
* - A search field with a leading icon
|
|
35
|
+
* - A URL field with an "https://" prefix or a units suffix
|
|
36
|
+
* - A textarea with a send button beneath it (a composer) — use `align="block-end"` on the addon
|
|
37
|
+
*
|
|
38
|
+
* NOT WHEN
|
|
39
|
+
* - A plain field with no icon/affix — use Input or Textarea directly
|
|
40
|
+
* - Two or more unrelated buttons standing alone — use ButtonGroup
|
|
41
|
+
*
|
|
42
|
+
* KEY RULES
|
|
43
|
+
* - Use InputGroupInput / InputGroupTextarea inside, not the plain Input/Textarea — they strip the inner border/ring so only the InputGroup shell shows focus/error state
|
|
44
|
+
* - InputGroupAddon align: inline-start, inline-end (default row, sit beside the control), block-start, block-end (full-width row above/below the control — this is what turns the group into a multi-row composer)
|
|
45
|
+
* - Clicking an addon focuses the control automatically, unless the click target is a button inside it
|
|
46
|
+
* - InputGroupButton sizes: xs (default), sm — plus `iconOnly` for a square icon button at that step
|
|
47
|
+
* (the old icon-xs/icon-sm values still work as deprecated aliases) — matches the group's compact height
|
|
48
|
+
*
|
|
49
|
+
* USAGE
|
|
50
|
+
* <InputGroup>
|
|
51
|
+
* <InputGroupAddon><SearchIcon /></InputGroupAddon>
|
|
52
|
+
* <InputGroupInput placeholder="Search..." />
|
|
53
|
+
* </InputGroup>
|
|
54
|
+
*
|
|
55
|
+
* <InputGroup>
|
|
56
|
+
* <InputGroupTextarea placeholder="Message..." />
|
|
57
|
+
* <InputGroupAddon align="block-end">
|
|
58
|
+
* <InputGroupButton size="sm" iconOnly aria-label="Send"><SendIcon /></InputGroupButton>
|
|
59
|
+
* </InputGroupAddon>
|
|
60
|
+
* </InputGroup>
|
|
61
|
+
*/
|
|
62
|
+
//# sourceMappingURL=input-group.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input-group.d.ts","sourceRoot":"","sources":["../../../src/components/ui/input-group.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAGjE,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAA;AAI/C,iBAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAYvE;AAED,QAAA,MAAM,uBAAuB;;8EAmB5B,CAAA;AAED,iBAAS,eAAe,CAAC,EACvB,SAAS,EACT,KAAsB,EACtB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG,YAAY,CAAC,OAAO,uBAAuB,CAAC,qBAgB5E;AAoBD,KAAK,oBAAoB,GAAG,IAAI,GAAG,IAAI,CAAA;AACvC,mFAAmF;AACnF,KAAK,wBAAwB,GAAG,SAAS,GAAG,SAAS,CAAA;AAMrD,iBAAS,gBAAgB,CAAC,EACxB,SAAS,EACT,IAAe,EACf,OAAiB,EACjB,IAAW,EACX,QAAgB,EAChB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,QAAQ,CAAC,GAC/B,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,OAAO,MAAM,CAAC,EAAE,SAAS,CAAC,GAAG;IACrD,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,uGAAuG;IACvG,IAAI,CAAC,EAAE,oBAAoB,GAAG,wBAAwB,CAAA;IACtD,0GAA0G;IAC1G,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,qBAqBF;AAED,iBAAS,cAAc,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,qBAU5E;AAED,iBAAS,eAAe,CAAC,EACvB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,qBAW7C;AAED,iBAAS,kBAAkB,CAAC,EAC1B,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,UAAU,CAAC,qBAWlC;AAED,OAAO,EACL,UAAU,EACV,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,kBAAkB,GACnB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG"}
|