@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,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
type ReactNode,
|
|
4
|
+
useCallback,
|
|
5
|
+
useContext,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react'
|
|
10
|
+
import {
|
|
11
|
+
AlertDialog,
|
|
12
|
+
AlertDialogAction,
|
|
13
|
+
AlertDialogCancel,
|
|
14
|
+
AlertDialogContent,
|
|
15
|
+
AlertDialogDescription,
|
|
16
|
+
AlertDialogFooter,
|
|
17
|
+
AlertDialogHeader,
|
|
18
|
+
AlertDialogTitle,
|
|
19
|
+
} from './alert-dialog'
|
|
20
|
+
|
|
21
|
+
export interface ConfirmOptions {
|
|
22
|
+
title: ReactNode
|
|
23
|
+
description?: ReactNode
|
|
24
|
+
/** Default "Confirm", or "Delete" when `tone` is `danger`. */
|
|
25
|
+
confirmLabel?: ReactNode
|
|
26
|
+
cancelLabel?: ReactNode
|
|
27
|
+
tone?: 'default' | 'danger'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type ConfirmFn = (options: ConfirmOptions) => Promise<boolean>
|
|
31
|
+
|
|
32
|
+
const ConfirmContext = createContext<ConfirmFn | null>(null)
|
|
33
|
+
|
|
34
|
+
interface Pending {
|
|
35
|
+
options: ConfirmOptions
|
|
36
|
+
resolve: (value: boolean) => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Enables `useConfirm()` for everything inside. Renders one shared AlertDialog.
|
|
41
|
+
* Mount once near the root (inside GlyphProvider).
|
|
42
|
+
*/
|
|
43
|
+
export function ConfirmProvider({ children }: { children: ReactNode }) {
|
|
44
|
+
const [pending, setPending] = useState<Pending | null>(null)
|
|
45
|
+
const [open, setOpen] = useState(false)
|
|
46
|
+
const pendingRef = useRef<Pending | null>(null)
|
|
47
|
+
|
|
48
|
+
const settle = useCallback((value: boolean) => {
|
|
49
|
+
pendingRef.current?.resolve(value)
|
|
50
|
+
pendingRef.current = null
|
|
51
|
+
setOpen(false)
|
|
52
|
+
}, [])
|
|
53
|
+
|
|
54
|
+
const confirm = useCallback<ConfirmFn>((options) => {
|
|
55
|
+
// A new request cancels one that is still open.
|
|
56
|
+
pendingRef.current?.resolve(false)
|
|
57
|
+
return new Promise<boolean>((resolve) => {
|
|
58
|
+
const next = { options, resolve }
|
|
59
|
+
pendingRef.current = next
|
|
60
|
+
setPending(next)
|
|
61
|
+
setOpen(true)
|
|
62
|
+
})
|
|
63
|
+
}, [])
|
|
64
|
+
|
|
65
|
+
const value = useMemo(() => confirm, [confirm])
|
|
66
|
+
const options = pending?.options
|
|
67
|
+
const danger = options?.tone === 'danger'
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<ConfirmContext.Provider value={value}>
|
|
71
|
+
{children}
|
|
72
|
+
<AlertDialog open={open} onOpenChange={(next) => !next && settle(false)}>
|
|
73
|
+
{options && (
|
|
74
|
+
<AlertDialogContent>
|
|
75
|
+
<AlertDialogHeader>
|
|
76
|
+
<AlertDialogTitle>{options.title}</AlertDialogTitle>
|
|
77
|
+
{options.description && (
|
|
78
|
+
<AlertDialogDescription>{options.description}</AlertDialogDescription>
|
|
79
|
+
)}
|
|
80
|
+
</AlertDialogHeader>
|
|
81
|
+
<AlertDialogFooter>
|
|
82
|
+
<AlertDialogCancel onClick={() => settle(false)}>
|
|
83
|
+
{options.cancelLabel ?? 'Cancel'}
|
|
84
|
+
</AlertDialogCancel>
|
|
85
|
+
<AlertDialogAction tone={options.tone} onClick={() => settle(true)}>
|
|
86
|
+
{options.confirmLabel ?? (danger ? 'Delete' : 'Confirm')}
|
|
87
|
+
</AlertDialogAction>
|
|
88
|
+
</AlertDialogFooter>
|
|
89
|
+
</AlertDialogContent>
|
|
90
|
+
)}
|
|
91
|
+
</AlertDialog>
|
|
92
|
+
</ConfirmContext.Provider>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Ask the user to confirm, in one line:
|
|
98
|
+
*
|
|
99
|
+
* ```tsx
|
|
100
|
+
* const confirm = useConfirm()
|
|
101
|
+
* if (await confirm({ title: 'Delete project?', tone: 'danger' })) await deleteProject()
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* Resolves `true` when confirmed, `false` when cancelled or dismissed with Escape.
|
|
105
|
+
*/
|
|
106
|
+
export function useConfirm(): ConfirmFn {
|
|
107
|
+
const confirm = useContext(ConfirmContext)
|
|
108
|
+
if (!confirm)
|
|
109
|
+
throw new Error('useConfirm must be used inside <ConfirmProvider> (or <GlyphProvider>).')
|
|
110
|
+
return confirm
|
|
111
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export {
|
|
2
|
+
AlertDialog,
|
|
3
|
+
AlertDialogAction,
|
|
4
|
+
type AlertDialogActionProps,
|
|
5
|
+
AlertDialogCancel,
|
|
6
|
+
AlertDialogContent,
|
|
7
|
+
type AlertDialogContentProps,
|
|
8
|
+
AlertDialogDescription,
|
|
9
|
+
AlertDialogFooter,
|
|
10
|
+
AlertDialogHeader,
|
|
11
|
+
AlertDialogTitle,
|
|
12
|
+
AlertDialogTrigger,
|
|
13
|
+
} from './alert-dialog'
|
|
14
|
+
export { alertDialogStyles } from './alert-dialog.styles'
|
|
15
|
+
export { type ConfirmFn, type ConfirmOptions, ConfirmProvider, useConfirm } from './confirm'
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AspectRatio as AspectRatioPrimitive } 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
|
+
export const aspectRatioStyles = tv({
|
|
8
|
+
slots: {
|
|
9
|
+
base: 'overflow-hidden [&>img]:size-full [&>img]:object-cover [&>video]:size-full [&>video]:object-cover',
|
|
10
|
+
},
|
|
11
|
+
variants: {
|
|
12
|
+
rounded: { true: { base: 'rounded-lg' } },
|
|
13
|
+
placeholder: { true: { base: 'bg-muted' } },
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
declare module '@glyphui/tokens' {
|
|
18
|
+
interface GlyphComponentRegistry {
|
|
19
|
+
AspectRatio: StylesEntry<typeof aspectRatioStyles>
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AspectRatioProps
|
|
24
|
+
extends ComponentProps<typeof AspectRatioPrimitive.Root>,
|
|
25
|
+
VariantProps<typeof aspectRatioStyles> {}
|
|
26
|
+
|
|
27
|
+
/** Keeps media at a fixed width-to-height ratio, e.g. `ratio={16 / 9}`. Images inside cover the box. */
|
|
28
|
+
export function AspectRatio(inProps: AspectRatioProps) {
|
|
29
|
+
const [props, slots] = useComponentConfig('AspectRatio', inProps)
|
|
30
|
+
const { rounded, placeholder, className, ...rest } = props
|
|
31
|
+
return (
|
|
32
|
+
<AspectRatioPrimitive.Root
|
|
33
|
+
data-slot="aspect-ratio"
|
|
34
|
+
className={aspectRatioStyles({ rounded, placeholder }).base({
|
|
35
|
+
class: [slots.base, className],
|
|
36
|
+
})}
|
|
37
|
+
{...rest}
|
|
38
|
+
/>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AspectRatio, type AspectRatioProps, aspectRatioStyles } from './aspect-ratio'
|
|
@@ -46,7 +46,7 @@ type Variant = keyof (typeof tones)[Tone]
|
|
|
46
46
|
export const badgeStyles = tv({
|
|
47
47
|
slots: {
|
|
48
48
|
base: [
|
|
49
|
-
'inline-flex h-5.5 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-pill border-transparent px-2',
|
|
49
|
+
'inline-flex h-5.5 w-fit shrink-0 items-center gap-1.5 whitespace-nowrap rounded-pill border-transparent px-2',
|
|
50
50
|
'text-caption tabular-nums',
|
|
51
51
|
themedBorder,
|
|
52
52
|
],
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
2
|
+
import { tv, type VariantProps } from '../../utils/tv'
|
|
3
|
+
|
|
4
|
+
export const bannerStyles = tv({
|
|
5
|
+
slots: {
|
|
6
|
+
root: ['w-full border-b-(length:--glyph-border-width) text-small'],
|
|
7
|
+
inner: 'mx-auto flex w-full max-w-7xl items-center gap-3 px-4 py-2.5 sm:px-6',
|
|
8
|
+
icon: 'flex size-4 shrink-0 items-center justify-center [&>svg]:size-4',
|
|
9
|
+
content: 'min-w-0 flex-1 [&_a]:font-medium [&_a]:underline [&_a]:underline-offset-2',
|
|
10
|
+
action: 'flex shrink-0 items-center gap-2',
|
|
11
|
+
dismiss: '-my-1 -mr-2 shrink-0 text-current hover:bg-current/10 hover:text-current',
|
|
12
|
+
},
|
|
13
|
+
variants: {
|
|
14
|
+
tone: {
|
|
15
|
+
neutral: { root: 'border-border bg-subtle text-foreground', icon: 'text-muted-foreground' },
|
|
16
|
+
primary: { root: 'border-primary bg-primary text-primary-foreground' },
|
|
17
|
+
info: {
|
|
18
|
+
root: 'border-info/25 bg-info/10 text-[color-mix(in_oklab,var(--color-info)_55%,var(--color-foreground))]',
|
|
19
|
+
},
|
|
20
|
+
success: {
|
|
21
|
+
root: 'border-success/25 bg-success/10 text-[color-mix(in_oklab,var(--color-success)_55%,var(--color-foreground))]',
|
|
22
|
+
},
|
|
23
|
+
warning: {
|
|
24
|
+
root: 'border-warning/30 bg-warning/14 text-[color-mix(in_oklab,var(--color-warning)_50%,var(--color-foreground))]',
|
|
25
|
+
},
|
|
26
|
+
danger: {
|
|
27
|
+
root: 'border-danger/25 bg-danger/10 text-[color-mix(in_oklab,var(--color-danger)_55%,var(--color-foreground))]',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
defaultVariants: { tone: 'neutral' },
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export type BannerVariants = VariantProps<typeof bannerStyles>
|
|
35
|
+
|
|
36
|
+
declare module '@glyphui/tokens' {
|
|
37
|
+
interface GlyphComponentRegistry {
|
|
38
|
+
Banner: StylesEntry<typeof bannerStyles>
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactNode, useState } from 'react'
|
|
2
|
+
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
3
|
+
import { XIcon } from '../../internal/icons'
|
|
4
|
+
import { toneIcons } from '../alert/alert-icons'
|
|
5
|
+
import { IconButton } from '../button'
|
|
6
|
+
import { type BannerVariants, bannerStyles } from './banner.styles'
|
|
7
|
+
|
|
8
|
+
export interface BannerProps extends ComponentProps<'section'>, BannerVariants {
|
|
9
|
+
/** Replaces the tone's default icon. `false` hides it. */
|
|
10
|
+
icon?: ReactNode | false
|
|
11
|
+
action?: ReactNode
|
|
12
|
+
/** Shows a close button. */
|
|
13
|
+
dismissible?: boolean
|
|
14
|
+
/** Remembers the dismissal in localStorage under this key, so the banner stays hidden. */
|
|
15
|
+
storageKey?: string
|
|
16
|
+
onDismiss?: () => void
|
|
17
|
+
dismissLabel?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readDismissed(key: string | undefined): boolean {
|
|
21
|
+
if (!key || typeof window === 'undefined') return false
|
|
22
|
+
try {
|
|
23
|
+
return window.localStorage.getItem(key) === 'dismissed'
|
|
24
|
+
} catch {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A full-width announcement at the top of a page or section. */
|
|
30
|
+
export function Banner(inProps: BannerProps) {
|
|
31
|
+
const [props, slots] = useComponentConfig('Banner', inProps)
|
|
32
|
+
const {
|
|
33
|
+
tone,
|
|
34
|
+
icon,
|
|
35
|
+
action,
|
|
36
|
+
dismissible = false,
|
|
37
|
+
storageKey,
|
|
38
|
+
onDismiss,
|
|
39
|
+
dismissLabel = 'Dismiss',
|
|
40
|
+
className,
|
|
41
|
+
children,
|
|
42
|
+
...rest
|
|
43
|
+
} = props
|
|
44
|
+
const [dismissed, setDismissed] = useState(() => readDismissed(storageKey))
|
|
45
|
+
if (dismissed) return null
|
|
46
|
+
|
|
47
|
+
const resolvedTone = tone ?? 'neutral'
|
|
48
|
+
const s = bannerStyles({ tone })
|
|
49
|
+
const iconNode = icon === false ? null : (icon ?? toneIcons[resolvedTone]())
|
|
50
|
+
|
|
51
|
+
const dismiss = () => {
|
|
52
|
+
setDismissed(true)
|
|
53
|
+
if (storageKey) {
|
|
54
|
+
try {
|
|
55
|
+
window.localStorage.setItem(storageKey, 'dismissed')
|
|
56
|
+
} catch {
|
|
57
|
+
// Storage unavailable: the banner stays hidden for this visit only.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
onDismiss?.()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<section
|
|
65
|
+
data-slot="banner"
|
|
66
|
+
data-tone={resolvedTone}
|
|
67
|
+
aria-label="Announcement"
|
|
68
|
+
className={s.root({ class: [slots.root, className] })}
|
|
69
|
+
{...rest}
|
|
70
|
+
>
|
|
71
|
+
<div className={s.inner({ class: slots.inner })}>
|
|
72
|
+
{iconNode && (
|
|
73
|
+
<span
|
|
74
|
+
data-slot="banner-icon"
|
|
75
|
+
className={s.icon({ class: slots.icon })}
|
|
76
|
+
aria-hidden="true"
|
|
77
|
+
>
|
|
78
|
+
{iconNode}
|
|
79
|
+
</span>
|
|
80
|
+
)}
|
|
81
|
+
<div data-slot="banner-content" className={s.content({ class: slots.content })}>
|
|
82
|
+
{children}
|
|
83
|
+
</div>
|
|
84
|
+
{action && (
|
|
85
|
+
<div data-slot="banner-action" className={s.action({ class: slots.action })}>
|
|
86
|
+
{action}
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
{dismissible && (
|
|
90
|
+
<IconButton
|
|
91
|
+
size="sm"
|
|
92
|
+
icon={<XIcon />}
|
|
93
|
+
aria-label={dismissLabel}
|
|
94
|
+
onClick={dismiss}
|
|
95
|
+
className={s.dismiss({ class: slots.dismiss })}
|
|
96
|
+
/>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
</section>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from 'react'
|
|
2
|
+
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
3
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
4
|
+
import { tv, type VariantProps } from '../../utils/tv'
|
|
5
|
+
|
|
6
|
+
export const blockquoteStyles = tv({
|
|
7
|
+
slots: {
|
|
8
|
+
root: 'grid gap-3 border-l-2 border-primary pl-5',
|
|
9
|
+
quote: 'font-display text-pretty text-foreground',
|
|
10
|
+
caption: 'text-small text-muted-foreground',
|
|
11
|
+
author: 'font-medium text-foreground not-italic',
|
|
12
|
+
},
|
|
13
|
+
variants: {
|
|
14
|
+
size: {
|
|
15
|
+
sm: { quote: 'text-body' },
|
|
16
|
+
md: { quote: 'text-lead' },
|
|
17
|
+
lg: { quote: 'text-h3' },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: { size: 'md' },
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
declare module '@glyphui/tokens' {
|
|
24
|
+
interface GlyphComponentRegistry {
|
|
25
|
+
Blockquote: StylesEntry<typeof blockquoteStyles>
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BlockquoteProps
|
|
30
|
+
extends ComponentProps<'figure'>,
|
|
31
|
+
VariantProps<typeof blockquoteStyles> {
|
|
32
|
+
/** Who said it. */
|
|
33
|
+
author?: ReactNode
|
|
34
|
+
/** Their role or the work it's from, shown after the author. */
|
|
35
|
+
source?: ReactNode
|
|
36
|
+
/** URL of the quoted source, set as the `cite` attribute. */
|
|
37
|
+
citeUrl?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A quotation with an optional attribution. */
|
|
41
|
+
export function Blockquote(inProps: BlockquoteProps) {
|
|
42
|
+
const [props, slots] = useComponentConfig('Blockquote', inProps)
|
|
43
|
+
const { author, source, citeUrl, size, className, children, ...rest } = props
|
|
44
|
+
const s = blockquoteStyles({ size })
|
|
45
|
+
return (
|
|
46
|
+
<figure data-slot="blockquote" className={s.root({ class: [slots.root, className] })} {...rest}>
|
|
47
|
+
<blockquote cite={citeUrl} className={s.quote({ class: slots.quote })}>
|
|
48
|
+
{children}
|
|
49
|
+
</blockquote>
|
|
50
|
+
{(author || source) && (
|
|
51
|
+
<figcaption className={s.caption({ class: slots.caption })}>
|
|
52
|
+
{author && <cite className={s.author({ class: slots.author })}>{author}</cite>}
|
|
53
|
+
{author && source && ', '}
|
|
54
|
+
{source}
|
|
55
|
+
</figcaption>
|
|
56
|
+
)}
|
|
57
|
+
</figure>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Blockquote, type BlockquoteProps, blockquoteStyles } from './blockquote'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
2
|
+
import { focusRing, transition } from '../../utils/styles'
|
|
3
|
+
import { tv } from '../../utils/tv'
|
|
4
|
+
|
|
5
|
+
export const breadcrumbStyles = tv({
|
|
6
|
+
slots: {
|
|
7
|
+
root: 'min-w-0',
|
|
8
|
+
list: 'flex flex-wrap items-center gap-1.5 text-small text-muted-foreground',
|
|
9
|
+
item: 'inline-flex min-w-0 items-center gap-1.5',
|
|
10
|
+
link: ['truncate rounded-xs hover:text-foreground', transition, focusRing],
|
|
11
|
+
page: 'truncate font-medium text-foreground',
|
|
12
|
+
separator: 'flex items-center text-faint-foreground [&>svg]:size-3.5',
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
declare module '@glyphui/tokens' {
|
|
17
|
+
interface GlyphComponentRegistry {
|
|
18
|
+
Breadcrumb: StylesEntry<typeof breadcrumbStyles>
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Slot } from 'radix-ui'
|
|
2
|
+
import type { ComponentProps, ReactNode } from 'react'
|
|
3
|
+
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
4
|
+
import { ChevronRightIcon } from '../../internal/icons'
|
|
5
|
+
import { useLinkComponent } from '../../provider/glyph-context'
|
|
6
|
+
import { breadcrumbStyles } from './breadcrumb.styles'
|
|
7
|
+
|
|
8
|
+
const s = breadcrumbStyles()
|
|
9
|
+
|
|
10
|
+
function useSlots() {
|
|
11
|
+
return useComponentConfig('Breadcrumb', {})[1]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface BreadcrumbItemData {
|
|
15
|
+
label: ReactNode
|
|
16
|
+
/** Omit for the current page (normally the last item). */
|
|
17
|
+
href?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BreadcrumbProps extends ComponentProps<'nav'> {
|
|
21
|
+
/** Data-driven trail. The last item is the current page. Omit to compose the parts yourself. */
|
|
22
|
+
items?: readonly BreadcrumbItemData[]
|
|
23
|
+
/** Custom separator between items. A chevron by default. */
|
|
24
|
+
separator?: ReactNode
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Shows where the current page sits in the hierarchy. */
|
|
28
|
+
export function Breadcrumb({ items, separator, className, children, ...props }: BreadcrumbProps) {
|
|
29
|
+
const slots = useSlots()
|
|
30
|
+
return (
|
|
31
|
+
<nav
|
|
32
|
+
aria-label="Breadcrumb"
|
|
33
|
+
data-slot="breadcrumb"
|
|
34
|
+
className={s.root({ class: [slots.root, className] })}
|
|
35
|
+
{...props}
|
|
36
|
+
>
|
|
37
|
+
{items ? (
|
|
38
|
+
<BreadcrumbList>
|
|
39
|
+
{items.map((item, index) => {
|
|
40
|
+
const last = index === items.length - 1
|
|
41
|
+
return (
|
|
42
|
+
<BreadcrumbItem key={index}>
|
|
43
|
+
{last || !item.href ? (
|
|
44
|
+
<BreadcrumbPage aria-current={last ? 'page' : undefined}>
|
|
45
|
+
{item.label}
|
|
46
|
+
</BreadcrumbPage>
|
|
47
|
+
) : (
|
|
48
|
+
<BreadcrumbLink href={item.href}>{item.label}</BreadcrumbLink>
|
|
49
|
+
)}
|
|
50
|
+
{!last && <BreadcrumbSeparator>{separator}</BreadcrumbSeparator>}
|
|
51
|
+
</BreadcrumbItem>
|
|
52
|
+
)
|
|
53
|
+
})}
|
|
54
|
+
</BreadcrumbList>
|
|
55
|
+
) : (
|
|
56
|
+
children
|
|
57
|
+
)}
|
|
58
|
+
</nav>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function BreadcrumbList({ className, ...props }: ComponentProps<'ol'>) {
|
|
63
|
+
const slots = useSlots()
|
|
64
|
+
return (
|
|
65
|
+
<ol
|
|
66
|
+
data-slot="breadcrumb-list"
|
|
67
|
+
className={s.list({ class: [slots.list, className] })}
|
|
68
|
+
{...props}
|
|
69
|
+
/>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function BreadcrumbItem({ className, ...props }: ComponentProps<'li'>) {
|
|
74
|
+
const slots = useSlots()
|
|
75
|
+
return (
|
|
76
|
+
<li
|
|
77
|
+
data-slot="breadcrumb-item"
|
|
78
|
+
className={s.item({ class: [slots.item, className] })}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface BreadcrumbLinkProps {
|
|
85
|
+
href: string
|
|
86
|
+
className?: string
|
|
87
|
+
children?: ReactNode
|
|
88
|
+
/** Render your own element (e.g. a router link) with breadcrumb link styles. */
|
|
89
|
+
asChild?: boolean
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function BreadcrumbLink({ href, className, children, asChild }: BreadcrumbLinkProps) {
|
|
93
|
+
const Link = useLinkComponent()
|
|
94
|
+
const slots = useSlots()
|
|
95
|
+
const classes = s.link({ class: [slots.link, className] })
|
|
96
|
+
if (asChild) {
|
|
97
|
+
return (
|
|
98
|
+
<Slot.Root data-slot="breadcrumb-link" className={classes}>
|
|
99
|
+
{children}
|
|
100
|
+
</Slot.Root>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
return (
|
|
104
|
+
<Link href={href} className={classes}>
|
|
105
|
+
{children}
|
|
106
|
+
</Link>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function BreadcrumbPage({ className, ...props }: ComponentProps<'span'>) {
|
|
111
|
+
const slots = useSlots()
|
|
112
|
+
return (
|
|
113
|
+
<span
|
|
114
|
+
data-slot="breadcrumb-page"
|
|
115
|
+
aria-current="page"
|
|
116
|
+
className={s.page({ class: [slots.page, className] })}
|
|
117
|
+
{...props}
|
|
118
|
+
/>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function BreadcrumbSeparator({ className, children, ...props }: ComponentProps<'span'>) {
|
|
123
|
+
const slots = useSlots()
|
|
124
|
+
return (
|
|
125
|
+
<span
|
|
126
|
+
data-slot="breadcrumb-separator"
|
|
127
|
+
role="presentation"
|
|
128
|
+
aria-hidden="true"
|
|
129
|
+
className={s.separator({ class: [slots.separator, className] })}
|
|
130
|
+
{...props}
|
|
131
|
+
>
|
|
132
|
+
{children ?? <ChevronRightIcon />}
|
|
133
|
+
</span>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export {
|
|
2
|
+
Breadcrumb,
|
|
3
|
+
BreadcrumbItem,
|
|
4
|
+
type BreadcrumbItemData,
|
|
5
|
+
BreadcrumbLink,
|
|
6
|
+
type BreadcrumbLinkProps,
|
|
7
|
+
BreadcrumbList,
|
|
8
|
+
BreadcrumbPage,
|
|
9
|
+
type BreadcrumbProps,
|
|
10
|
+
BreadcrumbSeparator,
|
|
11
|
+
} from './breadcrumb'
|
|
12
|
+
export { breadcrumbStyles } from './breadcrumb.styles'
|
|
@@ -48,14 +48,17 @@ export function Button(inProps: ButtonProps) {
|
|
|
48
48
|
{...(!asChild && { type: type ?? 'button', disabled: disabled || loading })}
|
|
49
49
|
{...rest}
|
|
50
50
|
>
|
|
51
|
-
{iconOnly
|
|
52
|
-
?
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
{iconOnly && asChild
|
|
52
|
+
? // The child element (e.g. a link) holds its own icon; Slot merges button props onto it.
|
|
53
|
+
children
|
|
54
|
+
: iconOnly
|
|
55
|
+
? icon(loading ? <Spinner size={size === 'lg' ? 'md' : 'sm'} /> : children)
|
|
56
|
+
: // Children stay direct (no Fragment) so Slot can find the Slottable when asChild is set.
|
|
57
|
+
[
|
|
58
|
+
<Fragment key="start">{icon(loading ? <Spinner size="sm" /> : leftIcon)}</Fragment>,
|
|
59
|
+
<Slot.Slottable key="content">{children}</Slot.Slottable>,
|
|
60
|
+
<Fragment key="end">{icon(rightIcon)}</Fragment>,
|
|
61
|
+
]}
|
|
59
62
|
</Comp>
|
|
60
63
|
)
|
|
61
64
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
2
|
+
import { focusRing, transition } from '../../utils/styles'
|
|
3
|
+
import { tv } from '../../utils/tv'
|
|
4
|
+
|
|
5
|
+
export const calendarStyles = tv({
|
|
6
|
+
slots: {
|
|
7
|
+
root: 'relative w-fit p-3 text-small text-foreground',
|
|
8
|
+
months: 'relative flex flex-col gap-6 sm:flex-row',
|
|
9
|
+
month: 'grid content-start gap-3',
|
|
10
|
+
monthCaption: 'flex h-8 items-center justify-center px-9',
|
|
11
|
+
captionLabel: 'font-medium text-foreground',
|
|
12
|
+
nav: 'pointer-events-none absolute inset-x-3 top-3 z-10 flex items-center justify-between',
|
|
13
|
+
navButton: [
|
|
14
|
+
'pointer-events-auto inline-flex size-8 cursor-pointer items-center justify-center rounded-md text-muted-foreground',
|
|
15
|
+
'hover:bg-muted hover:text-foreground disabled:pointer-events-none disabled:opacity-40',
|
|
16
|
+
transition,
|
|
17
|
+
focusRing,
|
|
18
|
+
],
|
|
19
|
+
chevron: 'size-4',
|
|
20
|
+
monthGrid: 'border-collapse',
|
|
21
|
+
weekdays: '',
|
|
22
|
+
weekday: 'size-9 p-0 text-caption font-normal text-faint-foreground',
|
|
23
|
+
weeks: '',
|
|
24
|
+
week: '',
|
|
25
|
+
day: 'size-9 p-0 text-center align-middle',
|
|
26
|
+
dayButton: [
|
|
27
|
+
'inline-flex size-9 cursor-pointer items-center justify-center rounded-md text-small tabular-nums text-foreground',
|
|
28
|
+
'hover:bg-muted',
|
|
29
|
+
transition,
|
|
30
|
+
focusRing,
|
|
31
|
+
],
|
|
32
|
+
today: '[&>button]:font-semibold [&>button]:text-primary',
|
|
33
|
+
selected:
|
|
34
|
+
'[&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:hover:bg-primary/90',
|
|
35
|
+
outside: '[&>button]:text-faint-foreground [&>button]:opacity-60',
|
|
36
|
+
disabled: '[&>button]:pointer-events-none [&>button]:opacity-35 [&>button]:line-through',
|
|
37
|
+
rangeStart: 'rounded-l-md bg-primary/14',
|
|
38
|
+
rangeMiddle:
|
|
39
|
+
'bg-primary/14 [&>button]:rounded-none [&>button]:bg-transparent! [&>button]:text-foreground! [&>button]:hover:bg-primary/10!',
|
|
40
|
+
rangeEnd: 'rounded-r-md bg-primary/14',
|
|
41
|
+
hidden: 'invisible',
|
|
42
|
+
footer: 'pt-2 text-small text-muted-foreground',
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
declare module '@glyphui/tokens' {
|
|
47
|
+
interface GlyphComponentRegistry {
|
|
48
|
+
Calendar: StylesEntry<typeof calendarStyles>
|
|
49
|
+
}
|
|
50
|
+
}
|