@glyphui/ui 0.1.0 → 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.
Files changed (85) hide show
  1. package/README.md +1 -1
  2. package/dist/data-table.js +2 -2
  3. package/dist/{field-DXZD0l8w.js → field-CCecIzdq.js} +1 -1
  4. package/dist/{field-context-B8cYheXz.js → field-context-B6WOcK5m.js} +99 -99
  5. package/dist/form.js +2 -2
  6. package/dist/index.d.ts +2743 -93
  7. package/dist/index.js +2748 -111
  8. package/dist/{states-BxPjCUxz.js → states-Rp8wZ_jH.js} +1 -1
  9. package/package.json +4 -2
  10. package/src/components/accordion/accordion.styles.ts +41 -0
  11. package/src/components/accordion/accordion.tsx +86 -0
  12. package/src/components/accordion/index.ts +8 -0
  13. package/src/components/alert/alert-icons.tsx +66 -0
  14. package/src/components/alert/alert.styles.ts +57 -0
  15. package/src/components/alert/alert.tsx +86 -0
  16. package/src/components/alert/index.ts +2 -0
  17. package/src/components/alert-dialog/alert-dialog.styles.ts +24 -0
  18. package/src/components/alert-dialog/alert-dialog.tsx +118 -0
  19. package/src/components/alert-dialog/confirm.tsx +111 -0
  20. package/src/components/alert-dialog/index.ts +15 -0
  21. package/src/components/aspect-ratio/aspect-ratio.tsx +40 -0
  22. package/src/components/aspect-ratio/index.ts +1 -0
  23. package/src/components/badge/badge.styles.ts +1 -1
  24. package/src/components/banner/banner.styles.ts +40 -0
  25. package/src/components/banner/banner.tsx +101 -0
  26. package/src/components/banner/index.ts +2 -0
  27. package/src/components/blockquote/blockquote.tsx +59 -0
  28. package/src/components/blockquote/index.ts +1 -0
  29. package/src/components/breadcrumb/breadcrumb.styles.ts +20 -0
  30. package/src/components/breadcrumb/breadcrumb.tsx +135 -0
  31. package/src/components/breadcrumb/index.ts +12 -0
  32. package/src/components/button/button.tsx +11 -8
  33. package/src/components/code/code.styles.ts +33 -0
  34. package/src/components/code/code.tsx +96 -0
  35. package/src/components/code/index.ts +2 -0
  36. package/src/components/combobox/combobox.styles.ts +50 -0
  37. package/src/components/combobox/combobox.tsx +203 -0
  38. package/src/components/combobox/index.ts +2 -0
  39. package/src/components/command-menu/command-menu.styles.ts +41 -0
  40. package/src/components/command-menu/command-menu.tsx +217 -0
  41. package/src/components/command-menu/index.ts +14 -0
  42. package/src/components/description-list/description-list.styles.ts +31 -0
  43. package/src/components/description-list/description-list.tsx +56 -0
  44. package/src/components/description-list/index.ts +7 -0
  45. package/src/components/dialog/close-icon.tsx +22 -0
  46. package/src/components/dialog/dialog.styles.ts +41 -0
  47. package/src/components/dialog/dialog.tsx +111 -0
  48. package/src/components/dialog/index.ts +13 -0
  49. package/src/components/dropdown-menu/dropdown-menu.styles.ts +45 -0
  50. package/src/components/dropdown-menu/dropdown-menu.tsx +230 -0
  51. package/src/components/dropdown-menu/index.ts +19 -0
  52. package/src/components/grid/grid.tsx +144 -0
  53. package/src/components/grid/index.ts +1 -0
  54. package/src/components/link/index.ts +2 -0
  55. package/src/components/link/link.styles.ts +39 -0
  56. package/src/components/link/link.tsx +62 -0
  57. package/src/components/pagination/index.ts +3 -0
  58. package/src/components/pagination/pagination-range.ts +35 -0
  59. package/src/components/pagination/pagination.styles.ts +41 -0
  60. package/src/components/pagination/pagination.tsx +130 -0
  61. package/src/components/popover/index.ts +9 -0
  62. package/src/components/popover/popover.tsx +66 -0
  63. package/src/components/progress/index.ts +7 -0
  64. package/src/components/progress/progress.styles.ts +68 -0
  65. package/src/components/progress/progress.tsx +158 -0
  66. package/src/components/prose/index.ts +1 -0
  67. package/src/components/prose/prose.tsx +82 -0
  68. package/src/components/radio-group/index.ts +7 -0
  69. package/src/components/radio-group/radio-group.styles.ts +64 -0
  70. package/src/components/radio-group/radio-group.tsx +128 -0
  71. package/src/components/scroll-area/index.ts +1 -0
  72. package/src/components/scroll-area/scroll-area.tsx +68 -0
  73. package/src/components/sheet/index.ts +13 -0
  74. package/src/components/sheet/sheet.styles.ts +60 -0
  75. package/src/components/sheet/sheet.tsx +108 -0
  76. package/src/components/slider/index.ts +2 -0
  77. package/src/components/slider/slider.styles.ts +32 -0
  78. package/src/components/slider/slider.tsx +131 -0
  79. package/src/components/stat-card/index.ts +2 -0
  80. package/src/components/stat-card/stat-card.styles.ts +24 -0
  81. package/src/components/stat-card/stat-card.tsx +108 -0
  82. package/src/components/toast/index.ts +1 -0
  83. package/src/components/toast/toast.tsx +91 -0
  84. package/src/index.ts +25 -0
  85. 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,2 @@
1
+ export { Link, type LinkProps } from './link'
2
+ export { type LinkVariants, linkStyles } from './link.styles'
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export { Pagination, type PaginationProps } from './pagination'
2
+ export { type PaginationVariants, paginationStyles } from './pagination.styles'
3
+ export { type PaginationItem, paginationRange } from './pagination-range'