@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
|
@@ -14,6 +14,18 @@ function useGlyph() {
|
|
|
14
14
|
function useLinkComponent() {
|
|
15
15
|
return useContext(GlyphContext)?.linkComponent ?? "a";
|
|
16
16
|
}
|
|
17
|
+
const neverActive = () => false;
|
|
18
|
+
/**
|
|
19
|
+
* Whether `href` is the current page, using the router passed to `GlyphProvider`.
|
|
20
|
+
* Returns `explicit` when it is set, so an `active` prop always wins.
|
|
21
|
+
*/
|
|
22
|
+
function useIsActive(href, options) {
|
|
23
|
+
const isActive = useContext(GlyphContext)?.useIsActive ?? neverActive;
|
|
24
|
+
const { explicit, ...matchOptions } = options ?? {};
|
|
25
|
+
const matched = isActive(href ?? "", matchOptions);
|
|
26
|
+
if (explicit !== void 0) return explicit;
|
|
27
|
+
return href ? matched : false;
|
|
28
|
+
}
|
|
17
29
|
//#endregion
|
|
18
30
|
//#region src/hooks/use-component-config.ts
|
|
19
31
|
/**
|
|
@@ -55,4 +67,4 @@ const twMergeConfig = { extend: { theme: {
|
|
|
55
67
|
/** `tailwind-variants` preconfigured with Glyph's merge rules. All component styles use this. */
|
|
56
68
|
const tv = createTV({ twMergeConfig });
|
|
57
69
|
//#endregion
|
|
58
|
-
export { themedBorder as a, GlyphContext as c, popover as i,
|
|
70
|
+
export { themedBorder as a, GlyphContext as c, useIsActive as d, useLinkComponent as f, popover as i, neverActive as l, twMergeConfig as n, transition as o, focusRing as r, useComponentConfig as s, tv as t, useGlyph as u };
|
package/dist/vite.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { t as ColorModeOptions } from "./color-mode-script-KdwItKN2.js";
|
|
2
|
+
//#region src/integrations/vite.d.ts
|
|
3
|
+
/** The subset of Vite's `Plugin` this plugin uses, so `vite` stays an optional peer. */
|
|
4
|
+
export interface GlyphVitePlugin {
|
|
5
|
+
name: string;
|
|
6
|
+
transformIndexHtml: {
|
|
7
|
+
order: 'pre';
|
|
8
|
+
handler: () => {
|
|
9
|
+
tag: 'script';
|
|
10
|
+
children: string;
|
|
11
|
+
injectTo: 'head-prepend';
|
|
12
|
+
}[];
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Injects Glyph's color-mode script into `index.html` so dark-mode visitors never see a
|
|
17
|
+
* light flash. Pass the same `defaultMode` and `storageKey` as `GlyphProvider`.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* // vite.config.ts
|
|
21
|
+
* import { glyphColorMode } from '@glyphui/ui/vite'
|
|
22
|
+
* export default defineConfig({ plugins: [react(), tailwindcss(), glyphColorMode()] })
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function glyphColorMode(options?: ColorModeOptions): GlyphVitePlugin;
|
|
26
|
+
//#endregion
|
|
27
|
+
export type { ColorModeOptions };
|
package/dist/vite.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { r as getColorModeScript } from "./color-mode-script-Bf7LYkOj.js";
|
|
3
|
+
//#region src/integrations/vite.ts
|
|
4
|
+
/**
|
|
5
|
+
* Injects Glyph's color-mode script into `index.html` so dark-mode visitors never see a
|
|
6
|
+
* light flash. Pass the same `defaultMode` and `storageKey` as `GlyphProvider`.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* // vite.config.ts
|
|
10
|
+
* import { glyphColorMode } from '@glyphui/ui/vite'
|
|
11
|
+
* export default defineConfig({ plugins: [react(), tailwindcss(), glyphColorMode()] })
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
function glyphColorMode(options = {}) {
|
|
15
|
+
const script = getColorModeScript(options);
|
|
16
|
+
return {
|
|
17
|
+
name: "glyph:color-mode",
|
|
18
|
+
transformIndexHtml: {
|
|
19
|
+
order: "pre",
|
|
20
|
+
handler: () => [{
|
|
21
|
+
tag: "script",
|
|
22
|
+
children: script,
|
|
23
|
+
injectTo: "head-prepend"
|
|
24
|
+
}]
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { glyphColorMode };
|
package/fonts.css
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Geist and Geist Mono, the fonts the default `glyph` theme uses. Import once, before the
|
|
3
|
+
* Glyph stylesheet:
|
|
4
|
+
*
|
|
5
|
+
* @import "@glyphui/ui/fonts.css";
|
|
6
|
+
* @import "tailwindcss";
|
|
7
|
+
* @import "@glyphui/ui/styles.css";
|
|
8
|
+
*/
|
|
9
|
+
@import "@fontsource-variable/geist";
|
|
10
|
+
@import "@fontsource-variable/geist-mono";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glyphui/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "React components on Tailwind CSS v4 that take their entire look from one theme file. Forms, data tables, app layouts and async states included.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Mohak Gupta (https://github.com/khalmohak)",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"theme",
|
|
25
25
|
"dark-mode",
|
|
26
26
|
"forms",
|
|
27
|
-
"data-table"
|
|
27
|
+
"data-table",
|
|
28
|
+
"react-router"
|
|
28
29
|
],
|
|
29
30
|
"type": "module",
|
|
30
31
|
"sideEffects": [
|
|
@@ -51,19 +52,33 @@
|
|
|
51
52
|
"types": "./dist/chart.d.ts",
|
|
52
53
|
"default": "./dist/chart.js"
|
|
53
54
|
},
|
|
55
|
+
"./react-router": {
|
|
56
|
+
"@glyphui/source": "./src/integrations/react-router.tsx",
|
|
57
|
+
"types": "./dist/react-router.d.ts",
|
|
58
|
+
"default": "./dist/react-router.js"
|
|
59
|
+
},
|
|
60
|
+
"./vite": {
|
|
61
|
+
"@glyphui/source": "./src/integrations/vite.ts",
|
|
62
|
+
"types": "./dist/vite.d.ts",
|
|
63
|
+
"default": "./dist/vite.js"
|
|
64
|
+
},
|
|
54
65
|
"./styles.css": "./styles.css",
|
|
66
|
+
"./fonts.css": "./fonts.css",
|
|
55
67
|
"./package.json": "./package.json"
|
|
56
68
|
},
|
|
57
69
|
"files": [
|
|
58
70
|
"dist",
|
|
59
71
|
"src",
|
|
60
72
|
"styles.css",
|
|
73
|
+
"fonts.css",
|
|
61
74
|
"!src/**/*.test.tsx",
|
|
62
75
|
"!src/**/*.test-d.tsx",
|
|
63
76
|
"!src/test"
|
|
64
77
|
],
|
|
65
78
|
"dependencies": {
|
|
66
|
-
"@
|
|
79
|
+
"@fontsource-variable/geist": "^5.3.0",
|
|
80
|
+
"@fontsource-variable/geist-mono": "^5.3.0",
|
|
81
|
+
"@glyphui/tokens": "^0.4.0",
|
|
67
82
|
"@standard-schema/spec": "^1.1.0",
|
|
68
83
|
"clsx": "^2.1.1",
|
|
69
84
|
"cmdk": "^1.1.1",
|
|
@@ -83,8 +98,10 @@
|
|
|
83
98
|
"react": ">=19",
|
|
84
99
|
"react-dom": ">=19",
|
|
85
100
|
"react-hook-form": "^7.55.0",
|
|
101
|
+
"react-router": ">=7",
|
|
86
102
|
"recharts": "^3.0.0",
|
|
87
|
-
"tailwindcss": ">=4.1"
|
|
103
|
+
"tailwindcss": ">=4.1",
|
|
104
|
+
"vite": ">=5"
|
|
88
105
|
},
|
|
89
106
|
"peerDependenciesMeta": {
|
|
90
107
|
"@hookform/resolvers": {
|
|
@@ -96,8 +113,14 @@
|
|
|
96
113
|
"react-hook-form": {
|
|
97
114
|
"optional": true
|
|
98
115
|
},
|
|
116
|
+
"react-router": {
|
|
117
|
+
"optional": true
|
|
118
|
+
},
|
|
99
119
|
"recharts": {
|
|
100
120
|
"optional": true
|
|
121
|
+
},
|
|
122
|
+
"vite": {
|
|
123
|
+
"optional": true
|
|
101
124
|
}
|
|
102
125
|
},
|
|
103
126
|
"devDependencies": {
|
|
@@ -110,13 +133,16 @@
|
|
|
110
133
|
"@testing-library/user-event": "^14.6.7",
|
|
111
134
|
"@types/react": "^19.2.18",
|
|
112
135
|
"@types/react-dom": "^19.2.7",
|
|
136
|
+
"@vitest/coverage-v8": "3.2.7",
|
|
113
137
|
"jsdom": "^27.4.0",
|
|
114
138
|
"react": "^19.2.8",
|
|
115
139
|
"react-dom": "^19.2.8",
|
|
116
140
|
"react-hook-form": "^7.89.0",
|
|
141
|
+
"react-router": "^7.18.4",
|
|
117
142
|
"recharts": "^3.10.1",
|
|
118
143
|
"tsdown": "^0.23.0",
|
|
119
144
|
"typescript": "~5.9.3",
|
|
145
|
+
"vite": "^7.3.6",
|
|
120
146
|
"vitest": "^3.2.7",
|
|
121
147
|
"zod": "^4.6.5"
|
|
122
148
|
},
|
|
@@ -128,6 +154,7 @@
|
|
|
128
154
|
"build": "tsdown",
|
|
129
155
|
"typecheck": "tsc",
|
|
130
156
|
"test": "vitest run",
|
|
131
|
-
"
|
|
157
|
+
"test:coverage": "vitest run --coverage",
|
|
158
|
+
"check:exports": "attw --pack . --profile esm-only --exclude-entrypoints ./styles.css ./fonts.css"
|
|
132
159
|
}
|
|
133
160
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { themedBorder } from '../../utils/styles'
|
|
2
2
|
import { tv, type VariantProps } from '../../utils/tv'
|
|
3
3
|
|
|
4
|
-
// Tinted text
|
|
4
|
+
// Tinted text uses the theme's status text colors, which stay readable on tinted backgrounds.
|
|
5
5
|
const tones = {
|
|
6
6
|
neutral: {
|
|
7
7
|
soft: 'bg-muted text-muted-foreground',
|
|
@@ -9,34 +9,29 @@ const tones = {
|
|
|
9
9
|
outline: 'border-border-strong text-foreground',
|
|
10
10
|
},
|
|
11
11
|
primary: {
|
|
12
|
-
soft: 'bg-primary/14 text-
|
|
12
|
+
soft: 'bg-primary/14 text-primary-text',
|
|
13
13
|
solid: 'bg-primary text-primary-foreground',
|
|
14
|
-
outline:
|
|
15
|
-
'border-primary/50 text-[color-mix(in_oklab,var(--color-primary)_62%,var(--color-foreground))]',
|
|
14
|
+
outline: 'border-primary/50 text-primary-text',
|
|
16
15
|
},
|
|
17
16
|
success: {
|
|
18
|
-
soft: 'bg-success/14 text-
|
|
17
|
+
soft: 'bg-success/14 text-success-text',
|
|
19
18
|
solid: 'bg-success text-success-foreground',
|
|
20
|
-
outline:
|
|
21
|
-
'border-success/50 text-[color-mix(in_oklab,var(--color-success)_62%,var(--color-foreground))]',
|
|
19
|
+
outline: 'border-success/50 text-success-text',
|
|
22
20
|
},
|
|
23
21
|
warning: {
|
|
24
|
-
soft: 'bg-warning/
|
|
22
|
+
soft: 'bg-warning/14 text-warning-text',
|
|
25
23
|
solid: 'bg-warning text-warning-foreground',
|
|
26
|
-
outline:
|
|
27
|
-
'border-warning/50 text-[color-mix(in_oklab,var(--color-warning)_62%,var(--color-foreground))]',
|
|
24
|
+
outline: 'border-warning/50 text-warning-text',
|
|
28
25
|
},
|
|
29
26
|
danger: {
|
|
30
|
-
soft: 'bg-danger/14 text-
|
|
27
|
+
soft: 'bg-danger/14 text-danger-text',
|
|
31
28
|
solid: 'bg-danger text-danger-foreground',
|
|
32
|
-
outline:
|
|
33
|
-
'border-danger/50 text-[color-mix(in_oklab,var(--color-danger)_62%,var(--color-foreground))]',
|
|
29
|
+
outline: 'border-danger/50 text-danger-text',
|
|
34
30
|
},
|
|
35
31
|
info: {
|
|
36
|
-
soft: 'bg-info/14 text-
|
|
32
|
+
soft: 'bg-info/14 text-info-text',
|
|
37
33
|
solid: 'bg-info text-info-foreground',
|
|
38
|
-
outline:
|
|
39
|
-
'border-info/50 text-[color-mix(in_oklab,var(--color-info)_62%,var(--color-foreground))]',
|
|
34
|
+
outline: 'border-info/50 text-info-text',
|
|
40
35
|
},
|
|
41
36
|
} as const
|
|
42
37
|
|
|
@@ -15,16 +15,16 @@ export const bannerStyles = tv({
|
|
|
15
15
|
neutral: { root: 'border-border bg-subtle text-foreground', icon: 'text-muted-foreground' },
|
|
16
16
|
primary: { root: 'border-primary bg-primary text-primary-foreground' },
|
|
17
17
|
info: {
|
|
18
|
-
root: 'border-info/25 bg-info/10 text-
|
|
18
|
+
root: 'border-info/25 bg-info/10 text-info-text',
|
|
19
19
|
},
|
|
20
20
|
success: {
|
|
21
|
-
root: 'border-success/25 bg-success/10 text-
|
|
21
|
+
root: 'border-success/25 bg-success/10 text-success-text',
|
|
22
22
|
},
|
|
23
23
|
warning: {
|
|
24
|
-
root: 'border-warning/30 bg-warning/14 text-
|
|
24
|
+
root: 'border-warning/30 bg-warning/14 text-warning-text',
|
|
25
25
|
},
|
|
26
26
|
danger: {
|
|
27
|
-
root: 'border-danger/25 bg-danger/10 text-
|
|
27
|
+
root: 'border-danger/25 bg-danger/10 text-danger-text',
|
|
28
28
|
},
|
|
29
29
|
},
|
|
30
30
|
},
|
|
@@ -22,7 +22,9 @@ export const buttonStyles = tv({
|
|
|
22
22
|
},
|
|
23
23
|
ghost: { base: 'text-muted-foreground hover:bg-muted hover:text-foreground' },
|
|
24
24
|
danger: { base: 'bg-danger text-danger-foreground shadow-sm hover:bg-danger/88' },
|
|
25
|
-
link: {
|
|
25
|
+
link: {
|
|
26
|
+
base: 'h-auto px-0 text-primary-text underline-offset-4 shadow-none hover:underline',
|
|
27
|
+
},
|
|
26
28
|
},
|
|
27
29
|
size: {
|
|
28
30
|
sm: { base: 'h-(--glyph-control-sm) px-3 text-small', icon: 'size-3.5' },
|
|
@@ -29,10 +29,11 @@ export const calendarStyles = tv({
|
|
|
29
29
|
transition,
|
|
30
30
|
focusRing,
|
|
31
31
|
],
|
|
32
|
-
|
|
32
|
+
// Selected days keep primary-foreground on primary, whatever else applies to them.
|
|
33
|
+
today: '[&>button]:font-semibold [&:not([data-selected])>button]:text-primary-text',
|
|
33
34
|
selected:
|
|
34
35
|
'[&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:hover:bg-primary/90',
|
|
35
|
-
outside: '[
|
|
36
|
+
outside: '[&:not([data-selected])>button]:text-faint-foreground',
|
|
36
37
|
disabled: '[&>button]:pointer-events-none [&>button]:opacity-35 [&>button]:line-through',
|
|
37
38
|
rangeStart: 'rounded-l-md bg-primary/14',
|
|
38
39
|
rangeMiddle:
|
|
@@ -27,8 +27,8 @@ export const dropdownMenuStyles = tv({
|
|
|
27
27
|
tone: {
|
|
28
28
|
default: {},
|
|
29
29
|
danger: {
|
|
30
|
-
item: 'text-danger data-highlighted:bg-danger/10',
|
|
31
|
-
icon: 'text-danger',
|
|
30
|
+
item: 'text-danger-text data-highlighted:bg-danger/10',
|
|
31
|
+
icon: 'text-danger-text',
|
|
32
32
|
},
|
|
33
33
|
},
|
|
34
34
|
inset: { true: { item: 'pl-8', label: 'pl-8' } },
|
|
@@ -2,6 +2,8 @@ import { createContext, useContext } from 'react'
|
|
|
2
2
|
|
|
3
3
|
export interface FieldContextValue {
|
|
4
4
|
id: string
|
|
5
|
+
/** Id of the visible label, for controls labelled by reference (aria-labelledby). */
|
|
6
|
+
labelId: string | undefined
|
|
5
7
|
descriptionId: string | undefined
|
|
6
8
|
errorId: string | undefined
|
|
7
9
|
invalid: boolean
|
|
@@ -4,10 +4,12 @@ export const fieldStyles = tv({
|
|
|
4
4
|
slots: {
|
|
5
5
|
root: 'grid gap-1.5',
|
|
6
6
|
label: '',
|
|
7
|
-
|
|
7
|
+
labelRow: 'flex items-center justify-between gap-2',
|
|
8
|
+
labelAction: 'text-small',
|
|
9
|
+
required: 'text-danger-text',
|
|
8
10
|
text: 'grid gap-0.5',
|
|
9
11
|
description: 'text-small text-faint-foreground',
|
|
10
|
-
error: 'text-small font-medium text-danger',
|
|
12
|
+
error: 'text-small font-medium text-danger-text',
|
|
11
13
|
},
|
|
12
14
|
variants: {
|
|
13
15
|
orientation: {
|
|
@@ -6,6 +6,11 @@ import { FieldContext } from './field-context'
|
|
|
6
6
|
|
|
7
7
|
export interface FieldProps extends Omit<ComponentProps<'div'>, 'children'>, FieldVariants {
|
|
8
8
|
label?: ReactNode
|
|
9
|
+
/**
|
|
10
|
+
* Shown at the end of the label row, outside the label, so it is not part of the
|
|
11
|
+
* control's accessible name. For example a "Forgot password?" link.
|
|
12
|
+
*/
|
|
13
|
+
labelAction?: ReactNode
|
|
9
14
|
description?: ReactNode
|
|
10
15
|
/** Error message. Marks the control invalid and is announced to screen readers. */
|
|
11
16
|
error?: ReactNode
|
|
@@ -27,6 +32,7 @@ export function Field(inProps: FieldProps) {
|
|
|
27
32
|
const [props, slots] = useComponentConfig('Field', inProps)
|
|
28
33
|
const {
|
|
29
34
|
label,
|
|
35
|
+
labelAction,
|
|
30
36
|
description,
|
|
31
37
|
error,
|
|
32
38
|
required = false,
|
|
@@ -40,17 +46,18 @@ export function Field(inProps: FieldProps) {
|
|
|
40
46
|
const generated = useId()
|
|
41
47
|
const id = controlId ?? generated
|
|
42
48
|
const invalid = Boolean(error)
|
|
49
|
+
const labelId = label ? `${id}-label` : undefined
|
|
43
50
|
const descriptionId = description ? `${id}-description` : undefined
|
|
44
51
|
const errorId = invalid ? `${id}-error` : undefined
|
|
45
52
|
const s = fieldStyles({ orientation })
|
|
46
53
|
|
|
47
54
|
const context = useMemo(
|
|
48
|
-
() => ({ id, descriptionId, errorId, invalid, required, disabled }),
|
|
49
|
-
[id, descriptionId, errorId, invalid, required, disabled],
|
|
55
|
+
() => ({ id, labelId, descriptionId, errorId, invalid, required, disabled }),
|
|
56
|
+
[id, labelId, descriptionId, errorId, invalid, required, disabled],
|
|
50
57
|
)
|
|
51
58
|
|
|
52
|
-
const
|
|
53
|
-
<Label htmlFor={id} className={s.label({ class: slots.label })}>
|
|
59
|
+
const labelElement = label ? (
|
|
60
|
+
<Label id={labelId} htmlFor={id} className={s.label({ class: slots.label })}>
|
|
54
61
|
{label}
|
|
55
62
|
{required && (
|
|
56
63
|
<span className={s.required({ class: slots.required })} aria-hidden="true">
|
|
@@ -59,6 +66,20 @@ export function Field(inProps: FieldProps) {
|
|
|
59
66
|
)}
|
|
60
67
|
</Label>
|
|
61
68
|
) : null
|
|
69
|
+
const labelNode =
|
|
70
|
+
labelElement && labelAction ? (
|
|
71
|
+
<div data-slot="field-label-row" className={s.labelRow({ class: slots.labelRow })}>
|
|
72
|
+
{labelElement}
|
|
73
|
+
<span
|
|
74
|
+
data-slot="field-label-action"
|
|
75
|
+
className={s.labelAction({ class: slots.labelAction })}
|
|
76
|
+
>
|
|
77
|
+
{labelAction}
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
) : (
|
|
81
|
+
labelElement
|
|
82
|
+
)
|
|
62
83
|
|
|
63
84
|
const messages = (
|
|
64
85
|
<>
|
|
@@ -16,9 +16,9 @@ export const fileUploadStyles = tv({
|
|
|
16
16
|
],
|
|
17
17
|
icon: 'flex size-10 items-center justify-center rounded-md border border-border bg-surface text-muted-foreground shadow-xs [&>svg]:size-5',
|
|
18
18
|
title: 'text-small font-medium text-foreground',
|
|
19
|
-
highlight: 'text-primary',
|
|
19
|
+
highlight: 'text-primary-text',
|
|
20
20
|
hint: 'text-caption text-faint-foreground',
|
|
21
|
-
rejections: 'grid gap-1 text-small font-medium text-danger',
|
|
21
|
+
rejections: 'grid gap-1 text-small font-medium text-danger-text',
|
|
22
22
|
list: 'grid gap-2',
|
|
23
23
|
item: [
|
|
24
24
|
'grid grid-cols-[auto_1fr_auto] items-center gap-3 rounded-md border-border bg-surface px-3 py-2.5',
|
|
@@ -29,7 +29,7 @@ export const fileUploadStyles = tv({
|
|
|
29
29
|
fileText: 'grid min-w-0 gap-1',
|
|
30
30
|
fileName: 'truncate text-small font-medium text-foreground',
|
|
31
31
|
fileMeta: 'text-caption tabular-nums text-faint-foreground',
|
|
32
|
-
fileError: 'text-caption font-medium text-danger',
|
|
32
|
+
fileError: 'text-caption font-medium text-danger-text',
|
|
33
33
|
progress: 'h-1 overflow-hidden rounded-pill bg-muted',
|
|
34
34
|
progressBar:
|
|
35
35
|
'h-full rounded-pill bg-primary transition-[width] duration-(--glyph-duration-base)',
|
|
@@ -17,7 +17,7 @@ export const linkStyles = tv({
|
|
|
17
17
|
tone: {
|
|
18
18
|
default: { base: 'text-foreground' },
|
|
19
19
|
muted: { base: 'text-muted-foreground hover:text-foreground' },
|
|
20
|
-
primary: { base: 'text-primary' },
|
|
20
|
+
primary: { base: 'text-primary-text' },
|
|
21
21
|
},
|
|
22
22
|
underline: {
|
|
23
23
|
hover: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ReactNode } from 'react'
|
|
1
|
+
import type { FormEvent, ReactNode } from 'react'
|
|
2
2
|
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
3
3
|
import {
|
|
4
4
|
Dialog,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from '../dialog'
|
|
15
15
|
import { modalStyles } from './modal.styles'
|
|
16
16
|
|
|
17
|
-
export interface ModalProps extends Omit<DialogContentProps, 'title' | 'children'> {
|
|
17
|
+
export interface ModalProps extends Omit<DialogContentProps, 'title' | 'children' | 'onSubmit'> {
|
|
18
18
|
open?: boolean
|
|
19
19
|
defaultOpen?: boolean
|
|
20
20
|
onOpenChange?: (open: boolean) => void
|
|
@@ -27,6 +27,12 @@ export interface ModalProps extends Omit<DialogContentProps, 'title' | 'children
|
|
|
27
27
|
footer?: ReactNode
|
|
28
28
|
/** Close when the backdrop is clicked. Default true. */
|
|
29
29
|
closeOnOverlayClick?: boolean
|
|
30
|
+
/**
|
|
31
|
+
* Turns the body and footer into one `<form>`, so a `type="submit"` button in the footer
|
|
32
|
+
* submits the fields in the body. The page does not reload. With `@glyphui/ui/form`, use
|
|
33
|
+
* `<Form>` in the body and `<FormSubmit form={form}>` in the footer instead.
|
|
34
|
+
*/
|
|
35
|
+
onSubmit?: (event: FormEvent<HTMLFormElement>) => void
|
|
30
36
|
children?: ReactNode
|
|
31
37
|
}
|
|
32
38
|
|
|
@@ -45,12 +51,21 @@ export function Modal(inProps: ModalProps) {
|
|
|
45
51
|
trigger,
|
|
46
52
|
footer,
|
|
47
53
|
closeOnOverlayClick = true,
|
|
54
|
+
onSubmit,
|
|
48
55
|
onPointerDownOutside,
|
|
49
56
|
className,
|
|
50
57
|
children,
|
|
51
58
|
...rest
|
|
52
59
|
} = props
|
|
53
60
|
const s = modalStyles()
|
|
61
|
+
const content = (
|
|
62
|
+
<>
|
|
63
|
+
{children && <DialogBody className={s.body({ class: slots.body })}>{children}</DialogBody>}
|
|
64
|
+
{footer && (
|
|
65
|
+
<DialogFooter className={s.footer({ class: slots.footer })}>{footer}</DialogFooter>
|
|
66
|
+
)}
|
|
67
|
+
</>
|
|
68
|
+
)
|
|
54
69
|
|
|
55
70
|
return (
|
|
56
71
|
<Dialog
|
|
@@ -73,9 +88,19 @@ export function Modal(inProps: ModalProps) {
|
|
|
73
88
|
<DialogTitle>{title}</DialogTitle>
|
|
74
89
|
{description && <DialogDescription>{description}</DialogDescription>}
|
|
75
90
|
</DialogHeader>
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
91
|
+
{onSubmit ? (
|
|
92
|
+
<form
|
|
93
|
+
data-slot="modal-form"
|
|
94
|
+
className={s.form({ class: slots.form })}
|
|
95
|
+
onSubmit={(event) => {
|
|
96
|
+
event.preventDefault()
|
|
97
|
+
onSubmit(event)
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
{content}
|
|
101
|
+
</form>
|
|
102
|
+
) : (
|
|
103
|
+
content
|
|
79
104
|
)}
|
|
80
105
|
</DialogContent>
|
|
81
106
|
</Dialog>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NavigationMenu as NavPrimitive } from 'radix-ui'
|
|
2
2
|
import type { ComponentProps, ReactNode } from 'react'
|
|
3
3
|
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
4
|
-
import { useLinkComponent } from '../../provider/glyph-context'
|
|
4
|
+
import { useIsActive, useLinkComponent } from '../../provider/glyph-context'
|
|
5
5
|
import { navigationMenuStyles } from './navigation-menu.styles'
|
|
6
6
|
|
|
7
7
|
const s = navigationMenuStyles()
|
|
@@ -132,25 +132,35 @@ export interface NavigationMenuLinkProps
|
|
|
132
132
|
href?: string
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
/**
|
|
135
|
+
/**
|
|
136
|
+
* A top-level link (no panel). Marked active automatically from the provider's `router`;
|
|
137
|
+
* set `active` to override.
|
|
138
|
+
*/
|
|
136
139
|
export function NavigationMenuLink({
|
|
137
140
|
href,
|
|
141
|
+
active: activeProp,
|
|
138
142
|
className,
|
|
139
143
|
children,
|
|
140
144
|
...props
|
|
141
145
|
}: NavigationMenuLinkProps) {
|
|
142
146
|
const slots = useSlots()
|
|
143
147
|
const Link = useLinkComponent()
|
|
148
|
+
const active = useIsActive(href, { explicit: activeProp })
|
|
144
149
|
const cls = s.link({ class: [slots.link, className] })
|
|
145
150
|
if (href === undefined) {
|
|
146
151
|
return (
|
|
147
|
-
<NavPrimitive.Link
|
|
152
|
+
<NavPrimitive.Link
|
|
153
|
+
data-slot="navigation-menu-link"
|
|
154
|
+
className={cls}
|
|
155
|
+
active={active}
|
|
156
|
+
{...props}
|
|
157
|
+
>
|
|
148
158
|
{children}
|
|
149
159
|
</NavPrimitive.Link>
|
|
150
160
|
)
|
|
151
161
|
}
|
|
152
162
|
return (
|
|
153
|
-
<NavPrimitive.Link data-slot="navigation-menu-link" asChild {...props}>
|
|
163
|
+
<NavPrimitive.Link data-slot="navigation-menu-link" asChild active={active} {...props}>
|
|
154
164
|
<Link href={href} className={cls}>
|
|
155
165
|
{children}
|
|
156
166
|
</Link>
|
|
@@ -16,9 +16,9 @@ export const passwordInputStyles = tv({
|
|
|
16
16
|
},
|
|
17
17
|
variants: {
|
|
18
18
|
strength: {
|
|
19
|
-
weak: { label: 'text-danger' },
|
|
20
|
-
fair: { label: 'text-warning' },
|
|
21
|
-
strong: { label: 'text-success' },
|
|
19
|
+
weak: { label: 'text-danger-text' },
|
|
20
|
+
fair: { label: 'text-warning-text' },
|
|
21
|
+
strong: { label: 'text-success-text' },
|
|
22
22
|
},
|
|
23
23
|
},
|
|
24
24
|
})
|
|
@@ -20,7 +20,7 @@ export const proseStyles = tv({
|
|
|
20
20
|
// Text
|
|
21
21
|
'[&_p]:my-[1em] [&_p]:text-pretty',
|
|
22
22
|
'[&_strong]:font-semibold [&_strong]:text-foreground',
|
|
23
|
-
'[&_a]:font-medium [&_a]:text-primary [&_a]:underline [&_a]:decoration-primary/40 [&_a]:underline-offset-4 [&_a:hover]:decoration-primary',
|
|
23
|
+
'[&_a]:font-medium [&_a]:text-primary-text [&_a]:underline [&_a]:decoration-primary/40 [&_a]:underline-offset-4 [&_a:hover]:decoration-primary',
|
|
24
24
|
// Lists
|
|
25
25
|
'[&_ul]:my-[1em] [&_ul]:list-disc [&_ul]:pl-[1.5em] [&_ol]:my-[1em] [&_ol]:list-decimal [&_ol]:pl-[1.5em]',
|
|
26
26
|
'[&_li]:my-[0.35em] [&_li]:pl-[0.25em] [&_li::marker]:text-faint-foreground',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ScrollArea as ScrollAreaPrimitive } from 'radix-ui'
|
|
2
|
-
import type
|
|
2
|
+
import { type ComponentProps, useEffect, useRef, useState } from 'react'
|
|
3
3
|
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
4
4
|
import type { StylesEntry } from '../../utils/registry'
|
|
5
5
|
import { focusRing } from '../../utils/styles'
|
|
@@ -31,6 +31,24 @@ export interface ScrollAreaProps extends ComponentProps<typeof ScrollAreaPrimiti
|
|
|
31
31
|
viewportClassName?: string
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/** True while the element's content overflows it, so keyboard users need to reach it. */
|
|
35
|
+
function useOverflowing<E extends HTMLElement>() {
|
|
36
|
+
const ref = useRef<E>(null)
|
|
37
|
+
const [overflowing, setOverflowing] = useState(false)
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const el = ref.current
|
|
40
|
+
if (!el) return
|
|
41
|
+
const measure = () =>
|
|
42
|
+
setOverflowing(el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth)
|
|
43
|
+
measure()
|
|
44
|
+
const observer = new ResizeObserver(measure)
|
|
45
|
+
observer.observe(el)
|
|
46
|
+
if (el.firstElementChild) observer.observe(el.firstElementChild)
|
|
47
|
+
return () => observer.disconnect()
|
|
48
|
+
}, [])
|
|
49
|
+
return [ref, overflowing] as const
|
|
50
|
+
}
|
|
51
|
+
|
|
34
52
|
/**
|
|
35
53
|
* A scrollable region with slim, theme-colored scrollbars. Give it a height (or width)
|
|
36
54
|
* with `className`, e.g. `h-72`.
|
|
@@ -39,6 +57,7 @@ export function ScrollArea(inProps: ScrollAreaProps) {
|
|
|
39
57
|
const [props, slots] = useComponentConfig('ScrollArea', inProps)
|
|
40
58
|
const { orientation = 'vertical', viewportClassName, className, children, ...rest } = props
|
|
41
59
|
const s = scrollAreaStyles()
|
|
60
|
+
const [viewportRef, overflowing] = useOverflowing<HTMLDivElement>()
|
|
42
61
|
const bar = (o: 'vertical' | 'horizontal') => (
|
|
43
62
|
<ScrollAreaPrimitive.Scrollbar
|
|
44
63
|
data-slot="scroll-area-scrollbar"
|
|
@@ -55,7 +74,10 @@ export function ScrollArea(inProps: ScrollAreaProps) {
|
|
|
55
74
|
{...rest}
|
|
56
75
|
>
|
|
57
76
|
<ScrollAreaPrimitive.Viewport
|
|
77
|
+
ref={viewportRef}
|
|
58
78
|
data-slot="scroll-area-viewport"
|
|
79
|
+
// Scrollable regions must be reachable by keyboard to scroll with arrow keys.
|
|
80
|
+
tabIndex={overflowing ? 0 : undefined}
|
|
59
81
|
className={s.viewport({ class: [slots.viewport, viewportClassName] })}
|
|
60
82
|
>
|
|
61
83
|
{children}
|