@glyphui/ui 0.1.1 → 0.2.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/data-table.js +2 -2
- package/dist/{field-DXZD0l8w.js → field-CCecIzdq.js} +1 -1
- package/dist/{field-context-B8cYheXz.js → field-context-B6WOcK5m.js} +99 -99
- package/dist/form.js +2 -2
- package/dist/index.d.ts +2743 -93
- package/dist/index.js +2748 -111
- package/dist/{states-BxPjCUxz.js → states-Rp8wZ_jH.js} +1 -1
- package/package.json +4 -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/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/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/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/grid/grid.tsx +144 -0
- package/src/components/grid/index.ts +1 -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/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/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/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/toast/index.ts +1 -0
- package/src/components/toast/toast.tsx +91 -0
- package/src/index.ts +25 -0
- package/src/provider/glyph-provider.tsx +16 -2
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Dialog as DialogPrimitive } from 'radix-ui'
|
|
2
|
+
import type { ComponentProps } from 'react'
|
|
3
|
+
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
4
|
+
import { CloseIcon } from './close-icon'
|
|
5
|
+
import { type DialogVariants, dialogStyles } from './dialog.styles'
|
|
6
|
+
|
|
7
|
+
function useParts() {
|
|
8
|
+
const [, slots] = useComponentConfig('Dialog', {})
|
|
9
|
+
return { s: dialogStyles(), slots }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A modal window. Focus is trapped inside and returns to the trigger on close. */
|
|
13
|
+
export const Dialog = DialogPrimitive.Root
|
|
14
|
+
export const DialogTrigger = DialogPrimitive.Trigger
|
|
15
|
+
export const DialogClose = DialogPrimitive.Close
|
|
16
|
+
|
|
17
|
+
export interface DialogContentProps
|
|
18
|
+
extends ComponentProps<typeof DialogPrimitive.Content>,
|
|
19
|
+
DialogVariants {
|
|
20
|
+
/** Show the close button in the corner. Default true. */
|
|
21
|
+
showClose?: boolean
|
|
22
|
+
overlayClassName?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function DialogContent(inProps: DialogContentProps) {
|
|
26
|
+
const [props, slots] = useComponentConfig('Dialog', inProps)
|
|
27
|
+
const { size, showClose = true, overlayClassName, className, children, ...rest } = props
|
|
28
|
+
const s = dialogStyles({ size })
|
|
29
|
+
return (
|
|
30
|
+
<DialogPrimitive.Portal>
|
|
31
|
+
<DialogPrimitive.Overlay
|
|
32
|
+
data-slot="dialog-overlay"
|
|
33
|
+
className={s.overlay({ class: [slots.overlay, overlayClassName] })}
|
|
34
|
+
/>
|
|
35
|
+
<DialogPrimitive.Content
|
|
36
|
+
data-slot="dialog-content"
|
|
37
|
+
className={s.content({ class: [slots.content, className] })}
|
|
38
|
+
{...rest}
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
{showClose && (
|
|
42
|
+
<DialogPrimitive.Close
|
|
43
|
+
data-slot="dialog-close"
|
|
44
|
+
aria-label="Close"
|
|
45
|
+
className={s.close({ class: slots.close })}
|
|
46
|
+
>
|
|
47
|
+
<CloseIcon />
|
|
48
|
+
</DialogPrimitive.Close>
|
|
49
|
+
)}
|
|
50
|
+
</DialogPrimitive.Content>
|
|
51
|
+
</DialogPrimitive.Portal>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function DialogHeader({ className, ...props }: ComponentProps<'div'>) {
|
|
56
|
+
const { s, slots } = useParts()
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
data-slot="dialog-header"
|
|
60
|
+
className={s.header({ class: [slots.header, className] })}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function DialogTitle({ className, ...props }: ComponentProps<typeof DialogPrimitive.Title>) {
|
|
67
|
+
const { s, slots } = useParts()
|
|
68
|
+
return (
|
|
69
|
+
<DialogPrimitive.Title
|
|
70
|
+
data-slot="dialog-title"
|
|
71
|
+
className={s.title({ class: [slots.title, className] })}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function DialogDescription({
|
|
78
|
+
className,
|
|
79
|
+
...props
|
|
80
|
+
}: ComponentProps<typeof DialogPrimitive.Description>) {
|
|
81
|
+
const { s, slots } = useParts()
|
|
82
|
+
return (
|
|
83
|
+
<DialogPrimitive.Description
|
|
84
|
+
data-slot="dialog-description"
|
|
85
|
+
className={s.description({ class: [slots.description, className] })}
|
|
86
|
+
{...props}
|
|
87
|
+
/>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function DialogBody({ className, ...props }: ComponentProps<'div'>) {
|
|
92
|
+
const { s, slots } = useParts()
|
|
93
|
+
return (
|
|
94
|
+
<div
|
|
95
|
+
data-slot="dialog-body"
|
|
96
|
+
className={s.body({ class: [slots.body, className] })}
|
|
97
|
+
{...props}
|
|
98
|
+
/>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function DialogFooter({ className, ...props }: ComponentProps<'div'>) {
|
|
103
|
+
const { s, slots } = useParts()
|
|
104
|
+
return (
|
|
105
|
+
<div
|
|
106
|
+
data-slot="dialog-footer"
|
|
107
|
+
className={s.footer({ class: [slots.footer, className] })}
|
|
108
|
+
{...props}
|
|
109
|
+
/>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
Dialog,
|
|
3
|
+
DialogBody,
|
|
4
|
+
DialogClose,
|
|
5
|
+
DialogContent,
|
|
6
|
+
type DialogContentProps,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
DialogFooter,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
DialogTrigger,
|
|
12
|
+
} from './dialog'
|
|
13
|
+
export { type DialogVariants, dialogStyles } from './dialog.styles'
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
2
|
+
import { popover } from '../../utils/styles'
|
|
3
|
+
import { tv, type VariantProps } from '../../utils/tv'
|
|
4
|
+
|
|
5
|
+
const item = [
|
|
6
|
+
'relative flex h-8 cursor-pointer select-none items-center gap-2 rounded-sm px-2 text-small text-foreground outline-none',
|
|
7
|
+
'data-highlighted:bg-muted data-disabled:pointer-events-none data-disabled:opacity-50',
|
|
8
|
+
'[&_svg]:size-4 [&_svg]:shrink-0',
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
export const dropdownMenuStyles = tv({
|
|
12
|
+
slots: {
|
|
13
|
+
content: [
|
|
14
|
+
popover,
|
|
15
|
+
'min-w-48 max-h-(--radix-dropdown-menu-content-available-height) overflow-y-auto p-1',
|
|
16
|
+
],
|
|
17
|
+
item,
|
|
18
|
+
icon: 'flex size-4 shrink-0 items-center justify-center text-muted-foreground',
|
|
19
|
+
indicator: 'absolute left-2 flex size-4 items-center justify-center text-primary',
|
|
20
|
+
label: 'px-2 pt-1.5 pb-1 text-caption font-medium text-faint-foreground',
|
|
21
|
+
separator: '-mx-1 my-1 h-px bg-border',
|
|
22
|
+
shortcut: 'ml-auto pl-4 font-mono text-caption tracking-wider text-faint-foreground',
|
|
23
|
+
subTrigger: [...item, 'data-[state=open]:bg-muted'],
|
|
24
|
+
subContent: [popover, 'min-w-40 p-1'],
|
|
25
|
+
},
|
|
26
|
+
variants: {
|
|
27
|
+
tone: {
|
|
28
|
+
default: {},
|
|
29
|
+
danger: {
|
|
30
|
+
item: 'text-danger data-highlighted:bg-danger/10',
|
|
31
|
+
icon: 'text-danger',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
inset: { true: { item: 'pl-8', label: 'pl-8' } },
|
|
35
|
+
},
|
|
36
|
+
defaultVariants: { tone: 'default' },
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
export type DropdownMenuVariants = VariantProps<typeof dropdownMenuStyles>
|
|
40
|
+
|
|
41
|
+
declare module '@glyphui/tokens' {
|
|
42
|
+
interface GlyphComponentRegistry {
|
|
43
|
+
DropdownMenu: StylesEntry<typeof dropdownMenuStyles>
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { DropdownMenu as MenuPrimitive } from 'radix-ui'
|
|
2
|
+
import type { ComponentProps, ReactNode, SVGProps } from 'react'
|
|
3
|
+
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
4
|
+
import { dropdownMenuStyles } from './dropdown-menu.styles'
|
|
5
|
+
|
|
6
|
+
function useParts(tone?: 'default' | 'danger', inset?: boolean) {
|
|
7
|
+
const [, slots] = useComponentConfig('DropdownMenu', {})
|
|
8
|
+
return { s: dropdownMenuStyles({ tone, inset }), slots }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const svg = {
|
|
12
|
+
xmlns: 'http://www.w3.org/2000/svg',
|
|
13
|
+
viewBox: '0 0 24 24',
|
|
14
|
+
fill: 'none',
|
|
15
|
+
stroke: 'currentColor',
|
|
16
|
+
strokeWidth: 2,
|
|
17
|
+
strokeLinecap: 'round',
|
|
18
|
+
strokeLinejoin: 'round',
|
|
19
|
+
} as const satisfies SVGProps<SVGSVGElement>
|
|
20
|
+
|
|
21
|
+
const CheckIcon = () => (
|
|
22
|
+
<svg aria-hidden="true" {...svg}>
|
|
23
|
+
<path d="M20 6 9 17l-5-5" />
|
|
24
|
+
</svg>
|
|
25
|
+
)
|
|
26
|
+
const DotIcon = () => (
|
|
27
|
+
<svg aria-hidden="true" {...svg}>
|
|
28
|
+
<circle cx="12" cy="12" r="4" fill="currentColor" />
|
|
29
|
+
</svg>
|
|
30
|
+
)
|
|
31
|
+
const ChevronRightIcon = () => (
|
|
32
|
+
<svg aria-hidden="true" {...svg}>
|
|
33
|
+
<path d="m9 18 6-6-6-6" />
|
|
34
|
+
</svg>
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
/** A menu of actions opened from a button: row actions, account menus, "more" buttons. */
|
|
38
|
+
export const DropdownMenu = MenuPrimitive.Root
|
|
39
|
+
export const DropdownMenuTrigger = MenuPrimitive.Trigger
|
|
40
|
+
export const DropdownMenuGroup = MenuPrimitive.Group
|
|
41
|
+
export const DropdownMenuRadioGroup = MenuPrimitive.RadioGroup
|
|
42
|
+
export const DropdownMenuSub = MenuPrimitive.Sub
|
|
43
|
+
|
|
44
|
+
export function DropdownMenuContent(inProps: ComponentProps<typeof MenuPrimitive.Content>) {
|
|
45
|
+
const [props, slots] = useComponentConfig('DropdownMenu', inProps)
|
|
46
|
+
const { sideOffset = 6, align = 'start', className, ...rest } = props
|
|
47
|
+
const s = dropdownMenuStyles()
|
|
48
|
+
return (
|
|
49
|
+
<MenuPrimitive.Portal>
|
|
50
|
+
<MenuPrimitive.Content
|
|
51
|
+
data-slot="dropdown-menu-content"
|
|
52
|
+
sideOffset={sideOffset}
|
|
53
|
+
align={align}
|
|
54
|
+
className={s.content({ class: [slots.content, className] })}
|
|
55
|
+
{...rest}
|
|
56
|
+
/>
|
|
57
|
+
</MenuPrimitive.Portal>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface DropdownMenuItemProps extends ComponentProps<typeof MenuPrimitive.Item> {
|
|
62
|
+
icon?: ReactNode
|
|
63
|
+
/** Keyboard hint shown at the end, e.g. "⌘D". Display only. */
|
|
64
|
+
shortcut?: ReactNode
|
|
65
|
+
/** `danger` for destructive actions. */
|
|
66
|
+
tone?: 'default' | 'danger'
|
|
67
|
+
/** Indent to line up with checkbox and radio items. */
|
|
68
|
+
inset?: boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function DropdownMenuItem({
|
|
72
|
+
icon,
|
|
73
|
+
shortcut,
|
|
74
|
+
tone,
|
|
75
|
+
inset,
|
|
76
|
+
className,
|
|
77
|
+
children,
|
|
78
|
+
...props
|
|
79
|
+
}: DropdownMenuItemProps) {
|
|
80
|
+
const { s, slots } = useParts(tone, inset)
|
|
81
|
+
return (
|
|
82
|
+
<MenuPrimitive.Item
|
|
83
|
+
data-slot="dropdown-menu-item"
|
|
84
|
+
data-tone={tone ?? 'default'}
|
|
85
|
+
className={s.item({ class: [slots.item, className] })}
|
|
86
|
+
{...props}
|
|
87
|
+
>
|
|
88
|
+
{icon && (
|
|
89
|
+
<span className={s.icon({ class: slots.icon })} aria-hidden="true">
|
|
90
|
+
{icon}
|
|
91
|
+
</span>
|
|
92
|
+
)}
|
|
93
|
+
{children}
|
|
94
|
+
{shortcut && <DropdownMenuShortcut>{shortcut}</DropdownMenuShortcut>}
|
|
95
|
+
</MenuPrimitive.Item>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function DropdownMenuCheckboxItem({
|
|
100
|
+
className,
|
|
101
|
+
children,
|
|
102
|
+
...props
|
|
103
|
+
}: ComponentProps<typeof MenuPrimitive.CheckboxItem>) {
|
|
104
|
+
const { s, slots } = useParts(undefined, true)
|
|
105
|
+
return (
|
|
106
|
+
<MenuPrimitive.CheckboxItem
|
|
107
|
+
data-slot="dropdown-menu-checkbox-item"
|
|
108
|
+
className={s.item({ class: [slots.item, className] })}
|
|
109
|
+
{...props}
|
|
110
|
+
>
|
|
111
|
+
<span className={s.indicator({ class: slots.indicator })}>
|
|
112
|
+
<MenuPrimitive.ItemIndicator>
|
|
113
|
+
<CheckIcon />
|
|
114
|
+
</MenuPrimitive.ItemIndicator>
|
|
115
|
+
</span>
|
|
116
|
+
{children}
|
|
117
|
+
</MenuPrimitive.CheckboxItem>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function DropdownMenuRadioItem({
|
|
122
|
+
className,
|
|
123
|
+
children,
|
|
124
|
+
...props
|
|
125
|
+
}: ComponentProps<typeof MenuPrimitive.RadioItem>) {
|
|
126
|
+
const { s, slots } = useParts(undefined, true)
|
|
127
|
+
return (
|
|
128
|
+
<MenuPrimitive.RadioItem
|
|
129
|
+
data-slot="dropdown-menu-radio-item"
|
|
130
|
+
className={s.item({ class: [slots.item, className] })}
|
|
131
|
+
{...props}
|
|
132
|
+
>
|
|
133
|
+
<span className={s.indicator({ class: slots.indicator })}>
|
|
134
|
+
<MenuPrimitive.ItemIndicator>
|
|
135
|
+
<DotIcon />
|
|
136
|
+
</MenuPrimitive.ItemIndicator>
|
|
137
|
+
</span>
|
|
138
|
+
{children}
|
|
139
|
+
</MenuPrimitive.RadioItem>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function DropdownMenuLabel({
|
|
144
|
+
inset,
|
|
145
|
+
className,
|
|
146
|
+
...props
|
|
147
|
+
}: ComponentProps<typeof MenuPrimitive.Label> & { inset?: boolean }) {
|
|
148
|
+
const { s, slots } = useParts(undefined, inset)
|
|
149
|
+
return (
|
|
150
|
+
<MenuPrimitive.Label
|
|
151
|
+
data-slot="dropdown-menu-label"
|
|
152
|
+
className={s.label({ class: [slots.label, className] })}
|
|
153
|
+
{...props}
|
|
154
|
+
/>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function DropdownMenuSeparator({
|
|
159
|
+
className,
|
|
160
|
+
...props
|
|
161
|
+
}: ComponentProps<typeof MenuPrimitive.Separator>) {
|
|
162
|
+
const { s, slots } = useParts()
|
|
163
|
+
return (
|
|
164
|
+
<MenuPrimitive.Separator
|
|
165
|
+
data-slot="dropdown-menu-separator"
|
|
166
|
+
className={s.separator({ class: [slots.separator, className] })}
|
|
167
|
+
{...props}
|
|
168
|
+
/>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function DropdownMenuShortcut({ className, ...props }: ComponentProps<'span'>) {
|
|
173
|
+
const { s, slots } = useParts()
|
|
174
|
+
return (
|
|
175
|
+
<span
|
|
176
|
+
data-slot="dropdown-menu-shortcut"
|
|
177
|
+
className={s.shortcut({ class: [slots.shortcut, className] })}
|
|
178
|
+
{...props}
|
|
179
|
+
/>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface DropdownMenuSubTriggerProps
|
|
184
|
+
extends ComponentProps<typeof MenuPrimitive.SubTrigger> {
|
|
185
|
+
icon?: ReactNode
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function DropdownMenuSubTrigger({
|
|
189
|
+
icon,
|
|
190
|
+
className,
|
|
191
|
+
children,
|
|
192
|
+
...props
|
|
193
|
+
}: DropdownMenuSubTriggerProps) {
|
|
194
|
+
const { s, slots } = useParts()
|
|
195
|
+
return (
|
|
196
|
+
<MenuPrimitive.SubTrigger
|
|
197
|
+
data-slot="dropdown-menu-sub-trigger"
|
|
198
|
+
className={s.subTrigger({ class: [slots.subTrigger, className] })}
|
|
199
|
+
{...props}
|
|
200
|
+
>
|
|
201
|
+
{icon && (
|
|
202
|
+
<span className={s.icon({ class: slots.icon })} aria-hidden="true">
|
|
203
|
+
{icon}
|
|
204
|
+
</span>
|
|
205
|
+
)}
|
|
206
|
+
{children}
|
|
207
|
+
<span className="ml-auto flex text-faint-foreground">
|
|
208
|
+
<ChevronRightIcon />
|
|
209
|
+
</span>
|
|
210
|
+
</MenuPrimitive.SubTrigger>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function DropdownMenuSubContent({
|
|
215
|
+
sideOffset = 4,
|
|
216
|
+
className,
|
|
217
|
+
...props
|
|
218
|
+
}: ComponentProps<typeof MenuPrimitive.SubContent>) {
|
|
219
|
+
const { s, slots } = useParts()
|
|
220
|
+
return (
|
|
221
|
+
<MenuPrimitive.Portal>
|
|
222
|
+
<MenuPrimitive.SubContent
|
|
223
|
+
data-slot="dropdown-menu-sub-content"
|
|
224
|
+
sideOffset={sideOffset}
|
|
225
|
+
className={s.subContent({ class: [slots.subContent, className] })}
|
|
226
|
+
{...props}
|
|
227
|
+
/>
|
|
228
|
+
</MenuPrimitive.Portal>
|
|
229
|
+
)
|
|
230
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export {
|
|
2
|
+
DropdownMenu,
|
|
3
|
+
DropdownMenuCheckboxItem,
|
|
4
|
+
DropdownMenuContent,
|
|
5
|
+
DropdownMenuGroup,
|
|
6
|
+
DropdownMenuItem,
|
|
7
|
+
type DropdownMenuItemProps,
|
|
8
|
+
DropdownMenuLabel,
|
|
9
|
+
DropdownMenuRadioGroup,
|
|
10
|
+
DropdownMenuRadioItem,
|
|
11
|
+
DropdownMenuSeparator,
|
|
12
|
+
DropdownMenuShortcut,
|
|
13
|
+
DropdownMenuSub,
|
|
14
|
+
DropdownMenuSubContent,
|
|
15
|
+
DropdownMenuSubTrigger,
|
|
16
|
+
type DropdownMenuSubTriggerProps,
|
|
17
|
+
DropdownMenuTrigger,
|
|
18
|
+
} from './dropdown-menu'
|
|
19
|
+
export { type DropdownMenuVariants, dropdownMenuStyles } from './dropdown-menu.styles'
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { Slot } from 'radix-ui'
|
|
2
|
+
import type { ComponentProps } from 'react'
|
|
3
|
+
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
4
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
5
|
+
import { tv, type VariantProps } from '../../utils/tv'
|
|
6
|
+
|
|
7
|
+
type Cols = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
|
|
8
|
+
type Breakpoint = 'base' | 'sm' | 'md' | 'lg' | 'xl'
|
|
9
|
+
|
|
10
|
+
// Literal class names per breakpoint so Tailwind's scanner generates them.
|
|
11
|
+
const colClasses: Record<Breakpoint, Record<Cols, string>> = {
|
|
12
|
+
base: {
|
|
13
|
+
1: 'grid-cols-1',
|
|
14
|
+
2: 'grid-cols-2',
|
|
15
|
+
3: 'grid-cols-3',
|
|
16
|
+
4: 'grid-cols-4',
|
|
17
|
+
5: 'grid-cols-5',
|
|
18
|
+
6: 'grid-cols-6',
|
|
19
|
+
7: 'grid-cols-7',
|
|
20
|
+
8: 'grid-cols-8',
|
|
21
|
+
9: 'grid-cols-9',
|
|
22
|
+
10: 'grid-cols-10',
|
|
23
|
+
11: 'grid-cols-11',
|
|
24
|
+
12: 'grid-cols-12',
|
|
25
|
+
},
|
|
26
|
+
sm: {
|
|
27
|
+
1: 'sm:grid-cols-1',
|
|
28
|
+
2: 'sm:grid-cols-2',
|
|
29
|
+
3: 'sm:grid-cols-3',
|
|
30
|
+
4: 'sm:grid-cols-4',
|
|
31
|
+
5: 'sm:grid-cols-5',
|
|
32
|
+
6: 'sm:grid-cols-6',
|
|
33
|
+
7: 'sm:grid-cols-7',
|
|
34
|
+
8: 'sm:grid-cols-8',
|
|
35
|
+
9: 'sm:grid-cols-9',
|
|
36
|
+
10: 'sm:grid-cols-10',
|
|
37
|
+
11: 'sm:grid-cols-11',
|
|
38
|
+
12: 'sm:grid-cols-12',
|
|
39
|
+
},
|
|
40
|
+
md: {
|
|
41
|
+
1: 'md:grid-cols-1',
|
|
42
|
+
2: 'md:grid-cols-2',
|
|
43
|
+
3: 'md:grid-cols-3',
|
|
44
|
+
4: 'md:grid-cols-4',
|
|
45
|
+
5: 'md:grid-cols-5',
|
|
46
|
+
6: 'md:grid-cols-6',
|
|
47
|
+
7: 'md:grid-cols-7',
|
|
48
|
+
8: 'md:grid-cols-8',
|
|
49
|
+
9: 'md:grid-cols-9',
|
|
50
|
+
10: 'md:grid-cols-10',
|
|
51
|
+
11: 'md:grid-cols-11',
|
|
52
|
+
12: 'md:grid-cols-12',
|
|
53
|
+
},
|
|
54
|
+
lg: {
|
|
55
|
+
1: 'lg:grid-cols-1',
|
|
56
|
+
2: 'lg:grid-cols-2',
|
|
57
|
+
3: 'lg:grid-cols-3',
|
|
58
|
+
4: 'lg:grid-cols-4',
|
|
59
|
+
5: 'lg:grid-cols-5',
|
|
60
|
+
6: 'lg:grid-cols-6',
|
|
61
|
+
7: 'lg:grid-cols-7',
|
|
62
|
+
8: 'lg:grid-cols-8',
|
|
63
|
+
9: 'lg:grid-cols-9',
|
|
64
|
+
10: 'lg:grid-cols-10',
|
|
65
|
+
11: 'lg:grid-cols-11',
|
|
66
|
+
12: 'lg:grid-cols-12',
|
|
67
|
+
},
|
|
68
|
+
xl: {
|
|
69
|
+
1: 'xl:grid-cols-1',
|
|
70
|
+
2: 'xl:grid-cols-2',
|
|
71
|
+
3: 'xl:grid-cols-3',
|
|
72
|
+
4: 'xl:grid-cols-4',
|
|
73
|
+
5: 'xl:grid-cols-5',
|
|
74
|
+
6: 'xl:grid-cols-6',
|
|
75
|
+
7: 'xl:grid-cols-7',
|
|
76
|
+
8: 'xl:grid-cols-8',
|
|
77
|
+
9: 'xl:grid-cols-9',
|
|
78
|
+
10: 'xl:grid-cols-10',
|
|
79
|
+
11: 'xl:grid-cols-11',
|
|
80
|
+
12: 'xl:grid-cols-12',
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type GridCols = Cols | Partial<Record<Breakpoint, Cols>>
|
|
85
|
+
|
|
86
|
+
function colsToClasses(cols: GridCols): string {
|
|
87
|
+
if (typeof cols === 'number') return colClasses.base[cols]
|
|
88
|
+
return (Object.entries(cols) as [Breakpoint, Cols][])
|
|
89
|
+
.map(([bp, n]) => colClasses[bp][n])
|
|
90
|
+
.join(' ')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const gridStyles = tv({
|
|
94
|
+
slots: { base: 'grid' },
|
|
95
|
+
variants: {
|
|
96
|
+
gap: {
|
|
97
|
+
0: { base: 'gap-0' },
|
|
98
|
+
1: { base: 'gap-1' },
|
|
99
|
+
2: { base: 'gap-2' },
|
|
100
|
+
3: { base: 'gap-3' },
|
|
101
|
+
4: { base: 'gap-4' },
|
|
102
|
+
5: { base: 'gap-5' },
|
|
103
|
+
6: { base: 'gap-6' },
|
|
104
|
+
8: { base: 'gap-8' },
|
|
105
|
+
10: { base: 'gap-10' },
|
|
106
|
+
12: { base: 'gap-12' },
|
|
107
|
+
},
|
|
108
|
+
align: {
|
|
109
|
+
start: { base: 'items-start' },
|
|
110
|
+
center: { base: 'items-center' },
|
|
111
|
+
end: { base: 'items-end' },
|
|
112
|
+
stretch: { base: 'items-stretch' },
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
defaultVariants: { gap: 4 },
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
declare module '@glyphui/tokens' {
|
|
119
|
+
interface GlyphComponentRegistry {
|
|
120
|
+
Grid: StylesEntry<typeof gridStyles>
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface GridProps extends ComponentProps<'div'>, VariantProps<typeof gridStyles> {
|
|
125
|
+
/** Column count, or per breakpoint: `{ base: 1, sm: 2, lg: 4 }`. Default 1. */
|
|
126
|
+
cols?: GridCols
|
|
127
|
+
asChild?: boolean
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Responsive equal-width columns with spacing from the scale. */
|
|
131
|
+
export function Grid(inProps: GridProps) {
|
|
132
|
+
const [props, slots] = useComponentConfig('Grid', inProps)
|
|
133
|
+
const { cols = 1, gap, align, asChild, className, ...rest } = props
|
|
134
|
+
const Comp = asChild ? Slot.Root : 'div'
|
|
135
|
+
return (
|
|
136
|
+
<Comp
|
|
137
|
+
data-slot="grid"
|
|
138
|
+
className={gridStyles({ gap, align }).base({
|
|
139
|
+
class: [colsToClasses(cols), slots.base, className],
|
|
140
|
+
})}
|
|
141
|
+
{...rest}
|
|
142
|
+
/>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Grid, type GridCols, type GridProps, gridStyles } from './grid'
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
2
|
+
import { focusRing } from '../../utils/styles'
|
|
3
|
+
import { tv, type VariantProps } from '../../utils/tv'
|
|
4
|
+
|
|
5
|
+
export const linkStyles = tv({
|
|
6
|
+
slots: {
|
|
7
|
+
base: [
|
|
8
|
+
'inline-flex items-baseline gap-1 rounded-xs font-medium',
|
|
9
|
+
// Underline drawn as a background so it can animate its width.
|
|
10
|
+
'bg-[linear-gradient(currentColor,currentColor)] bg-[position:0_100%] bg-no-repeat',
|
|
11
|
+
'transition-[background-size,color] duration-(--glyph-duration-base) ease-standard',
|
|
12
|
+
focusRing,
|
|
13
|
+
],
|
|
14
|
+
icon: 'size-3 shrink-0 self-center opacity-70',
|
|
15
|
+
},
|
|
16
|
+
variants: {
|
|
17
|
+
tone: {
|
|
18
|
+
default: { base: 'text-foreground' },
|
|
19
|
+
muted: { base: 'text-muted-foreground hover:text-foreground' },
|
|
20
|
+
primary: { base: 'text-primary' },
|
|
21
|
+
},
|
|
22
|
+
underline: {
|
|
23
|
+
hover: {
|
|
24
|
+
base: 'bg-[length:0%_1px] hover:bg-[length:100%_1px] focus-visible:bg-[length:100%_1px]',
|
|
25
|
+
},
|
|
26
|
+
always: { base: 'bg-[length:100%_1px] hover:bg-[length:100%_2px]' },
|
|
27
|
+
none: { base: 'bg-none' },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: { tone: 'primary', underline: 'hover' },
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export type LinkVariants = VariantProps<typeof linkStyles>
|
|
34
|
+
|
|
35
|
+
declare module '@glyphui/tokens' {
|
|
36
|
+
interface GlyphComponentRegistry {
|
|
37
|
+
Link: StylesEntry<typeof linkStyles>
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ComponentProps, ElementType } from 'react'
|
|
2
|
+
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
3
|
+
import { useLinkComponent } from '../../provider/glyph-context'
|
|
4
|
+
import { type LinkVariants, linkStyles } from './link.styles'
|
|
5
|
+
|
|
6
|
+
const ExternalIcon = (props: { className?: string }) => (
|
|
7
|
+
<svg
|
|
8
|
+
viewBox="0 0 24 24"
|
|
9
|
+
fill="none"
|
|
10
|
+
stroke="currentColor"
|
|
11
|
+
strokeWidth="2"
|
|
12
|
+
strokeLinecap="round"
|
|
13
|
+
strokeLinejoin="round"
|
|
14
|
+
aria-hidden="true"
|
|
15
|
+
{...props}
|
|
16
|
+
>
|
|
17
|
+
<path d="M7 17 17 7M7 7h10v10" />
|
|
18
|
+
</svg>
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
export interface LinkProps extends Omit<ComponentProps<'a'>, 'href'>, LinkVariants {
|
|
22
|
+
href: string
|
|
23
|
+
/** Opens in a new tab with `rel="noopener noreferrer"`, shows an arrow and announces it. */
|
|
24
|
+
external?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A text link with an animated underline. Internal links go through the provider's
|
|
29
|
+
* `linkComponent` (your router); external links render a plain `<a>`.
|
|
30
|
+
*/
|
|
31
|
+
export function Link(inProps: LinkProps) {
|
|
32
|
+
const [props, slots] = useComponentConfig('Link', inProps)
|
|
33
|
+
const { href, external = false, tone, underline, className, children, ...rest } = props
|
|
34
|
+
const RouterLink = useLinkComponent()
|
|
35
|
+
const s = linkStyles({ tone, underline })
|
|
36
|
+
const classes = s.base({ class: [slots.base, className] })
|
|
37
|
+
|
|
38
|
+
if (external) {
|
|
39
|
+
return (
|
|
40
|
+
<a
|
|
41
|
+
data-slot="link"
|
|
42
|
+
href={href}
|
|
43
|
+
target="_blank"
|
|
44
|
+
rel="noopener noreferrer"
|
|
45
|
+
className={classes}
|
|
46
|
+
{...rest}
|
|
47
|
+
>
|
|
48
|
+
{children}
|
|
49
|
+
<ExternalIcon className={s.icon({ class: slots.icon })} />{' '}
|
|
50
|
+
<span className="sr-only">(opens in a new tab)</span>
|
|
51
|
+
</a>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Router links accept at least LinkComponentProps; extra anchor attributes pass through.
|
|
56
|
+
const Comp = RouterLink as ElementType
|
|
57
|
+
return (
|
|
58
|
+
<Comp data-slot="link" href={href} className={classes} {...rest}>
|
|
59
|
+
{children}
|
|
60
|
+
</Comp>
|
|
61
|
+
)
|
|
62
|
+
}
|