@nexub/react 6.0.0 → 6.2.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.
Files changed (33) hide show
  1. package/accessibility.json +64 -1
  2. package/dist/components/DateTimePicker/DateTimePicker.d.ts +20 -0
  3. package/dist/components/DateTimePicker/DateTimePicker.js +67 -0
  4. package/dist/components/DateTimePicker/index.d.ts +2 -0
  5. package/dist/components/DateTimePicker/index.js +4 -0
  6. package/dist/components/EventCalendar/EventCalendar.d.ts +38 -0
  7. package/dist/components/EventCalendar/EventCalendar.js +204 -0
  8. package/dist/components/EventCalendar/index.d.ts +2 -0
  9. package/dist/components/EventCalendar/index.js +4 -0
  10. package/dist/components/Fieldset/Fieldset.d.ts +11 -0
  11. package/dist/components/Fieldset/Fieldset.js +23 -0
  12. package/dist/components/Fieldset/index.d.ts +2 -0
  13. package/dist/components/Fieldset/index.js +4 -0
  14. package/dist/components/Form/Form.d.ts +33 -0
  15. package/dist/components/Form/Form.js +79 -0
  16. package/dist/components/Form/index.d.ts +2 -0
  17. package/dist/components/Form/index.js +5 -0
  18. package/dist/components/FormField/FormField.d.ts +2 -0
  19. package/dist/components/FormField/FormField.js +10 -5
  20. package/dist/components/InlineEdit/InlineEdit.d.ts +21 -0
  21. package/dist/components/InlineEdit/InlineEdit.js +103 -0
  22. package/dist/components/InlineEdit/index.d.ts +2 -0
  23. package/dist/components/InlineEdit/index.js +4 -0
  24. package/dist/components/TimePicker/TimePicker.d.ts +15 -0
  25. package/dist/components/TimePicker/TimePicker.js +26 -0
  26. package/dist/components/TimePicker/index.d.ts +2 -0
  27. package/dist/components/TimePicker/index.js +4 -0
  28. package/dist/hooks/useUnsavedChanges.d.ts +6 -0
  29. package/dist/hooks/useUnsavedChanges.js +15 -0
  30. package/dist/index.d.ts +13 -0
  31. package/dist/index.js +16 -1
  32. package/dist/styles.css +1 -1
  33. package/package.json +6 -6
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "6.0.0",
2
+ "version": "6.2.0",
3
3
  "standard": "WCAG 2.2 Level AA",
4
4
  "method": {
5
5
  "automated": "axe-core rules tagged wcag2a, wcag2aa, wcag21a, wcag21aa and wcag22aa, run on every Storybook story in light and dark, and on open popups (menus, listboxes, dialogs), on every pull request; a new violation fails the build.",
@@ -342,6 +342,15 @@
342
342
  "keyboard": [],
343
343
  "screenReader": []
344
344
  },
345
+ {
346
+ "slug": "date-time-picker",
347
+ "name": "Date Time Picker",
348
+ "package": "@nexub/react",
349
+ "automated": "pass",
350
+ "exceptions": [],
351
+ "keyboard": [],
352
+ "screenReader": []
353
+ },
345
354
  {
346
355
  "slug": "dialog",
347
356
  "name": "Dialog",
@@ -401,6 +410,15 @@
401
410
  "keyboard": [],
402
411
  "screenReader": []
403
412
  },
413
+ {
414
+ "slug": "event-calendar",
415
+ "name": "Event Calendar",
416
+ "package": "@nexub/react",
417
+ "automated": "pass",
418
+ "exceptions": [],
419
+ "keyboard": [],
420
+ "screenReader": []
421
+ },
404
422
  {
405
423
  "slug": "eyebrow-chip",
406
424
  "name": "Eyebrow Chip",
@@ -410,6 +428,15 @@
410
428
  "keyboard": [],
411
429
  "screenReader": []
412
430
  },
431
+ {
432
+ "slug": "fieldset",
433
+ "name": "Fieldset",
434
+ "package": "@nexub/react",
435
+ "automated": "pass",
436
+ "exceptions": [],
437
+ "keyboard": [],
438
+ "screenReader": []
439
+ },
413
440
  {
414
441
  "slug": "filter-builder",
415
442
  "name": "Filter Builder",
@@ -459,6 +486,17 @@
459
486
  "keyboard": [],
460
487
  "screenReader": []
461
488
  },
489
+ {
490
+ "slug": "form",
491
+ "name": "Form",
492
+ "package": "@nexub/react",
493
+ "automated": "pass",
494
+ "exceptions": [],
495
+ "keyboard": [
496
+ "Enter: Submits from any text field; the first invalid field takes focus."
497
+ ],
498
+ "screenReader": []
499
+ },
462
500
  {
463
501
  "slug": "funnel",
464
502
  "name": "Funnel",
@@ -546,6 +584,19 @@
546
584
  "keyboard": [],
547
585
  "screenReader": []
548
586
  },
587
+ {
588
+ "slug": "inline-edit",
589
+ "name": "Inline Edit",
590
+ "package": "@nexub/react",
591
+ "automated": "pass",
592
+ "exceptions": [],
593
+ "keyboard": [
594
+ "Enter / Space: On the text: starts editing.",
595
+ "Enter: In the input: saves.",
596
+ "Escape: Reverts and returns focus to the text."
597
+ ],
598
+ "screenReader": []
599
+ },
549
600
  {
550
601
  "slug": "input",
551
602
  "name": "Input",
@@ -1094,6 +1145,18 @@
1094
1145
  "Render it only while waiting and remove it when tokens start, so it is not announced again."
1095
1146
  ]
1096
1147
  },
1148
+ {
1149
+ "slug": "time-picker",
1150
+ "name": "Time Picker",
1151
+ "package": "@nexub/react",
1152
+ "automated": "pass",
1153
+ "exceptions": [],
1154
+ "keyboard": [
1155
+ "ArrowUp / ArrowDown: Changes the focused segment (hour, minute, AM/PM).",
1156
+ "ArrowLeft / ArrowRight: Moves between segments."
1157
+ ],
1158
+ "screenReader": []
1159
+ },
1097
1160
  {
1098
1161
  "slug": "toast",
1099
1162
  "name": "Toast",
@@ -0,0 +1,20 @@
1
+ import * as React from 'react';
2
+ import type { Locale } from 'date-fns';
3
+ export interface DateTimePickerProps {
4
+ /** Date and time in the reader's local time. */
5
+ value?: Date;
6
+ onChange?: (value: Date | undefined) => void;
7
+ /** Time used when a date is picked first. @default '09:00' */
8
+ defaultTime?: string;
9
+ minuteStep?: number;
10
+ minDate?: Date;
11
+ maxDate?: Date;
12
+ disabled?: boolean;
13
+ /** IANA zone to name next to the fields (label only; the value stays local). */
14
+ timeZone?: string;
15
+ 'aria-label'?: string;
16
+ className?: string;
17
+ locale?: Locale;
18
+ }
19
+ /** A DatePicker and a TimePicker that edit one `Date` together. */
20
+ export declare function DateTimePicker({ value, onChange, defaultTime, minuteStep, minDate, maxDate, disabled, timeZone, 'aria-label': ariaLabel, className, locale, }: DateTimePickerProps): React.JSX.Element;
@@ -0,0 +1,67 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "../../utils/cn.js";
5
+ import { DatePicker } from "../DatePicker/DatePicker.js";
6
+ import { TimePicker } from "../TimePicker/TimePicker.js";
7
+ import { useIntlLocale, useNexubMessages } from "../NexubProvider/NexubProvider.js";
8
+ const pad = (n) => String(n).padStart(2, "0");
9
+ const timeOf = (d) => `${pad(d.getHours())}:${pad(d.getMinutes())}`;
10
+ const withTime = (d, time) => {
11
+ const [h = 0, m = 0] = time.split(":").map(Number);
12
+ const next = new Date(d);
13
+ next.setHours(h, m, 0, 0);
14
+ return next;
15
+ };
16
+ function DateTimePicker({
17
+ value,
18
+ onChange,
19
+ defaultTime = "09:00",
20
+ minuteStep,
21
+ minDate,
22
+ maxDate,
23
+ disabled,
24
+ timeZone,
25
+ "aria-label": ariaLabel,
26
+ className,
27
+ locale
28
+ }) {
29
+ const messages = useNexubMessages();
30
+ const intlLocale = useIntlLocale();
31
+ const [pendingTime, setPendingTime] = React.useState(defaultTime);
32
+ const time = value ? timeOf(value) : pendingTime;
33
+ const zoneName = timeZone ? new Intl.DateTimeFormat(intlLocale, { timeZone, timeZoneName: "shortOffset" }).formatToParts(value ?? /* @__PURE__ */ new Date()).find((p) => p.type === "timeZoneName")?.value ?? timeZone : void 0;
34
+ return /* @__PURE__ */ jsxs("div", { role: "group", "aria-label": ariaLabel, className: cn("flex flex-wrap items-center gap-2", className), children: [
35
+ /* @__PURE__ */ jsx("div", { className: "min-w-40 flex-1", children: /* @__PURE__ */ jsx(
36
+ DatePicker,
37
+ {
38
+ "aria-label": messages.dateLabel,
39
+ value,
40
+ onChange: (date) => onChange?.(date ? withTime(date, time) : void 0),
41
+ minDate,
42
+ maxDate,
43
+ disabled,
44
+ locale
45
+ }
46
+ ) }),
47
+ /* @__PURE__ */ jsx(
48
+ TimePicker,
49
+ {
50
+ "aria-label": messages.timeLabel,
51
+ wrapperClassName: "w-36",
52
+ value: time,
53
+ minuteStep,
54
+ disabled,
55
+ onValueChange: (next) => {
56
+ if (!next) return;
57
+ if (value) onChange?.(withTime(value, next));
58
+ else setPendingTime(next);
59
+ }
60
+ }
61
+ ),
62
+ zoneName && /* @__PURE__ */ jsx("span", { className: "text-body-sm text-[var(--color-on-surface-muted)]", children: messages.timeZoneLabel(`${zoneName} (${timeZone})`) })
63
+ ] });
64
+ }
65
+ export {
66
+ DateTimePicker
67
+ };
@@ -0,0 +1,2 @@
1
+ export { DateTimePicker } from './DateTimePicker.js';
2
+ export type { DateTimePickerProps } from './DateTimePicker.js';
@@ -0,0 +1,4 @@
1
+ import { DateTimePicker } from "./DateTimePicker.js";
2
+ export {
3
+ DateTimePicker
4
+ };
@@ -0,0 +1,38 @@
1
+ import * as React from 'react';
2
+ import { type Day } from 'date-fns';
3
+ export type EventCalendarView = 'month' | 'week';
4
+ export type EventTone = 'primary' | 'success' | 'warning' | 'error' | 'info';
5
+ export interface CalendarEvent {
6
+ id: string;
7
+ title: string;
8
+ start: Date;
9
+ /** Inclusive end; events spanning days show on each of them. */
10
+ end?: Date;
11
+ /** Hides the start time. */
12
+ allDay?: boolean;
13
+ /** @default 'primary' */
14
+ tone?: EventTone;
15
+ }
16
+ export interface EventCalendarProps {
17
+ events: CalendarEvent[];
18
+ /** Any day inside the shown month or week. */
19
+ date?: Date;
20
+ defaultDate?: Date;
21
+ onDateChange?: (date: Date) => void;
22
+ view?: EventCalendarView;
23
+ defaultView?: EventCalendarView;
24
+ onViewChange?: (view: EventCalendarView) => void;
25
+ onEventClick?: (event: CalendarEvent) => void;
26
+ /** A day's number, or its "N more" link in month view. */
27
+ onDayClick?: (date: Date) => void;
28
+ /** Events shown per day in month view before "N more". @default 3 */
29
+ maxEventsPerDay?: number;
30
+ /** 0 = Sunday. Defaults to the locale's first day of the week. */
31
+ weekStartsOn?: Day;
32
+ className?: string;
33
+ }
34
+ /**
35
+ * A month or week grid of events. Display only: your app owns the events and
36
+ * decides what a click on an event or a day does.
37
+ */
38
+ export declare function EventCalendar({ events, date: dateProp, defaultDate, onDateChange, view: viewProp, defaultView, onViewChange, onEventClick, onDayClick, maxEventsPerDay, weekStartsOn: weekStartsOnProp, className, }: EventCalendarProps): React.JSX.Element;
@@ -0,0 +1,204 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import {
5
+ addDays,
6
+ addMonths,
7
+ addWeeks,
8
+ endOfDay,
9
+ endOfMonth,
10
+ endOfWeek,
11
+ format,
12
+ isSameDay,
13
+ isSameMonth,
14
+ startOfDay,
15
+ startOfMonth,
16
+ startOfWeek
17
+ } from "date-fns";
18
+ import { ChevronLeft, ChevronRight } from "@nexub/icons";
19
+ import { cn } from "../../utils/cn.js";
20
+ import { Button } from "../Button/Button.js";
21
+ import { IconButton } from "../IconButton/IconButton.js";
22
+ import { SegmentedControl } from "../SegmentedControl/SegmentedControl.js";
23
+ import { useControllableState } from "../../hooks/useControllableState.js";
24
+ import { useIntlLocale, useLocale, useNexubMessages } from "../NexubProvider/NexubProvider.js";
25
+ const tones = {
26
+ primary: "bg-[var(--color-primary-container)] text-[var(--color-on-primary-container)]",
27
+ success: "bg-[var(--color-success-container)] text-[var(--color-on-success-container)]",
28
+ warning: "bg-[var(--color-warning-container)] text-[var(--color-on-warning-container)]",
29
+ error: "bg-[var(--color-error-container)] text-[var(--color-on-error-container)]",
30
+ info: "bg-[var(--color-info-container)] text-[var(--color-on-info-container)]"
31
+ };
32
+ function localeWeekStart(locale) {
33
+ try {
34
+ const loc = new Intl.Locale(locale ?? new Intl.DateTimeFormat().resolvedOptions().locale);
35
+ const firstDay = loc.getWeekInfo?.().firstDay ?? loc.weekInfo?.firstDay;
36
+ return firstDay ? firstDay % 7 : 0;
37
+ } catch {
38
+ return 0;
39
+ }
40
+ }
41
+ function EventCalendar({
42
+ events,
43
+ date: dateProp,
44
+ defaultDate,
45
+ onDateChange,
46
+ view: viewProp,
47
+ defaultView = "month",
48
+ onViewChange,
49
+ onEventClick,
50
+ onDayClick,
51
+ maxEventsPerDay = 3,
52
+ weekStartsOn: weekStartsOnProp,
53
+ className
54
+ }) {
55
+ const t = useNexubMessages();
56
+ const { dir } = useLocale();
57
+ const intlLocale = useIntlLocale();
58
+ const [date = /* @__PURE__ */ new Date(), setDate] = useControllableState({
59
+ value: dateProp,
60
+ defaultValue: defaultDate ?? /* @__PURE__ */ new Date(),
61
+ onChange: onDateChange
62
+ });
63
+ const [view = "month", setView] = useControllableState({
64
+ value: viewProp,
65
+ defaultValue: defaultView,
66
+ onChange: onViewChange
67
+ });
68
+ const weekStartsOn = weekStartsOnProp ?? localeWeekStart(intlLocale);
69
+ const opts = { weekStartsOn };
70
+ const month = view === "month";
71
+ const first = startOfWeek(month ? startOfMonth(date) : date, opts);
72
+ const last = endOfWeek(month ? endOfMonth(date) : date, opts);
73
+ const days = [];
74
+ for (let d = first; d <= last; d = addDays(d, 1)) days.push(d);
75
+ const weeks = [];
76
+ for (let i = 0; i < days.length; i += 7) weeks.push(days.slice(i, i + 7));
77
+ const sorted = React.useMemo(
78
+ () => [...events].sort((a, b) => a.start.getTime() - b.start.getTime()),
79
+ [events]
80
+ );
81
+ const eventsOn = (day) => sorted.filter((e) => e.start <= endOfDay(day) && (e.end ?? e.start) >= startOfDay(day));
82
+ const fmt = (o) => new Intl.DateTimeFormat(intlLocale, o);
83
+ const title = month ? fmt({ month: "long", year: "numeric" }).format(date) : fmt({ month: "short", day: "numeric", year: "numeric" }).formatRange(days[0], days[6]);
84
+ const timeFmt = fmt({ timeStyle: "short" });
85
+ const fullFmt = fmt({ dateStyle: "full" });
86
+ const today = /* @__PURE__ */ new Date();
87
+ const step = (n) => setDate(month ? addMonths(date, n) : addWeeks(date, n));
88
+ const [Back, Forward] = dir === "rtl" ? [ChevronRight, ChevronLeft] : [ChevronLeft, ChevronRight];
89
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-3", className), children: [
90
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
91
+ /* @__PURE__ */ jsx("h2", { "aria-live": "polite", className: "me-auto text-title-lg font-semibold text-[var(--color-on-surface)]", children: title }),
92
+ /* @__PURE__ */ jsx(Button, { variant: "outlined", size: "sm", onClick: () => setDate(/* @__PURE__ */ new Date()), children: t.today }),
93
+ /* @__PURE__ */ jsx(IconButton, { variant: "ghost", size: "sm", label: month ? t.previousMonth : t.previousWeek, onClick: () => step(-1), children: /* @__PURE__ */ jsx(Back, { size: "sm", "aria-hidden": "true" }) }),
94
+ /* @__PURE__ */ jsx(IconButton, { variant: "ghost", size: "sm", label: month ? t.nextMonth : t.nextWeek, onClick: () => step(1), children: /* @__PURE__ */ jsx(Forward, { size: "sm", "aria-hidden": "true" }) }),
95
+ /* @__PURE__ */ jsxs(
96
+ SegmentedControl.Root,
97
+ {
98
+ size: "sm",
99
+ "aria-label": t.calendarView,
100
+ value: view,
101
+ onValueChange: (v) => v && setView(v),
102
+ children: [
103
+ /* @__PURE__ */ jsx(SegmentedControl.Item, { value: "month", children: t.monthView }),
104
+ /* @__PURE__ */ jsx(SegmentedControl.Item, { value: "week", children: t.weekView })
105
+ ]
106
+ }
107
+ )
108
+ ] }),
109
+ /* @__PURE__ */ jsxs("table", { className: "w-full table-fixed border-collapse text-body-sm", children: [
110
+ /* @__PURE__ */ jsx("caption", { className: "sr-only", children: title }),
111
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: weeks[0].map((d) => /* @__PURE__ */ jsx(
112
+ "th",
113
+ {
114
+ scope: "col",
115
+ abbr: fmt({ weekday: "long" }).format(d),
116
+ className: "pb-2 text-label-md font-medium text-[var(--color-on-surface-muted)]",
117
+ children: fmt({ weekday: "short" }).format(d)
118
+ },
119
+ d.getDay()
120
+ )) }) }),
121
+ /* @__PURE__ */ jsx("tbody", { children: weeks.map((week) => /* @__PURE__ */ jsx("tr", { children: week.map((day) => {
122
+ const list = eventsOn(day);
123
+ const shown = month ? list.slice(0, maxEventsPerDay) : list;
124
+ const hidden = list.length - shown.length;
125
+ const isToday = isSameDay(day, today);
126
+ const outside = month && !isSameMonth(day, date);
127
+ return /* @__PURE__ */ jsxs(
128
+ "td",
129
+ {
130
+ "aria-current": isToday ? "date" : void 0,
131
+ className: cn(
132
+ "border border-[var(--color-outline-variant)] p-1 align-top",
133
+ month ? "h-28" : "h-80",
134
+ outside && "bg-[var(--color-surface-container-low)]"
135
+ ),
136
+ children: [
137
+ /* @__PURE__ */ jsx(
138
+ DayNumber,
139
+ {
140
+ day,
141
+ label: fullFmt.format(day),
142
+ isToday,
143
+ muted: outside,
144
+ onClick: onDayClick
145
+ }
146
+ ),
147
+ /* @__PURE__ */ jsx("ul", { className: "mt-1 flex flex-col gap-0.5", children: shown.map((e) => {
148
+ const time = e.allDay || !isSameDay(e.start, day) ? "" : timeFmt.format(e.start);
149
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [
150
+ time && /* @__PURE__ */ jsx("span", { className: "me-1 tabular-nums opacity-80", children: time }),
151
+ e.title
152
+ ] });
153
+ const cls = cn(
154
+ "block w-full truncate rounded-shape-min px-1.5 py-0.5 text-start text-label-md",
155
+ tones[e.tone ?? "primary"]
156
+ );
157
+ return /* @__PURE__ */ jsx("li", { children: onEventClick ? /* @__PURE__ */ jsx("button", { type: "button", className: cn(cls, "hover:opacity-90"), onClick: () => onEventClick(e), children: body }) : /* @__PURE__ */ jsx("span", { className: cls, children: body }) }, e.id);
158
+ }) }),
159
+ hidden > 0 && (onDayClick ? /* @__PURE__ */ jsx(
160
+ "button",
161
+ {
162
+ type: "button",
163
+ className: "min-h-6 px-1.5 text-label-md text-[var(--color-on-surface-variant)] hover:underline",
164
+ onClick: () => onDayClick(day),
165
+ children: t.moreCount(hidden)
166
+ }
167
+ ) : /* @__PURE__ */ jsx("span", { className: "mt-0.5 block px-1.5 text-label-md text-[var(--color-on-surface-variant)]", children: t.moreCount(hidden) }))
168
+ ]
169
+ },
170
+ day.toISOString()
171
+ );
172
+ }) }, week[0].toISOString())) })
173
+ ] })
174
+ ] });
175
+ }
176
+ function DayNumber({
177
+ day,
178
+ label,
179
+ isToday,
180
+ muted,
181
+ onClick
182
+ }) {
183
+ const cls = cn(
184
+ "inline-flex size-7 items-center justify-center rounded-full text-label-md tabular-nums",
185
+ isToday ? "bg-[var(--color-primary)] text-[var(--color-on-primary)]" : muted ? "text-[var(--color-on-surface-muted)]" : "text-[var(--color-on-surface)]"
186
+ );
187
+ const n = day.getDate();
188
+ return onClick ? /* @__PURE__ */ jsx(
189
+ "button",
190
+ {
191
+ type: "button",
192
+ "aria-label": label,
193
+ className: cn(cls, !isToday && "hover:bg-[var(--color-surface-container-high)]"),
194
+ onClick: () => onClick(day),
195
+ children: n
196
+ }
197
+ ) : /* @__PURE__ */ jsxs("time", { dateTime: format(day, "yyyy-MM-dd"), className: cls, children: [
198
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: n }),
199
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: label })
200
+ ] });
201
+ }
202
+ export {
203
+ EventCalendar
204
+ };
@@ -0,0 +1,2 @@
1
+ export { EventCalendar } from './EventCalendar.js';
2
+ export type { EventCalendarProps, EventCalendarView, CalendarEvent, EventTone } from './EventCalendar.js';
@@ -0,0 +1,4 @@
1
+ import { EventCalendar } from "./EventCalendar.js";
2
+ export {
3
+ EventCalendar
4
+ };
@@ -0,0 +1,11 @@
1
+ import * as React from 'react';
2
+ export interface FieldsetProps extends React.FieldsetHTMLAttributes<HTMLFieldSetElement> {
3
+ /** The group's name, read before each field inside it. */
4
+ legend: React.ReactNode;
5
+ description?: React.ReactNode;
6
+ }
7
+ /**
8
+ * Groups related fields under a heading (`<fieldset>` + `<legend>`). `disabled`
9
+ * disables every control inside it, natively.
10
+ */
11
+ export declare const Fieldset: React.ForwardRefExoticComponent<FieldsetProps & React.RefAttributes<HTMLFieldSetElement>>;
@@ -0,0 +1,23 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { cn } from "../../utils/cn.js";
4
+ const Fieldset = React.forwardRef(function Fieldset2({ legend, description, className, children, ...props }, ref) {
5
+ const descriptionId = React.useId();
6
+ return /* @__PURE__ */ jsxs(
7
+ "fieldset",
8
+ {
9
+ ref,
10
+ "aria-describedby": description ? descriptionId : void 0,
11
+ className: cn("m-0 flex min-w-0 flex-col gap-4 border-0 p-0", className),
12
+ ...props,
13
+ children: [
14
+ /* @__PURE__ */ jsx("legend", { className: "mb-1 p-0 text-title-sm font-semibold text-[var(--color-on-surface)]", children: legend }),
15
+ description && /* @__PURE__ */ jsx("p", { id: descriptionId, className: "text-body-sm text-[var(--color-on-surface-muted)]", children: description }),
16
+ children
17
+ ]
18
+ }
19
+ );
20
+ });
21
+ export {
22
+ Fieldset
23
+ };
@@ -0,0 +1,2 @@
1
+ export { Fieldset } from './Fieldset.js';
2
+ export type { FieldsetProps } from './Fieldset.js';
@@ -0,0 +1,4 @@
1
+ import { Fieldset } from "./Fieldset.js";
2
+ export {
3
+ Fieldset
4
+ };
@@ -0,0 +1,33 @@
1
+ import * as React from 'react';
2
+ export interface FormErrorItem {
3
+ /** The field's `name`. */
4
+ name: string;
5
+ /** The field's DOM id, or its name when it has none (ErrorSummary link target). */
6
+ id: string;
7
+ message: string;
8
+ }
9
+ export type FormErrors = Record<string, string>;
10
+ export interface FormContextValue {
11
+ errors: FormErrors;
12
+ submitting: boolean;
13
+ }
14
+ /** Errors and submitting state of the closest `Form`, or null outside one. */
15
+ export declare function useForm(): FormContextValue | null;
16
+ export interface FormProps extends Omit<React.FormHTMLAttributes<HTMLFormElement>, 'onSubmit' | 'onInvalid'> {
17
+ /**
18
+ * Extra checks after the browser's own (`required`, `type`, `pattern`, `min`…).
19
+ * Return `{ fieldName: message }`; empty values mean valid. Plug zod or any
20
+ * schema library in here.
21
+ */
22
+ validate?: (data: FormData) => Record<string, string | null | undefined | false> | void;
23
+ /** Called with the form data only when every field is valid. A promise sets `submitting`. */
24
+ onSubmit?: (data: FormData, event: React.FormEvent<HTMLFormElement>) => void | Promise<unknown>;
25
+ /** Called after a failed submit, in field order; pass the list to ErrorSummary. */
26
+ onInvalidSubmit?: (errors: FormErrorItem[]) => void;
27
+ }
28
+ /**
29
+ * A `<form>` that validates on submit with the browser's constraint validation
30
+ * plus your `validate`, shows the messages through FormField, and focuses the
31
+ * first invalid field. A field's error clears as soon as it is edited.
32
+ */
33
+ export declare const Form: React.ForwardRefExoticComponent<FormProps & React.RefAttributes<HTMLFormElement>>;
@@ -0,0 +1,79 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "../../utils/cn.js";
5
+ const FormContext = React.createContext(null);
6
+ function useForm() {
7
+ return React.useContext(FormContext);
8
+ }
9
+ const isControl = (el) => "validity" in el && "name" in el && Boolean(el.name);
10
+ const Form = React.forwardRef(function Form2({ validate, onSubmit, onInvalidSubmit, onInput, className, children, ...props }, ref) {
11
+ const [errors, setErrors] = React.useState({});
12
+ const [submitting, setSubmitting] = React.useState(false);
13
+ const handleSubmit = async (event) => {
14
+ event.preventDefault();
15
+ if (submitting) return;
16
+ const form = event.currentTarget;
17
+ const data = new FormData(form);
18
+ const controls = Array.from(form.elements).filter(isControl).filter((el) => !el.disabled);
19
+ const next = {};
20
+ for (const el of controls) {
21
+ if (!el.validity.valid && !next[el.name]) next[el.name] = el.validationMessage;
22
+ }
23
+ for (const [name, message] of Object.entries(validate?.(data) ?? {})) {
24
+ if (message && !next[name]) next[name] = message;
25
+ }
26
+ setErrors(next);
27
+ if (Object.keys(next).length) {
28
+ const seen = /* @__PURE__ */ new Set();
29
+ const items = [];
30
+ for (const el of controls) {
31
+ if (next[el.name] && !seen.has(el.name)) {
32
+ seen.add(el.name);
33
+ items.push({ name: el.name, id: el.id || el.name, message: next[el.name] });
34
+ }
35
+ }
36
+ for (const name of Object.keys(next)) {
37
+ if (!seen.has(name)) items.push({ name, id: name, message: next[name] });
38
+ }
39
+ onInvalidSubmit?.(items);
40
+ const first = controls.find((el) => next[el.name] && el.type !== "hidden");
41
+ first?.focus();
42
+ return;
43
+ }
44
+ const result = onSubmit?.(data, event);
45
+ if (result instanceof Promise) {
46
+ setSubmitting(true);
47
+ try {
48
+ await result;
49
+ } finally {
50
+ setSubmitting(false);
51
+ }
52
+ }
53
+ };
54
+ const handleInput = (event) => {
55
+ onInput?.(event);
56
+ const name = event.target.name;
57
+ if (name && errors[name]) {
58
+ setErrors(({ [name]: _cleared, ...rest }) => rest);
59
+ }
60
+ };
61
+ const value = React.useMemo(() => ({ errors, submitting }), [errors, submitting]);
62
+ return /* @__PURE__ */ jsx(FormContext.Provider, { value, children: /* @__PURE__ */ jsx(
63
+ "form",
64
+ {
65
+ ref,
66
+ noValidate: true,
67
+ "aria-busy": submitting || void 0,
68
+ className: cn("flex flex-col gap-5", className),
69
+ onSubmit: handleSubmit,
70
+ onInput: handleInput,
71
+ ...props,
72
+ children
73
+ }
74
+ ) });
75
+ });
76
+ export {
77
+ Form,
78
+ useForm
79
+ };
@@ -0,0 +1,2 @@
1
+ export { Form, useForm } from './Form.js';
2
+ export type { FormProps, FormErrorItem, FormErrors, FormContextValue } from './Form.js';
@@ -0,0 +1,5 @@
1
+ import { Form, useForm } from "./Form.js";
2
+ export {
3
+ Form,
4
+ useForm
5
+ };
@@ -10,6 +10,8 @@ export interface FormFieldContextValue {
10
10
  disabled?: boolean;
11
11
  descriptionId: string;
12
12
  errorId: string;
13
+ /** The message from the enclosing Form for this field's `name`. */
14
+ formError?: string;
13
15
  }
14
16
  export interface FormFieldRootProps extends React.HTMLAttributes<HTMLDivElement> {
15
17
  id?: string;