@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.
Files changed (63) hide show
  1. package/README.md +65 -12
  2. package/dist/chart.d.ts +1 -1
  3. package/dist/chart.js +28 -19
  4. package/dist/color-mode-script-Bf7LYkOj.js +13 -0
  5. package/dist/color-mode-script-KdwItKN2.d.ts +16 -0
  6. package/dist/data-table.js +3 -3
  7. package/dist/{field-CWZ_FdkD.js → field-CVbywksE.js} +21 -6
  8. package/dist/{field-context-anGPYXbW.js → field-context-7I-cWvvS.js} +2 -2
  9. package/dist/form.d.ts +11 -4
  10. package/dist/form.js +34 -9
  11. package/dist/glyph-context-DxGJrdvr.d.ts +50 -0
  12. package/dist/{index-BWK4czIG.d.ts → index-Bi7rZ6ba.d.ts} +11 -0
  13. package/dist/index.d.ts +87 -80
  14. package/dist/index.js +155 -82
  15. package/dist/react-router.d.ts +18 -0
  16. package/dist/react-router.js +44 -0
  17. package/dist/{states-DR-OoDT-.js → states-IvhjBgna.js} +2 -2
  18. package/dist/{tv-DBAGlgXJ.js → tv-DLbT66dC.js} +13 -1
  19. package/dist/vite.d.ts +27 -0
  20. package/dist/vite.js +29 -0
  21. package/fonts.css +10 -0
  22. package/package.json +32 -5
  23. package/src/components/badge/badge.styles.ts +11 -16
  24. package/src/components/banner/banner.styles.ts +4 -4
  25. package/src/components/button/button.styles.ts +3 -1
  26. package/src/components/calendar/calendar.styles.ts +3 -2
  27. package/src/components/dropdown-menu/dropdown-menu.styles.ts +2 -2
  28. package/src/components/eyebrow/eyebrow.styles.ts +1 -1
  29. package/src/components/field/field-context.ts +2 -0
  30. package/src/components/field/field.styles.ts +4 -2
  31. package/src/components/field/field.tsx +25 -4
  32. package/src/components/file-upload/file-upload.styles.ts +3 -3
  33. package/src/components/link/link.styles.ts +1 -1
  34. package/src/components/modal/modal.styles.ts +2 -0
  35. package/src/components/modal/modal.tsx +30 -5
  36. package/src/components/navigation-menu/navigation-menu.tsx +14 -4
  37. package/src/components/password-input/password-input.styles.ts +3 -3
  38. package/src/components/prose/prose.tsx +1 -1
  39. package/src/components/scroll-area/scroll-area.tsx +23 -1
  40. package/src/components/segmented-control/segmented-control.tsx +17 -2
  41. package/src/components/slider/slider.tsx +9 -2
  42. package/src/components/stepper/stepper.styles.ts +2 -2
  43. package/src/components/tag-input/tag-input.styles.ts +1 -1
  44. package/src/components/text/text.styles.ts +4 -4
  45. package/src/integrations/react-router.tsx +39 -0
  46. package/src/integrations/vite.test-d.ts +6 -0
  47. package/src/integrations/vite.test.ts +28 -0
  48. package/src/integrations/vite.ts +33 -0
  49. package/src/internal/dev.ts +12 -0
  50. package/src/patterns/app-shell/app-shell.styles.ts +1 -1
  51. package/src/patterns/app-shell/navbar.tsx +34 -16
  52. package/src/patterns/app-shell/sidebar.tsx +7 -2
  53. package/src/patterns/chart/chart-parts.tsx +10 -2
  54. package/src/patterns/chart/chart.tsx +22 -5
  55. package/src/patterns/form/form.tsx +48 -6
  56. package/src/patterns/form/index.ts +1 -0
  57. package/src/provider/color-mode-script.ts +28 -0
  58. package/src/provider/color-mode.tsx +14 -20
  59. package/src/provider/glyph-context.ts +38 -0
  60. package/src/provider/glyph-provider.tsx +16 -4
  61. package/src/provider/index.ts +4 -0
  62. package/src/provider/theme-scope.tsx +7 -3
  63. 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: the form and data-table patterns
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
- <GlyphProvider theme={presets.glyph} defaultMode="system">
31
- <App />
32
- </GlyphProvider>,
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
- To avoid a light flash for dark-mode visitors, run `getColorModeScript()` in `<head>`, or render `<ColorModeScript />` with an SSR framework.
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
- `Button` · `IconButton` · `Badge` · `Card` · `Heading` · `Text` · `Eyebrow` · `Kbd` · `Avatar` · `Separator` · `Skeleton` · `Spinner` · `Label` · `Field` · `Input` · `Textarea` · `Checkbox` · `Switch` · `Select` · `SegmentedControl` · `Tabs` · `Tooltip` · `Table` · `Container` · `Section` · `Stack` · `ThemeScope`
72
-
73
- Accessible behavior comes from [Radix UI](https://www.radix-ui.com/). Every component accepts `className`, and most accept `asChild`.
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} active>Inbox</SidebarItem>
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. To use your router's link component, pass it to `GlyphProvider`: `linkComponent={RouterLink}`.
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-DBAGlgXJ.js";
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
- "primary",
28
- "info",
29
- "success",
30
- "warning",
31
- "danger"
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-]+$/.test(value) ? `var(--color-${value})` : value;
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, { children: [tooltip(valueFormatter, false), /* @__PURE__ */ jsx(R.Pie, {
385
- data: rows,
386
- dataKey: value,
387
- nameKey: category,
388
- innerRadius: "68%",
389
- outerRadius: "100%",
390
- paddingAngle: 2,
391
- stroke: "var(--color-surface)",
392
- strokeWidth: 2,
393
- isAnimationActive: false,
394
- children: rows.map((_, i) => /* @__PURE__ */ jsx(R.Cell, { fill: colors[i] }, i))
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 };
@@ -1,7 +1,7 @@
1
1
  "use client";
2
- import { a as themedBorder, r as focusRing, s as useComponentConfig, t as tv } from "./tv-DBAGlgXJ.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-anGPYXbW.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-DR-OoDT-.js";
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-DBAGlgXJ.js";
3
- import { t as FieldContext } from "./field-context-anGPYXbW.js";
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
- required: "text-danger",
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 labelNode = label ? /* @__PURE__ */ jsxs(Label$1, {
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-DBAGlgXJ.js";
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-BWK4czIG.js";
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: Omit<ButtonProps, 'type'>): import("react").JSX.Element;
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-DBAGlgXJ.js";
3
- import { c as AlertIcon, r as Button } from "./field-context-anGPYXbW.js";
4
- import { t as Field } from "./field-CWZ_FdkD.js";
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
- function Form({ form, onSubmit, children, ...props }) {
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 { isSubmitting } = useFormState();
61
- return /* @__PURE__ */ jsx(Button, {
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;