@glyphui/ui 0.1.1 → 0.3.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 +1 -1
- package/dist/chart.d.ts +202 -0
- package/dist/chart.js +457 -0
- package/dist/data-table.js +3 -2
- package/dist/{field-DXZD0l8w.js → field-CWZ_FdkD.js} +2 -1
- package/dist/{field-context-B8cYheXz.js → field-context-anGPYXbW.js} +89 -143
- package/dist/form.js +3 -2
- package/dist/index.d.ts +4960 -266
- package/dist/index.js +5336 -146
- package/dist/{states-BxPjCUxz.js → states-DR-OoDT-.js} +2 -1
- package/dist/tv-DBAGlgXJ.js +58 -0
- package/package.json +19 -2
- package/src/components/accordion/accordion.styles.ts +41 -0
- package/src/components/accordion/accordion.tsx +86 -0
- package/src/components/accordion/index.ts +8 -0
- package/src/components/alert/alert-icons.tsx +66 -0
- package/src/components/alert/alert.styles.ts +57 -0
- package/src/components/alert/alert.tsx +86 -0
- package/src/components/alert/index.ts +2 -0
- package/src/components/alert-dialog/alert-dialog.styles.ts +24 -0
- package/src/components/alert-dialog/alert-dialog.tsx +118 -0
- package/src/components/alert-dialog/confirm.tsx +111 -0
- package/src/components/alert-dialog/index.ts +15 -0
- package/src/components/aspect-ratio/aspect-ratio.tsx +40 -0
- package/src/components/aspect-ratio/index.ts +1 -0
- package/src/components/badge/badge.styles.ts +1 -1
- package/src/components/banner/banner.styles.ts +40 -0
- package/src/components/banner/banner.tsx +101 -0
- package/src/components/banner/index.ts +2 -0
- package/src/components/blockquote/blockquote.tsx +59 -0
- package/src/components/blockquote/index.ts +1 -0
- package/src/components/breadcrumb/breadcrumb.styles.ts +20 -0
- package/src/components/breadcrumb/breadcrumb.tsx +135 -0
- package/src/components/breadcrumb/index.ts +12 -0
- package/src/components/button/button.tsx +11 -8
- package/src/components/calendar/calendar.styles.ts +50 -0
- package/src/components/calendar/calendar.tsx +84 -0
- package/src/components/calendar/index.ts +3 -0
- package/src/components/carousel/carousel.styles.ts +43 -0
- package/src/components/carousel/carousel.tsx +278 -0
- package/src/components/carousel/index.ts +13 -0
- package/src/components/code/code.styles.ts +33 -0
- package/src/components/code/code.tsx +96 -0
- package/src/components/code/index.ts +2 -0
- package/src/components/collapsible/collapsible.tsx +66 -0
- package/src/components/collapsible/index.ts +6 -0
- package/src/components/combobox/combobox.styles.ts +50 -0
- package/src/components/combobox/combobox.tsx +203 -0
- package/src/components/combobox/index.ts +2 -0
- package/src/components/command-menu/command-menu.styles.ts +41 -0
- package/src/components/command-menu/command-menu.tsx +217 -0
- package/src/components/command-menu/index.ts +14 -0
- package/src/components/context-menu/context-menu.styles.ts +19 -0
- package/src/components/context-menu/context-menu.tsx +226 -0
- package/src/components/context-menu/index.ts +19 -0
- package/src/components/copy-button/copy-button.tsx +117 -0
- package/src/components/copy-button/index.ts +1 -0
- package/src/components/date-picker/date-picker.styles.ts +42 -0
- package/src/components/date-picker/date-picker.tsx +322 -0
- package/src/components/date-picker/index.ts +8 -0
- package/src/components/description-list/description-list.styles.ts +31 -0
- package/src/components/description-list/description-list.tsx +56 -0
- package/src/components/description-list/index.ts +7 -0
- package/src/components/dialog/close-icon.tsx +22 -0
- package/src/components/dialog/dialog.styles.ts +41 -0
- package/src/components/dialog/dialog.tsx +111 -0
- package/src/components/dialog/index.ts +13 -0
- package/src/components/dropdown-menu/dropdown-menu.styles.ts +45 -0
- package/src/components/dropdown-menu/dropdown-menu.tsx +230 -0
- package/src/components/dropdown-menu/index.ts +19 -0
- package/src/components/file-upload/file-upload.styles.ts +43 -0
- package/src/components/file-upload/file-upload.tsx +343 -0
- package/src/components/file-upload/index.ts +10 -0
- package/src/components/grid/grid.tsx +144 -0
- package/src/components/grid/index.ts +1 -0
- package/src/components/hover-card/hover-card.tsx +41 -0
- package/src/components/hover-card/index.ts +1 -0
- package/src/components/input-otp/index.ts +10 -0
- package/src/components/input-otp/input-otp.styles.ts +38 -0
- package/src/components/input-otp/input-otp.tsx +116 -0
- package/src/components/link/index.ts +2 -0
- package/src/components/link/link.styles.ts +39 -0
- package/src/components/link/link.tsx +62 -0
- package/src/components/modal/index.ts +2 -0
- package/src/components/modal/modal.styles.ts +18 -0
- package/src/components/modal/modal.tsx +86 -0
- package/src/components/navigation-menu/index.ts +14 -0
- package/src/components/navigation-menu/navigation-menu.styles.ts +48 -0
- package/src/components/navigation-menu/navigation-menu.tsx +198 -0
- package/src/components/number-input/index.ts +2 -0
- package/src/components/number-input/number-input.styles.ts +41 -0
- package/src/components/number-input/number-input.tsx +211 -0
- package/src/components/pagination/index.ts +3 -0
- package/src/components/pagination/pagination-range.ts +35 -0
- package/src/components/pagination/pagination.styles.ts +41 -0
- package/src/components/pagination/pagination.tsx +130 -0
- package/src/components/password-input/index.ts +7 -0
- package/src/components/password-input/password-input.styles.ts +30 -0
- package/src/components/password-input/password-input.tsx +127 -0
- package/src/components/popover/index.ts +9 -0
- package/src/components/popover/popover.tsx +66 -0
- package/src/components/progress/index.ts +7 -0
- package/src/components/progress/progress.styles.ts +68 -0
- package/src/components/progress/progress.tsx +158 -0
- package/src/components/prose/index.ts +1 -0
- package/src/components/prose/prose.tsx +82 -0
- package/src/components/radio-group/index.ts +7 -0
- package/src/components/radio-group/radio-group.styles.ts +64 -0
- package/src/components/radio-group/radio-group.tsx +128 -0
- package/src/components/resizable/index.ts +9 -0
- package/src/components/resizable/resizable.styles.ts +28 -0
- package/src/components/resizable/resizable.tsx +69 -0
- package/src/components/scroll-area/index.ts +1 -0
- package/src/components/scroll-area/scroll-area.tsx +68 -0
- package/src/components/sheet/index.ts +13 -0
- package/src/components/sheet/sheet.styles.ts +60 -0
- package/src/components/sheet/sheet.tsx +108 -0
- package/src/components/slider/index.ts +2 -0
- package/src/components/slider/slider.styles.ts +32 -0
- package/src/components/slider/slider.tsx +131 -0
- package/src/components/stat-card/index.ts +2 -0
- package/src/components/stat-card/stat-card.styles.ts +24 -0
- package/src/components/stat-card/stat-card.tsx +108 -0
- package/src/components/stepper/index.ts +2 -0
- package/src/components/stepper/stepper.styles.ts +74 -0
- package/src/components/stepper/stepper.tsx +138 -0
- package/src/components/tag-input/index.ts +2 -0
- package/src/components/tag-input/tag-input.styles.ts +39 -0
- package/src/components/tag-input/tag-input.tsx +198 -0
- package/src/components/timeline/index.ts +8 -0
- package/src/components/timeline/timeline.styles.ts +56 -0
- package/src/components/timeline/timeline.tsx +91 -0
- package/src/components/toast/index.ts +1 -0
- package/src/components/toast/toast.tsx +91 -0
- package/src/components/toggle/index.ts +9 -0
- package/src/components/toggle/toggle.styles.ts +39 -0
- package/src/components/toggle/toggle.tsx +61 -0
- package/src/components/tree-view/index.ts +2 -0
- package/src/components/tree-view/tree-view.styles.ts +42 -0
- package/src/components/tree-view/tree-view.tsx +274 -0
- package/src/components/visually-hidden/index.ts +1 -0
- package/src/components/visually-hidden/visually-hidden.tsx +10 -0
- package/src/index.ts +45 -0
- package/src/patterns/chart/chart-parts.tsx +171 -0
- package/src/patterns/chart/chart.styles.ts +35 -0
- package/src/patterns/chart/chart.tsx +369 -0
- package/src/patterns/chart/index.ts +13 -0
- package/src/provider/glyph-provider.tsx +16 -2
- package/styles.css +16 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { a as themedBorder, o as transition, r as focusRing, s as useComponentConfig, t as tv } from "./tv-DBAGlgXJ.js";
|
|
3
|
+
import { c as AlertIcon, d as CheckIcon, g as InboxIcon, n as useFieldControl, o as Spinner, r as Button, v as MinusIcon } from "./field-context-anGPYXbW.js";
|
|
3
4
|
import { Checkbox } from "radix-ui";
|
|
4
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
6
|
//#region src/components/checkbox/checkbox.tsx
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { textTokens } from "@glyphui/tokens";
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
import { createTV } from "tailwind-variants";
|
|
5
|
+
//#region src/provider/glyph-context.ts
|
|
6
|
+
const GlyphContext = createContext(null);
|
|
7
|
+
/** The active theme and app-level settings. Must be used under `GlyphProvider`. */
|
|
8
|
+
function useGlyph() {
|
|
9
|
+
const value = useContext(GlyphContext);
|
|
10
|
+
if (!value) throw new Error("useGlyph must be used inside <GlyphProvider>.");
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
/** The router-aware link component configured on `GlyphProvider`. */
|
|
14
|
+
function useLinkComponent() {
|
|
15
|
+
return useContext(GlyphContext)?.linkComponent ?? "a";
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/hooks/use-component-config.ts
|
|
19
|
+
/**
|
|
20
|
+
* Applies the theme's per-component settings (`theme.components[name]`):
|
|
21
|
+
* fills unset variant props from `defaultVariants` and returns the extra slot classes.
|
|
22
|
+
* Every styled component calls this first.
|
|
23
|
+
*/
|
|
24
|
+
function useComponentConfig(name, props) {
|
|
25
|
+
const override = (useContext(GlyphContext)?.theme)?.components[name];
|
|
26
|
+
if (!override) return [props, {}];
|
|
27
|
+
let merged = props;
|
|
28
|
+
const defaults = override.defaultVariants;
|
|
29
|
+
if (defaults) {
|
|
30
|
+
merged = { ...props };
|
|
31
|
+
for (const [key, value] of Object.entries(defaults)) if (merged[key] === void 0) merged[key] = value;
|
|
32
|
+
}
|
|
33
|
+
return [merged, override.className ?? {}];
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/utils/styles.ts
|
|
37
|
+
/** Shared class fragments. Kept as literals so Tailwind's scanner picks them up. */
|
|
38
|
+
const focusRing = "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background";
|
|
39
|
+
const themedBorder = "border-(length:--glyph-border-width)";
|
|
40
|
+
const transition = "transition-[color,background-color,border-color,box-shadow,transform,opacity] duration-(--glyph-duration-base) ease-standard";
|
|
41
|
+
/** Popover surfaces: Select, Tooltip-like menus. */
|
|
42
|
+
const popover = "z-50 overflow-hidden rounded-[min(var(--radius-md),var(--radius-lg))] border-(length:--glyph-border-width) border-border bg-raised text-foreground shadow-lg animate-pop-in";
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/utils/tw-merge-config.ts
|
|
45
|
+
/**
|
|
46
|
+
* Teaches tailwind-merge about Glyph's custom utilities so `cn('text-h1', 'text-small')`
|
|
47
|
+
* keeps only the last size instead of treating `text-h1` as a color.
|
|
48
|
+
*/
|
|
49
|
+
const twMergeConfig = { extend: { theme: {
|
|
50
|
+
text: [...textTokens],
|
|
51
|
+
radius: ["pill"]
|
|
52
|
+
} } };
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/utils/tv.ts
|
|
55
|
+
/** `tailwind-variants` preconfigured with Glyph's merge rules. All component styles use this. */
|
|
56
|
+
const tv = createTV({ twMergeConfig });
|
|
57
|
+
//#endregion
|
|
58
|
+
export { themedBorder as a, GlyphContext as c, popover as i, useGlyph as l, twMergeConfig as n, transition as o, focusRing as r, useComponentConfig as s, tv as t, useLinkComponent as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glyphui/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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)",
|
|
@@ -46,6 +46,11 @@
|
|
|
46
46
|
"types": "./dist/data-table.d.ts",
|
|
47
47
|
"default": "./dist/data-table.js"
|
|
48
48
|
},
|
|
49
|
+
"./chart": {
|
|
50
|
+
"@glyphui/source": "./src/patterns/chart/index.ts",
|
|
51
|
+
"types": "./dist/chart.d.ts",
|
|
52
|
+
"default": "./dist/chart.js"
|
|
53
|
+
},
|
|
49
54
|
"./styles.css": "./styles.css",
|
|
50
55
|
"./package.json": "./package.json"
|
|
51
56
|
},
|
|
@@ -58,10 +63,17 @@
|
|
|
58
63
|
"!src/test"
|
|
59
64
|
],
|
|
60
65
|
"dependencies": {
|
|
61
|
-
"@glyphui/tokens": "^0.
|
|
66
|
+
"@glyphui/tokens": "^0.3.0",
|
|
62
67
|
"@standard-schema/spec": "^1.1.0",
|
|
63
68
|
"clsx": "^2.1.1",
|
|
69
|
+
"cmdk": "^1.1.1",
|
|
70
|
+
"date-fns": "^4.4.0",
|
|
71
|
+
"embla-carousel-react": "^8.6.0",
|
|
72
|
+
"input-otp": "^1.5.0",
|
|
64
73
|
"radix-ui": "^1.6.7",
|
|
74
|
+
"react-day-picker": "^9.14.0",
|
|
75
|
+
"react-resizable-panels": "^3.0.6",
|
|
76
|
+
"sonner": "^2.0.8",
|
|
65
77
|
"tailwind-merge": "^3.7.0",
|
|
66
78
|
"tailwind-variants": "^3.3.1"
|
|
67
79
|
},
|
|
@@ -71,6 +83,7 @@
|
|
|
71
83
|
"react": ">=19",
|
|
72
84
|
"react-dom": ">=19",
|
|
73
85
|
"react-hook-form": "^7.55.0",
|
|
86
|
+
"recharts": "^3.0.0",
|
|
74
87
|
"tailwindcss": ">=4.1"
|
|
75
88
|
},
|
|
76
89
|
"peerDependenciesMeta": {
|
|
@@ -82,6 +95,9 @@
|
|
|
82
95
|
},
|
|
83
96
|
"react-hook-form": {
|
|
84
97
|
"optional": true
|
|
98
|
+
},
|
|
99
|
+
"recharts": {
|
|
100
|
+
"optional": true
|
|
85
101
|
}
|
|
86
102
|
},
|
|
87
103
|
"devDependencies": {
|
|
@@ -98,6 +114,7 @@
|
|
|
98
114
|
"react": "^19.2.8",
|
|
99
115
|
"react-dom": "^19.2.8",
|
|
100
116
|
"react-hook-form": "^7.89.0",
|
|
117
|
+
"recharts": "^3.10.1",
|
|
101
118
|
"tsdown": "^0.23.0",
|
|
102
119
|
"typescript": "~5.9.3",
|
|
103
120
|
"vitest": "^3.2.7",
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
2
|
+
import { focusRing, themedBorder, transition } from '../../utils/styles'
|
|
3
|
+
import { tv, type VariantProps } from '../../utils/tv'
|
|
4
|
+
|
|
5
|
+
export const accordionStyles = tv({
|
|
6
|
+
slots: {
|
|
7
|
+
root: 'w-full',
|
|
8
|
+
item: '',
|
|
9
|
+
header: 'flex',
|
|
10
|
+
trigger: [
|
|
11
|
+
'group flex flex-1 cursor-pointer items-center justify-between gap-4 rounded-xs py-4 text-left text-body font-medium text-foreground',
|
|
12
|
+
transition,
|
|
13
|
+
focusRing,
|
|
14
|
+
'hover:text-foreground/80 disabled:cursor-not-allowed disabled:opacity-50',
|
|
15
|
+
],
|
|
16
|
+
chevron:
|
|
17
|
+
'size-4 shrink-0 text-faint-foreground transition-transform duration-(--glyph-duration-base) ease-standard group-data-[state=open]:rotate-180',
|
|
18
|
+
content:
|
|
19
|
+
'overflow-hidden text-small text-muted-foreground data-[state=open]:animate-collapse-down data-[state=closed]:animate-collapse-up',
|
|
20
|
+
// Height animates open and closed using Radix's measured content height.
|
|
21
|
+
contentInner: 'pb-4',
|
|
22
|
+
},
|
|
23
|
+
variants: {
|
|
24
|
+
variant: {
|
|
25
|
+
default: { item: 'border-b border-border last:border-b-0' },
|
|
26
|
+
separated: {
|
|
27
|
+
root: 'grid gap-2',
|
|
28
|
+
item: ['rounded-lg border-border bg-surface px-4 shadow-xs', themedBorder],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: { variant: 'default' },
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
export type AccordionVariants = VariantProps<typeof accordionStyles>
|
|
36
|
+
|
|
37
|
+
declare module '@glyphui/tokens' {
|
|
38
|
+
interface GlyphComponentRegistry {
|
|
39
|
+
Accordion: StylesEntry<typeof accordionStyles>
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Accordion as AccordionPrimitive } from 'radix-ui'
|
|
2
|
+
import { type ComponentProps, createContext, useContext } from 'react'
|
|
3
|
+
import { type SlotClasses, useComponentConfig } from '../../hooks/use-component-config'
|
|
4
|
+
import { ChevronDownIcon } from '../../internal/icons'
|
|
5
|
+
import { type AccordionVariants, accordionStyles } from './accordion.styles'
|
|
6
|
+
|
|
7
|
+
const AccordionContext = createContext<{
|
|
8
|
+
s: ReturnType<typeof accordionStyles>
|
|
9
|
+
slots: SlotClasses<'Accordion'>
|
|
10
|
+
} | null>(null)
|
|
11
|
+
|
|
12
|
+
function useParts() {
|
|
13
|
+
return useContext(AccordionContext) ?? { s: accordionStyles(), slots: {} }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type AccordionProps = ComponentProps<typeof AccordionPrimitive.Root> & AccordionVariants
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Stacked sections that expand one at a time (`type="single"`, collapsible) or
|
|
20
|
+
* independently (`type="multiple"`). Good for FAQs and dense settings.
|
|
21
|
+
*/
|
|
22
|
+
export function Accordion(inProps: AccordionProps) {
|
|
23
|
+
const [props, slots] = useComponentConfig('Accordion', inProps)
|
|
24
|
+
const { variant, className, ...rest } = props
|
|
25
|
+
const s = accordionStyles({ variant })
|
|
26
|
+
return (
|
|
27
|
+
<AccordionContext.Provider value={{ s, slots }}>
|
|
28
|
+
<AccordionPrimitive.Root
|
|
29
|
+
data-slot="accordion"
|
|
30
|
+
className={s.root({ class: [slots.root, className] })}
|
|
31
|
+
{...(rest as ComponentProps<typeof AccordionPrimitive.Root>)}
|
|
32
|
+
/>
|
|
33
|
+
</AccordionContext.Provider>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function AccordionItem({
|
|
38
|
+
className,
|
|
39
|
+
...props
|
|
40
|
+
}: ComponentProps<typeof AccordionPrimitive.Item>) {
|
|
41
|
+
const { s, slots } = useParts()
|
|
42
|
+
return (
|
|
43
|
+
<AccordionPrimitive.Item
|
|
44
|
+
data-slot="accordion-item"
|
|
45
|
+
className={s.item({ class: [slots.item, className] })}
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function AccordionTrigger({
|
|
52
|
+
className,
|
|
53
|
+
children,
|
|
54
|
+
...props
|
|
55
|
+
}: ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
|
56
|
+
const { s, slots } = useParts()
|
|
57
|
+
return (
|
|
58
|
+
<AccordionPrimitive.Header className={s.header({ class: slots.header })}>
|
|
59
|
+
<AccordionPrimitive.Trigger
|
|
60
|
+
data-slot="accordion-trigger"
|
|
61
|
+
className={s.trigger({ class: [slots.trigger, className] })}
|
|
62
|
+
{...props}
|
|
63
|
+
>
|
|
64
|
+
{children}
|
|
65
|
+
<ChevronDownIcon className={s.chevron({ class: slots.chevron })} />
|
|
66
|
+
</AccordionPrimitive.Trigger>
|
|
67
|
+
</AccordionPrimitive.Header>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function AccordionContent({
|
|
72
|
+
className,
|
|
73
|
+
children,
|
|
74
|
+
...props
|
|
75
|
+
}: ComponentProps<typeof AccordionPrimitive.Content>) {
|
|
76
|
+
const { s, slots } = useParts()
|
|
77
|
+
return (
|
|
78
|
+
<AccordionPrimitive.Content
|
|
79
|
+
data-slot="accordion-content"
|
|
80
|
+
className={s.content({ class: slots.content })}
|
|
81
|
+
{...props}
|
|
82
|
+
>
|
|
83
|
+
<div className={s.contentInner({ class: [slots.contentInner, className] })}>{children}</div>
|
|
84
|
+
</AccordionPrimitive.Content>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ReactNode, SVGProps } from 'react'
|
|
2
|
+
|
|
3
|
+
type IconProps = SVGProps<SVGSVGElement>
|
|
4
|
+
|
|
5
|
+
function Icon({ children, ...props }: IconProps) {
|
|
6
|
+
return (
|
|
7
|
+
<svg
|
|
8
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
9
|
+
width="16"
|
|
10
|
+
height="16"
|
|
11
|
+
viewBox="0 0 24 24"
|
|
12
|
+
fill="none"
|
|
13
|
+
stroke="currentColor"
|
|
14
|
+
strokeWidth="2"
|
|
15
|
+
strokeLinecap="round"
|
|
16
|
+
strokeLinejoin="round"
|
|
17
|
+
aria-hidden="true"
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
{children}
|
|
21
|
+
</svg>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const InfoIcon = (p: IconProps) => (
|
|
26
|
+
<Icon {...p}>
|
|
27
|
+
<circle cx="12" cy="12" r="10" />
|
|
28
|
+
<path d="M12 16v-4M12 8h.01" />
|
|
29
|
+
</Icon>
|
|
30
|
+
)
|
|
31
|
+
export const SuccessIcon = (p: IconProps) => (
|
|
32
|
+
<Icon {...p}>
|
|
33
|
+
<circle cx="12" cy="12" r="10" />
|
|
34
|
+
<path d="m9 12 2 2 4-4" />
|
|
35
|
+
</Icon>
|
|
36
|
+
)
|
|
37
|
+
export const WarningIcon = (p: IconProps) => (
|
|
38
|
+
<Icon {...p}>
|
|
39
|
+
<path d="m21.73 18-8-14a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
|
|
40
|
+
<path d="M12 9v4M12 17h.01" />
|
|
41
|
+
</Icon>
|
|
42
|
+
)
|
|
43
|
+
export const DangerIcon = (p: IconProps) => (
|
|
44
|
+
<Icon {...p}>
|
|
45
|
+
<circle cx="12" cy="12" r="10" />
|
|
46
|
+
<path d="m15 9-6 6M9 9l6 6" />
|
|
47
|
+
</Icon>
|
|
48
|
+
)
|
|
49
|
+
export const MegaphoneIcon = (p: IconProps) => (
|
|
50
|
+
<Icon {...p}>
|
|
51
|
+
<path d="m3 11 18-5v12L3 14v-3z" />
|
|
52
|
+
<path d="M11.6 16.8a3 3 0 1 1-5.8-1.6" />
|
|
53
|
+
</Icon>
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
export type IconTone = 'neutral' | 'primary' | 'info' | 'success' | 'warning' | 'danger'
|
|
57
|
+
|
|
58
|
+
/** The default icon for each tone. */
|
|
59
|
+
export const toneIcons: Record<IconTone, () => ReactNode> = {
|
|
60
|
+
neutral: () => <InfoIcon />,
|
|
61
|
+
primary: () => <MegaphoneIcon />,
|
|
62
|
+
info: () => <InfoIcon />,
|
|
63
|
+
success: () => <SuccessIcon />,
|
|
64
|
+
warning: () => <WarningIcon />,
|
|
65
|
+
danger: () => <DangerIcon />,
|
|
66
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
2
|
+
import { themedBorder } from '../../utils/styles'
|
|
3
|
+
import { tv, type VariantProps } from '../../utils/tv'
|
|
4
|
+
|
|
5
|
+
// Soft fills per tone. Icon color is mixed toward the foreground so it stays readable in both modes.
|
|
6
|
+
export const alertStyles = tv({
|
|
7
|
+
slots: {
|
|
8
|
+
root: ['relative flex w-full items-start gap-3 rounded-md px-4 py-3 text-small', themedBorder],
|
|
9
|
+
icon: 'mt-0.5 flex size-4 shrink-0 items-center justify-center [&>svg]:size-4',
|
|
10
|
+
content: 'grid min-w-0 flex-1 gap-0.5',
|
|
11
|
+
title: 'font-medium text-foreground',
|
|
12
|
+
description:
|
|
13
|
+
'text-muted-foreground [&_a]:font-medium [&_a]:text-foreground [&_a]:underline [&_a]:underline-offset-2',
|
|
14
|
+
action: 'flex shrink-0 items-center gap-2 self-center',
|
|
15
|
+
dismiss: '-my-1 -mr-2 shrink-0',
|
|
16
|
+
},
|
|
17
|
+
variants: {
|
|
18
|
+
tone: {
|
|
19
|
+
neutral: { icon: 'text-muted-foreground' },
|
|
20
|
+
info: { icon: 'text-[color-mix(in_oklab,var(--color-info)_80%,var(--color-foreground))]' },
|
|
21
|
+
success: {
|
|
22
|
+
icon: 'text-[color-mix(in_oklab,var(--color-success)_80%,var(--color-foreground))]',
|
|
23
|
+
},
|
|
24
|
+
warning: {
|
|
25
|
+
icon: 'text-[color-mix(in_oklab,var(--color-warning)_80%,var(--color-foreground))]',
|
|
26
|
+
},
|
|
27
|
+
danger: {
|
|
28
|
+
icon: 'text-[color-mix(in_oklab,var(--color-danger)_80%,var(--color-foreground))]',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
variant: {
|
|
32
|
+
soft: {},
|
|
33
|
+
outline: { root: 'bg-surface' },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
compoundVariants: [
|
|
37
|
+
{ variant: 'soft', tone: 'neutral', class: { root: 'border-border bg-muted' } },
|
|
38
|
+
{ variant: 'soft', tone: 'info', class: { root: 'border-info/25 bg-info/8' } },
|
|
39
|
+
{ variant: 'soft', tone: 'success', class: { root: 'border-success/25 bg-success/8' } },
|
|
40
|
+
{ variant: 'soft', tone: 'warning', class: { root: 'border-warning/30 bg-warning/10' } },
|
|
41
|
+
{ variant: 'soft', tone: 'danger', class: { root: 'border-danger/25 bg-danger/8' } },
|
|
42
|
+
{ variant: 'outline', tone: 'neutral', class: { root: 'border-border-strong' } },
|
|
43
|
+
{ variant: 'outline', tone: 'info', class: { root: 'border-info/50' } },
|
|
44
|
+
{ variant: 'outline', tone: 'success', class: { root: 'border-success/50' } },
|
|
45
|
+
{ variant: 'outline', tone: 'warning', class: { root: 'border-warning/60' } },
|
|
46
|
+
{ variant: 'outline', tone: 'danger', class: { root: 'border-danger/50' } },
|
|
47
|
+
],
|
|
48
|
+
defaultVariants: { tone: 'neutral', variant: 'soft' },
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
export type AlertVariants = VariantProps<typeof alertStyles>
|
|
52
|
+
|
|
53
|
+
declare module '@glyphui/tokens' {
|
|
54
|
+
interface GlyphComponentRegistry {
|
|
55
|
+
Alert: StylesEntry<typeof alertStyles>
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from 'react'
|
|
2
|
+
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
3
|
+
import { XIcon } from '../../internal/icons'
|
|
4
|
+
import { IconButton } from '../button'
|
|
5
|
+
import { type AlertVariants, alertStyles } from './alert.styles'
|
|
6
|
+
import { toneIcons } from './alert-icons'
|
|
7
|
+
|
|
8
|
+
export interface AlertProps extends Omit<ComponentProps<'div'>, 'title'>, AlertVariants {
|
|
9
|
+
title?: ReactNode
|
|
10
|
+
/** Replaces the tone's default icon. `false` hides it. */
|
|
11
|
+
icon?: ReactNode | false
|
|
12
|
+
/** Buttons or links shown at the end. */
|
|
13
|
+
action?: ReactNode
|
|
14
|
+
/** Shows a close button that calls this. */
|
|
15
|
+
onDismiss?: () => void
|
|
16
|
+
dismissLabel?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* An inline message about the content around it. Danger and warning alerts are announced
|
|
21
|
+
* immediately (`role="alert"`); other tones politely (`role="status"`).
|
|
22
|
+
*/
|
|
23
|
+
export function Alert(inProps: AlertProps) {
|
|
24
|
+
const [props, slots] = useComponentConfig('Alert', inProps)
|
|
25
|
+
const {
|
|
26
|
+
tone,
|
|
27
|
+
variant,
|
|
28
|
+
title,
|
|
29
|
+
icon,
|
|
30
|
+
action,
|
|
31
|
+
onDismiss,
|
|
32
|
+
dismissLabel = 'Dismiss',
|
|
33
|
+
className,
|
|
34
|
+
children,
|
|
35
|
+
...rest
|
|
36
|
+
} = props
|
|
37
|
+
const resolvedTone = tone ?? 'neutral'
|
|
38
|
+
const s = alertStyles({ tone, variant })
|
|
39
|
+
const iconNode = icon === false ? null : (icon ?? toneIcons[resolvedTone]())
|
|
40
|
+
const urgent = resolvedTone === 'danger' || resolvedTone === 'warning'
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
data-slot="alert"
|
|
45
|
+
data-tone={resolvedTone}
|
|
46
|
+
role={urgent ? 'alert' : 'status'}
|
|
47
|
+
className={s.root({ class: [slots.root, className] })}
|
|
48
|
+
{...rest}
|
|
49
|
+
>
|
|
50
|
+
{iconNode && (
|
|
51
|
+
<span data-slot="alert-icon" className={s.icon({ class: slots.icon })} aria-hidden="true">
|
|
52
|
+
{iconNode}
|
|
53
|
+
</span>
|
|
54
|
+
)}
|
|
55
|
+
<div data-slot="alert-content" className={s.content({ class: slots.content })}>
|
|
56
|
+
{title && (
|
|
57
|
+
<p data-slot="alert-title" className={s.title({ class: slots.title })}>
|
|
58
|
+
{title}
|
|
59
|
+
</p>
|
|
60
|
+
)}
|
|
61
|
+
{children && (
|
|
62
|
+
<div
|
|
63
|
+
data-slot="alert-description"
|
|
64
|
+
className={s.description({ class: slots.description })}
|
|
65
|
+
>
|
|
66
|
+
{children}
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
{action && (
|
|
71
|
+
<div data-slot="alert-action" className={s.action({ class: slots.action })}>
|
|
72
|
+
{action}
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
{onDismiss && (
|
|
76
|
+
<IconButton
|
|
77
|
+
size="sm"
|
|
78
|
+
icon={<XIcon />}
|
|
79
|
+
aria-label={dismissLabel}
|
|
80
|
+
onClick={onDismiss}
|
|
81
|
+
className={s.dismiss({ class: slots.dismiss })}
|
|
82
|
+
/>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
2
|
+
import { themedBorder } from '../../utils/styles'
|
|
3
|
+
import { tv } from '../../utils/tv'
|
|
4
|
+
|
|
5
|
+
export const alertDialogStyles = tv({
|
|
6
|
+
slots: {
|
|
7
|
+
overlay: 'fixed inset-0 z-50 animate-fade-in bg-overlay',
|
|
8
|
+
content: [
|
|
9
|
+
'fixed top-1/2 left-1/2 z-50 grid w-[calc(100vw-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 gap-5',
|
|
10
|
+
'rounded-xl border-border bg-surface p-6 text-foreground shadow-lg outline-none animate-pop-in',
|
|
11
|
+
themedBorder,
|
|
12
|
+
],
|
|
13
|
+
header: 'flex flex-col gap-1.5',
|
|
14
|
+
title: 'font-display text-h4 text-foreground',
|
|
15
|
+
description: 'text-small text-muted-foreground',
|
|
16
|
+
footer: 'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
declare module '@glyphui/tokens' {
|
|
21
|
+
interface GlyphComponentRegistry {
|
|
22
|
+
AlertDialog: StylesEntry<typeof alertDialogStyles>
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { AlertDialog as AlertDialogPrimitive } from 'radix-ui'
|
|
2
|
+
import type { ComponentProps } from 'react'
|
|
3
|
+
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
4
|
+
import { Button, type ButtonProps } from '../button'
|
|
5
|
+
import { alertDialogStyles } from './alert-dialog.styles'
|
|
6
|
+
|
|
7
|
+
function useParts() {
|
|
8
|
+
const [, slots] = useComponentConfig('AlertDialog', {})
|
|
9
|
+
return { s: alertDialogStyles(), slots }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A modal that interrupts to ask for a decision, e.g. confirming a destructive action.
|
|
14
|
+
* Unlike Dialog it can't be dismissed by clicking outside. For a one-line confirm, use `useConfirm()`.
|
|
15
|
+
*/
|
|
16
|
+
export const AlertDialog = AlertDialogPrimitive.Root
|
|
17
|
+
export const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
|
18
|
+
|
|
19
|
+
export interface AlertDialogContentProps
|
|
20
|
+
extends ComponentProps<typeof AlertDialogPrimitive.Content> {
|
|
21
|
+
overlayClassName?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function AlertDialogContent(inProps: AlertDialogContentProps) {
|
|
25
|
+
const [props, slots] = useComponentConfig('AlertDialog', inProps)
|
|
26
|
+
const { overlayClassName, className, ...rest } = props
|
|
27
|
+
const s = alertDialogStyles()
|
|
28
|
+
return (
|
|
29
|
+
<AlertDialogPrimitive.Portal>
|
|
30
|
+
<AlertDialogPrimitive.Overlay
|
|
31
|
+
data-slot="alert-dialog-overlay"
|
|
32
|
+
className={s.overlay({ class: [slots.overlay, overlayClassName] })}
|
|
33
|
+
/>
|
|
34
|
+
<AlertDialogPrimitive.Content
|
|
35
|
+
data-slot="alert-dialog-content"
|
|
36
|
+
className={s.content({ class: [slots.content, className] })}
|
|
37
|
+
{...rest}
|
|
38
|
+
/>
|
|
39
|
+
</AlertDialogPrimitive.Portal>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function AlertDialogHeader({ className, ...props }: ComponentProps<'div'>) {
|
|
44
|
+
const { s, slots } = useParts()
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
data-slot="alert-dialog-header"
|
|
48
|
+
className={s.header({ class: [slots.header, className] })}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function AlertDialogTitle({
|
|
55
|
+
className,
|
|
56
|
+
...props
|
|
57
|
+
}: ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
|
58
|
+
const { s, slots } = useParts()
|
|
59
|
+
return (
|
|
60
|
+
<AlertDialogPrimitive.Title
|
|
61
|
+
data-slot="alert-dialog-title"
|
|
62
|
+
className={s.title({ class: [slots.title, className] })}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function AlertDialogDescription({
|
|
69
|
+
className,
|
|
70
|
+
...props
|
|
71
|
+
}: ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
|
72
|
+
const { s, slots } = useParts()
|
|
73
|
+
return (
|
|
74
|
+
<AlertDialogPrimitive.Description
|
|
75
|
+
data-slot="alert-dialog-description"
|
|
76
|
+
className={s.description({ class: [slots.description, className] })}
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function AlertDialogFooter({ className, ...props }: ComponentProps<'div'>) {
|
|
83
|
+
const { s, slots } = useParts()
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
data-slot="alert-dialog-footer"
|
|
87
|
+
className={s.footer({ class: [slots.footer, className] })}
|
|
88
|
+
{...props}
|
|
89
|
+
/>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface AlertDialogActionProps extends Omit<ButtonProps, 'variant' | 'asChild'> {
|
|
94
|
+
/** `danger` for destructive actions. */
|
|
95
|
+
tone?: 'default' | 'danger'
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The confirming button. Closes the dialog. */
|
|
99
|
+
export function AlertDialogAction({ tone = 'default', ...props }: AlertDialogActionProps) {
|
|
100
|
+
return (
|
|
101
|
+
<AlertDialogPrimitive.Action asChild>
|
|
102
|
+
<Button
|
|
103
|
+
data-slot="alert-dialog-action"
|
|
104
|
+
variant={tone === 'danger' ? 'danger' : 'primary'}
|
|
105
|
+
{...props}
|
|
106
|
+
/>
|
|
107
|
+
</AlertDialogPrimitive.Action>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The dismissing button. Receives focus when the dialog opens, so Enter never confirms by accident. */
|
|
112
|
+
export function AlertDialogCancel(props: Omit<ButtonProps, 'variant' | 'asChild'>) {
|
|
113
|
+
return (
|
|
114
|
+
<AlertDialogPrimitive.Cancel asChild>
|
|
115
|
+
<Button data-slot="alert-dialog-cancel" variant="outline" {...props} />
|
|
116
|
+
</AlertDialogPrimitive.Cancel>
|
|
117
|
+
)
|
|
118
|
+
}
|