@shadow-js/ui 0.0.0-stage → 1.0.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/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +598 -2
- package/dist/cli.mjs +2293 -0
- package/dist/dev/cli/schema.d.ts +73 -0
- package/dist/dev/index.d.ts +6 -0
- package/dist/dev/index.js +5 -0
- package/dist/dev/locales/ar.d.ts +141 -0
- package/dist/dev/locales/ar.js +128 -0
- package/dist/dev/locales/ar.js.map +1 -0
- package/dist/dev/locales/en.d.ts +141 -0
- package/dist/dev/locales/en.js +128 -0
- package/dist/dev/locales/en.js.map +1 -0
- package/dist/dev/registry/utils/utils.d.ts +29 -0
- package/dist/dev/registry/utils/utils.js +27 -0
- package/dist/dev/registry/utils/utils.js.map +1 -0
- package/dist/dev/theme/theme-logic.d.ts +187 -0
- package/dist/dev/theme/theme-logic.js +184 -0
- package/dist/dev/theme/theme-logic.js.map +1 -0
- package/dist/dev/theme/theme-provider.d.ts +90 -0
- package/dist/dev/theme/theme-provider.js +150 -0
- package/dist/dev/theme/theme-provider.js.map +1 -0
- package/dist/prod/index.js +1 -0
- package/dist/prod/locales/ar.js +2 -0
- package/dist/prod/locales/ar.js.map +1 -0
- package/dist/prod/locales/en.js +2 -0
- package/dist/prod/locales/en.js.map +1 -0
- package/dist/prod/registry/utils/utils.js +2 -0
- package/dist/prod/registry/utils/utils.js.map +1 -0
- package/dist/prod/theme/theme-logic.js +2 -0
- package/dist/prod/theme/theme-logic.js.map +1 -0
- package/dist/prod/theme/theme-provider.js +2 -0
- package/dist/prod/theme/theme-provider.js.map +1 -0
- package/package.json +102 -4
- package/registry/accordion/accordion.tsx +210 -0
- package/registry/accordion/item.json +4 -0
- package/registry/alert/alert.tsx +200 -0
- package/registry/alert/item.json +4 -0
- package/registry/alert-dialog/alert-dialog.tsx +530 -0
- package/registry/alert-dialog/item.json +4 -0
- package/registry/aspect-ratio/aspect-ratio.tsx +71 -0
- package/registry/aspect-ratio/item.json +4 -0
- package/registry/attachment/attachment.tsx +459 -0
- package/registry/attachment/item.json +4 -0
- package/registry/avatar/avatar.tsx +385 -0
- package/registry/avatar/item.json +4 -0
- package/registry/badge/badge.tsx +108 -0
- package/registry/badge/item.json +4 -0
- package/registry/breadcrumb/breadcrumb.tsx +354 -0
- package/registry/breadcrumb/item.json +4 -0
- package/registry/bubble/bubble.tsx +260 -0
- package/registry/bubble/item.json +4 -0
- package/registry/button/button.tsx +125 -0
- package/registry/button/item.json +4 -0
- package/registry/button-group/button-group.tsx +189 -0
- package/registry/button-group/item.json +4 -0
- package/registry/calendar/calendar.tsx +348 -0
- package/registry/calendar/item.json +4 -0
- package/registry/card/card.tsx +311 -0
- package/registry/card/item.json +4 -0
- package/registry/carousel/carousel.tsx +851 -0
- package/registry/carousel/item.json +4 -0
- package/registry/chart/chart.tsx +1956 -0
- package/registry/chart/item.json +4 -0
- package/registry/chart-scale/chart-scale.ts +670 -0
- package/registry/chart-scale/item.json +5 -0
- package/registry/checkbox/checkbox.tsx +79 -0
- package/registry/checkbox/item.json +4 -0
- package/registry/code-block/code-block.tsx +470 -0
- package/registry/code-block/item.json +4 -0
- package/registry/collapsible/collapsible.tsx +117 -0
- package/registry/collapsible/item.json +4 -0
- package/registry/combobox/combobox.tsx +977 -0
- package/registry/combobox/item.json +4 -0
- package/registry/command/command.tsx +757 -0
- package/registry/command/item.json +4 -0
- package/registry/context-menu/context-menu.tsx +697 -0
- package/registry/context-menu/item.json +4 -0
- package/registry/data-table/data-table.tsx +437 -0
- package/registry/data-table/item.json +4 -0
- package/registry/date-picker/date-picker.tsx +349 -0
- package/registry/date-picker/item.json +4 -0
- package/registry/dialog/dialog.tsx +412 -0
- package/registry/dialog/item.json +4 -0
- package/registry/direction/direction.tsx +88 -0
- package/registry/direction/item.json +4 -0
- package/registry/drawer/drawer.tsx +787 -0
- package/registry/drawer/item.json +4 -0
- package/registry/dropdown-menu/dropdown-menu.tsx +692 -0
- package/registry/dropdown-menu/item.json +4 -0
- package/registry/empty/empty.tsx +240 -0
- package/registry/empty/item.json +4 -0
- package/registry/field/field.tsx +624 -0
- package/registry/field/item.json +4 -0
- package/registry/form/form.tsx +1112 -0
- package/registry/form/item.json +4 -0
- package/registry/hover-card/hover-card.tsx +144 -0
- package/registry/hover-card/item.json +4 -0
- package/registry/icons/icons.tsx +1691 -0
- package/registry/icons/item.json +4 -0
- package/registry/input/input.tsx +43 -0
- package/registry/input/item.json +4 -0
- package/registry/input-group/input-group.tsx +315 -0
- package/registry/input-group/item.json +4 -0
- package/registry/input-otp/input-otp.tsx +329 -0
- package/registry/input-otp/item.json +4 -0
- package/registry/item/item.json +4 -0
- package/registry/item/item.tsx +439 -0
- package/registry/kbd/item.json +4 -0
- package/registry/kbd/kbd.tsx +65 -0
- package/registry/label/item.json +4 -0
- package/registry/label/label.tsx +60 -0
- package/registry/marker/item.json +4 -0
- package/registry/marker/marker.tsx +175 -0
- package/registry/menubar/item.json +4 -0
- package/registry/menubar/menubar.tsx +694 -0
- package/registry/message/item.json +4 -0
- package/registry/message/message.tsx +280 -0
- package/registry/message-scroller/item.json +4 -0
- package/registry/message-scroller/message-scroller.tsx +262 -0
- package/registry/native-select/item.json +4 -0
- package/registry/native-select/native-select.tsx +113 -0
- package/registry/navigation-menu/item.json +4 -0
- package/registry/navigation-menu/navigation-menu.tsx +362 -0
- package/registry/pagination/item.json +4 -0
- package/registry/pagination/pagination.tsx +460 -0
- package/registry/password-input/item.json +5 -0
- package/registry/password-input/password-input.tsx +494 -0
- package/registry/popover/item.json +4 -0
- package/registry/popover/popover.tsx +284 -0
- package/registry/progress/item.json +4 -0
- package/registry/progress/progress.tsx +190 -0
- package/registry/questionnaire/item.json +4 -0
- package/registry/questionnaire/questionnaire.tsx +569 -0
- package/registry/radio-group/item.json +4 -0
- package/registry/radio-group/radio-group.tsx +101 -0
- package/registry/resizable/item.json +4 -0
- package/registry/resizable/resizable.tsx +455 -0
- package/registry/scroll-area/item.json +4 -0
- package/registry/scroll-area/scroll-area.tsx +149 -0
- package/registry/select/item.json +4 -0
- package/registry/select/select.tsx +571 -0
- package/registry/separator/item.json +4 -0
- package/registry/separator/separator.tsx +52 -0
- package/registry/sheet/item.json +4 -0
- package/registry/sheet/sheet.tsx +476 -0
- package/registry/sidebar/item.json +4 -0
- package/registry/sidebar/sidebar.tsx +1870 -0
- package/registry/skeleton/item.json +4 -0
- package/registry/skeleton/skeleton.tsx +47 -0
- package/registry/slider/item.json +4 -0
- package/registry/slider/slider.tsx +149 -0
- package/registry/spinner/item.json +4 -0
- package/registry/spinner/spinner.tsx +51 -0
- package/registry/switch/item.json +4 -0
- package/registry/switch/switch.tsx +93 -0
- package/registry/table/item.json +4 -0
- package/registry/table/table.tsx +373 -0
- package/registry/tabs/item.json +4 -0
- package/registry/tabs/tabs.tsx +209 -0
- package/registry/textarea/item.json +4 -0
- package/registry/textarea/textarea.tsx +51 -0
- package/registry/toast/item.json +4 -0
- package/registry/toast/toast.tsx +949 -0
- package/registry/toggle/item.json +4 -0
- package/registry/toggle/toggle.tsx +110 -0
- package/registry/toggle-group/item.json +4 -0
- package/registry/toggle-group/toggle-group.tsx +189 -0
- package/registry/tooltip/item.json +4 -0
- package/registry/tooltip/tooltip.tsx +169 -0
- package/registry/trace/item.json +4 -0
- package/registry/trace/trace.tsx +356 -0
- package/registry/ui-strings/item.json +5 -0
- package/registry/ui-strings/ui-strings.tsx +454 -0
- package/registry/use-highlighted-code/item.json +5 -0
- package/registry/use-highlighted-code/use-highlighted-code.tsx +145 -0
- package/registry/use-mobile/item.json +5 -0
- package/registry/use-mobile/use-mobile.ts +46 -0
- package/registry/utils/item.json +5 -0
- package/registry/utils/utils.ts +38 -0
- package/registry.json +3274 -0
- package/styles/animations.css +321 -0
- package/styles/theme-core.css +55 -0
- package/styles/theme.css +12 -0
- package/styles/themes/gray.css +69 -0
- package/styles/themes/neutral.css +69 -0
- package/styles/themes/slate.css +69 -0
- package/styles/themes/stone.css +69 -0
- package/styles/themes/zinc.css +69 -0
- package/styles/typeset.css +251 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
import { type Accessor, splitProps } from "@shadow-js/core";
|
|
2
|
+
import {
|
|
3
|
+
createPartContext,
|
|
4
|
+
Label as LabelPrimitive,
|
|
5
|
+
partId,
|
|
6
|
+
useBaseId,
|
|
7
|
+
usePartRegistration,
|
|
8
|
+
} from "@shadow-js/primitives";
|
|
9
|
+
import { type ComponentProps, For, type JSX, Show } from "@shadow-js/web";
|
|
10
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
11
|
+
import { cn } from "@/lib/utils";
|
|
12
|
+
|
|
13
|
+
/** What a field shares with its label, description, error and control. @internal */
|
|
14
|
+
interface FieldContextValue {
|
|
15
|
+
readonly controlId: () => string;
|
|
16
|
+
readonly description: ReturnType<typeof usePartRegistration>;
|
|
17
|
+
readonly error: ReturnType<typeof usePartRegistration>;
|
|
18
|
+
readonly invalid: () => boolean;
|
|
19
|
+
readonly disabled: () => boolean;
|
|
20
|
+
readonly descriptionId: () => string;
|
|
21
|
+
readonly errorId: () => string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const fieldContext = createPartContext<FieldContextValue>("Field");
|
|
25
|
+
|
|
26
|
+
const fieldVariants = cva("group/field flex w-full gap-3 data-[invalid=true]:text-destructive", {
|
|
27
|
+
variants: {
|
|
28
|
+
orientation: {
|
|
29
|
+
vertical: "flex-col [&>*]:w-full",
|
|
30
|
+
horizontal: "flex-row items-center [&>[data-slot=field-label]]:flex-auto",
|
|
31
|
+
responsive: "flex-col sm:flex-row sm:items-center [&>[data-slot=field-label]]:sm:flex-auto",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
defaultVariants: { orientation: "vertical" },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Props of {@link Field}: the attributes of the rendered `div`, `orientation` (`"vertical"`,
|
|
39
|
+
* `"horizontal"` or `"responsive"`), and the state of the entry.
|
|
40
|
+
* @see {@link Field}
|
|
41
|
+
* @since 1.0.0
|
|
42
|
+
*/
|
|
43
|
+
export type FieldProps = ComponentProps<"div"> &
|
|
44
|
+
VariantProps<typeof fieldVariants> & {
|
|
45
|
+
/**
|
|
46
|
+
* Marks the entry invalid: the label and the error take the destructive color, and the control
|
|
47
|
+
* gets `aria-invalid` from {@link useField}.
|
|
48
|
+
* @default false
|
|
49
|
+
*/
|
|
50
|
+
readonly invalid?: boolean | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Disables the entry's label and description, and the control through {@link useField}.
|
|
53
|
+
* @default false
|
|
54
|
+
*/
|
|
55
|
+
readonly disabled?: boolean | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* The id of the control. The label points at it. Generated when omitted.
|
|
58
|
+
* @default generated
|
|
59
|
+
*/
|
|
60
|
+
readonly controlId?: string | undefined;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type Loose = { readonly class?: unknown; readonly [prop: string]: unknown };
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A form entry: the layout of a label, a control, a description and an error. The label points at the
|
|
67
|
+
* control, and the control describes itself with the description and the error while they are shown.
|
|
68
|
+
* That wiring is done by the control, which reads {@link useField}.
|
|
69
|
+
*
|
|
70
|
+
* Keyboard and ARIA: the field is a `div` with `role="group"` and no keys of its own. The label is a
|
|
71
|
+
* `label` whose `for` is the control's id. The control gets its `id`, `aria-describedby` (the
|
|
72
|
+
* description and the error, only while they are rendered), `aria-invalid` and `disabled` from
|
|
73
|
+
* {@link useField}. The control is named by its label: the label's `for` is the control's id. The error
|
|
74
|
+
* has `role="alert"`, so a screen reader announces it when it appears.
|
|
75
|
+
*
|
|
76
|
+
* shadcn/ui differences: the prop is `class`, not `className`, and every part has a `data-slot`. The control
|
|
77
|
+
* gets its ids and state from {@link useField}, which returns values to set on the control.
|
|
78
|
+
*
|
|
79
|
+
* @param props The field props: see {@link FieldProps}.
|
|
80
|
+
* @returns A `div` with `role="group"`.
|
|
81
|
+
* @example
|
|
82
|
+
* import { Field, FieldDescription, FieldLabel, useField } from "@/components/ui/field";
|
|
83
|
+
* import { Input } from "@/components/ui/input";
|
|
84
|
+
*
|
|
85
|
+
* function EmailControl() {
|
|
86
|
+
* const field = useField();
|
|
87
|
+
* return (
|
|
88
|
+
* <Input
|
|
89
|
+
* id={field.id()}
|
|
90
|
+
* type="email"
|
|
91
|
+
* aria-describedby={field.describedBy()}
|
|
92
|
+
* aria-invalid={field.invalid() ? true : undefined}
|
|
93
|
+
* disabled={field.disabled()}
|
|
94
|
+
* />
|
|
95
|
+
* );
|
|
96
|
+
* }
|
|
97
|
+
*
|
|
98
|
+
* export function EmailField() {
|
|
99
|
+
* return (
|
|
100
|
+
* <Field>
|
|
101
|
+
* <FieldLabel>Email</FieldLabel>
|
|
102
|
+
* <EmailControl />
|
|
103
|
+
* <FieldDescription>We never share it.</FieldDescription>
|
|
104
|
+
* </Field>
|
|
105
|
+
* );
|
|
106
|
+
* }
|
|
107
|
+
* @see {@link useField}
|
|
108
|
+
* @see {@link FieldLabel}
|
|
109
|
+
* @since 1.0.0
|
|
110
|
+
*/
|
|
111
|
+
function Field(props: FieldProps): JSX.Element {
|
|
112
|
+
const [local, rest] = splitProps(props as unknown as Loose & FieldProps, [
|
|
113
|
+
"class",
|
|
114
|
+
"orientation",
|
|
115
|
+
"invalid",
|
|
116
|
+
"disabled",
|
|
117
|
+
"controlId",
|
|
118
|
+
]);
|
|
119
|
+
const base = useBaseId(() => local.controlId as string | undefined);
|
|
120
|
+
const description = usePartRegistration();
|
|
121
|
+
const error = usePartRegistration();
|
|
122
|
+
const context: FieldContextValue = {
|
|
123
|
+
// The application's controlId is the control's id as given; the generated one derives from the base.
|
|
124
|
+
controlId: () => (local.controlId as string | undefined) ?? partId(base(), "control"),
|
|
125
|
+
description,
|
|
126
|
+
error,
|
|
127
|
+
invalid: () => local.invalid === true,
|
|
128
|
+
disabled: () => local.disabled === true,
|
|
129
|
+
descriptionId: () => partId(base(), "description"),
|
|
130
|
+
errorId: () => partId(base(), "error"),
|
|
131
|
+
};
|
|
132
|
+
return (
|
|
133
|
+
<fieldContext.Provider value={context}>
|
|
134
|
+
<div
|
|
135
|
+
role="group"
|
|
136
|
+
data-slot="field"
|
|
137
|
+
data-invalid={local.invalid === true ? "true" : undefined}
|
|
138
|
+
data-disabled={local.disabled === true ? "true" : undefined}
|
|
139
|
+
data-orientation={(local.orientation as string | undefined) ?? "vertical"}
|
|
140
|
+
class={cn(
|
|
141
|
+
fieldVariants({
|
|
142
|
+
orientation: local.orientation as "vertical" | "horizontal" | "responsive" | undefined,
|
|
143
|
+
}),
|
|
144
|
+
local.class as string | undefined,
|
|
145
|
+
)}
|
|
146
|
+
{...rest}
|
|
147
|
+
/>
|
|
148
|
+
</fieldContext.Provider>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The id and state of the control of the enclosing {@link Field}, as accessors. Read them in the
|
|
154
|
+
* control's JSX so the attributes stay current. `describedBy` lists the description and the error only
|
|
155
|
+
* while they are rendered. Call it inside a `Field`, in the component that renders the control.
|
|
156
|
+
*
|
|
157
|
+
* @returns The accessors: `id`, `describedBy`, `invalid` and `disabled`.
|
|
158
|
+
* @throws Error when called outside a {@link Field}.
|
|
159
|
+
* @example
|
|
160
|
+
* import { Field, FieldLabel, useField } from "@/components/ui/field";
|
|
161
|
+
* import { Input } from "@/components/ui/input";
|
|
162
|
+
*
|
|
163
|
+
* function Control() {
|
|
164
|
+
* const field = useField();
|
|
165
|
+
* return (
|
|
166
|
+
* <Input
|
|
167
|
+
* id={field.id()}
|
|
168
|
+
* aria-describedby={field.describedBy()}
|
|
169
|
+
* aria-invalid={field.invalid() ? true : undefined}
|
|
170
|
+
* />
|
|
171
|
+
* );
|
|
172
|
+
* }
|
|
173
|
+
*
|
|
174
|
+
* export function NameField() {
|
|
175
|
+
* return (
|
|
176
|
+
* <Field>
|
|
177
|
+
* <FieldLabel>Name</FieldLabel>
|
|
178
|
+
* <Control />
|
|
179
|
+
* </Field>
|
|
180
|
+
* );
|
|
181
|
+
* }
|
|
182
|
+
* @see {@link Field}
|
|
183
|
+
* @since 1.0.0
|
|
184
|
+
*/
|
|
185
|
+
function useField(): {
|
|
186
|
+
readonly id: () => string;
|
|
187
|
+
readonly describedBy: () => string | undefined;
|
|
188
|
+
readonly invalid: () => boolean;
|
|
189
|
+
readonly disabled: () => boolean;
|
|
190
|
+
} {
|
|
191
|
+
const context = fieldContext.use("useField");
|
|
192
|
+
return {
|
|
193
|
+
id: () => context.controlId(),
|
|
194
|
+
describedBy: () => {
|
|
195
|
+
const ids = [
|
|
196
|
+
context.description.present() ? context.descriptionId() : undefined,
|
|
197
|
+
context.error.present() ? context.errorId() : undefined,
|
|
198
|
+
].filter((id): id is string => id !== undefined);
|
|
199
|
+
return ids.length === 0 ? undefined : ids.join(" ");
|
|
200
|
+
},
|
|
201
|
+
invalid: () => context.invalid(),
|
|
202
|
+
disabled: () => context.disabled(),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* A group of related fields under a {@link FieldLegend}. It is a native `fieldset`, so the browser and
|
|
208
|
+
* assistive technology treat its fields as one group.
|
|
209
|
+
*
|
|
210
|
+
* @param props The set props: the attributes of the rendered `fieldset` and `class`.
|
|
211
|
+
* @returns The fieldset.
|
|
212
|
+
* @example
|
|
213
|
+
* import { Field, FieldGroup, FieldLabel, FieldLegend, FieldSet } from "@/components/ui/field";
|
|
214
|
+
* import { Input } from "@/components/ui/input";
|
|
215
|
+
*
|
|
216
|
+
* export function Address() {
|
|
217
|
+
* return (
|
|
218
|
+
* <FieldSet>
|
|
219
|
+
* <FieldLegend>Address</FieldLegend>
|
|
220
|
+
* <FieldGroup>
|
|
221
|
+
* <Field controlId="street">
|
|
222
|
+
* <FieldLabel>Street</FieldLabel>
|
|
223
|
+
* <Input id="street" />
|
|
224
|
+
* </Field>
|
|
225
|
+
* </FieldGroup>
|
|
226
|
+
* </FieldSet>
|
|
227
|
+
* );
|
|
228
|
+
* }
|
|
229
|
+
* @see {@link FieldLegend}
|
|
230
|
+
* @see {@link FieldGroup}
|
|
231
|
+
* @since 1.0.0
|
|
232
|
+
*/
|
|
233
|
+
function FieldSet(props: ComponentProps<"fieldset">): JSX.Element {
|
|
234
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
235
|
+
return (
|
|
236
|
+
<fieldset
|
|
237
|
+
data-slot="field-set"
|
|
238
|
+
class={cn("flex flex-col gap-6 has-[>[data-slot=checkbox-group]]:gap-3", local.class)}
|
|
239
|
+
{...rest}
|
|
240
|
+
/>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The legend of a {@link FieldSet}, which names the set. `variant="label"` styles it like a small label,
|
|
246
|
+
* and `variant="legend"` (the default) like a heading.
|
|
247
|
+
*
|
|
248
|
+
* @param props The legend props: the attributes of the rendered `legend`, and `variant`.
|
|
249
|
+
* @returns The legend.
|
|
250
|
+
* @example
|
|
251
|
+
* import { FieldLegend, FieldSet } from "@/components/ui/field";
|
|
252
|
+
*
|
|
253
|
+
* export function Notifications() {
|
|
254
|
+
* return (
|
|
255
|
+
* <FieldSet>
|
|
256
|
+
* <FieldLegend variant="label">Notifications</FieldLegend>
|
|
257
|
+
* </FieldSet>
|
|
258
|
+
* );
|
|
259
|
+
* }
|
|
260
|
+
* @see {@link FieldSet}
|
|
261
|
+
* @since 1.0.0
|
|
262
|
+
*/
|
|
263
|
+
function FieldLegend(
|
|
264
|
+
props: ComponentProps<"legend"> & { readonly variant?: "legend" | "label" | undefined },
|
|
265
|
+
): JSX.Element {
|
|
266
|
+
const [local, rest] = splitProps(
|
|
267
|
+
props as unknown as Loose & { readonly variant?: "legend" | "label" },
|
|
268
|
+
["class", "variant"],
|
|
269
|
+
);
|
|
270
|
+
return (
|
|
271
|
+
<legend
|
|
272
|
+
data-slot="field-legend"
|
|
273
|
+
data-variant={(local.variant as string | undefined) ?? "legend"}
|
|
274
|
+
class={cn(
|
|
275
|
+
"mb-3 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base",
|
|
276
|
+
local.class as string | undefined,
|
|
277
|
+
)}
|
|
278
|
+
{...rest}
|
|
279
|
+
/>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* A column of fields with space between them. It has no role.
|
|
285
|
+
*
|
|
286
|
+
* @param props The group props: the attributes of the rendered `div` and `class`.
|
|
287
|
+
* @returns The group.
|
|
288
|
+
* @example
|
|
289
|
+
* import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
|
|
290
|
+
* import { Input } from "@/components/ui/input";
|
|
291
|
+
*
|
|
292
|
+
* export function Contact() {
|
|
293
|
+
* return (
|
|
294
|
+
* <FieldGroup>
|
|
295
|
+
* <Field controlId="email">
|
|
296
|
+
* <FieldLabel>Email</FieldLabel>
|
|
297
|
+
* <Input id="email" type="email" />
|
|
298
|
+
* </Field>
|
|
299
|
+
* </FieldGroup>
|
|
300
|
+
* );
|
|
301
|
+
* }
|
|
302
|
+
* @see {@link Field}
|
|
303
|
+
* @since 1.0.0
|
|
304
|
+
*/
|
|
305
|
+
function FieldGroup(props: ComponentProps<"div">): JSX.Element {
|
|
306
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
307
|
+
return (
|
|
308
|
+
<div
|
|
309
|
+
data-slot="field-group"
|
|
310
|
+
class={cn("group/field-group @container/field-group flex w-full flex-col gap-7", local.class)}
|
|
311
|
+
{...rest}
|
|
312
|
+
/>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The text column of a horizontal field: the title, label and description that sit beside the control.
|
|
318
|
+
* It has no role.
|
|
319
|
+
*
|
|
320
|
+
* @param props The content props: the attributes of the rendered `div` and `class`.
|
|
321
|
+
* @returns The text column.
|
|
322
|
+
* @example
|
|
323
|
+
* import { Field, FieldContent, FieldDescription, FieldTitle } from "@/components/ui/field";
|
|
324
|
+
* import { Switch } from "@/components/ui/switch";
|
|
325
|
+
*
|
|
326
|
+
* export function Digest() {
|
|
327
|
+
* return (
|
|
328
|
+
* <Field orientation="horizontal">
|
|
329
|
+
* <FieldContent>
|
|
330
|
+
* <FieldTitle>Weekly digest</FieldTitle>
|
|
331
|
+
* <FieldDescription>One email a week.</FieldDescription>
|
|
332
|
+
* </FieldContent>
|
|
333
|
+
* <Switch aria-label="Weekly digest" />
|
|
334
|
+
* </Field>
|
|
335
|
+
* );
|
|
336
|
+
* }
|
|
337
|
+
* @see {@link Field}
|
|
338
|
+
* @since 1.0.0
|
|
339
|
+
*/
|
|
340
|
+
function FieldContent(props: ComponentProps<"div">): JSX.Element {
|
|
341
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
342
|
+
return (
|
|
343
|
+
<div
|
|
344
|
+
data-slot="field-content"
|
|
345
|
+
class={cn("flex flex-1 flex-col gap-1.5 leading-snug", local.class)}
|
|
346
|
+
{...rest}
|
|
347
|
+
/>
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* The label of the field's control. It points at the control (its `for` is set from the field, so it
|
|
353
|
+
* overrides a `htmlFor` you pass), and it reads as disabled and invalid with the field.
|
|
354
|
+
*
|
|
355
|
+
* @param props The label props: the attributes of the rendered `label` and `class`.
|
|
356
|
+
* @returns The label.
|
|
357
|
+
* @throws Error when rendered outside a {@link Field}.
|
|
358
|
+
* @example
|
|
359
|
+
* import { Field, FieldLabel } from "@/components/ui/field";
|
|
360
|
+
* import { Input } from "@/components/ui/input";
|
|
361
|
+
*
|
|
362
|
+
* export function NameField() {
|
|
363
|
+
* return (
|
|
364
|
+
* <Field controlId="name">
|
|
365
|
+
* <FieldLabel>Name</FieldLabel>
|
|
366
|
+
* <Input id="name" />
|
|
367
|
+
* </Field>
|
|
368
|
+
* );
|
|
369
|
+
* }
|
|
370
|
+
* @see {@link Field}
|
|
371
|
+
* @see {@link useField}
|
|
372
|
+
* @since 1.0.0
|
|
373
|
+
*/
|
|
374
|
+
function FieldLabel(props: ComponentProps<"label">): JSX.Element {
|
|
375
|
+
const context = fieldContext.use("FieldLabel");
|
|
376
|
+
const [local, rest] = splitProps(props as unknown as Loose, ["class"]);
|
|
377
|
+
return (
|
|
378
|
+
<LabelPrimitive
|
|
379
|
+
data-slot="field-label"
|
|
380
|
+
class={cn(
|
|
381
|
+
"flex w-fit items-center gap-2 text-sm leading-snug font-medium select-none group-data-[disabled=true]/field:opacity-50 peer-disabled:opacity-50 group-data-[invalid=true]/field:text-destructive has-[>[data-slot=field]]:w-full",
|
|
382
|
+
local.class as string | undefined,
|
|
383
|
+
)}
|
|
384
|
+
{...rest}
|
|
385
|
+
htmlFor={context.controlId()}
|
|
386
|
+
/>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* The title of a horizontal field: a medium-weight line of text for the whole entry. It is a `div`, so
|
|
392
|
+
* it names nothing by itself; use a {@link FieldLabel} for the control's name.
|
|
393
|
+
*
|
|
394
|
+
* @param props The title props: the attributes of the rendered `div` and `class`.
|
|
395
|
+
* @returns The title.
|
|
396
|
+
* @example
|
|
397
|
+
* import { FieldContent, FieldTitle } from "@/components/ui/field";
|
|
398
|
+
*
|
|
399
|
+
* export const Billing = (
|
|
400
|
+
* <FieldContent>
|
|
401
|
+
* <FieldTitle>Billing</FieldTitle>
|
|
402
|
+
* </FieldContent>
|
|
403
|
+
* );
|
|
404
|
+
* @see {@link FieldContent}
|
|
405
|
+
* @since 1.0.0
|
|
406
|
+
*/
|
|
407
|
+
function FieldTitle(props: ComponentProps<"div">): JSX.Element {
|
|
408
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
409
|
+
return (
|
|
410
|
+
<div
|
|
411
|
+
data-slot="field-title"
|
|
412
|
+
class={cn("flex w-fit items-center gap-2 text-sm leading-snug font-medium", local.class)}
|
|
413
|
+
{...rest}
|
|
414
|
+
/>
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* A helper text for the control. It has an id, and the control's `aria-describedby` (from
|
|
420
|
+
* {@link useField}) names it while it is rendered, so a screen reader reads it with the control.
|
|
421
|
+
*
|
|
422
|
+
* @param props The description props: the attributes of the rendered `p` and `class`.
|
|
423
|
+
* @returns The paragraph.
|
|
424
|
+
* @throws Error when rendered outside a {@link Field}.
|
|
425
|
+
* @example
|
|
426
|
+
* import { Field, FieldDescription, FieldLabel, useField } from "@/components/ui/field";
|
|
427
|
+
* import { Input } from "@/components/ui/input";
|
|
428
|
+
*
|
|
429
|
+
* function PasswordControl() {
|
|
430
|
+
* const field = useField();
|
|
431
|
+
* return <Input id={field.id()} type="password" aria-describedby={field.describedBy()} />;
|
|
432
|
+
* }
|
|
433
|
+
*
|
|
434
|
+
* export function PasswordField() {
|
|
435
|
+
* return (
|
|
436
|
+
* <Field>
|
|
437
|
+
* <FieldLabel>Password</FieldLabel>
|
|
438
|
+
* <PasswordControl />
|
|
439
|
+
* <FieldDescription>Use at least 8 characters.</FieldDescription>
|
|
440
|
+
* </Field>
|
|
441
|
+
* );
|
|
442
|
+
* }
|
|
443
|
+
* @see {@link useField}
|
|
444
|
+
* @since 1.0.0
|
|
445
|
+
*/
|
|
446
|
+
function FieldDescription(props: ComponentProps<"p">): JSX.Element {
|
|
447
|
+
const context = fieldContext.use("FieldDescription");
|
|
448
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
449
|
+
context.description.register();
|
|
450
|
+
return (
|
|
451
|
+
<p
|
|
452
|
+
data-slot="field-description"
|
|
453
|
+
id={context.descriptionId()}
|
|
454
|
+
class={cn(
|
|
455
|
+
"text-sm leading-normal font-normal text-muted-foreground group-has-[>input]/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
|
|
456
|
+
local.class,
|
|
457
|
+
)}
|
|
458
|
+
{...rest}
|
|
459
|
+
/>
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** One error of a validator: any object with a `message`, such as a Standard Schema issue. */
|
|
464
|
+
type FieldErrorEntry = { readonly message?: string | undefined } | undefined;
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Props of {@link FieldError}: the attributes of the rendered `div`, `class`, the `children` (the
|
|
468
|
+
* message, which wins when given), and `errors`, the messages of a validator.
|
|
469
|
+
* @see {@link FieldError}
|
|
470
|
+
* @since 1.0.0
|
|
471
|
+
*/
|
|
472
|
+
export type FieldErrorProps = ComponentProps<"div"> & {
|
|
473
|
+
/**
|
|
474
|
+
* The errors of a validator. Each entry with a `message` is shown once: one message is text, several
|
|
475
|
+
* are a list. An empty list hides the error. Ignored when `children` is given.
|
|
476
|
+
* @default undefined
|
|
477
|
+
*/
|
|
478
|
+
readonly errors?: ReadonlyArray<FieldErrorEntry> | undefined;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* The error of a field. It has `role="alert"`, so it is announced when it appears, and the control's
|
|
483
|
+
* `aria-describedby` (from {@link useField}) names it while it has content. Without children and
|
|
484
|
+
* without messages it is hidden, and the control does not describe itself with it.
|
|
485
|
+
*
|
|
486
|
+
* Keyboard and ARIA: no keys of its own. The `alert` role makes a screen reader announce the message
|
|
487
|
+
* when it appears. With several messages, they are a `ul` of `li` items, so each one is read on its own.
|
|
488
|
+
*
|
|
489
|
+
* @param props The error props: see {@link FieldErrorProps}.
|
|
490
|
+
* @returns The error message, hidden while it has no content.
|
|
491
|
+
* @throws Error when rendered outside a {@link Field}.
|
|
492
|
+
* @example
|
|
493
|
+
* import { Field, FieldError, FieldLabel, useField } from "@/components/ui/field";
|
|
494
|
+
* import { Input } from "@/components/ui/input";
|
|
495
|
+
*
|
|
496
|
+
* function EmailControl() {
|
|
497
|
+
* const field = useField();
|
|
498
|
+
* return <Input id={field.id()} aria-describedby={field.describedBy()} aria-invalid={field.invalid() ? true : undefined} />;
|
|
499
|
+
* }
|
|
500
|
+
*
|
|
501
|
+
* export function EmailError() {
|
|
502
|
+
* return (
|
|
503
|
+
* <Field invalid>
|
|
504
|
+
* <FieldLabel>Email</FieldLabel>
|
|
505
|
+
* <EmailControl />
|
|
506
|
+
* <FieldError>Enter a valid email.</FieldError>
|
|
507
|
+
* </Field>
|
|
508
|
+
* );
|
|
509
|
+
* }
|
|
510
|
+
* @example
|
|
511
|
+
* // The messages of a validator: one line each, once each.
|
|
512
|
+
* <FieldError errors={[{ message: "Too short." }, { message: "No digits." }]} />
|
|
513
|
+
* @see {@link useField}
|
|
514
|
+
* @see {@link FieldErrorProps}
|
|
515
|
+
* @since 1.0.0
|
|
516
|
+
*/
|
|
517
|
+
function FieldError(props: FieldErrorProps): JSX.Element {
|
|
518
|
+
const context = fieldContext.use("FieldError");
|
|
519
|
+
const [local, rest] = splitProps(props as unknown as Loose & FieldErrorProps, [
|
|
520
|
+
"class",
|
|
521
|
+
"children",
|
|
522
|
+
"errors",
|
|
523
|
+
]);
|
|
524
|
+
const hasChildren = (): boolean =>
|
|
525
|
+
local.children !== undefined && local.children !== null && local.children !== false;
|
|
526
|
+
// The distinct messages of `errors`, in order. A repeated message is shown once.
|
|
527
|
+
const messages = (): string[] => {
|
|
528
|
+
const seen = new Set<string>();
|
|
529
|
+
const out: string[] = [];
|
|
530
|
+
for (const entry of (local.errors as ReadonlyArray<FieldErrorEntry> | undefined) ?? []) {
|
|
531
|
+
const message = entry?.message;
|
|
532
|
+
if (message === undefined || message === "" || seen.has(message)) continue;
|
|
533
|
+
seen.add(message);
|
|
534
|
+
out.push(message);
|
|
535
|
+
}
|
|
536
|
+
return out;
|
|
537
|
+
};
|
|
538
|
+
// An error with no content is not an error: it is hidden, and the control does not describe
|
|
539
|
+
// itself with it. The registration sits in a branch, so it follows the content as it appears
|
|
540
|
+
// and clears, not only the content the error had at mount.
|
|
541
|
+
const hasContent = (): boolean => hasChildren() || messages().length > 0;
|
|
542
|
+
return (
|
|
543
|
+
<div
|
|
544
|
+
role="alert"
|
|
545
|
+
data-slot="field-error"
|
|
546
|
+
id={context.errorId()}
|
|
547
|
+
hidden={hasContent() ? undefined : true}
|
|
548
|
+
class={cn("text-sm font-normal text-destructive", local.class as string | undefined)}
|
|
549
|
+
{...rest}
|
|
550
|
+
>
|
|
551
|
+
<Show when={hasContent()}>
|
|
552
|
+
{() => {
|
|
553
|
+
context.error.register();
|
|
554
|
+
return null;
|
|
555
|
+
}}
|
|
556
|
+
</Show>
|
|
557
|
+
<Show
|
|
558
|
+
when={hasChildren()}
|
|
559
|
+
fallback={
|
|
560
|
+
<Show when={messages().length > 1} fallback={messages()[0]}>
|
|
561
|
+
<ul class="ms-4 flex list-disc flex-col gap-1">
|
|
562
|
+
<For each={messages()} key={(message: string) => message}>
|
|
563
|
+
{(message: Accessor<string>) => <li>{message()}</li>}
|
|
564
|
+
</For>
|
|
565
|
+
</ul>
|
|
566
|
+
</Show>
|
|
567
|
+
}
|
|
568
|
+
>
|
|
569
|
+
{local.children}
|
|
570
|
+
</Show>
|
|
571
|
+
</div>
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* A divider between parts of a field group, with optional text in its middle, such as "or". It is
|
|
577
|
+
* decorative and has no role.
|
|
578
|
+
*
|
|
579
|
+
* @param props The separator props: the attributes of the rendered `div`, and `children` (the text).
|
|
580
|
+
* @returns The separator.
|
|
581
|
+
* @example
|
|
582
|
+
* import { FieldGroup, FieldSeparator } from "@/components/ui/field";
|
|
583
|
+
*
|
|
584
|
+
* export function Divider() {
|
|
585
|
+
* return (
|
|
586
|
+
* <FieldGroup>
|
|
587
|
+
* <FieldSeparator>or</FieldSeparator>
|
|
588
|
+
* </FieldGroup>
|
|
589
|
+
* );
|
|
590
|
+
* }
|
|
591
|
+
* @see {@link FieldGroup}
|
|
592
|
+
* @since 1.0.0
|
|
593
|
+
*/
|
|
594
|
+
function FieldSeparator(props: ComponentProps<"div">): JSX.Element {
|
|
595
|
+
const [local, rest] = splitProps(props, ["class", "children"]);
|
|
596
|
+
return (
|
|
597
|
+
<div
|
|
598
|
+
data-slot="field-separator"
|
|
599
|
+
class={cn("relative -my-2 h-5 text-sm", local.class)}
|
|
600
|
+
{...rest}
|
|
601
|
+
>
|
|
602
|
+
<div class="absolute inset-0 top-1/2 border-t" />
|
|
603
|
+
{local.children !== undefined && (
|
|
604
|
+
<span class="relative mx-auto block w-fit bg-background px-2 text-muted-foreground">
|
|
605
|
+
{local.children}
|
|
606
|
+
</span>
|
|
607
|
+
)}
|
|
608
|
+
</div>
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export {
|
|
613
|
+
Field,
|
|
614
|
+
FieldContent,
|
|
615
|
+
FieldDescription,
|
|
616
|
+
FieldError,
|
|
617
|
+
FieldGroup,
|
|
618
|
+
FieldLabel,
|
|
619
|
+
FieldLegend,
|
|
620
|
+
FieldSeparator,
|
|
621
|
+
FieldSet,
|
|
622
|
+
FieldTitle,
|
|
623
|
+
useField,
|
|
624
|
+
};
|