@glyphui/ui 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/data-table.js +2 -2
- package/dist/{field-DXZD0l8w.js → field-CCecIzdq.js} +1 -1
- package/dist/{field-context-B8cYheXz.js → field-context-B6WOcK5m.js} +99 -99
- package/dist/form.js +2 -2
- package/dist/index.d.ts +2743 -93
- package/dist/index.js +2748 -111
- package/dist/{states-BxPjCUxz.js → states-Rp8wZ_jH.js} +1 -1
- package/package.json +4 -2
- package/src/components/accordion/accordion.styles.ts +41 -0
- package/src/components/accordion/accordion.tsx +86 -0
- package/src/components/accordion/index.ts +8 -0
- package/src/components/alert/alert-icons.tsx +66 -0
- package/src/components/alert/alert.styles.ts +57 -0
- package/src/components/alert/alert.tsx +86 -0
- package/src/components/alert/index.ts +2 -0
- package/src/components/alert-dialog/alert-dialog.styles.ts +24 -0
- package/src/components/alert-dialog/alert-dialog.tsx +118 -0
- package/src/components/alert-dialog/confirm.tsx +111 -0
- package/src/components/alert-dialog/index.ts +15 -0
- package/src/components/aspect-ratio/aspect-ratio.tsx +40 -0
- package/src/components/aspect-ratio/index.ts +1 -0
- package/src/components/badge/badge.styles.ts +1 -1
- package/src/components/banner/banner.styles.ts +40 -0
- package/src/components/banner/banner.tsx +101 -0
- package/src/components/banner/index.ts +2 -0
- package/src/components/blockquote/blockquote.tsx +59 -0
- package/src/components/blockquote/index.ts +1 -0
- package/src/components/breadcrumb/breadcrumb.styles.ts +20 -0
- package/src/components/breadcrumb/breadcrumb.tsx +135 -0
- package/src/components/breadcrumb/index.ts +12 -0
- package/src/components/button/button.tsx +11 -8
- package/src/components/code/code.styles.ts +33 -0
- package/src/components/code/code.tsx +96 -0
- package/src/components/code/index.ts +2 -0
- package/src/components/combobox/combobox.styles.ts +50 -0
- package/src/components/combobox/combobox.tsx +203 -0
- package/src/components/combobox/index.ts +2 -0
- package/src/components/command-menu/command-menu.styles.ts +41 -0
- package/src/components/command-menu/command-menu.tsx +217 -0
- package/src/components/command-menu/index.ts +14 -0
- package/src/components/description-list/description-list.styles.ts +31 -0
- package/src/components/description-list/description-list.tsx +56 -0
- package/src/components/description-list/index.ts +7 -0
- package/src/components/dialog/close-icon.tsx +22 -0
- package/src/components/dialog/dialog.styles.ts +41 -0
- package/src/components/dialog/dialog.tsx +111 -0
- package/src/components/dialog/index.ts +13 -0
- package/src/components/dropdown-menu/dropdown-menu.styles.ts +45 -0
- package/src/components/dropdown-menu/dropdown-menu.tsx +230 -0
- package/src/components/dropdown-menu/index.ts +19 -0
- package/src/components/grid/grid.tsx +144 -0
- package/src/components/grid/index.ts +1 -0
- package/src/components/link/index.ts +2 -0
- package/src/components/link/link.styles.ts +39 -0
- package/src/components/link/link.tsx +62 -0
- package/src/components/pagination/index.ts +3 -0
- package/src/components/pagination/pagination-range.ts +35 -0
- package/src/components/pagination/pagination.styles.ts +41 -0
- package/src/components/pagination/pagination.tsx +130 -0
- package/src/components/popover/index.ts +9 -0
- package/src/components/popover/popover.tsx +66 -0
- package/src/components/progress/index.ts +7 -0
- package/src/components/progress/progress.styles.ts +68 -0
- package/src/components/progress/progress.tsx +158 -0
- package/src/components/prose/index.ts +1 -0
- package/src/components/prose/prose.tsx +82 -0
- package/src/components/radio-group/index.ts +7 -0
- package/src/components/radio-group/radio-group.styles.ts +64 -0
- package/src/components/radio-group/radio-group.tsx +128 -0
- package/src/components/scroll-area/index.ts +1 -0
- package/src/components/scroll-area/scroll-area.tsx +68 -0
- package/src/components/sheet/index.ts +13 -0
- package/src/components/sheet/sheet.styles.ts +60 -0
- package/src/components/sheet/sheet.tsx +108 -0
- package/src/components/slider/index.ts +2 -0
- package/src/components/slider/slider.styles.ts +32 -0
- package/src/components/slider/slider.tsx +131 -0
- package/src/components/stat-card/index.ts +2 -0
- package/src/components/stat-card/stat-card.styles.ts +24 -0
- package/src/components/stat-card/stat-card.tsx +108 -0
- package/src/components/toast/index.ts +1 -0
- package/src/components/toast/toast.tsx +91 -0
- package/src/index.ts +25 -0
- package/src/provider/glyph-provider.tsx +16 -2
|
@@ -0,0 +1,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,33 @@
|
|
|
1
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
2
|
+
import { themedBorder } from '../../utils/styles'
|
|
3
|
+
import { tv, type VariantProps } from '../../utils/tv'
|
|
4
|
+
|
|
5
|
+
export const codeStyles = tv({
|
|
6
|
+
slots: {
|
|
7
|
+
base: 'rounded-xs bg-muted px-[0.3em] py-[0.15em] font-mono text-[0.875em] text-foreground',
|
|
8
|
+
},
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export const codeBlockStyles = tv({
|
|
12
|
+
slots: {
|
|
13
|
+
root: ['relative min-w-0 overflow-hidden rounded-lg border-border bg-subtle', themedBorder],
|
|
14
|
+
header:
|
|
15
|
+
'flex min-h-10 items-center justify-between gap-2 border-b border-border py-1 pr-1.5 pl-4',
|
|
16
|
+
title: 'truncate font-mono text-caption text-faint-foreground',
|
|
17
|
+
copy: 'shrink-0',
|
|
18
|
+
floatingCopy: 'absolute top-2 right-2',
|
|
19
|
+
pre: 'overflow-x-auto p-4 font-mono text-small leading-relaxed text-foreground',
|
|
20
|
+
},
|
|
21
|
+
variants: {
|
|
22
|
+
wrap: { true: { pre: 'whitespace-pre-wrap break-words' } },
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export type CodeBlockVariants = VariantProps<typeof codeBlockStyles>
|
|
27
|
+
|
|
28
|
+
declare module '@glyphui/tokens' {
|
|
29
|
+
interface GlyphComponentRegistry {
|
|
30
|
+
Code: StylesEntry<typeof codeStyles>
|
|
31
|
+
CodeBlock: StylesEntry<typeof codeBlockStyles>
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactNode, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
3
|
+
import { CheckIcon } from '../../internal/icons'
|
|
4
|
+
import { IconButton } from '../button'
|
|
5
|
+
import { type CodeBlockVariants, codeBlockStyles, codeStyles } from './code.styles'
|
|
6
|
+
|
|
7
|
+
/** Inline code within running text. */
|
|
8
|
+
export function Code(inProps: ComponentProps<'code'>) {
|
|
9
|
+
const [{ className, ...rest }, slots] = useComponentConfig('Code', inProps)
|
|
10
|
+
return (
|
|
11
|
+
<code
|
|
12
|
+
data-slot="code"
|
|
13
|
+
className={codeStyles().base({ class: [slots.base, className] })}
|
|
14
|
+
{...rest}
|
|
15
|
+
/>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const CopyIcon = () => (
|
|
20
|
+
<svg
|
|
21
|
+
viewBox="0 0 24 24"
|
|
22
|
+
fill="none"
|
|
23
|
+
stroke="currentColor"
|
|
24
|
+
strokeWidth="2"
|
|
25
|
+
strokeLinecap="round"
|
|
26
|
+
strokeLinejoin="round"
|
|
27
|
+
aria-hidden="true"
|
|
28
|
+
>
|
|
29
|
+
<rect width="14" height="14" x="8" y="8" rx="2" />
|
|
30
|
+
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
|
31
|
+
</svg>
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
export interface CodeBlockProps
|
|
35
|
+
extends Omit<ComponentProps<'figure'>, 'children' | 'title'>,
|
|
36
|
+
CodeBlockVariants {
|
|
37
|
+
/** The code to show and copy. */
|
|
38
|
+
code: string
|
|
39
|
+
/** File name or caption shown in the header. */
|
|
40
|
+
title?: ReactNode
|
|
41
|
+
/** Shown in the header when there is no title, e.g. "tsx". */
|
|
42
|
+
language?: string
|
|
43
|
+
/** Show a copy button. Default true. */
|
|
44
|
+
copyable?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A block of code with an optional file name and a copy button. No syntax highlighting is applied. */
|
|
48
|
+
export function CodeBlock(inProps: CodeBlockProps) {
|
|
49
|
+
const [props, slots] = useComponentConfig('CodeBlock', inProps)
|
|
50
|
+
const { code, title, language, copyable = true, wrap, className, ...rest } = props
|
|
51
|
+
const [copied, setCopied] = useState(false)
|
|
52
|
+
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
53
|
+
const s = codeBlockStyles({ wrap })
|
|
54
|
+
const heading = title ?? language
|
|
55
|
+
|
|
56
|
+
useEffect(() => () => clearTimeout(timer.current), [])
|
|
57
|
+
|
|
58
|
+
const copy = async () => {
|
|
59
|
+
try {
|
|
60
|
+
await navigator.clipboard.writeText(code)
|
|
61
|
+
setCopied(true)
|
|
62
|
+
clearTimeout(timer.current)
|
|
63
|
+
timer.current = setTimeout(() => setCopied(false), 1600)
|
|
64
|
+
} catch {
|
|
65
|
+
// Clipboard blocked (permissions, insecure context): the code stays selectable.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const copyButton = copyable ? (
|
|
70
|
+
<IconButton
|
|
71
|
+
size="sm"
|
|
72
|
+
icon={copied ? <CheckIcon /> : <CopyIcon />}
|
|
73
|
+
aria-label={copied ? 'Copied' : 'Copy code'}
|
|
74
|
+
onClick={copy}
|
|
75
|
+
className={
|
|
76
|
+
heading ? s.copy({ class: slots.copy }) : s.floatingCopy({ class: slots.floatingCopy })
|
|
77
|
+
}
|
|
78
|
+
/>
|
|
79
|
+
) : null
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<figure data-slot="code-block" className={s.root({ class: [slots.root, className] })} {...rest}>
|
|
83
|
+
{heading ? (
|
|
84
|
+
<figcaption className={s.header({ class: slots.header })}>
|
|
85
|
+
<span className={s.title({ class: slots.title })}>{heading}</span>
|
|
86
|
+
{copyButton}
|
|
87
|
+
</figcaption>
|
|
88
|
+
) : (
|
|
89
|
+
copyButton
|
|
90
|
+
)}
|
|
91
|
+
<pre className={s.pre({ class: slots.pre })}>
|
|
92
|
+
<code>{code}</code>
|
|
93
|
+
</pre>
|
|
94
|
+
</figure>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
2
|
+
import { popover } from '../../utils/styles'
|
|
3
|
+
import { tv, type VariantProps } from '../../utils/tv'
|
|
4
|
+
import { fieldControl } from '../input/input.styles'
|
|
5
|
+
|
|
6
|
+
export const comboboxStyles = tv({
|
|
7
|
+
slots: {
|
|
8
|
+
trigger: [
|
|
9
|
+
...fieldControl,
|
|
10
|
+
'flex cursor-pointer items-center justify-between gap-2 px-3 text-left outline-none',
|
|
11
|
+
'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/20',
|
|
12
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
13
|
+
],
|
|
14
|
+
value: 'flex min-w-0 flex-1 items-center gap-1 truncate',
|
|
15
|
+
placeholder: 'truncate text-faint-foreground',
|
|
16
|
+
chip: 'inline-flex h-5.5 max-w-40 shrink-0 items-center truncate rounded-pill bg-muted px-2 text-caption text-foreground',
|
|
17
|
+
icon: 'size-4 shrink-0 text-faint-foreground',
|
|
18
|
+
content: [popover, 'w-(--radix-popover-trigger-width) min-w-56 p-0'],
|
|
19
|
+
search:
|
|
20
|
+
'flex items-center gap-2 border-b border-border px-3 text-faint-foreground [&>svg]:size-4 [&>svg]:shrink-0',
|
|
21
|
+
input:
|
|
22
|
+
'h-10 w-full min-w-0 bg-transparent text-small text-foreground outline-none placeholder:text-faint-foreground',
|
|
23
|
+
list: 'max-h-72 overflow-y-auto overscroll-contain p-1',
|
|
24
|
+
empty: 'px-3 py-6 text-center text-small text-muted-foreground',
|
|
25
|
+
item: [
|
|
26
|
+
'relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-small text-foreground outline-none',
|
|
27
|
+
'data-[selected=true]:bg-muted data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50',
|
|
28
|
+
],
|
|
29
|
+
itemText: 'grid min-w-0 flex-1',
|
|
30
|
+
itemDescription: 'truncate text-caption text-faint-foreground',
|
|
31
|
+
check: 'size-4 shrink-0 text-primary',
|
|
32
|
+
},
|
|
33
|
+
variants: {
|
|
34
|
+
size: {
|
|
35
|
+
sm: { trigger: 'min-h-(--glyph-control-sm) text-small' },
|
|
36
|
+
md: { trigger: 'min-h-(--glyph-control-md) text-small' },
|
|
37
|
+
lg: { trigger: 'min-h-(--glyph-control-lg) text-body' },
|
|
38
|
+
},
|
|
39
|
+
multiple: { true: { trigger: 'py-1', value: 'flex-wrap overflow-visible whitespace-normal' } },
|
|
40
|
+
},
|
|
41
|
+
defaultVariants: { size: 'md' },
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
export type ComboboxVariants = VariantProps<typeof comboboxStyles>
|
|
45
|
+
|
|
46
|
+
declare module '@glyphui/tokens' {
|
|
47
|
+
interface GlyphComponentRegistry {
|
|
48
|
+
Combobox: StylesEntry<typeof comboboxStyles>
|
|
49
|
+
}
|
|
50
|
+
}
|