@glyphui/ui 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -12
- package/dist/chart.d.ts +1 -1
- package/dist/chart.js +28 -19
- package/dist/color-mode-script-Bf7LYkOj.js +13 -0
- package/dist/color-mode-script-KdwItKN2.d.ts +16 -0
- package/dist/data-table.js +3 -3
- package/dist/{field-CWZ_FdkD.js → field-CVbywksE.js} +21 -6
- package/dist/{field-context-anGPYXbW.js → field-context-7I-cWvvS.js} +2 -2
- package/dist/form.d.ts +11 -4
- package/dist/form.js +34 -9
- package/dist/glyph-context-DxGJrdvr.d.ts +50 -0
- package/dist/{index-BWK4czIG.d.ts → index-Bi7rZ6ba.d.ts} +11 -0
- package/dist/index.d.ts +87 -80
- package/dist/index.js +155 -82
- package/dist/react-router.d.ts +18 -0
- package/dist/react-router.js +44 -0
- package/dist/{states-DR-OoDT-.js → states-IvhjBgna.js} +2 -2
- package/dist/{tv-DBAGlgXJ.js → tv-DLbT66dC.js} +13 -1
- package/dist/vite.d.ts +27 -0
- package/dist/vite.js +29 -0
- package/fonts.css +10 -0
- package/package.json +32 -5
- package/src/components/badge/badge.styles.ts +11 -16
- package/src/components/banner/banner.styles.ts +4 -4
- package/src/components/button/button.styles.ts +3 -1
- package/src/components/calendar/calendar.styles.ts +3 -2
- package/src/components/dropdown-menu/dropdown-menu.styles.ts +2 -2
- package/src/components/eyebrow/eyebrow.styles.ts +1 -1
- package/src/components/field/field-context.ts +2 -0
- package/src/components/field/field.styles.ts +4 -2
- package/src/components/field/field.tsx +25 -4
- package/src/components/file-upload/file-upload.styles.ts +3 -3
- package/src/components/link/link.styles.ts +1 -1
- package/src/components/modal/modal.styles.ts +2 -0
- package/src/components/modal/modal.tsx +30 -5
- package/src/components/navigation-menu/navigation-menu.tsx +14 -4
- package/src/components/password-input/password-input.styles.ts +3 -3
- package/src/components/prose/prose.tsx +1 -1
- package/src/components/scroll-area/scroll-area.tsx +23 -1
- package/src/components/segmented-control/segmented-control.tsx +17 -2
- package/src/components/slider/slider.tsx +9 -2
- package/src/components/stepper/stepper.styles.ts +2 -2
- package/src/components/tag-input/tag-input.styles.ts +1 -1
- package/src/components/text/text.styles.ts +4 -4
- package/src/integrations/react-router.tsx +39 -0
- package/src/integrations/vite.test-d.ts +6 -0
- package/src/integrations/vite.test.ts +28 -0
- package/src/integrations/vite.ts +33 -0
- package/src/internal/dev.ts +12 -0
- package/src/patterns/app-shell/app-shell.styles.ts +1 -1
- package/src/patterns/app-shell/navbar.tsx +34 -16
- package/src/patterns/app-shell/sidebar.tsx +7 -2
- package/src/patterns/chart/chart-parts.tsx +10 -2
- package/src/patterns/chart/chart.tsx +22 -5
- package/src/patterns/form/form.tsx +48 -6
- package/src/patterns/form/index.ts +1 -0
- package/src/provider/color-mode-script.ts +28 -0
- package/src/provider/color-mode.tsx +14 -20
- package/src/provider/glyph-context.ts +38 -0
- package/src/provider/glyph-provider.tsx +16 -4
- package/src/provider/index.ts +4 -0
- package/src/provider/theme-scope.tsx +7 -3
- package/styles.css +72 -0
package/README.md
CHANGED
|
@@ -8,8 +8,10 @@ React components on Tailwind CSS v4 that take their whole look from **one typed
|
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
npm install @glyphui/ui
|
|
11
|
-
# optional:
|
|
12
|
-
npm install react-hook-form @hookform/resolvers zod @tanstack/react-table
|
|
11
|
+
# optional: forms and data tables (TanStack Table version 8)
|
|
12
|
+
npm install react-hook-form @hookform/resolvers zod @tanstack/react-table@8
|
|
13
|
+
# optional: charts from @glyphui/ui/chart
|
|
14
|
+
npm install recharts
|
|
13
15
|
```
|
|
14
16
|
|
|
15
17
|
Requires React 19 and Tailwind CSS 4.1 or later.
|
|
@@ -18,22 +20,39 @@ Requires React 19 and Tailwind CSS 4.1 or later.
|
|
|
18
20
|
|
|
19
21
|
```css
|
|
20
22
|
/* index.css */
|
|
23
|
+
@import "@glyphui/ui/fonts.css"; /* Geist, used by the default theme */
|
|
21
24
|
@import "tailwindcss";
|
|
22
25
|
@import "@glyphui/ui/styles.css";
|
|
26
|
+
@import "@glyphui/blocks/styles.css"; /* only with @glyphui/blocks */
|
|
23
27
|
```
|
|
24
28
|
|
|
25
29
|
```tsx
|
|
26
30
|
// main.tsx
|
|
27
31
|
import { GlyphProvider, presets } from '@glyphui/ui'
|
|
32
|
+
import { reactRouter } from '@glyphui/ui/react-router'
|
|
28
33
|
|
|
29
34
|
createRoot(document.getElementById('root')!).render(
|
|
30
|
-
<
|
|
31
|
-
<
|
|
32
|
-
|
|
35
|
+
<BrowserRouter>
|
|
36
|
+
<GlyphProvider theme={presets.glyph} defaultMode="system" router={reactRouter} toaster>
|
|
37
|
+
<App />
|
|
38
|
+
</GlyphProvider>
|
|
39
|
+
</BrowserRouter>,
|
|
33
40
|
)
|
|
34
41
|
```
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
- `router` (optional) makes Glyph's links client-side and marks the current Sidebar, Navbar and NavigationMenu item automatically. For other routers, pass `{ Link, useIsActive }`; the [getting-started guide](https://khalmohak.github.io/glyph/getting-started) has TanStack Router and Next.js versions.
|
|
44
|
+
- `toaster` mounts the toast container, so `toast.success('Saved')` works anywhere. `useConfirm()` needs no setup.
|
|
45
|
+
|
|
46
|
+
To avoid a light flash for dark-mode visitors, add the Vite plugin:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// vite.config.ts
|
|
50
|
+
import { glyphColorMode } from '@glyphui/ui/vite'
|
|
51
|
+
|
|
52
|
+
export default defineConfig({ plugins: [react(), tailwindcss(), glyphColorMode()] })
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
With an SSR framework, render `<ColorModeScript />` in the document head instead.
|
|
37
56
|
|
|
38
57
|
## Your theme, in one file
|
|
39
58
|
|
|
@@ -57,7 +76,7 @@ export default defineTheme({
|
|
|
57
76
|
```
|
|
58
77
|
|
|
59
78
|
A theme controls:
|
|
60
|
-
- colors, separately for light and dark
|
|
79
|
+
- colors, separately for light and dark, including a five-color chart palette
|
|
61
80
|
- a nine-step type scale (size, line height, tracking, weight)
|
|
62
81
|
- radius, shadows, border width, motion and control height
|
|
63
82
|
- defaults and extra classes for each component
|
|
@@ -68,9 +87,20 @@ The same tokens work in your own markup: `bg-primary`, `text-muted-foreground`,
|
|
|
68
87
|
|
|
69
88
|
## Components
|
|
70
89
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
90
|
+
| Category | Components (95) |
|
|
91
|
+
|---|---|
|
|
92
|
+
| Actions | Button · CopyButton · Toggle |
|
|
93
|
+
| Forms | Calendar · Checkbox · Combobox · DatePicker · Field · FileUpload · Input · InputOTP · Label · NumberInput · PasswordInput · RadioGroup · SegmentedControl · Select · Slider · Switch · TagInput · Textarea |
|
|
94
|
+
| Overlays | Alert Dialog · ContextMenu · Dialog · Dropdown Menu · HoverCard · Modal · Popover · Sheet · Tooltip |
|
|
95
|
+
| Navigation | Breadcrumb · CommandMenu · Link · NavigationMenu · Pagination · Stepper · Tabs |
|
|
96
|
+
| Feedback | Alert · Banner · Progress · Skeleton · Spinner · Toast |
|
|
97
|
+
| Data display | Accordion · Avatar · Badge · Carousel · Chart · Collapsible · DescriptionList · Kbd · StatCard · Table · Timeline · TreeView |
|
|
98
|
+
| Typography | Blockquote · Code · Eyebrow · Heading · Prose · Text |
|
|
99
|
+
| Layout | AspectRatio · Card · Container · Grid · Resizable · ScrollArea · Section · Separator · Stack · VisuallyHidden |
|
|
100
|
+
| Patterns | AppShell · Async · DataTable · Form · Navbar · PageHeader · Router integration · Sidebar · Theming · Utilities · Vite plugin |
|
|
101
|
+
| Blocks (`@glyphui/blocks`) | AuthCard · Cta · Faq · FeatureGrid · FeatureSplit · Hero · LogoCloud · Newsletter · Pricing · SectionHeader · SiteFooter · Stats · Testimonials |
|
|
102
|
+
|
|
103
|
+
Every component has a page with live examples and props in the [docs](https://khalmohak.github.io/glyph/components). Accessible behavior comes from [Radix UI](https://www.radix-ui.com/). Every component accepts `className`, and most accept `asChild`.
|
|
74
104
|
|
|
75
105
|
## Patterns
|
|
76
106
|
|
|
@@ -80,13 +110,13 @@ Accessible behavior comes from [Radix UI](https://www.radix-ui.com/). Every comp
|
|
|
80
110
|
import { AppShell, Sidebar, SidebarContent, SidebarItem, PageHeader } from '@glyphui/ui'
|
|
81
111
|
|
|
82
112
|
<AppShell sidebar={<Sidebar><SidebarContent>
|
|
83
|
-
<SidebarItem href="/inbox" icon={<Inbox />} badge={3}
|
|
113
|
+
<SidebarItem href="/inbox" icon={<Inbox />} badge={3}>Inbox</SidebarItem>
|
|
84
114
|
</SidebarContent></Sidebar>}>
|
|
85
115
|
<PageHeader title="Inbox" actions={<Button>New</Button>} />
|
|
86
116
|
</AppShell>
|
|
87
117
|
```
|
|
88
118
|
|
|
89
|
-
The sidebar collapses to an icon strip on desktop and opens as a focus-trapped drawer on mobile. `Navbar` builds a responsive site header from `links` data.
|
|
119
|
+
The sidebar collapses to an icon strip on desktop and opens as a focus-trapped drawer on mobile. `Navbar` builds a responsive site header from `links` data. With `router` on `GlyphProvider`, the item for the current URL is marked active; pass `active` to override.
|
|
90
120
|
|
|
91
121
|
### Forms: `@glyphui/ui/form`
|
|
92
122
|
|
|
@@ -108,6 +138,20 @@ const form = useForm({ schema, defaultValues: { email: '' } }) // zod, valibot,
|
|
|
108
138
|
|
|
109
139
|
Field names are checked against the schema. Server errors you return appear on their fields. If `onSubmit` throws, the error appears above the form in `<FormError />`.
|
|
110
140
|
|
|
141
|
+
### Modals and confirmations
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
<Modal title="New invoice" trigger={<Button>New invoice</Button>}
|
|
145
|
+
footer={<FormSubmit form={form}>Create</FormSubmit>}> {/* outside <Form>, so pass form */}
|
|
146
|
+
<Form form={form} onSubmit={save}>…</Form>
|
|
147
|
+
</Modal>
|
|
148
|
+
|
|
149
|
+
const confirm = useConfirm()
|
|
150
|
+
if (await confirm({ title: 'Delete invoice?', tone: 'danger' })) remove()
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
For a plain form, pass `onSubmit` to `Modal` and a `type="submit"` button in the footer.
|
|
154
|
+
|
|
111
155
|
### Data tables: `@glyphui/ui/data-table`
|
|
112
156
|
|
|
113
157
|
```tsx
|
|
@@ -125,6 +169,15 @@ const columns = [
|
|
|
125
169
|
|
|
126
170
|
Includes sorting, search, row selection and pages, plus loading, empty, no-results and error states.
|
|
127
171
|
|
|
172
|
+
### Charts: `@glyphui/ui/chart`
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
<AreaChart title="Revenue" data={revenue} index="month"
|
|
176
|
+
series={[{ key: 'recurring', label: 'Recurring' }, { key: 'oneOff', label: 'One-off' }]} />
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Series use the theme's `chart-1` to `chart-5` colors. Every chart includes a visually hidden data table for screen readers. Needs `recharts`.
|
|
180
|
+
|
|
128
181
|
### Async states
|
|
129
182
|
|
|
130
183
|
```tsx
|
package/dist/chart.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { t as StylesEntry } from "./registry-BGSJoiLq.js";
|
|
|
3
3
|
import { ReactNode } from "react";
|
|
4
4
|
//#region src/patterns/chart/chart-parts.d.ts
|
|
5
5
|
/** Theme colors a series can use by name. Any CSS color also works. */
|
|
6
|
-
type ChartColor = 'primary' | 'info' | 'success' | 'warning' | 'danger' | 'foreground' | 'muted-foreground';
|
|
6
|
+
type ChartColor = 'chart-1' | 'chart-2' | 'chart-3' | 'chart-4' | 'chart-5' | 'primary' | 'info' | 'success' | 'warning' | 'danger' | 'foreground' | 'muted-foreground';
|
|
7
7
|
//#endregion
|
|
8
8
|
//#region src/patterns/chart/chart.d.ts
|
|
9
9
|
type Key<T> = Extract<keyof T, string>;
|
package/dist/chart.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { a as themedBorder, s as useComponentConfig, t as tv } from "./tv-
|
|
2
|
+
import { a as themedBorder, s as useComponentConfig, t as tv } from "./tv-DLbT66dC.js";
|
|
3
3
|
import { useId } from "react";
|
|
4
4
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
import * as R from "recharts";
|
|
@@ -23,16 +23,17 @@ const chartStyles = tv({ slots: {
|
|
|
23
23
|
} });
|
|
24
24
|
//#endregion
|
|
25
25
|
//#region src/patterns/chart/chart-parts.tsx
|
|
26
|
+
/** Series without a color use the theme's chart palette, in order. */
|
|
26
27
|
const palette = [
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
28
|
+
"chart-1",
|
|
29
|
+
"chart-2",
|
|
30
|
+
"chart-3",
|
|
31
|
+
"chart-4",
|
|
32
|
+
"chart-5"
|
|
32
33
|
];
|
|
33
34
|
function resolveColor(color, index) {
|
|
34
35
|
const value = color ?? palette[index % palette.length];
|
|
35
|
-
return /^[a-z-]
|
|
36
|
+
return /^[a-z][a-z\d-]*$/.test(value) ? `var(--color-${value})` : value;
|
|
36
37
|
}
|
|
37
38
|
const defaultFormatter = (value) => typeof value === "number" ? value.toLocaleString() : String(value);
|
|
38
39
|
function useChartParts() {
|
|
@@ -212,6 +213,7 @@ function LineChart(props) {
|
|
|
212
213
|
width: "100%",
|
|
213
214
|
height: props.height ?? 240,
|
|
214
215
|
children: /* @__PURE__ */ jsxs(R.LineChart, {
|
|
216
|
+
accessibilityLayer: false,
|
|
215
217
|
data: props.data,
|
|
216
218
|
margin: {
|
|
217
219
|
top: 8,
|
|
@@ -257,6 +259,7 @@ function AreaChart(props) {
|
|
|
257
259
|
width: "100%",
|
|
258
260
|
height: props.height ?? 240,
|
|
259
261
|
children: /* @__PURE__ */ jsxs(R.AreaChart, {
|
|
262
|
+
accessibilityLayer: false,
|
|
260
263
|
data: props.data,
|
|
261
264
|
margin: {
|
|
262
265
|
top: 8,
|
|
@@ -315,6 +318,7 @@ function BarChart(props) {
|
|
|
315
318
|
width: "100%",
|
|
316
319
|
height: props.height ?? 240,
|
|
317
320
|
children: /* @__PURE__ */ jsxs(R.BarChart, {
|
|
321
|
+
accessibilityLayer: false,
|
|
318
322
|
data: props.data,
|
|
319
323
|
margin: {
|
|
320
324
|
top: 8,
|
|
@@ -381,18 +385,22 @@ function DonutChart(props) {
|
|
|
381
385
|
children: [/* @__PURE__ */ jsx(R.ResponsiveContainer, {
|
|
382
386
|
width: "100%",
|
|
383
387
|
height: "100%",
|
|
384
|
-
children: /* @__PURE__ */ jsxs(R.PieChart, {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
388
|
+
children: /* @__PURE__ */ jsxs(R.PieChart, {
|
|
389
|
+
accessibilityLayer: false,
|
|
390
|
+
children: [tooltip(valueFormatter, false), /* @__PURE__ */ jsx(R.Pie, {
|
|
391
|
+
rootTabIndex: -1,
|
|
392
|
+
data: rows,
|
|
393
|
+
dataKey: value,
|
|
394
|
+
nameKey: category,
|
|
395
|
+
innerRadius: "68%",
|
|
396
|
+
outerRadius: "100%",
|
|
397
|
+
paddingAngle: 2,
|
|
398
|
+
stroke: "var(--color-surface)",
|
|
399
|
+
strokeWidth: 2,
|
|
400
|
+
isAnimationActive: false,
|
|
401
|
+
children: rows.map((_, i) => /* @__PURE__ */ jsx(R.Cell, { fill: colors[i] }, i))
|
|
402
|
+
})]
|
|
403
|
+
})
|
|
396
404
|
}), props.centerLabel && /* @__PURE__ */ jsx("div", {
|
|
397
405
|
className: s.center({ class: slots.center }),
|
|
398
406
|
children: props.centerLabel
|
|
@@ -418,6 +426,7 @@ function Sparkline({ values, title, color, height = 36, area = true, className }
|
|
|
418
426
|
width: "100%",
|
|
419
427
|
height: "100%",
|
|
420
428
|
children: /* @__PURE__ */ jsxs(R.AreaChart, {
|
|
429
|
+
accessibilityLayer: false,
|
|
421
430
|
data,
|
|
422
431
|
margin: {
|
|
423
432
|
top: 2,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
//#region src/provider/color-mode-script.ts
|
|
3
|
+
const DARK_QUERY = "(prefers-color-scheme: dark)";
|
|
4
|
+
const DEFAULT_STORAGE_KEY = "glyph-mode";
|
|
5
|
+
/**
|
|
6
|
+
* A tiny inline script that sets `data-mode` on `<html>` before first paint, so dark-mode
|
|
7
|
+
* visitors never see a light flash. Use the same options as `GlyphProvider`.
|
|
8
|
+
*/
|
|
9
|
+
function getColorModeScript({ defaultMode = "system", storageKey = DEFAULT_STORAGE_KEY } = {}) {
|
|
10
|
+
return `(function(){try{var d=${JSON.stringify(defaultMode)},m=null;${storageKey ? `m=localStorage.getItem(${JSON.stringify(storageKey)});` : ""}if(m!=='light'&&m!=='dark'&&m!=='system')m=d;if(m==='system')m=matchMedia('${DARK_QUERY}').matches?'dark':'light';var r=document.documentElement;r.dataset.mode=m;r.style.colorScheme=m}catch(e){}})()`;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { DEFAULT_STORAGE_KEY as n, getColorModeScript as r, DARK_QUERY as t };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ColorMode } from "@glyphui/tokens";
|
|
2
|
+
//#region src/provider/color-mode-script.d.ts
|
|
3
|
+
type ColorModePreference = ColorMode | 'system';
|
|
4
|
+
interface ColorModeOptions {
|
|
5
|
+
/** Used when nothing is stored yet. */
|
|
6
|
+
defaultMode?: ColorModePreference;
|
|
7
|
+
/** localStorage key. Set to `null` to disable persistence. */
|
|
8
|
+
storageKey?: string | null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A tiny inline script that sets `data-mode` on `<html>` before first paint, so dark-mode
|
|
12
|
+
* visitors never see a light flash. Use the same options as `GlyphProvider`.
|
|
13
|
+
*/
|
|
14
|
+
declare function getColorModeScript({ defaultMode, storageKey }?: ColorModeOptions): string;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { ColorModePreference as n, getColorModeScript as r, ColorModeOptions as t };
|
package/dist/data-table.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { a as themedBorder, r as focusRing, s as useComponentConfig, t as tv } from "./tv-
|
|
3
|
-
import { b as SearchIcon, h as ChevronsUpDownIcon, i as IconButton, l as ArrowDownIcon, m as ChevronRightIcon, p as ChevronLeftIcon, r as Button, u as ArrowUpIcon } from "./field-context-
|
|
4
|
-
import { a as Table, c as TableCell, d as TableRow, h as Input, l as TableHead, n as ErrorState, o as TableBody, p as Skeleton, t as EmptyState, u as TableHeader, v as Checkbox } from "./states-
|
|
2
|
+
import { a as themedBorder, r as focusRing, s as useComponentConfig, t as tv } from "./tv-DLbT66dC.js";
|
|
3
|
+
import { b as SearchIcon, h as ChevronsUpDownIcon, i as IconButton, l as ArrowDownIcon, m as ChevronRightIcon, p as ChevronLeftIcon, r as Button, u as ArrowUpIcon } from "./field-context-7I-cWvvS.js";
|
|
4
|
+
import { a as Table, c as TableCell, d as TableRow, h as Input, l as TableHead, n as ErrorState, o as TableBody, p as Skeleton, t as EmptyState, u as TableHeader, v as Checkbox } from "./states-IvhjBgna.js";
|
|
5
5
|
import { useEffect, useMemo, useState } from "react";
|
|
6
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
7
|
import { createColumnHelper, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { s as useComponentConfig, t as tv } from "./tv-
|
|
3
|
-
import { t as FieldContext } from "./field-context-
|
|
2
|
+
import { s as useComponentConfig, t as tv } from "./tv-DLbT66dC.js";
|
|
3
|
+
import { t as FieldContext } from "./field-context-7I-cWvvS.js";
|
|
4
4
|
import { Label } from "radix-ui";
|
|
5
5
|
import { useId, useMemo } from "react";
|
|
6
6
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -20,10 +20,12 @@ const fieldStyles = tv({
|
|
|
20
20
|
slots: {
|
|
21
21
|
root: "grid gap-1.5",
|
|
22
22
|
label: "",
|
|
23
|
-
|
|
23
|
+
labelRow: "flex items-center justify-between gap-2",
|
|
24
|
+
labelAction: "text-small",
|
|
25
|
+
required: "text-danger-text",
|
|
24
26
|
text: "grid gap-0.5",
|
|
25
27
|
description: "text-small text-faint-foreground",
|
|
26
|
-
error: "text-small font-medium text-danger"
|
|
28
|
+
error: "text-small font-medium text-danger-text"
|
|
27
29
|
},
|
|
28
30
|
variants: { orientation: {
|
|
29
31
|
vertical: {},
|
|
@@ -42,15 +44,17 @@ const fieldStyles = tv({
|
|
|
42
44
|
*/
|
|
43
45
|
function Field(inProps) {
|
|
44
46
|
const [props, slots] = useComponentConfig("Field", inProps);
|
|
45
|
-
const { label, description, error, required = false, disabled = false, controlId, orientation, className, children, ...rest } = props;
|
|
47
|
+
const { label, labelAction, description, error, required = false, disabled = false, controlId, orientation, className, children, ...rest } = props;
|
|
46
48
|
const generated = useId();
|
|
47
49
|
const id = controlId ?? generated;
|
|
48
50
|
const invalid = Boolean(error);
|
|
51
|
+
const labelId = label ? `${id}-label` : void 0;
|
|
49
52
|
const descriptionId = description ? `${id}-description` : void 0;
|
|
50
53
|
const errorId = invalid ? `${id}-error` : void 0;
|
|
51
54
|
const s = fieldStyles({ orientation });
|
|
52
55
|
const context = useMemo(() => ({
|
|
53
56
|
id,
|
|
57
|
+
labelId,
|
|
54
58
|
descriptionId,
|
|
55
59
|
errorId,
|
|
56
60
|
invalid,
|
|
@@ -58,13 +62,15 @@ function Field(inProps) {
|
|
|
58
62
|
disabled
|
|
59
63
|
}), [
|
|
60
64
|
id,
|
|
65
|
+
labelId,
|
|
61
66
|
descriptionId,
|
|
62
67
|
errorId,
|
|
63
68
|
invalid,
|
|
64
69
|
required,
|
|
65
70
|
disabled
|
|
66
71
|
]);
|
|
67
|
-
const
|
|
72
|
+
const labelElement = label ? /* @__PURE__ */ jsxs(Label$1, {
|
|
73
|
+
id: labelId,
|
|
68
74
|
htmlFor: id,
|
|
69
75
|
className: s.label({ class: slots.label }),
|
|
70
76
|
children: [label, required && /* @__PURE__ */ jsx("span", {
|
|
@@ -73,6 +79,15 @@ function Field(inProps) {
|
|
|
73
79
|
children: "*"
|
|
74
80
|
})]
|
|
75
81
|
}) : null;
|
|
82
|
+
const labelNode = labelElement && labelAction ? /* @__PURE__ */ jsxs("div", {
|
|
83
|
+
"data-slot": "field-label-row",
|
|
84
|
+
className: s.labelRow({ class: slots.labelRow }),
|
|
85
|
+
children: [labelElement, /* @__PURE__ */ jsx("span", {
|
|
86
|
+
"data-slot": "field-label-action",
|
|
87
|
+
className: s.labelAction({ class: slots.labelAction }),
|
|
88
|
+
children: labelAction
|
|
89
|
+
})]
|
|
90
|
+
}) : labelElement;
|
|
76
91
|
const messages = /* @__PURE__ */ jsxs(Fragment$1, { children: [description && /* @__PURE__ */ jsx("p", {
|
|
77
92
|
id: descriptionId,
|
|
78
93
|
"data-slot": "field-description",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { a as themedBorder, o as transition, r as focusRing, s as useComponentConfig, t as tv } from "./tv-
|
|
2
|
+
import { a as themedBorder, o as transition, r as focusRing, s as useComponentConfig, t as tv } from "./tv-DLbT66dC.js";
|
|
3
3
|
import { Slot } from "radix-ui";
|
|
4
4
|
import { Fragment, createContext, useContext } from "react";
|
|
5
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -157,7 +157,7 @@ const buttonStyles = tv({
|
|
|
157
157
|
outline: { base: "border-border bg-surface text-foreground shadow-xs hover:border-border-strong hover:bg-muted" },
|
|
158
158
|
ghost: { base: "text-muted-foreground hover:bg-muted hover:text-foreground" },
|
|
159
159
|
danger: { base: "bg-danger text-danger-foreground shadow-sm hover:bg-danger/88" },
|
|
160
|
-
link: { base: "h-auto px-0 text-primary underline-offset-4 shadow-none hover:underline" }
|
|
160
|
+
link: { base: "h-auto px-0 text-primary-text underline-offset-4 shadow-none hover:underline" }
|
|
161
161
|
},
|
|
162
162
|
size: {
|
|
163
163
|
sm: {
|
package/dist/form.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as ButtonProps, n as FieldProps } from "./index-
|
|
1
|
+
import { i as ButtonProps, n as FieldProps } from "./index-Bi7rZ6ba.js";
|
|
2
2
|
import { ComponentProps, ReactNode } from "react";
|
|
3
3
|
import { Control, ControllerRenderProps, FieldPath, FieldValues, UseFormProps, UseFormReturn } from "react-hook-form";
|
|
4
4
|
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
@@ -28,7 +28,7 @@ interface FormProps<TInput extends FieldValues, TOutput> extends Omit<ComponentP
|
|
|
28
28
|
*/
|
|
29
29
|
onSubmit: (values: TOutput) => SubmitResult<TInput> | Promise<SubmitResult<TInput>>;
|
|
30
30
|
}
|
|
31
|
-
export declare function Form<TInput extends FieldValues, TOutput>({ form, onSubmit, children, ...props }: FormProps<TInput, TOutput>): import("react").JSX.Element;
|
|
31
|
+
export declare function Form<TInput extends FieldValues, TOutput>({ form, onSubmit, children, ref, ...props }: FormProps<TInput, TOutput>): import("react").JSX.Element;
|
|
32
32
|
interface FormFieldProps<TInput extends FieldValues, TName extends FieldPath<TInput>> extends Omit<FieldProps, 'children' | 'error'> {
|
|
33
33
|
control: Control<TInput, unknown, any>;
|
|
34
34
|
/** Typed from the schema: a misspelled name is a compile error. */
|
|
@@ -38,8 +38,15 @@ interface FormFieldProps<TInput extends FieldValues, TName extends FieldPath<TIn
|
|
|
38
38
|
}
|
|
39
39
|
/** One schema field: label, control, description and validation message, all wired. */
|
|
40
40
|
export declare function FormField<TInput extends FieldValues, TName extends FieldPath<TInput>>({ control, name, children, ...fieldProps }: FormFieldProps<TInput, TName>): import("react").JSX.Element;
|
|
41
|
+
interface FormSubmitProps extends Omit<ButtonProps, 'type' | 'form'> {
|
|
42
|
+
/**
|
|
43
|
+
* The form to submit, for a button outside `<Form>` such as a Modal footer:
|
|
44
|
+
* `<FormSubmit form={form}>`. Not needed inside `<Form>`.
|
|
45
|
+
*/
|
|
46
|
+
form?: UseFormReturn<any, any, any>;
|
|
47
|
+
}
|
|
41
48
|
/** Submit button that shows progress while the form submits. */
|
|
42
|
-
export declare function FormSubmit(props:
|
|
49
|
+
export declare function FormSubmit({ form, onClick, ...props }: FormSubmitProps): import("react").JSX.Element;
|
|
43
50
|
export declare const formErrorStyles: import("tailwind-variants").TVReturnType<{
|
|
44
51
|
[key: string]: {
|
|
45
52
|
[key: string]: import("tailwind-variants").ClassValue | {
|
|
@@ -76,4 +83,4 @@ export declare function FormError({ className }: {
|
|
|
76
83
|
className?: string;
|
|
77
84
|
}): import("react").JSX.Element | null;
|
|
78
85
|
//#endregion
|
|
79
|
-
export type { FormErrors, FormFieldProps, FormProps, SubmitResult, UseFormOptions };
|
|
86
|
+
export type { FormErrors, FormFieldProps, FormProps, FormSubmitProps, SubmitResult, UseFormOptions };
|
package/dist/form.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { t as tv } from "./tv-
|
|
3
|
-
import { c as AlertIcon, r as Button } from "./field-context-
|
|
4
|
-
import { t as Field } from "./field-
|
|
2
|
+
import { t as tv } from "./tv-DLbT66dC.js";
|
|
3
|
+
import { c as AlertIcon, r as Button } from "./field-context-7I-cWvvS.js";
|
|
4
|
+
import { t as Field } from "./field-CVbywksE.js";
|
|
5
|
+
import { useCallback } from "react";
|
|
5
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
7
|
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
|
|
7
|
-
import { Controller, FormProvider, useForm as useForm$1, useFormState } from "react-hook-form";
|
|
8
|
+
import { Controller, FormProvider, useForm as useForm$1, useFormContext, useFormState } from "react-hook-form";
|
|
8
9
|
//#region src/patterns/form/form.tsx
|
|
9
10
|
/**
|
|
10
11
|
* react-hook-form's `useForm`, validated by a schema. Field names and submitted values are
|
|
@@ -17,7 +18,16 @@ function useForm({ schema, ...options }) {
|
|
|
17
18
|
resolver: standardSchemaResolver(schema)
|
|
18
19
|
});
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
+
/** The rendered `<form>` of each form, so a FormSubmit outside it can submit it. */
|
|
22
|
+
const formElements = /* @__PURE__ */ new WeakMap();
|
|
23
|
+
function Form({ form, onSubmit, children, ref, ...props }) {
|
|
24
|
+
const control = form.control;
|
|
25
|
+
const registerElement = useCallback((element) => {
|
|
26
|
+
if (element) formElements.set(control, element);
|
|
27
|
+
else formElements.delete(control);
|
|
28
|
+
if (typeof ref === "function") ref(element);
|
|
29
|
+
else if (ref) ref.current = element;
|
|
30
|
+
}, [control, ref]);
|
|
21
31
|
const submit = form.handleSubmit(async (values) => {
|
|
22
32
|
try {
|
|
23
33
|
const result = await onSubmit(values);
|
|
@@ -38,6 +48,7 @@ function Form({ form, onSubmit, children, ...props }) {
|
|
|
38
48
|
children: /* @__PURE__ */ jsx("form", {
|
|
39
49
|
noValidate: true,
|
|
40
50
|
onSubmit: submit,
|
|
51
|
+
ref: registerElement,
|
|
41
52
|
...props,
|
|
42
53
|
children
|
|
43
54
|
})
|
|
@@ -56,16 +67,30 @@ function FormField({ control, name, children, ...fieldProps }) {
|
|
|
56
67
|
});
|
|
57
68
|
}
|
|
58
69
|
/** Submit button that shows progress while the form submits. */
|
|
59
|
-
function FormSubmit(props) {
|
|
60
|
-
const
|
|
61
|
-
|
|
70
|
+
function FormSubmit({ form, onClick, ...props }) {
|
|
71
|
+
const context = useFormContext();
|
|
72
|
+
const control = (form ?? context)?.control;
|
|
73
|
+
if (!control) throw new Error("FormSubmit must be inside <Form> or receive form={form}, e.g. <FormSubmit form={form}> in a Modal footer.");
|
|
74
|
+
const { isSubmitting } = useFormState({ control });
|
|
75
|
+
if (!form) return /* @__PURE__ */ jsx(Button, {
|
|
62
76
|
type: "submit",
|
|
63
77
|
loading: isSubmitting,
|
|
78
|
+
onClick,
|
|
79
|
+
...props
|
|
80
|
+
});
|
|
81
|
+
const submitOutside = (event) => {
|
|
82
|
+
onClick?.(event);
|
|
83
|
+
if (!event.defaultPrevented) formElements.get(control)?.requestSubmit();
|
|
84
|
+
};
|
|
85
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
86
|
+
type: "button",
|
|
87
|
+
loading: isSubmitting,
|
|
88
|
+
onClick: submitOutside,
|
|
64
89
|
...props
|
|
65
90
|
});
|
|
66
91
|
}
|
|
67
92
|
const formErrorStyles = tv({ slots: {
|
|
68
|
-
base: "flex items-start gap-2 rounded-md border border-danger/30 bg-danger/8 px-3 py-2.5 text-small text-danger",
|
|
93
|
+
base: "flex items-start gap-2 rounded-md border border-danger/30 bg-danger/8 px-3 py-2.5 text-small text-danger-text",
|
|
69
94
|
icon: "mt-0.5 size-4 shrink-0"
|
|
70
95
|
} });
|
|
71
96
|
/** Form-level error (thrown from `onSubmit` or returned as `errors.root`). */
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Theme } from "@glyphui/tokens";
|
|
2
|
+
import { ElementType, MouseEventHandler, ReactNode } from "react";
|
|
3
|
+
//#region src/provider/glyph-context.d.ts
|
|
4
|
+
/** Props Glyph passes to your router's link component. */
|
|
5
|
+
interface LinkComponentProps {
|
|
6
|
+
href: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
onClick?: MouseEventHandler<HTMLAnchorElement>;
|
|
10
|
+
'aria-current'?: 'page' | undefined;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Any component that renders a link from these props. Plain `'a'` by default.
|
|
14
|
+
* React Router: `({ href, ...props }) => <Link to={href} {...props} />`
|
|
15
|
+
*/
|
|
16
|
+
type LinkComponent = ElementType<LinkComponentProps>;
|
|
17
|
+
interface IsActiveOptions {
|
|
18
|
+
/** Match only this exact path, not its children. Default false. */
|
|
19
|
+
end?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A hook that reports whether `href` is the current location. Glyph calls it once per
|
|
23
|
+
* navigation item on every render, so it must follow the rules of hooks.
|
|
24
|
+
*/
|
|
25
|
+
type UseIsActive = (href: string, options?: IsActiveOptions) => boolean;
|
|
26
|
+
/** Connects Glyph's navigation components to your router. */
|
|
27
|
+
interface GlyphRouter {
|
|
28
|
+
/** Renders links client-side. */
|
|
29
|
+
Link: LinkComponent;
|
|
30
|
+
/** Marks Sidebar, Navbar and NavigationMenu items active automatically. */
|
|
31
|
+
useIsActive?: UseIsActive;
|
|
32
|
+
}
|
|
33
|
+
interface GlyphContextValue {
|
|
34
|
+
theme: Theme;
|
|
35
|
+
linkComponent: LinkComponent;
|
|
36
|
+
useIsActive: UseIsActive;
|
|
37
|
+
}
|
|
38
|
+
/** The active theme and app-level settings. Must be used under `GlyphProvider`. */
|
|
39
|
+
declare function useGlyph(): GlyphContextValue;
|
|
40
|
+
/** The router-aware link component configured on `GlyphProvider`. */
|
|
41
|
+
declare function useLinkComponent(): LinkComponent;
|
|
42
|
+
/**
|
|
43
|
+
* Whether `href` is the current page, using the router passed to `GlyphProvider`.
|
|
44
|
+
* Returns `explicit` when it is set, so an `active` prop always wins.
|
|
45
|
+
*/
|
|
46
|
+
declare function useIsActive(href: string | undefined, options?: IsActiveOptions & {
|
|
47
|
+
explicit?: boolean | undefined;
|
|
48
|
+
}): boolean;
|
|
49
|
+
//#endregion
|
|
50
|
+
export { LinkComponentProps as a, useIsActive as c, LinkComponent as i, useLinkComponent as l, GlyphRouter as n, UseIsActive as o, IsActiveOptions as r, useGlyph as s, GlyphContextValue as t };
|
|
@@ -156,6 +156,8 @@ declare const fieldStyles: import("tailwind-variants").TVReturnType<{
|
|
|
156
156
|
}, {
|
|
157
157
|
root: string;
|
|
158
158
|
label: string;
|
|
159
|
+
labelRow: string;
|
|
160
|
+
labelAction: string;
|
|
159
161
|
required: string;
|
|
160
162
|
text: string;
|
|
161
163
|
description: string;
|
|
@@ -170,6 +172,8 @@ declare const fieldStyles: import("tailwind-variants").TVReturnType<{
|
|
|
170
172
|
}, {
|
|
171
173
|
root: string;
|
|
172
174
|
label: string;
|
|
175
|
+
labelRow: string;
|
|
176
|
+
labelAction: string;
|
|
173
177
|
required: string;
|
|
174
178
|
text: string;
|
|
175
179
|
description: string;
|
|
@@ -184,6 +188,8 @@ declare const fieldStyles: import("tailwind-variants").TVReturnType<{
|
|
|
184
188
|
}, {
|
|
185
189
|
root: string;
|
|
186
190
|
label: string;
|
|
191
|
+
labelRow: string;
|
|
192
|
+
labelAction: string;
|
|
187
193
|
required: string;
|
|
188
194
|
text: string;
|
|
189
195
|
description: string;
|
|
@@ -212,6 +218,11 @@ declare function IconButton({ icon, variant, ...props }: IconButtonProps): impor
|
|
|
212
218
|
//#region src/components/field/field.d.ts
|
|
213
219
|
interface FieldProps extends Omit<ComponentProps<'div'>, 'children'>, FieldVariants {
|
|
214
220
|
label?: ReactNode;
|
|
221
|
+
/**
|
|
222
|
+
* Shown at the end of the label row, outside the label, so it is not part of the
|
|
223
|
+
* control's accessible name. For example a "Forgot password?" link.
|
|
224
|
+
*/
|
|
225
|
+
labelAction?: ReactNode;
|
|
215
226
|
description?: ReactNode;
|
|
216
227
|
/** Error message. Marks the control invalid and is announced to screen readers. */
|
|
217
228
|
error?: ReactNode;
|