@glyphui/ui 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +1 -1
  2. package/dist/chart.d.ts +202 -0
  3. package/dist/chart.js +457 -0
  4. package/dist/data-table.js +3 -2
  5. package/dist/{field-DXZD0l8w.js → field-CWZ_FdkD.js} +2 -1
  6. package/dist/{field-context-B8cYheXz.js → field-context-anGPYXbW.js} +89 -143
  7. package/dist/form.js +3 -2
  8. package/dist/index.d.ts +4960 -266
  9. package/dist/index.js +5336 -146
  10. package/dist/{states-BxPjCUxz.js → states-DR-OoDT-.js} +2 -1
  11. package/dist/tv-DBAGlgXJ.js +58 -0
  12. package/package.json +19 -2
  13. package/src/components/accordion/accordion.styles.ts +41 -0
  14. package/src/components/accordion/accordion.tsx +86 -0
  15. package/src/components/accordion/index.ts +8 -0
  16. package/src/components/alert/alert-icons.tsx +66 -0
  17. package/src/components/alert/alert.styles.ts +57 -0
  18. package/src/components/alert/alert.tsx +86 -0
  19. package/src/components/alert/index.ts +2 -0
  20. package/src/components/alert-dialog/alert-dialog.styles.ts +24 -0
  21. package/src/components/alert-dialog/alert-dialog.tsx +118 -0
  22. package/src/components/alert-dialog/confirm.tsx +111 -0
  23. package/src/components/alert-dialog/index.ts +15 -0
  24. package/src/components/aspect-ratio/aspect-ratio.tsx +40 -0
  25. package/src/components/aspect-ratio/index.ts +1 -0
  26. package/src/components/badge/badge.styles.ts +1 -1
  27. package/src/components/banner/banner.styles.ts +40 -0
  28. package/src/components/banner/banner.tsx +101 -0
  29. package/src/components/banner/index.ts +2 -0
  30. package/src/components/blockquote/blockquote.tsx +59 -0
  31. package/src/components/blockquote/index.ts +1 -0
  32. package/src/components/breadcrumb/breadcrumb.styles.ts +20 -0
  33. package/src/components/breadcrumb/breadcrumb.tsx +135 -0
  34. package/src/components/breadcrumb/index.ts +12 -0
  35. package/src/components/button/button.tsx +11 -8
  36. package/src/components/calendar/calendar.styles.ts +50 -0
  37. package/src/components/calendar/calendar.tsx +84 -0
  38. package/src/components/calendar/index.ts +3 -0
  39. package/src/components/carousel/carousel.styles.ts +43 -0
  40. package/src/components/carousel/carousel.tsx +278 -0
  41. package/src/components/carousel/index.ts +13 -0
  42. package/src/components/code/code.styles.ts +33 -0
  43. package/src/components/code/code.tsx +96 -0
  44. package/src/components/code/index.ts +2 -0
  45. package/src/components/collapsible/collapsible.tsx +66 -0
  46. package/src/components/collapsible/index.ts +6 -0
  47. package/src/components/combobox/combobox.styles.ts +50 -0
  48. package/src/components/combobox/combobox.tsx +203 -0
  49. package/src/components/combobox/index.ts +2 -0
  50. package/src/components/command-menu/command-menu.styles.ts +41 -0
  51. package/src/components/command-menu/command-menu.tsx +217 -0
  52. package/src/components/command-menu/index.ts +14 -0
  53. package/src/components/context-menu/context-menu.styles.ts +19 -0
  54. package/src/components/context-menu/context-menu.tsx +226 -0
  55. package/src/components/context-menu/index.ts +19 -0
  56. package/src/components/copy-button/copy-button.tsx +117 -0
  57. package/src/components/copy-button/index.ts +1 -0
  58. package/src/components/date-picker/date-picker.styles.ts +42 -0
  59. package/src/components/date-picker/date-picker.tsx +322 -0
  60. package/src/components/date-picker/index.ts +8 -0
  61. package/src/components/description-list/description-list.styles.ts +31 -0
  62. package/src/components/description-list/description-list.tsx +56 -0
  63. package/src/components/description-list/index.ts +7 -0
  64. package/src/components/dialog/close-icon.tsx +22 -0
  65. package/src/components/dialog/dialog.styles.ts +41 -0
  66. package/src/components/dialog/dialog.tsx +111 -0
  67. package/src/components/dialog/index.ts +13 -0
  68. package/src/components/dropdown-menu/dropdown-menu.styles.ts +45 -0
  69. package/src/components/dropdown-menu/dropdown-menu.tsx +230 -0
  70. package/src/components/dropdown-menu/index.ts +19 -0
  71. package/src/components/file-upload/file-upload.styles.ts +43 -0
  72. package/src/components/file-upload/file-upload.tsx +343 -0
  73. package/src/components/file-upload/index.ts +10 -0
  74. package/src/components/grid/grid.tsx +144 -0
  75. package/src/components/grid/index.ts +1 -0
  76. package/src/components/hover-card/hover-card.tsx +41 -0
  77. package/src/components/hover-card/index.ts +1 -0
  78. package/src/components/input-otp/index.ts +10 -0
  79. package/src/components/input-otp/input-otp.styles.ts +38 -0
  80. package/src/components/input-otp/input-otp.tsx +116 -0
  81. package/src/components/link/index.ts +2 -0
  82. package/src/components/link/link.styles.ts +39 -0
  83. package/src/components/link/link.tsx +62 -0
  84. package/src/components/modal/index.ts +2 -0
  85. package/src/components/modal/modal.styles.ts +18 -0
  86. package/src/components/modal/modal.tsx +86 -0
  87. package/src/components/navigation-menu/index.ts +14 -0
  88. package/src/components/navigation-menu/navigation-menu.styles.ts +48 -0
  89. package/src/components/navigation-menu/navigation-menu.tsx +198 -0
  90. package/src/components/number-input/index.ts +2 -0
  91. package/src/components/number-input/number-input.styles.ts +41 -0
  92. package/src/components/number-input/number-input.tsx +211 -0
  93. package/src/components/pagination/index.ts +3 -0
  94. package/src/components/pagination/pagination-range.ts +35 -0
  95. package/src/components/pagination/pagination.styles.ts +41 -0
  96. package/src/components/pagination/pagination.tsx +130 -0
  97. package/src/components/password-input/index.ts +7 -0
  98. package/src/components/password-input/password-input.styles.ts +30 -0
  99. package/src/components/password-input/password-input.tsx +127 -0
  100. package/src/components/popover/index.ts +9 -0
  101. package/src/components/popover/popover.tsx +66 -0
  102. package/src/components/progress/index.ts +7 -0
  103. package/src/components/progress/progress.styles.ts +68 -0
  104. package/src/components/progress/progress.tsx +158 -0
  105. package/src/components/prose/index.ts +1 -0
  106. package/src/components/prose/prose.tsx +82 -0
  107. package/src/components/radio-group/index.ts +7 -0
  108. package/src/components/radio-group/radio-group.styles.ts +64 -0
  109. package/src/components/radio-group/radio-group.tsx +128 -0
  110. package/src/components/resizable/index.ts +9 -0
  111. package/src/components/resizable/resizable.styles.ts +28 -0
  112. package/src/components/resizable/resizable.tsx +69 -0
  113. package/src/components/scroll-area/index.ts +1 -0
  114. package/src/components/scroll-area/scroll-area.tsx +68 -0
  115. package/src/components/sheet/index.ts +13 -0
  116. package/src/components/sheet/sheet.styles.ts +60 -0
  117. package/src/components/sheet/sheet.tsx +108 -0
  118. package/src/components/slider/index.ts +2 -0
  119. package/src/components/slider/slider.styles.ts +32 -0
  120. package/src/components/slider/slider.tsx +131 -0
  121. package/src/components/stat-card/index.ts +2 -0
  122. package/src/components/stat-card/stat-card.styles.ts +24 -0
  123. package/src/components/stat-card/stat-card.tsx +108 -0
  124. package/src/components/stepper/index.ts +2 -0
  125. package/src/components/stepper/stepper.styles.ts +74 -0
  126. package/src/components/stepper/stepper.tsx +138 -0
  127. package/src/components/tag-input/index.ts +2 -0
  128. package/src/components/tag-input/tag-input.styles.ts +39 -0
  129. package/src/components/tag-input/tag-input.tsx +198 -0
  130. package/src/components/timeline/index.ts +8 -0
  131. package/src/components/timeline/timeline.styles.ts +56 -0
  132. package/src/components/timeline/timeline.tsx +91 -0
  133. package/src/components/toast/index.ts +1 -0
  134. package/src/components/toast/toast.tsx +91 -0
  135. package/src/components/toggle/index.ts +9 -0
  136. package/src/components/toggle/toggle.styles.ts +39 -0
  137. package/src/components/toggle/toggle.tsx +61 -0
  138. package/src/components/tree-view/index.ts +2 -0
  139. package/src/components/tree-view/tree-view.styles.ts +42 -0
  140. package/src/components/tree-view/tree-view.tsx +274 -0
  141. package/src/components/visually-hidden/index.ts +1 -0
  142. package/src/components/visually-hidden/visually-hidden.tsx +10 -0
  143. package/src/index.ts +45 -0
  144. package/src/patterns/chart/chart-parts.tsx +171 -0
  145. package/src/patterns/chart/chart.styles.ts +35 -0
  146. package/src/patterns/chart/chart.tsx +369 -0
  147. package/src/patterns/chart/index.ts +13 -0
  148. package/src/provider/glyph-provider.tsx +16 -2
  149. package/styles.css +16 -0
@@ -0,0 +1,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,2 @@
1
+ export { Banner, type BannerProps } from './banner'
2
+ export { type BannerVariants, bannerStyles } from './banner.styles'
@@ -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
- ? icon(loading ? <Spinner size={size === 'lg' ? 'md' : 'sm'} /> : children)
53
- : // Children stay direct (no Fragment) so Slot can find the Slottable when asChild is set.
54
- [
55
- <Fragment key="start">{icon(loading ? <Spinner size="sm" /> : leftIcon)}</Fragment>,
56
- <Slot.Slottable key="content">{children}</Slot.Slottable>,
57
- <Fragment key="end">{icon(rightIcon)}</Fragment>,
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
+ }