@glyphui/ui 0.0.0-stage → 0.1.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 (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -2
  3. package/dist/data-table.d.ts +110 -0
  4. package/dist/data-table.js +225 -0
  5. package/dist/field-DXZD0l8w.js +107 -0
  6. package/dist/field-context-B8cYheXz.js +321 -0
  7. package/dist/form.d.ts +79 -0
  8. package/dist/form.js +84 -0
  9. package/dist/index-BWK4czIG.d.ts +233 -0
  10. package/dist/index.d.ts +2155 -0
  11. package/dist/index.js +1529 -0
  12. package/dist/registry-BGSJoiLq.d.ts +9 -0
  13. package/dist/states-BxPjCUxz.js +287 -0
  14. package/dist/tv-DuMh7UH2.d.ts +6 -0
  15. package/package.json +113 -3
  16. package/src/components/avatar/avatar.styles.ts +22 -0
  17. package/src/components/avatar/avatar.tsx +62 -0
  18. package/src/components/avatar/index.ts +2 -0
  19. package/src/components/badge/badge.styles.ts +69 -0
  20. package/src/components/badge/badge.tsx +30 -0
  21. package/src/components/badge/index.ts +2 -0
  22. package/src/components/button/button.styles.ts +43 -0
  23. package/src/components/button/button.tsx +77 -0
  24. package/src/components/button/index.ts +2 -0
  25. package/src/components/card/card.styles.ts +32 -0
  26. package/src/components/card/card.tsx +91 -0
  27. package/src/components/card/index.ts +11 -0
  28. package/src/components/checkbox/checkbox.tsx +39 -0
  29. package/src/components/checkbox/index.ts +1 -0
  30. package/src/components/container/container.tsx +35 -0
  31. package/src/components/container/index.ts +1 -0
  32. package/src/components/eyebrow/eyebrow.styles.ts +14 -0
  33. package/src/components/eyebrow/eyebrow.tsx +19 -0
  34. package/src/components/eyebrow/index.ts +2 -0
  35. package/src/components/field/field-context.ts +40 -0
  36. package/src/components/field/field.styles.ts +23 -0
  37. package/src/components/field/field.tsx +109 -0
  38. package/src/components/field/index.ts +3 -0
  39. package/src/components/heading/heading.styles.ts +16 -0
  40. package/src/components/heading/heading.tsx +26 -0
  41. package/src/components/heading/index.ts +2 -0
  42. package/src/components/input/index.ts +2 -0
  43. package/src/components/input/input.styles.ts +36 -0
  44. package/src/components/input/input.tsx +33 -0
  45. package/src/components/kbd/index.ts +1 -0
  46. package/src/components/kbd/kbd.tsx +21 -0
  47. package/src/components/label/index.ts +1 -0
  48. package/src/components/label/label.tsx +21 -0
  49. package/src/components/section/index.ts +1 -0
  50. package/src/components/section/section.tsx +39 -0
  51. package/src/components/segmented-control/index.ts +6 -0
  52. package/src/components/segmented-control/segmented-control.tsx +79 -0
  53. package/src/components/select/index.ts +1 -0
  54. package/src/components/select/select.tsx +133 -0
  55. package/src/components/separator/index.ts +1 -0
  56. package/src/components/separator/separator.tsx +29 -0
  57. package/src/components/skeleton/index.ts +1 -0
  58. package/src/components/skeleton/skeleton.tsx +20 -0
  59. package/src/components/spinner/index.ts +2 -0
  60. package/src/components/spinner/spinner.styles.ts +19 -0
  61. package/src/components/spinner/spinner.tsx +42 -0
  62. package/src/components/stack/index.ts +1 -0
  63. package/src/components/stack/stack.tsx +62 -0
  64. package/src/components/switch/index.ts +1 -0
  65. package/src/components/switch/switch.tsx +48 -0
  66. package/src/components/table/index.ts +10 -0
  67. package/src/components/table/table.tsx +90 -0
  68. package/src/components/tabs/index.ts +1 -0
  69. package/src/components/tabs/tabs.tsx +92 -0
  70. package/src/components/text/index.ts +2 -0
  71. package/src/components/text/text.styles.ts +32 -0
  72. package/src/components/text/text.tsx +17 -0
  73. package/src/components/textarea/index.ts +1 -0
  74. package/src/components/textarea/textarea.tsx +27 -0
  75. package/src/components/tooltip/index.ts +1 -0
  76. package/src/components/tooltip/tooltip.tsx +42 -0
  77. package/src/hooks/index.ts +4 -0
  78. package/src/hooks/use-component-config.ts +42 -0
  79. package/src/hooks/use-controllable-state.ts +27 -0
  80. package/src/hooks/use-media-query.ts +14 -0
  81. package/src/hooks/use-scrolled.ts +14 -0
  82. package/src/index.ts +52 -0
  83. package/src/internal/icons.tsx +99 -0
  84. package/src/patterns/app-shell/app-shell-context.ts +27 -0
  85. package/src/patterns/app-shell/app-shell.styles.ts +127 -0
  86. package/src/patterns/app-shell/app-shell.tsx +81 -0
  87. package/src/patterns/app-shell/index.ts +16 -0
  88. package/src/patterns/app-shell/navbar.tsx +119 -0
  89. package/src/patterns/app-shell/page-header.tsx +30 -0
  90. package/src/patterns/app-shell/sidebar.tsx +163 -0
  91. package/src/patterns/async/async.tsx +83 -0
  92. package/src/patterns/async/index.ts +10 -0
  93. package/src/patterns/async/states.tsx +128 -0
  94. package/src/patterns/data-table/data-table.tsx +367 -0
  95. package/src/patterns/data-table/index.ts +2 -0
  96. package/src/patterns/form/form.tsx +150 -0
  97. package/src/patterns/form/index.ts +13 -0
  98. package/src/provider/color-mode.tsx +133 -0
  99. package/src/provider/glyph-context.ts +42 -0
  100. package/src/provider/glyph-provider.tsx +41 -0
  101. package/src/provider/index.ts +18 -0
  102. package/src/provider/theme-scope.tsx +36 -0
  103. package/src/provider/theme-style.tsx +19 -0
  104. package/src/register.ts +69 -0
  105. package/src/themes.ts +56 -0
  106. package/src/utils/cn.ts +12 -0
  107. package/src/utils/compose-refs.ts +11 -0
  108. package/src/utils/registry.ts +9 -0
  109. package/src/utils/styles.ts +13 -0
  110. package/src/utils/tv.ts +7 -0
  111. package/src/utils/tw-merge-config.ts +14 -0
  112. package/styles.css +178 -0
@@ -0,0 +1,321 @@
1
+ "use client";
2
+ import { textTokens } from "@glyphui/tokens";
3
+ import { Slot } from "radix-ui";
4
+ import { Fragment, createContext, useContext } from "react";
5
+ import { createTV } from "tailwind-variants";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ //#region src/provider/glyph-context.ts
8
+ const GlyphContext = createContext(null);
9
+ /** The active theme and app-level settings. Must be used under `GlyphProvider`. */
10
+ function useGlyph() {
11
+ const value = useContext(GlyphContext);
12
+ if (!value) throw new Error("useGlyph must be used inside <GlyphProvider>.");
13
+ return value;
14
+ }
15
+ /** The router-aware link component configured on `GlyphProvider`. */
16
+ function useLinkComponent() {
17
+ return useContext(GlyphContext)?.linkComponent ?? "a";
18
+ }
19
+ //#endregion
20
+ //#region src/hooks/use-component-config.ts
21
+ /**
22
+ * Applies the theme's per-component settings (`theme.components[name]`):
23
+ * fills unset variant props from `defaultVariants` and returns the extra slot classes.
24
+ * Every styled component calls this first.
25
+ */
26
+ function useComponentConfig(name, props) {
27
+ const override = (useContext(GlyphContext)?.theme)?.components[name];
28
+ if (!override) return [props, {}];
29
+ let merged = props;
30
+ const defaults = override.defaultVariants;
31
+ if (defaults) {
32
+ merged = { ...props };
33
+ for (const [key, value] of Object.entries(defaults)) if (merged[key] === void 0) merged[key] = value;
34
+ }
35
+ return [merged, override.className ?? {}];
36
+ }
37
+ //#endregion
38
+ //#region src/utils/tw-merge-config.ts
39
+ /**
40
+ * Teaches tailwind-merge about Glyph's custom utilities so `cn('text-h1', 'text-small')`
41
+ * keeps only the last size instead of treating `text-h1` as a color.
42
+ */
43
+ const twMergeConfig = { extend: { theme: {
44
+ text: [...textTokens],
45
+ radius: ["pill"]
46
+ } } };
47
+ //#endregion
48
+ //#region src/utils/tv.ts
49
+ /** `tailwind-variants` preconfigured with Glyph's merge rules. All component styles use this. */
50
+ const tv = createTV({ twMergeConfig });
51
+ //#endregion
52
+ //#region src/utils/styles.ts
53
+ /** Shared class fragments. Kept as literals so Tailwind's scanner picks them up. */
54
+ const focusRing = "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background";
55
+ const themedBorder = "border-(length:--glyph-border-width)";
56
+ const transition = "transition-[color,background-color,border-color,box-shadow,transform,opacity] duration-(--glyph-duration-base) ease-standard";
57
+ /** Popover surfaces: Select, Tooltip-like menus. */
58
+ const popover = "z-50 overflow-hidden rounded-[min(var(--radius-md),var(--radius-lg))] border-(length:--glyph-border-width) border-border bg-raised text-foreground shadow-lg animate-pop-in";
59
+ //#endregion
60
+ //#region src/components/spinner/spinner.styles.ts
61
+ const spinnerStyles = tv({
62
+ slots: {
63
+ base: "inline-block shrink-0 animate-spin",
64
+ track: "opacity-20",
65
+ arc: ""
66
+ },
67
+ variants: { size: {
68
+ sm: { base: "size-3.5" },
69
+ md: { base: "size-4" },
70
+ lg: { base: "size-6" }
71
+ } },
72
+ defaultVariants: { size: "md" }
73
+ });
74
+ //#endregion
75
+ //#region src/components/spinner/spinner.tsx
76
+ function Spinner(inProps) {
77
+ const [props, slots] = useComponentConfig("Spinner", inProps);
78
+ const { size, label, className, ...rest } = props;
79
+ const s = spinnerStyles({ size });
80
+ return /* @__PURE__ */ jsxs("svg", {
81
+ "data-slot": "spinner",
82
+ viewBox: "0 0 24 24",
83
+ fill: "none",
84
+ role: label ? "status" : void 0,
85
+ "aria-label": label,
86
+ "aria-hidden": label ? void 0 : true,
87
+ className: s.base({ class: [slots.base, className] }),
88
+ ...rest,
89
+ children: [/* @__PURE__ */ jsx("circle", {
90
+ className: s.track({ class: slots.track }),
91
+ cx: "12",
92
+ cy: "12",
93
+ r: "9",
94
+ stroke: "currentColor",
95
+ strokeWidth: "3"
96
+ }), /* @__PURE__ */ jsx("path", {
97
+ className: s.arc({ class: slots.arc }),
98
+ d: "M21 12a9 9 0 0 0-9-9",
99
+ stroke: "currentColor",
100
+ strokeWidth: "3",
101
+ strokeLinecap: "round"
102
+ })]
103
+ });
104
+ }
105
+ //#endregion
106
+ //#region src/components/button/button.styles.ts
107
+ const buttonStyles = tv({
108
+ slots: {
109
+ base: [
110
+ "relative inline-flex shrink-0 cursor-pointer select-none items-center justify-center gap-2 whitespace-nowrap",
111
+ "rounded-md border-transparent font-medium",
112
+ themedBorder,
113
+ transition,
114
+ focusRing,
115
+ "disabled:pointer-events-none disabled:opacity-50 aria-busy:cursor-progress"
116
+ ],
117
+ icon: "inline-flex size-4 shrink-0 items-center justify-center [&>svg]:size-full"
118
+ },
119
+ variants: {
120
+ variant: {
121
+ primary: { base: "bg-primary text-primary-foreground shadow-sm hover:bg-primary/88" },
122
+ secondary: { base: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/88" },
123
+ outline: { base: "border-border bg-surface text-foreground shadow-xs hover:border-border-strong hover:bg-muted" },
124
+ ghost: { base: "text-muted-foreground hover:bg-muted hover:text-foreground" },
125
+ danger: { base: "bg-danger text-danger-foreground shadow-sm hover:bg-danger/88" },
126
+ link: { base: "h-auto px-0 text-primary underline-offset-4 shadow-none hover:underline" }
127
+ },
128
+ size: {
129
+ sm: {
130
+ base: "h-(--glyph-control-sm) px-3 text-small",
131
+ icon: "size-3.5"
132
+ },
133
+ md: { base: "h-(--glyph-control-md) px-4 text-small" },
134
+ lg: {
135
+ base: "h-(--glyph-control-lg) px-5 text-body",
136
+ icon: "size-[1.125rem]"
137
+ }
138
+ },
139
+ iconOnly: { true: { base: "px-0" } },
140
+ fullWidth: { true: { base: "w-full" } }
141
+ },
142
+ compoundVariants: [
143
+ {
144
+ iconOnly: true,
145
+ size: "sm",
146
+ class: { base: "w-(--glyph-control-sm)" }
147
+ },
148
+ {
149
+ iconOnly: true,
150
+ size: "md",
151
+ class: { base: "w-(--glyph-control-md)" }
152
+ },
153
+ {
154
+ iconOnly: true,
155
+ size: "lg",
156
+ class: { base: "w-(--glyph-control-lg)" }
157
+ }
158
+ ],
159
+ defaultVariants: {
160
+ variant: "primary",
161
+ size: "md"
162
+ }
163
+ });
164
+ //#endregion
165
+ //#region src/components/button/button.tsx
166
+ function Button(inProps) {
167
+ const [props, slots] = useComponentConfig("Button", inProps);
168
+ const { variant, size, iconOnly, fullWidth, asChild = false, loading = false, leftIcon, rightIcon, disabled, type, className, children, ...rest } = props;
169
+ const s = buttonStyles({
170
+ variant,
171
+ size,
172
+ iconOnly,
173
+ fullWidth
174
+ });
175
+ const Comp = asChild ? Slot.Root : "button";
176
+ const icon = (node) => node ? /* @__PURE__ */ jsx("span", {
177
+ "data-slot": "button-icon",
178
+ className: s.icon({ class: slots.icon }),
179
+ "aria-hidden": "true",
180
+ children: node
181
+ }) : null;
182
+ return /* @__PURE__ */ jsx(Comp, {
183
+ "data-slot": "button",
184
+ "data-variant": variant ?? "primary",
185
+ className: s.base({ class: [slots.base, className] }),
186
+ "aria-busy": loading || void 0,
187
+ ...!asChild && {
188
+ type: type ?? "button",
189
+ disabled: disabled || loading
190
+ },
191
+ ...rest,
192
+ children: iconOnly ? icon(loading ? /* @__PURE__ */ jsx(Spinner, { size: size === "lg" ? "md" : "sm" }) : children) : [
193
+ /* @__PURE__ */ jsx(Fragment, { children: icon(loading ? /* @__PURE__ */ jsx(Spinner, { size: "sm" }) : leftIcon) }, "start"),
194
+ /* @__PURE__ */ jsx(Slot.Slottable, { children }, "content"),
195
+ /* @__PURE__ */ jsx(Fragment, { children: icon(rightIcon) }, "end")
196
+ ]
197
+ });
198
+ }
199
+ /** A square button holding one icon. Shares Button's variants and theme overrides. */
200
+ function IconButton({ icon, variant = "ghost", ...props }) {
201
+ return /* @__PURE__ */ jsx(Button, {
202
+ variant,
203
+ ...props,
204
+ iconOnly: true,
205
+ children: icon
206
+ });
207
+ }
208
+ //#endregion
209
+ //#region src/internal/icons.tsx
210
+ function Icon({ children, ...props }) {
211
+ return /* @__PURE__ */ jsx("svg", {
212
+ xmlns: "http://www.w3.org/2000/svg",
213
+ width: "16",
214
+ height: "16",
215
+ viewBox: "0 0 24 24",
216
+ fill: "none",
217
+ stroke: "currentColor",
218
+ strokeWidth: "2",
219
+ strokeLinecap: "round",
220
+ strokeLinejoin: "round",
221
+ "aria-hidden": "true",
222
+ ...props,
223
+ children
224
+ });
225
+ }
226
+ const CheckIcon = (p) => /* @__PURE__ */ jsx(Icon, {
227
+ ...p,
228
+ children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" })
229
+ });
230
+ const MinusIcon = (p) => /* @__PURE__ */ jsx(Icon, {
231
+ ...p,
232
+ children: /* @__PURE__ */ jsx("path", { d: "M5 12h14" })
233
+ });
234
+ const ChevronDownIcon = (p) => /* @__PURE__ */ jsx(Icon, {
235
+ ...p,
236
+ children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" })
237
+ });
238
+ const ChevronLeftIcon = (p) => /* @__PURE__ */ jsx(Icon, {
239
+ ...p,
240
+ children: /* @__PURE__ */ jsx("path", { d: "m15 18-6-6 6-6" })
241
+ });
242
+ const ChevronRightIcon = (p) => /* @__PURE__ */ jsx(Icon, {
243
+ ...p,
244
+ children: /* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" })
245
+ });
246
+ const ArrowUpIcon = (p) => /* @__PURE__ */ jsx(Icon, {
247
+ ...p,
248
+ children: /* @__PURE__ */ jsx("path", { d: "m5 12 7-7 7 7M12 19V5" })
249
+ });
250
+ const ArrowDownIcon = (p) => /* @__PURE__ */ jsx(Icon, {
251
+ ...p,
252
+ children: /* @__PURE__ */ jsx("path", { d: "M12 5v14M19 12l-7 7-7-7" })
253
+ });
254
+ const ChevronsUpDownIcon = (p) => /* @__PURE__ */ jsx(Icon, {
255
+ ...p,
256
+ children: /* @__PURE__ */ jsx("path", { d: "m7 15 5 5 5-5M7 9l5-5 5 5" })
257
+ });
258
+ const XIcon = (p) => /* @__PURE__ */ jsx(Icon, {
259
+ ...p,
260
+ children: /* @__PURE__ */ jsx("path", { d: "M18 6 6 18M6 6l12 12" })
261
+ });
262
+ const MenuIcon = (p) => /* @__PURE__ */ jsx(Icon, {
263
+ ...p,
264
+ children: /* @__PURE__ */ jsx("path", { d: "M4 6h16M4 12h16M4 18h16" })
265
+ });
266
+ const PanelLeftIcon = (p) => /* @__PURE__ */ jsxs(Icon, {
267
+ ...p,
268
+ children: [/* @__PURE__ */ jsx("rect", {
269
+ width: "18",
270
+ height: "18",
271
+ x: "3",
272
+ y: "3",
273
+ rx: "2"
274
+ }), /* @__PURE__ */ jsx("path", { d: "M9 3v18" })]
275
+ });
276
+ const SearchIcon = (p) => /* @__PURE__ */ jsxs(Icon, {
277
+ ...p,
278
+ children: [/* @__PURE__ */ jsx("circle", {
279
+ cx: "11",
280
+ cy: "11",
281
+ r: "7"
282
+ }), /* @__PURE__ */ jsx("path", { d: "m20 20-3.5-3.5" })]
283
+ });
284
+ const InboxIcon = (p) => /* @__PURE__ */ jsxs(Icon, {
285
+ ...p,
286
+ children: [/* @__PURE__ */ jsx("path", { d: "M22 12h-6l-2 3h-4l-2-3H2" }), /* @__PURE__ */ jsx("path", { d: "M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" })]
287
+ });
288
+ const AlertIcon = (p) => /* @__PURE__ */ jsxs(Icon, {
289
+ ...p,
290
+ children: [/* @__PURE__ */ jsx("circle", {
291
+ cx: "12",
292
+ cy: "12",
293
+ r: "10"
294
+ }), /* @__PURE__ */ jsx("path", { d: "M12 8v4M12 16h.01" })]
295
+ });
296
+ //#endregion
297
+ //#region src/components/field/field-context.ts
298
+ const FieldContext = createContext(null);
299
+ /**
300
+ * Connects a form control to the surrounding `Field`: id for the label, `aria-describedby`
301
+ * for description and error, `aria-invalid`, `required`, `disabled`. Props passed directly win.
302
+ */
303
+ function useFieldControl(props) {
304
+ const field = useContext(FieldContext);
305
+ if (!field) return props;
306
+ const describedBy = [
307
+ field.descriptionId,
308
+ field.errorId,
309
+ props["aria-describedby"]
310
+ ].filter(Boolean).join(" ");
311
+ return {
312
+ ...props,
313
+ id: props.id ?? field.id,
314
+ disabled: props.disabled ?? (field.disabled || void 0),
315
+ required: props.required ?? (field.required || void 0),
316
+ "aria-describedby": describedBy || void 0,
317
+ "aria-invalid": props["aria-invalid"] ?? (field.invalid || void 0)
318
+ };
319
+ }
320
+ //#endregion
321
+ export { useGlyph as A, popover as C, twMergeConfig as D, tv as E, useComponentConfig as O, focusRing as S, transition as T, Button as _, ArrowUpIcon as a, Spinner as b, ChevronLeftIcon as c, InboxIcon as d, MenuIcon as f, XIcon as g, SearchIcon as h, ArrowDownIcon as i, useLinkComponent as j, GlyphContext as k, ChevronRightIcon as l, PanelLeftIcon as m, useFieldControl as n, CheckIcon as o, MinusIcon as p, AlertIcon as r, ChevronDownIcon as s, FieldContext as t, ChevronsUpDownIcon as u, IconButton as v, themedBorder as w, spinnerStyles as x, buttonStyles as y };
package/dist/form.d.ts ADDED
@@ -0,0 +1,79 @@
1
+ import { i as ButtonProps, n as FieldProps } from "./index-BWK4czIG.js";
2
+ import { ComponentProps, ReactNode } from "react";
3
+ import { Control, ControllerRenderProps, FieldPath, FieldValues, UseFormProps, UseFormReturn } from "react-hook-form";
4
+ import { StandardSchemaV1 } from "@standard-schema/spec";
5
+ //#region src/patterns/form/form.d.ts
6
+ type AnySchema = StandardSchemaV1<FieldValues, FieldValues>;
7
+ type InputOf<S extends AnySchema> = StandardSchemaV1.InferInput<S>;
8
+ type OutputOf<S extends AnySchema> = StandardSchemaV1.InferOutput<S>;
9
+ interface UseFormOptions<S extends AnySchema> extends Omit<UseFormProps<InputOf<S>, unknown, OutputOf<S>>, 'resolver'> {
10
+ /** Any Standard Schema validator: zod, valibot, arktype, ... */
11
+ schema: S;
12
+ }
13
+ /**
14
+ * react-hook-form's `useForm`, validated by a schema. Field names and submitted values are
15
+ * typed from the schema.
16
+ */
17
+ export declare function useForm<S extends AnySchema>({ schema, ...options }: UseFormOptions<S>): UseFormReturn<InputOf<S>, unknown, OutputOf<S>>;
18
+ /** Field-level errors from the server, keyed by field name. `root` shows above the form. */
19
+ type FormErrors<TInput extends FieldValues> = Partial<Record<FieldPath<TInput> | 'root', string>>;
20
+ type SubmitResult<TInput extends FieldValues> = void | {
21
+ errors: FormErrors<TInput>;
22
+ };
23
+ interface FormProps<TInput extends FieldValues, TOutput> extends Omit<ComponentProps<'form'>, 'onSubmit'> {
24
+ form: UseFormReturn<TInput, unknown, TOutput>;
25
+ /**
26
+ * Receives validated values. Return `{ errors }` to show server-side field errors, or throw
27
+ * to show a form-level error. The submit button shows a spinner until this settles.
28
+ */
29
+ onSubmit: (values: TOutput) => SubmitResult<TInput> | Promise<SubmitResult<TInput>>;
30
+ }
31
+ export declare function Form<TInput extends FieldValues, TOutput>({ form, onSubmit, children, ...props }: FormProps<TInput, TOutput>): import("react").JSX.Element;
32
+ interface FormFieldProps<TInput extends FieldValues, TName extends FieldPath<TInput>> extends Omit<FieldProps, 'children' | 'error'> {
33
+ control: Control<TInput, unknown, any>;
34
+ /** Typed from the schema: a misspelled name is a compile error. */
35
+ name: TName;
36
+ /** Render the control and spread `field` onto it: `{(field) => <Input {...field} />}`. */
37
+ children: (field: ControllerRenderProps<TInput, TName>) => ReactNode;
38
+ }
39
+ /** One schema field: label, control, description and validation message, all wired. */
40
+ export declare function FormField<TInput extends FieldValues, TName extends FieldPath<TInput>>({ control, name, children, ...fieldProps }: FormFieldProps<TInput, TName>): import("react").JSX.Element;
41
+ /** Submit button that shows progress while the form submits. */
42
+ export declare function FormSubmit(props: Omit<ButtonProps, 'type'>): import("react").JSX.Element;
43
+ export declare const formErrorStyles: import("tailwind-variants").TVReturnType<{
44
+ [key: string]: {
45
+ [key: string]: import("tailwind-variants").ClassValue | {
46
+ base?: import("tailwind-variants").ClassValue;
47
+ icon?: import("tailwind-variants").ClassValue;
48
+ };
49
+ };
50
+ } | {
51
+ [x: string]: {
52
+ [x: string]: import("tailwind-variants").ClassValue | {
53
+ base?: import("tailwind-variants").ClassValue;
54
+ icon?: import("tailwind-variants").ClassValue;
55
+ };
56
+ };
57
+ } | {}, {
58
+ base: string;
59
+ icon: string;
60
+ }, undefined, {
61
+ [key: string]: {
62
+ [key: string]: import("tailwind-variants").ClassValue | {
63
+ base?: import("tailwind-variants").ClassValue;
64
+ icon?: import("tailwind-variants").ClassValue;
65
+ };
66
+ };
67
+ } | {}, {
68
+ base: string;
69
+ icon: string;
70
+ }, import("tailwind-variants").TVReturnTypeLike<unknown, {
71
+ base: string;
72
+ icon: string;
73
+ }>>;
74
+ /** Form-level error (thrown from `onSubmit` or returned as `errors.root`). */
75
+ export declare function FormError({ className }: {
76
+ className?: string;
77
+ }): import("react").JSX.Element | null;
78
+ //#endregion
79
+ export type { FormErrors, FormFieldProps, FormProps, SubmitResult, UseFormOptions };
package/dist/form.js ADDED
@@ -0,0 +1,84 @@
1
+ "use client";
2
+ import { E as tv, _ as Button, r as AlertIcon } from "./field-context-B8cYheXz.js";
3
+ import { t as Field } from "./field-DXZD0l8w.js";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
6
+ import { Controller, FormProvider, useForm as useForm$1, useFormState } from "react-hook-form";
7
+ //#region src/patterns/form/form.tsx
8
+ /**
9
+ * react-hook-form's `useForm`, validated by a schema. Field names and submitted values are
10
+ * typed from the schema.
11
+ */
12
+ function useForm({ schema, ...options }) {
13
+ return useForm$1({
14
+ mode: "onTouched",
15
+ ...options,
16
+ resolver: standardSchemaResolver(schema)
17
+ });
18
+ }
19
+ function Form({ form, onSubmit, children, ...props }) {
20
+ const submit = form.handleSubmit(async (values) => {
21
+ try {
22
+ const result = await onSubmit(values);
23
+ if (!result?.errors) return;
24
+ for (const [name, message] of Object.entries(result.errors)) if (message) form.setError(name, {
25
+ type: "server",
26
+ message
27
+ });
28
+ } catch (error) {
29
+ form.setError("root", {
30
+ type: "server",
31
+ message: error instanceof Error ? error.message : "Something went wrong. Try again."
32
+ });
33
+ }
34
+ });
35
+ return /* @__PURE__ */ jsx(FormProvider, {
36
+ ...form,
37
+ children: /* @__PURE__ */ jsx("form", {
38
+ noValidate: true,
39
+ onSubmit: submit,
40
+ ...props,
41
+ children
42
+ })
43
+ });
44
+ }
45
+ /** One schema field: label, control, description and validation message, all wired. */
46
+ function FormField({ control, name, children, ...fieldProps }) {
47
+ return /* @__PURE__ */ jsx(Controller, {
48
+ control,
49
+ name,
50
+ render: ({ field, fieldState }) => /* @__PURE__ */ jsx(Field, {
51
+ ...fieldProps,
52
+ error: fieldState.error?.message,
53
+ children: children(field)
54
+ })
55
+ });
56
+ }
57
+ /** Submit button that shows progress while the form submits. */
58
+ function FormSubmit(props) {
59
+ const { isSubmitting } = useFormState();
60
+ return /* @__PURE__ */ jsx(Button, {
61
+ type: "submit",
62
+ loading: isSubmitting,
63
+ ...props
64
+ });
65
+ }
66
+ const formErrorStyles = tv({ slots: {
67
+ base: "flex items-start gap-2 rounded-md border border-danger/30 bg-danger/8 px-3 py-2.5 text-small text-danger",
68
+ icon: "mt-0.5 size-4 shrink-0"
69
+ } });
70
+ /** Form-level error (thrown from `onSubmit` or returned as `errors.root`). */
71
+ function FormError({ className }) {
72
+ const { errors } = useFormState();
73
+ const message = errors.root?.message;
74
+ if (!message) return null;
75
+ const s = formErrorStyles();
76
+ return /* @__PURE__ */ jsxs("div", {
77
+ role: "alert",
78
+ "data-slot": "form-error",
79
+ className: s.base({ class: className }),
80
+ children: [/* @__PURE__ */ jsx(AlertIcon, { className: s.icon() }), /* @__PURE__ */ jsx("span", { children: message })]
81
+ });
82
+ }
83
+ //#endregion
84
+ export { Form, FormError, FormField, FormSubmit, formErrorStyles, useForm };