@glyphui/ui 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +137 -2
- package/dist/data-table.d.ts +110 -0
- package/dist/data-table.js +225 -0
- package/dist/field-DXZD0l8w.js +107 -0
- package/dist/field-context-B8cYheXz.js +321 -0
- package/dist/form.d.ts +79 -0
- package/dist/form.js +84 -0
- package/dist/index-BWK4czIG.d.ts +233 -0
- package/dist/index.d.ts +2155 -0
- package/dist/index.js +1529 -0
- package/dist/registry-BGSJoiLq.d.ts +9 -0
- package/dist/states-BxPjCUxz.js +287 -0
- package/dist/tv-DuMh7UH2.d.ts +6 -0
- package/package.json +113 -3
- package/src/components/avatar/avatar.styles.ts +22 -0
- package/src/components/avatar/avatar.tsx +62 -0
- package/src/components/avatar/index.ts +2 -0
- package/src/components/badge/badge.styles.ts +69 -0
- package/src/components/badge/badge.tsx +30 -0
- package/src/components/badge/index.ts +2 -0
- package/src/components/button/button.styles.ts +43 -0
- package/src/components/button/button.tsx +77 -0
- package/src/components/button/index.ts +2 -0
- package/src/components/card/card.styles.ts +32 -0
- package/src/components/card/card.tsx +91 -0
- package/src/components/card/index.ts +11 -0
- package/src/components/checkbox/checkbox.tsx +39 -0
- package/src/components/checkbox/index.ts +1 -0
- package/src/components/container/container.tsx +35 -0
- package/src/components/container/index.ts +1 -0
- package/src/components/eyebrow/eyebrow.styles.ts +14 -0
- package/src/components/eyebrow/eyebrow.tsx +19 -0
- package/src/components/eyebrow/index.ts +2 -0
- package/src/components/field/field-context.ts +40 -0
- package/src/components/field/field.styles.ts +23 -0
- package/src/components/field/field.tsx +109 -0
- package/src/components/field/index.ts +3 -0
- package/src/components/heading/heading.styles.ts +16 -0
- package/src/components/heading/heading.tsx +26 -0
- package/src/components/heading/index.ts +2 -0
- package/src/components/input/index.ts +2 -0
- package/src/components/input/input.styles.ts +36 -0
- package/src/components/input/input.tsx +33 -0
- package/src/components/kbd/index.ts +1 -0
- package/src/components/kbd/kbd.tsx +21 -0
- package/src/components/label/index.ts +1 -0
- package/src/components/label/label.tsx +21 -0
- package/src/components/section/index.ts +1 -0
- package/src/components/section/section.tsx +39 -0
- package/src/components/segmented-control/index.ts +6 -0
- package/src/components/segmented-control/segmented-control.tsx +79 -0
- package/src/components/select/index.ts +1 -0
- package/src/components/select/select.tsx +133 -0
- package/src/components/separator/index.ts +1 -0
- package/src/components/separator/separator.tsx +29 -0
- package/src/components/skeleton/index.ts +1 -0
- package/src/components/skeleton/skeleton.tsx +20 -0
- package/src/components/spinner/index.ts +2 -0
- package/src/components/spinner/spinner.styles.ts +19 -0
- package/src/components/spinner/spinner.tsx +42 -0
- package/src/components/stack/index.ts +1 -0
- package/src/components/stack/stack.tsx +62 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/switch/switch.tsx +48 -0
- package/src/components/table/index.ts +10 -0
- package/src/components/table/table.tsx +90 -0
- package/src/components/tabs/index.ts +1 -0
- package/src/components/tabs/tabs.tsx +92 -0
- package/src/components/text/index.ts +2 -0
- package/src/components/text/text.styles.ts +32 -0
- package/src/components/text/text.tsx +17 -0
- package/src/components/textarea/index.ts +1 -0
- package/src/components/textarea/textarea.tsx +27 -0
- package/src/components/tooltip/index.ts +1 -0
- package/src/components/tooltip/tooltip.tsx +42 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/use-component-config.ts +42 -0
- package/src/hooks/use-controllable-state.ts +27 -0
- package/src/hooks/use-media-query.ts +14 -0
- package/src/hooks/use-scrolled.ts +14 -0
- package/src/index.ts +52 -0
- package/src/internal/icons.tsx +99 -0
- package/src/patterns/app-shell/app-shell-context.ts +27 -0
- package/src/patterns/app-shell/app-shell.styles.ts +127 -0
- package/src/patterns/app-shell/app-shell.tsx +81 -0
- package/src/patterns/app-shell/index.ts +16 -0
- package/src/patterns/app-shell/navbar.tsx +119 -0
- package/src/patterns/app-shell/page-header.tsx +30 -0
- package/src/patterns/app-shell/sidebar.tsx +163 -0
- package/src/patterns/async/async.tsx +83 -0
- package/src/patterns/async/index.ts +10 -0
- package/src/patterns/async/states.tsx +128 -0
- package/src/patterns/data-table/data-table.tsx +367 -0
- package/src/patterns/data-table/index.ts +2 -0
- package/src/patterns/form/form.tsx +150 -0
- package/src/patterns/form/index.ts +13 -0
- package/src/provider/color-mode.tsx +133 -0
- package/src/provider/glyph-context.ts +42 -0
- package/src/provider/glyph-provider.tsx +41 -0
- package/src/provider/index.ts +18 -0
- package/src/provider/theme-scope.tsx +36 -0
- package/src/provider/theme-style.tsx +19 -0
- package/src/register.ts +69 -0
- package/src/themes.ts +56 -0
- package/src/utils/cn.ts +12 -0
- package/src/utils/compose-refs.ts +11 -0
- package/src/utils/registry.ts +9 -0
- package/src/utils/styles.ts +13 -0
- package/src/utils/tv.ts +7 -0
- package/src/utils/tw-merge-config.ts +14 -0
- 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 };
|