@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.
Files changed (149) hide show
  1. package/README.md +1 -1
  2. package/dist/chart.d.ts +202 -0
  3. package/dist/chart.js +457 -0
  4. package/dist/data-table.js +3 -2
  5. package/dist/{field-DXZD0l8w.js → field-CWZ_FdkD.js} +2 -1
  6. package/dist/{field-context-B8cYheXz.js → field-context-anGPYXbW.js} +89 -143
  7. package/dist/form.js +3 -2
  8. package/dist/index.d.ts +4960 -266
  9. package/dist/index.js +5336 -146
  10. package/dist/{states-BxPjCUxz.js → states-DR-OoDT-.js} +2 -1
  11. package/dist/tv-DBAGlgXJ.js +58 -0
  12. package/package.json +19 -2
  13. package/src/components/accordion/accordion.styles.ts +41 -0
  14. package/src/components/accordion/accordion.tsx +86 -0
  15. package/src/components/accordion/index.ts +8 -0
  16. package/src/components/alert/alert-icons.tsx +66 -0
  17. package/src/components/alert/alert.styles.ts +57 -0
  18. package/src/components/alert/alert.tsx +86 -0
  19. package/src/components/alert/index.ts +2 -0
  20. package/src/components/alert-dialog/alert-dialog.styles.ts +24 -0
  21. package/src/components/alert-dialog/alert-dialog.tsx +118 -0
  22. package/src/components/alert-dialog/confirm.tsx +111 -0
  23. package/src/components/alert-dialog/index.ts +15 -0
  24. package/src/components/aspect-ratio/aspect-ratio.tsx +40 -0
  25. package/src/components/aspect-ratio/index.ts +1 -0
  26. package/src/components/badge/badge.styles.ts +1 -1
  27. package/src/components/banner/banner.styles.ts +40 -0
  28. package/src/components/banner/banner.tsx +101 -0
  29. package/src/components/banner/index.ts +2 -0
  30. package/src/components/blockquote/blockquote.tsx +59 -0
  31. package/src/components/blockquote/index.ts +1 -0
  32. package/src/components/breadcrumb/breadcrumb.styles.ts +20 -0
  33. package/src/components/breadcrumb/breadcrumb.tsx +135 -0
  34. package/src/components/breadcrumb/index.ts +12 -0
  35. package/src/components/button/button.tsx +11 -8
  36. package/src/components/calendar/calendar.styles.ts +50 -0
  37. package/src/components/calendar/calendar.tsx +84 -0
  38. package/src/components/calendar/index.ts +3 -0
  39. package/src/components/carousel/carousel.styles.ts +43 -0
  40. package/src/components/carousel/carousel.tsx +278 -0
  41. package/src/components/carousel/index.ts +13 -0
  42. package/src/components/code/code.styles.ts +33 -0
  43. package/src/components/code/code.tsx +96 -0
  44. package/src/components/code/index.ts +2 -0
  45. package/src/components/collapsible/collapsible.tsx +66 -0
  46. package/src/components/collapsible/index.ts +6 -0
  47. package/src/components/combobox/combobox.styles.ts +50 -0
  48. package/src/components/combobox/combobox.tsx +203 -0
  49. package/src/components/combobox/index.ts +2 -0
  50. package/src/components/command-menu/command-menu.styles.ts +41 -0
  51. package/src/components/command-menu/command-menu.tsx +217 -0
  52. package/src/components/command-menu/index.ts +14 -0
  53. package/src/components/context-menu/context-menu.styles.ts +19 -0
  54. package/src/components/context-menu/context-menu.tsx +226 -0
  55. package/src/components/context-menu/index.ts +19 -0
  56. package/src/components/copy-button/copy-button.tsx +117 -0
  57. package/src/components/copy-button/index.ts +1 -0
  58. package/src/components/date-picker/date-picker.styles.ts +42 -0
  59. package/src/components/date-picker/date-picker.tsx +322 -0
  60. package/src/components/date-picker/index.ts +8 -0
  61. package/src/components/description-list/description-list.styles.ts +31 -0
  62. package/src/components/description-list/description-list.tsx +56 -0
  63. package/src/components/description-list/index.ts +7 -0
  64. package/src/components/dialog/close-icon.tsx +22 -0
  65. package/src/components/dialog/dialog.styles.ts +41 -0
  66. package/src/components/dialog/dialog.tsx +111 -0
  67. package/src/components/dialog/index.ts +13 -0
  68. package/src/components/dropdown-menu/dropdown-menu.styles.ts +45 -0
  69. package/src/components/dropdown-menu/dropdown-menu.tsx +230 -0
  70. package/src/components/dropdown-menu/index.ts +19 -0
  71. package/src/components/file-upload/file-upload.styles.ts +43 -0
  72. package/src/components/file-upload/file-upload.tsx +343 -0
  73. package/src/components/file-upload/index.ts +10 -0
  74. package/src/components/grid/grid.tsx +144 -0
  75. package/src/components/grid/index.ts +1 -0
  76. package/src/components/hover-card/hover-card.tsx +41 -0
  77. package/src/components/hover-card/index.ts +1 -0
  78. package/src/components/input-otp/index.ts +10 -0
  79. package/src/components/input-otp/input-otp.styles.ts +38 -0
  80. package/src/components/input-otp/input-otp.tsx +116 -0
  81. package/src/components/link/index.ts +2 -0
  82. package/src/components/link/link.styles.ts +39 -0
  83. package/src/components/link/link.tsx +62 -0
  84. package/src/components/modal/index.ts +2 -0
  85. package/src/components/modal/modal.styles.ts +18 -0
  86. package/src/components/modal/modal.tsx +86 -0
  87. package/src/components/navigation-menu/index.ts +14 -0
  88. package/src/components/navigation-menu/navigation-menu.styles.ts +48 -0
  89. package/src/components/navigation-menu/navigation-menu.tsx +198 -0
  90. package/src/components/number-input/index.ts +2 -0
  91. package/src/components/number-input/number-input.styles.ts +41 -0
  92. package/src/components/number-input/number-input.tsx +211 -0
  93. package/src/components/pagination/index.ts +3 -0
  94. package/src/components/pagination/pagination-range.ts +35 -0
  95. package/src/components/pagination/pagination.styles.ts +41 -0
  96. package/src/components/pagination/pagination.tsx +130 -0
  97. package/src/components/password-input/index.ts +7 -0
  98. package/src/components/password-input/password-input.styles.ts +30 -0
  99. package/src/components/password-input/password-input.tsx +127 -0
  100. package/src/components/popover/index.ts +9 -0
  101. package/src/components/popover/popover.tsx +66 -0
  102. package/src/components/progress/index.ts +7 -0
  103. package/src/components/progress/progress.styles.ts +68 -0
  104. package/src/components/progress/progress.tsx +158 -0
  105. package/src/components/prose/index.ts +1 -0
  106. package/src/components/prose/prose.tsx +82 -0
  107. package/src/components/radio-group/index.ts +7 -0
  108. package/src/components/radio-group/radio-group.styles.ts +64 -0
  109. package/src/components/radio-group/radio-group.tsx +128 -0
  110. package/src/components/resizable/index.ts +9 -0
  111. package/src/components/resizable/resizable.styles.ts +28 -0
  112. package/src/components/resizable/resizable.tsx +69 -0
  113. package/src/components/scroll-area/index.ts +1 -0
  114. package/src/components/scroll-area/scroll-area.tsx +68 -0
  115. package/src/components/sheet/index.ts +13 -0
  116. package/src/components/sheet/sheet.styles.ts +60 -0
  117. package/src/components/sheet/sheet.tsx +108 -0
  118. package/src/components/slider/index.ts +2 -0
  119. package/src/components/slider/slider.styles.ts +32 -0
  120. package/src/components/slider/slider.tsx +131 -0
  121. package/src/components/stat-card/index.ts +2 -0
  122. package/src/components/stat-card/stat-card.styles.ts +24 -0
  123. package/src/components/stat-card/stat-card.tsx +108 -0
  124. package/src/components/stepper/index.ts +2 -0
  125. package/src/components/stepper/stepper.styles.ts +74 -0
  126. package/src/components/stepper/stepper.tsx +138 -0
  127. package/src/components/tag-input/index.ts +2 -0
  128. package/src/components/tag-input/tag-input.styles.ts +39 -0
  129. package/src/components/tag-input/tag-input.tsx +198 -0
  130. package/src/components/timeline/index.ts +8 -0
  131. package/src/components/timeline/timeline.styles.ts +56 -0
  132. package/src/components/timeline/timeline.tsx +91 -0
  133. package/src/components/toast/index.ts +1 -0
  134. package/src/components/toast/toast.tsx +91 -0
  135. package/src/components/toggle/index.ts +9 -0
  136. package/src/components/toggle/toggle.styles.ts +39 -0
  137. package/src/components/toggle/toggle.tsx +61 -0
  138. package/src/components/tree-view/index.ts +2 -0
  139. package/src/components/tree-view/tree-view.styles.ts +42 -0
  140. package/src/components/tree-view/tree-view.tsx +274 -0
  141. package/src/components/visually-hidden/index.ts +1 -0
  142. package/src/components/visually-hidden/visually-hidden.tsx +10 -0
  143. package/src/index.ts +45 -0
  144. package/src/patterns/chart/chart-parts.tsx +171 -0
  145. package/src/patterns/chart/chart.styles.ts +35 -0
  146. package/src/patterns/chart/chart.tsx +369 -0
  147. package/src/patterns/chart/index.ts +13 -0
  148. package/src/provider/glyph-provider.tsx +16 -2
  149. package/styles.css +16 -0
@@ -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,41 @@
1
+ import { HoverCard as HoverCardPrimitive } 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 { popover } from '../../utils/styles'
6
+ import { tv } from '../../utils/tv'
7
+
8
+ export const hoverCardStyles = tv({
9
+ slots: {
10
+ content: [popover, 'w-72 max-w-[calc(100vw-2rem)] p-4 text-small outline-none'],
11
+ },
12
+ })
13
+
14
+ declare module '@glyphui/tokens' {
15
+ interface GlyphComponentRegistry {
16
+ HoverCard: StylesEntry<typeof hoverCardStyles>
17
+ }
18
+ }
19
+
20
+ /**
21
+ * A rich preview shown while hovering or focusing a link, such as a user or repository card.
22
+ * Sighted mouse users only see it on hover; keep the same information reachable elsewhere.
23
+ */
24
+ export const HoverCard = HoverCardPrimitive.Root
25
+ export const HoverCardTrigger = HoverCardPrimitive.Trigger
26
+
27
+ export function HoverCardContent(inProps: ComponentProps<typeof HoverCardPrimitive.Content>) {
28
+ const [props, slots] = useComponentConfig('HoverCard', inProps)
29
+ const { sideOffset = 6, align = 'center', className, ...rest } = props
30
+ return (
31
+ <HoverCardPrimitive.Portal>
32
+ <HoverCardPrimitive.Content
33
+ data-slot="hover-card-content"
34
+ sideOffset={sideOffset}
35
+ align={align}
36
+ className={hoverCardStyles().content({ class: [slots.content, className] })}
37
+ {...rest}
38
+ />
39
+ </HoverCardPrimitive.Portal>
40
+ )
41
+ }
@@ -0,0 +1 @@
1
+ export { HoverCard, HoverCardContent, HoverCardTrigger, hoverCardStyles } from './hover-card'
@@ -0,0 +1,10 @@
1
+ export { REGEXP_ONLY_CHARS, REGEXP_ONLY_DIGITS, REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp'
2
+ export {
3
+ InputOTP,
4
+ InputOTPGroup,
5
+ type InputOTPProps,
6
+ InputOTPSeparator,
7
+ InputOTPSlot,
8
+ type InputOTPSlotProps,
9
+ } from './input-otp'
10
+ export { type InputOTPVariants, inputOtpStyles } from './input-otp.styles'
@@ -0,0 +1,38 @@
1
+ import type { StylesEntry } from '../../utils/registry'
2
+ import { transition } from '../../utils/styles'
3
+ import { tv, type VariantProps } from '../../utils/tv'
4
+
5
+ export const inputOtpStyles = tv({
6
+ slots: {
7
+ root: 'flex items-center gap-2 has-disabled:opacity-50',
8
+ input: 'disabled:cursor-not-allowed',
9
+ group: 'flex items-center',
10
+ slot: [
11
+ 'relative flex items-center justify-center border-input bg-surface font-medium tabular-nums text-foreground shadow-xs',
12
+ 'border-y-(length:--glyph-border-width) border-r-(length:--glyph-border-width)',
13
+ 'first:rounded-l-md first:border-l-(length:--glyph-border-width) last:rounded-r-md',
14
+ 'data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-3 data-[active=true]:ring-ring/20',
15
+ 'aria-invalid:border-danger',
16
+ transition,
17
+ ],
18
+ caret: 'pointer-events-none absolute inset-0 flex items-center justify-center',
19
+ caretBar: 'h-4 w-px animate-pulse bg-foreground',
20
+ separator: 'flex items-center text-faint-foreground [&>svg]:size-4',
21
+ },
22
+ variants: {
23
+ size: {
24
+ sm: { slot: 'size-8 text-small' },
25
+ md: { slot: 'size-10 text-body' },
26
+ lg: { slot: 'size-12 text-h4' },
27
+ },
28
+ },
29
+ defaultVariants: { size: 'md' },
30
+ })
31
+
32
+ export type InputOTPVariants = VariantProps<typeof inputOtpStyles>
33
+
34
+ declare module '@glyphui/tokens' {
35
+ interface GlyphComponentRegistry {
36
+ InputOTP: StylesEntry<typeof inputOtpStyles>
37
+ }
38
+ }
@@ -0,0 +1,116 @@
1
+ import { OTPInput, OTPInputContext, type OTPInputProps } from 'input-otp'
2
+ import { type ComponentProps, createContext, type ReactNode, useContext } from 'react'
3
+ import { type SlotClasses, useComponentConfig } from '../../hooks/use-component-config'
4
+ import { useFieldControl } from '../field/field-context'
5
+ import { type InputOTPVariants, inputOtpStyles } from './input-otp.styles'
6
+
7
+ type Styles = ReturnType<typeof inputOtpStyles>
8
+ const PartsContext = createContext<{
9
+ s: Styles
10
+ slots: SlotClasses<'InputOTP'>
11
+ invalid: boolean
12
+ } | null>(null)
13
+
14
+ function useParts() {
15
+ return useContext(PartsContext) ?? { s: inputOtpStyles(), slots: {}, invalid: false }
16
+ }
17
+
18
+ export type InputOTPProps = Omit<OTPInputProps, 'render' | 'children' | 'maxLength'> &
19
+ InputOTPVariants & {
20
+ /** Number of characters. Default 6. */
21
+ length?: number
22
+ /** Custom slot layout (InputOTPGroup, InputOTPSlot, InputOTPSeparator). Defaults to one group of `length` slots. */
23
+ children?: ReactNode
24
+ }
25
+
26
+ /**
27
+ * One-time code input: one box per character, paste support, and autofill from SMS on
28
+ * mobile. Built on input-otp. Restrict characters with `pattern`, e.g. `REGEXP_ONLY_DIGITS`.
29
+ */
30
+ export function InputOTP(inProps: InputOTPProps) {
31
+ const [props, slots] = useComponentConfig('InputOTP', useFieldControl(inProps))
32
+ const { length = 6, size, children, className, containerClassName, ...rest } = props
33
+ const s = inputOtpStyles({ size })
34
+ const invalid = rest['aria-invalid'] === true || rest['aria-invalid'] === 'true'
35
+ return (
36
+ <PartsContext.Provider value={{ s, slots, invalid }}>
37
+ <OTPInput
38
+ data-slot="input-otp"
39
+ maxLength={length}
40
+ autoComplete="one-time-code"
41
+ inputMode="numeric"
42
+ containerClassName={s.root({ class: [slots.root, containerClassName] })}
43
+ className={s.input({ class: [slots.input, className] })}
44
+ {...rest}
45
+ >
46
+ {children ?? (
47
+ <InputOTPGroup>
48
+ {Array.from({ length }, (_, index) => (
49
+ <InputOTPSlot key={index} index={index} />
50
+ ))}
51
+ </InputOTPGroup>
52
+ )}
53
+ </OTPInput>
54
+ </PartsContext.Provider>
55
+ )
56
+ }
57
+
58
+ export function InputOTPGroup({ className, ...props }: ComponentProps<'div'>) {
59
+ const { s, slots } = useParts()
60
+ return (
61
+ <div
62
+ data-slot="input-otp-group"
63
+ className={s.group({ class: [slots.group, className] })}
64
+ {...props}
65
+ />
66
+ )
67
+ }
68
+
69
+ export interface InputOTPSlotProps extends ComponentProps<'div'> {
70
+ /** Position of the character this box shows, starting at 0. */
71
+ index: number
72
+ }
73
+
74
+ export function InputOTPSlot({ index, className, ...props }: InputOTPSlotProps) {
75
+ const { s, slots, invalid } = useParts()
76
+ const slot = useContext(OTPInputContext).slots[index]
77
+ return (
78
+ <div
79
+ data-slot="input-otp-slot"
80
+ data-active={slot?.isActive ?? false}
81
+ aria-invalid={invalid || undefined}
82
+ className={s.slot({ class: [slots.slot, className] })}
83
+ {...props}
84
+ >
85
+ {slot?.char ?? slot?.placeholderChar}
86
+ {slot?.hasFakeCaret && (
87
+ <div className={s.caret({ class: slots.caret })}>
88
+ <div className={s.caretBar({ class: slots.caretBar })} />
89
+ </div>
90
+ )}
91
+ </div>
92
+ )
93
+ }
94
+
95
+ export function InputOTPSeparator({ className, ...props }: ComponentProps<'div'>) {
96
+ const { s, slots } = useParts()
97
+ return (
98
+ <div
99
+ data-slot="input-otp-separator"
100
+ aria-hidden="true"
101
+ className={s.separator({ class: [slots.separator, className] })}
102
+ {...props}
103
+ >
104
+ <svg
105
+ viewBox="0 0 24 24"
106
+ fill="none"
107
+ stroke="currentColor"
108
+ strokeWidth="2"
109
+ strokeLinecap="round"
110
+ aria-hidden="true"
111
+ >
112
+ <path d="M6 12h12" />
113
+ </svg>
114
+ </div>
115
+ )
116
+ }
@@ -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,2 @@
1
+ export { Modal, ModalClose, type ModalProps } from './modal'
2
+ export { modalStyles } from './modal.styles'
@@ -0,0 +1,18 @@
1
+ import type { StylesEntry } from '../../utils/registry'
2
+ import { tv } from '../../utils/tv'
3
+
4
+ /** Modal is built from Dialog parts; these slots add Modal-specific classes on top. */
5
+ export const modalStyles = tv({
6
+ slots: {
7
+ content: '',
8
+ header: '',
9
+ body: 'grid gap-4',
10
+ footer: '',
11
+ },
12
+ })
13
+
14
+ declare module '@glyphui/tokens' {
15
+ interface GlyphComponentRegistry {
16
+ Modal: StylesEntry<typeof modalStyles>
17
+ }
18
+ }
@@ -0,0 +1,86 @@
1
+ import type { ReactNode } from 'react'
2
+ import { useComponentConfig } from '../../hooks/use-component-config'
3
+ import {
4
+ Dialog,
5
+ DialogBody,
6
+ DialogClose,
7
+ DialogContent,
8
+ type DialogContentProps,
9
+ DialogDescription,
10
+ DialogFooter,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ DialogTrigger,
14
+ } from '../dialog'
15
+ import { modalStyles } from './modal.styles'
16
+
17
+ export interface ModalProps extends Omit<DialogContentProps, 'title' | 'children'> {
18
+ open?: boolean
19
+ defaultOpen?: boolean
20
+ onOpenChange?: (open: boolean) => void
21
+ /** Heading of the modal. Also its accessible name. */
22
+ title: ReactNode
23
+ description?: ReactNode
24
+ /** Element that opens the modal, e.g. a Button. Optional when you control `open`. */
25
+ trigger?: ReactNode
26
+ /** Actions at the bottom. Wrap a button in `ModalClose` to close on click. */
27
+ footer?: ReactNode
28
+ /** Close when the backdrop is clicked. Default true. */
29
+ closeOnOverlayClick?: boolean
30
+ children?: ReactNode
31
+ }
32
+
33
+ /**
34
+ * The everyday modal in one component: title, description, body and footer as props.
35
+ * For custom layouts use the Dialog parts; for yes/no decisions use `useConfirm()`.
36
+ */
37
+ export function Modal(inProps: ModalProps) {
38
+ const [props, slots] = useComponentConfig('Modal', inProps)
39
+ const {
40
+ open,
41
+ defaultOpen,
42
+ onOpenChange,
43
+ title,
44
+ description,
45
+ trigger,
46
+ footer,
47
+ closeOnOverlayClick = true,
48
+ onPointerDownOutside,
49
+ className,
50
+ children,
51
+ ...rest
52
+ } = props
53
+ const s = modalStyles()
54
+
55
+ return (
56
+ <Dialog
57
+ {...(open !== undefined && { open })}
58
+ {...(defaultOpen !== undefined && { defaultOpen })}
59
+ {...(onOpenChange && { onOpenChange })}
60
+ >
61
+ {trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
62
+ <DialogContent
63
+ data-slot="modal"
64
+ className={s.content({ class: [slots.content, className] })}
65
+ onPointerDownOutside={(event) => {
66
+ onPointerDownOutside?.(event)
67
+ if (!closeOnOverlayClick) event.preventDefault()
68
+ }}
69
+ {...(!description && { 'aria-describedby': undefined })}
70
+ {...rest}
71
+ >
72
+ <DialogHeader className={s.header({ class: slots.header })}>
73
+ <DialogTitle>{title}</DialogTitle>
74
+ {description && <DialogDescription>{description}</DialogDescription>}
75
+ </DialogHeader>
76
+ {children && <DialogBody className={s.body({ class: slots.body })}>{children}</DialogBody>}
77
+ {footer && (
78
+ <DialogFooter className={s.footer({ class: slots.footer })}>{footer}</DialogFooter>
79
+ )}
80
+ </DialogContent>
81
+ </Dialog>
82
+ )
83
+ }
84
+
85
+ /** Closes the surrounding Modal. Use with `asChild` around a button. */
86
+ export const ModalClose: typeof DialogClose = DialogClose
@@ -0,0 +1,14 @@
1
+ export {
2
+ NavigationMenu,
3
+ NavigationMenuCard,
4
+ type NavigationMenuCardProps,
5
+ NavigationMenuContent,
6
+ NavigationMenuItem,
7
+ NavigationMenuLink,
8
+ type NavigationMenuLinkProps,
9
+ NavigationMenuList,
10
+ type NavigationMenuProps,
11
+ NavigationMenuTrigger,
12
+ NavigationMenuViewport,
13
+ } from './navigation-menu'
14
+ export { navigationMenuStyles } from './navigation-menu.styles'
@@ -0,0 +1,48 @@
1
+ import type { StylesEntry } from '../../utils/registry'
2
+ import { focusRing, themedBorder, transition } from '../../utils/styles'
3
+ import { tv } from '../../utils/tv'
4
+
5
+ const topLevel = [
6
+ 'group inline-flex h-9 cursor-pointer select-none items-center justify-center gap-1 rounded-md px-3 text-small font-medium text-muted-foreground',
7
+ transition,
8
+ focusRing,
9
+ 'hover:bg-muted hover:text-foreground data-[state=open]:bg-muted data-[state=open]:text-foreground',
10
+ 'data-active:text-foreground',
11
+ ]
12
+
13
+ export const navigationMenuStyles = tv({
14
+ slots: {
15
+ root: 'relative z-10 flex max-w-max flex-1 items-center justify-center',
16
+ list: 'group flex flex-1 list-none items-center justify-center gap-1',
17
+ item: 'relative',
18
+ trigger: topLevel,
19
+ chevron:
20
+ 'size-3 text-faint-foreground transition-transform duration-(--glyph-duration-base) ease-standard group-data-[state=open]:rotate-180',
21
+ content: 'top-0 left-0 w-full p-2 animate-fade-in md:absolute md:w-auto',
22
+ viewportWrapper: 'absolute top-full left-0 flex justify-center',
23
+ viewport: [
24
+ 'relative mt-1.5 h-(--radix-navigation-menu-viewport-height) w-[calc(100vw-2rem)] origin-top overflow-hidden rounded-lg border-border bg-raised text-foreground shadow-lg animate-pop-in',
25
+ 'md:w-(--radix-navigation-menu-viewport-width)',
26
+ 'transition-[width,height] duration-(--glyph-duration-base) ease-standard',
27
+ themedBorder,
28
+ ],
29
+ link: topLevel,
30
+ card: [
31
+ 'flex select-none gap-3 rounded-md p-3 text-foreground no-underline outline-none',
32
+ transition,
33
+ focusRing,
34
+ 'hover:bg-muted focus-visible:bg-muted',
35
+ ],
36
+ cardIcon:
37
+ 'flex size-9 shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted-foreground shadow-xs [&_svg]:size-4',
38
+ cardText: 'grid min-w-0 gap-0.5',
39
+ cardTitle: 'text-small font-medium text-foreground',
40
+ cardDescription: 'text-small text-muted-foreground',
41
+ },
42
+ })
43
+
44
+ declare module '@glyphui/tokens' {
45
+ interface GlyphComponentRegistry {
46
+ NavigationMenu: StylesEntry<typeof navigationMenuStyles>
47
+ }
48
+ }