@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
|
@@ -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,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,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
|
+
}
|