@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.
Files changed (85) hide show
  1. package/README.md +1 -1
  2. package/dist/data-table.js +2 -2
  3. package/dist/{field-DXZD0l8w.js → field-CCecIzdq.js} +1 -1
  4. package/dist/{field-context-B8cYheXz.js → field-context-B6WOcK5m.js} +99 -99
  5. package/dist/form.js +2 -2
  6. package/dist/index.d.ts +2743 -93
  7. package/dist/index.js +2748 -111
  8. package/dist/{states-BxPjCUxz.js → states-Rp8wZ_jH.js} +1 -1
  9. package/package.json +4 -2
  10. package/src/components/accordion/accordion.styles.ts +41 -0
  11. package/src/components/accordion/accordion.tsx +86 -0
  12. package/src/components/accordion/index.ts +8 -0
  13. package/src/components/alert/alert-icons.tsx +66 -0
  14. package/src/components/alert/alert.styles.ts +57 -0
  15. package/src/components/alert/alert.tsx +86 -0
  16. package/src/components/alert/index.ts +2 -0
  17. package/src/components/alert-dialog/alert-dialog.styles.ts +24 -0
  18. package/src/components/alert-dialog/alert-dialog.tsx +118 -0
  19. package/src/components/alert-dialog/confirm.tsx +111 -0
  20. package/src/components/alert-dialog/index.ts +15 -0
  21. package/src/components/aspect-ratio/aspect-ratio.tsx +40 -0
  22. package/src/components/aspect-ratio/index.ts +1 -0
  23. package/src/components/badge/badge.styles.ts +1 -1
  24. package/src/components/banner/banner.styles.ts +40 -0
  25. package/src/components/banner/banner.tsx +101 -0
  26. package/src/components/banner/index.ts +2 -0
  27. package/src/components/blockquote/blockquote.tsx +59 -0
  28. package/src/components/blockquote/index.ts +1 -0
  29. package/src/components/breadcrumb/breadcrumb.styles.ts +20 -0
  30. package/src/components/breadcrumb/breadcrumb.tsx +135 -0
  31. package/src/components/breadcrumb/index.ts +12 -0
  32. package/src/components/button/button.tsx +11 -8
  33. package/src/components/code/code.styles.ts +33 -0
  34. package/src/components/code/code.tsx +96 -0
  35. package/src/components/code/index.ts +2 -0
  36. package/src/components/combobox/combobox.styles.ts +50 -0
  37. package/src/components/combobox/combobox.tsx +203 -0
  38. package/src/components/combobox/index.ts +2 -0
  39. package/src/components/command-menu/command-menu.styles.ts +41 -0
  40. package/src/components/command-menu/command-menu.tsx +217 -0
  41. package/src/components/command-menu/index.ts +14 -0
  42. package/src/components/description-list/description-list.styles.ts +31 -0
  43. package/src/components/description-list/description-list.tsx +56 -0
  44. package/src/components/description-list/index.ts +7 -0
  45. package/src/components/dialog/close-icon.tsx +22 -0
  46. package/src/components/dialog/dialog.styles.ts +41 -0
  47. package/src/components/dialog/dialog.tsx +111 -0
  48. package/src/components/dialog/index.ts +13 -0
  49. package/src/components/dropdown-menu/dropdown-menu.styles.ts +45 -0
  50. package/src/components/dropdown-menu/dropdown-menu.tsx +230 -0
  51. package/src/components/dropdown-menu/index.ts +19 -0
  52. package/src/components/grid/grid.tsx +144 -0
  53. package/src/components/grid/index.ts +1 -0
  54. package/src/components/link/index.ts +2 -0
  55. package/src/components/link/link.styles.ts +39 -0
  56. package/src/components/link/link.tsx +62 -0
  57. package/src/components/pagination/index.ts +3 -0
  58. package/src/components/pagination/pagination-range.ts +35 -0
  59. package/src/components/pagination/pagination.styles.ts +41 -0
  60. package/src/components/pagination/pagination.tsx +130 -0
  61. package/src/components/popover/index.ts +9 -0
  62. package/src/components/popover/popover.tsx +66 -0
  63. package/src/components/progress/index.ts +7 -0
  64. package/src/components/progress/progress.styles.ts +68 -0
  65. package/src/components/progress/progress.tsx +158 -0
  66. package/src/components/prose/index.ts +1 -0
  67. package/src/components/prose/prose.tsx +82 -0
  68. package/src/components/radio-group/index.ts +7 -0
  69. package/src/components/radio-group/radio-group.styles.ts +64 -0
  70. package/src/components/radio-group/radio-group.tsx +128 -0
  71. package/src/components/scroll-area/index.ts +1 -0
  72. package/src/components/scroll-area/scroll-area.tsx +68 -0
  73. package/src/components/sheet/index.ts +13 -0
  74. package/src/components/sheet/sheet.styles.ts +60 -0
  75. package/src/components/sheet/sheet.tsx +108 -0
  76. package/src/components/slider/index.ts +2 -0
  77. package/src/components/slider/slider.styles.ts +32 -0
  78. package/src/components/slider/slider.tsx +131 -0
  79. package/src/components/stat-card/index.ts +2 -0
  80. package/src/components/stat-card/stat-card.styles.ts +24 -0
  81. package/src/components/stat-card/stat-card.tsx +108 -0
  82. package/src/components/toast/index.ts +1 -0
  83. package/src/components/toast/toast.tsx +91 -0
  84. package/src/index.ts +25 -0
  85. package/src/provider/glyph-provider.tsx +16 -2
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { E as tv, O as useComponentConfig, S as focusRing, T as transition, _ as Button, b as Spinner, d as InboxIcon, n as useFieldControl, o as CheckIcon, p as MinusIcon, r as AlertIcon, w as themedBorder } from "./field-context-B8cYheXz.js";
2
+ import { O as useComponentConfig, S as InboxIcon, _ as CheckIcon, c as tv, f as themedBorder, m as AlertIcon, n as useFieldControl, o as Spinner, p as transition, r as Button, u as focusRing, w as MinusIcon } from "./field-context-B6WOcK5m.js";
3
3
  import { Checkbox } from "radix-ui";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/components/checkbox/checkbox.tsx
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glyphui/ui",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "React components on Tailwind CSS v4 that take their entire look from one theme file. Forms, data tables, app layouts and async states included.",
5
5
  "license": "MIT",
6
6
  "author": "Mohak Gupta (https://github.com/khalmohak)",
@@ -58,10 +58,12 @@
58
58
  "!src/test"
59
59
  ],
60
60
  "dependencies": {
61
- "@glyphui/tokens": "^0.1.1",
61
+ "@glyphui/tokens": "^0.2.0",
62
62
  "@standard-schema/spec": "^1.1.0",
63
63
  "clsx": "^2.1.1",
64
+ "cmdk": "^1.1.1",
64
65
  "radix-ui": "^1.6.7",
66
+ "sonner": "^2.0.8",
65
67
  "tailwind-merge": "^3.7.0",
66
68
  "tailwind-variants": "^3.3.1"
67
69
  },
@@ -0,0 +1,41 @@
1
+ import type { StylesEntry } from '../../utils/registry'
2
+ import { focusRing, themedBorder, transition } from '../../utils/styles'
3
+ import { tv, type VariantProps } from '../../utils/tv'
4
+
5
+ export const accordionStyles = tv({
6
+ slots: {
7
+ root: 'w-full',
8
+ item: '',
9
+ header: 'flex',
10
+ trigger: [
11
+ 'group flex flex-1 cursor-pointer items-center justify-between gap-4 rounded-xs py-4 text-left text-body font-medium text-foreground',
12
+ transition,
13
+ focusRing,
14
+ 'hover:text-foreground/80 disabled:cursor-not-allowed disabled:opacity-50',
15
+ ],
16
+ chevron:
17
+ 'size-4 shrink-0 text-faint-foreground transition-transform duration-(--glyph-duration-base) ease-standard group-data-[state=open]:rotate-180',
18
+ content: 'overflow-hidden text-small text-muted-foreground',
19
+ // Opening fades and slides in (via @starting-style); closing is instant.
20
+ contentInner:
21
+ 'pb-4 transition-[opacity,translate] duration-(--glyph-duration-base) ease-standard starting:-translate-y-1 starting:opacity-0',
22
+ },
23
+ variants: {
24
+ variant: {
25
+ default: { item: 'border-b border-border last:border-b-0' },
26
+ separated: {
27
+ root: 'grid gap-2',
28
+ item: ['rounded-lg border-border bg-surface px-4 shadow-xs', themedBorder],
29
+ },
30
+ },
31
+ },
32
+ defaultVariants: { variant: 'default' },
33
+ })
34
+
35
+ export type AccordionVariants = VariantProps<typeof accordionStyles>
36
+
37
+ declare module '@glyphui/tokens' {
38
+ interface GlyphComponentRegistry {
39
+ Accordion: StylesEntry<typeof accordionStyles>
40
+ }
41
+ }
@@ -0,0 +1,86 @@
1
+ import { Accordion as AccordionPrimitive } from 'radix-ui'
2
+ import { type ComponentProps, createContext, useContext } from 'react'
3
+ import { type SlotClasses, useComponentConfig } from '../../hooks/use-component-config'
4
+ import { ChevronDownIcon } from '../../internal/icons'
5
+ import { type AccordionVariants, accordionStyles } from './accordion.styles'
6
+
7
+ const AccordionContext = createContext<{
8
+ s: ReturnType<typeof accordionStyles>
9
+ slots: SlotClasses<'Accordion'>
10
+ } | null>(null)
11
+
12
+ function useParts() {
13
+ return useContext(AccordionContext) ?? { s: accordionStyles(), slots: {} }
14
+ }
15
+
16
+ export type AccordionProps = ComponentProps<typeof AccordionPrimitive.Root> & AccordionVariants
17
+
18
+ /**
19
+ * Stacked sections that expand one at a time (`type="single"`, collapsible) or
20
+ * independently (`type="multiple"`). Good for FAQs and dense settings.
21
+ */
22
+ export function Accordion(inProps: AccordionProps) {
23
+ const [props, slots] = useComponentConfig('Accordion', inProps)
24
+ const { variant, className, ...rest } = props
25
+ const s = accordionStyles({ variant })
26
+ return (
27
+ <AccordionContext.Provider value={{ s, slots }}>
28
+ <AccordionPrimitive.Root
29
+ data-slot="accordion"
30
+ className={s.root({ class: [slots.root, className] })}
31
+ {...(rest as ComponentProps<typeof AccordionPrimitive.Root>)}
32
+ />
33
+ </AccordionContext.Provider>
34
+ )
35
+ }
36
+
37
+ export function AccordionItem({
38
+ className,
39
+ ...props
40
+ }: ComponentProps<typeof AccordionPrimitive.Item>) {
41
+ const { s, slots } = useParts()
42
+ return (
43
+ <AccordionPrimitive.Item
44
+ data-slot="accordion-item"
45
+ className={s.item({ class: [slots.item, className] })}
46
+ {...props}
47
+ />
48
+ )
49
+ }
50
+
51
+ export function AccordionTrigger({
52
+ className,
53
+ children,
54
+ ...props
55
+ }: ComponentProps<typeof AccordionPrimitive.Trigger>) {
56
+ const { s, slots } = useParts()
57
+ return (
58
+ <AccordionPrimitive.Header className={s.header({ class: slots.header })}>
59
+ <AccordionPrimitive.Trigger
60
+ data-slot="accordion-trigger"
61
+ className={s.trigger({ class: [slots.trigger, className] })}
62
+ {...props}
63
+ >
64
+ {children}
65
+ <ChevronDownIcon className={s.chevron({ class: slots.chevron })} />
66
+ </AccordionPrimitive.Trigger>
67
+ </AccordionPrimitive.Header>
68
+ )
69
+ }
70
+
71
+ export function AccordionContent({
72
+ className,
73
+ children,
74
+ ...props
75
+ }: ComponentProps<typeof AccordionPrimitive.Content>) {
76
+ const { s, slots } = useParts()
77
+ return (
78
+ <AccordionPrimitive.Content
79
+ data-slot="accordion-content"
80
+ className={s.content({ class: slots.content })}
81
+ {...props}
82
+ >
83
+ <div className={s.contentInner({ class: [slots.contentInner, className] })}>{children}</div>
84
+ </AccordionPrimitive.Content>
85
+ )
86
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ Accordion,
3
+ AccordionContent,
4
+ AccordionItem,
5
+ type AccordionProps,
6
+ AccordionTrigger,
7
+ } from './accordion'
8
+ export { type AccordionVariants, accordionStyles } from './accordion.styles'
@@ -0,0 +1,66 @@
1
+ import type { ReactNode, SVGProps } from 'react'
2
+
3
+ type IconProps = SVGProps<SVGSVGElement>
4
+
5
+ function Icon({ children, ...props }: IconProps) {
6
+ return (
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ width="16"
10
+ height="16"
11
+ viewBox="0 0 24 24"
12
+ fill="none"
13
+ stroke="currentColor"
14
+ strokeWidth="2"
15
+ strokeLinecap="round"
16
+ strokeLinejoin="round"
17
+ aria-hidden="true"
18
+ {...props}
19
+ >
20
+ {children}
21
+ </svg>
22
+ )
23
+ }
24
+
25
+ export const InfoIcon = (p: IconProps) => (
26
+ <Icon {...p}>
27
+ <circle cx="12" cy="12" r="10" />
28
+ <path d="M12 16v-4M12 8h.01" />
29
+ </Icon>
30
+ )
31
+ export const SuccessIcon = (p: IconProps) => (
32
+ <Icon {...p}>
33
+ <circle cx="12" cy="12" r="10" />
34
+ <path d="m9 12 2 2 4-4" />
35
+ </Icon>
36
+ )
37
+ export const WarningIcon = (p: IconProps) => (
38
+ <Icon {...p}>
39
+ <path d="m21.73 18-8-14a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
40
+ <path d="M12 9v4M12 17h.01" />
41
+ </Icon>
42
+ )
43
+ export const DangerIcon = (p: IconProps) => (
44
+ <Icon {...p}>
45
+ <circle cx="12" cy="12" r="10" />
46
+ <path d="m15 9-6 6M9 9l6 6" />
47
+ </Icon>
48
+ )
49
+ export const MegaphoneIcon = (p: IconProps) => (
50
+ <Icon {...p}>
51
+ <path d="m3 11 18-5v12L3 14v-3z" />
52
+ <path d="M11.6 16.8a3 3 0 1 1-5.8-1.6" />
53
+ </Icon>
54
+ )
55
+
56
+ export type IconTone = 'neutral' | 'primary' | 'info' | 'success' | 'warning' | 'danger'
57
+
58
+ /** The default icon for each tone. */
59
+ export const toneIcons: Record<IconTone, () => ReactNode> = {
60
+ neutral: () => <InfoIcon />,
61
+ primary: () => <MegaphoneIcon />,
62
+ info: () => <InfoIcon />,
63
+ success: () => <SuccessIcon />,
64
+ warning: () => <WarningIcon />,
65
+ danger: () => <DangerIcon />,
66
+ }
@@ -0,0 +1,57 @@
1
+ import type { StylesEntry } from '../../utils/registry'
2
+ import { themedBorder } from '../../utils/styles'
3
+ import { tv, type VariantProps } from '../../utils/tv'
4
+
5
+ // Soft fills per tone. Icon color is mixed toward the foreground so it stays readable in both modes.
6
+ export const alertStyles = tv({
7
+ slots: {
8
+ root: ['relative flex w-full items-start gap-3 rounded-md px-4 py-3 text-small', themedBorder],
9
+ icon: 'mt-0.5 flex size-4 shrink-0 items-center justify-center [&>svg]:size-4',
10
+ content: 'grid min-w-0 flex-1 gap-0.5',
11
+ title: 'font-medium text-foreground',
12
+ description:
13
+ 'text-muted-foreground [&_a]:font-medium [&_a]:text-foreground [&_a]:underline [&_a]:underline-offset-2',
14
+ action: 'flex shrink-0 items-center gap-2 self-center',
15
+ dismiss: '-my-1 -mr-2 shrink-0',
16
+ },
17
+ variants: {
18
+ tone: {
19
+ neutral: { icon: 'text-muted-foreground' },
20
+ info: { icon: 'text-[color-mix(in_oklab,var(--color-info)_80%,var(--color-foreground))]' },
21
+ success: {
22
+ icon: 'text-[color-mix(in_oklab,var(--color-success)_80%,var(--color-foreground))]',
23
+ },
24
+ warning: {
25
+ icon: 'text-[color-mix(in_oklab,var(--color-warning)_80%,var(--color-foreground))]',
26
+ },
27
+ danger: {
28
+ icon: 'text-[color-mix(in_oklab,var(--color-danger)_80%,var(--color-foreground))]',
29
+ },
30
+ },
31
+ variant: {
32
+ soft: {},
33
+ outline: { root: 'bg-surface' },
34
+ },
35
+ },
36
+ compoundVariants: [
37
+ { variant: 'soft', tone: 'neutral', class: { root: 'border-border bg-muted' } },
38
+ { variant: 'soft', tone: 'info', class: { root: 'border-info/25 bg-info/8' } },
39
+ { variant: 'soft', tone: 'success', class: { root: 'border-success/25 bg-success/8' } },
40
+ { variant: 'soft', tone: 'warning', class: { root: 'border-warning/30 bg-warning/10' } },
41
+ { variant: 'soft', tone: 'danger', class: { root: 'border-danger/25 bg-danger/8' } },
42
+ { variant: 'outline', tone: 'neutral', class: { root: 'border-border-strong' } },
43
+ { variant: 'outline', tone: 'info', class: { root: 'border-info/50' } },
44
+ { variant: 'outline', tone: 'success', class: { root: 'border-success/50' } },
45
+ { variant: 'outline', tone: 'warning', class: { root: 'border-warning/60' } },
46
+ { variant: 'outline', tone: 'danger', class: { root: 'border-danger/50' } },
47
+ ],
48
+ defaultVariants: { tone: 'neutral', variant: 'soft' },
49
+ })
50
+
51
+ export type AlertVariants = VariantProps<typeof alertStyles>
52
+
53
+ declare module '@glyphui/tokens' {
54
+ interface GlyphComponentRegistry {
55
+ Alert: StylesEntry<typeof alertStyles>
56
+ }
57
+ }
@@ -0,0 +1,86 @@
1
+ import type { ComponentProps, ReactNode } from 'react'
2
+ import { useComponentConfig } from '../../hooks/use-component-config'
3
+ import { XIcon } from '../../internal/icons'
4
+ import { IconButton } from '../button'
5
+ import { type AlertVariants, alertStyles } from './alert.styles'
6
+ import { toneIcons } from './alert-icons'
7
+
8
+ export interface AlertProps extends Omit<ComponentProps<'div'>, 'title'>, AlertVariants {
9
+ title?: ReactNode
10
+ /** Replaces the tone's default icon. `false` hides it. */
11
+ icon?: ReactNode | false
12
+ /** Buttons or links shown at the end. */
13
+ action?: ReactNode
14
+ /** Shows a close button that calls this. */
15
+ onDismiss?: () => void
16
+ dismissLabel?: string
17
+ }
18
+
19
+ /**
20
+ * An inline message about the content around it. Danger and warning alerts are announced
21
+ * immediately (`role="alert"`); other tones politely (`role="status"`).
22
+ */
23
+ export function Alert(inProps: AlertProps) {
24
+ const [props, slots] = useComponentConfig('Alert', inProps)
25
+ const {
26
+ tone,
27
+ variant,
28
+ title,
29
+ icon,
30
+ action,
31
+ onDismiss,
32
+ dismissLabel = 'Dismiss',
33
+ className,
34
+ children,
35
+ ...rest
36
+ } = props
37
+ const resolvedTone = tone ?? 'neutral'
38
+ const s = alertStyles({ tone, variant })
39
+ const iconNode = icon === false ? null : (icon ?? toneIcons[resolvedTone]())
40
+ const urgent = resolvedTone === 'danger' || resolvedTone === 'warning'
41
+
42
+ return (
43
+ <div
44
+ data-slot="alert"
45
+ data-tone={resolvedTone}
46
+ role={urgent ? 'alert' : 'status'}
47
+ className={s.root({ class: [slots.root, className] })}
48
+ {...rest}
49
+ >
50
+ {iconNode && (
51
+ <span data-slot="alert-icon" className={s.icon({ class: slots.icon })} aria-hidden="true">
52
+ {iconNode}
53
+ </span>
54
+ )}
55
+ <div data-slot="alert-content" className={s.content({ class: slots.content })}>
56
+ {title && (
57
+ <p data-slot="alert-title" className={s.title({ class: slots.title })}>
58
+ {title}
59
+ </p>
60
+ )}
61
+ {children && (
62
+ <div
63
+ data-slot="alert-description"
64
+ className={s.description({ class: slots.description })}
65
+ >
66
+ {children}
67
+ </div>
68
+ )}
69
+ </div>
70
+ {action && (
71
+ <div data-slot="alert-action" className={s.action({ class: slots.action })}>
72
+ {action}
73
+ </div>
74
+ )}
75
+ {onDismiss && (
76
+ <IconButton
77
+ size="sm"
78
+ icon={<XIcon />}
79
+ aria-label={dismissLabel}
80
+ onClick={onDismiss}
81
+ className={s.dismiss({ class: slots.dismiss })}
82
+ />
83
+ )}
84
+ </div>
85
+ )
86
+ }
@@ -0,0 +1,2 @@
1
+ export { Alert, type AlertProps } from './alert'
2
+ export { type AlertVariants, alertStyles } from './alert.styles'
@@ -0,0 +1,24 @@
1
+ import type { StylesEntry } from '../../utils/registry'
2
+ import { themedBorder } from '../../utils/styles'
3
+ import { tv } from '../../utils/tv'
4
+
5
+ export const alertDialogStyles = tv({
6
+ slots: {
7
+ overlay: 'fixed inset-0 z-50 animate-fade-in bg-overlay',
8
+ content: [
9
+ 'fixed top-1/2 left-1/2 z-50 grid w-[calc(100vw-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 gap-5',
10
+ 'rounded-xl border-border bg-surface p-6 text-foreground shadow-lg outline-none animate-pop-in',
11
+ themedBorder,
12
+ ],
13
+ header: 'flex flex-col gap-1.5',
14
+ title: 'font-display text-h4 text-foreground',
15
+ description: 'text-small text-muted-foreground',
16
+ footer: 'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
17
+ },
18
+ })
19
+
20
+ declare module '@glyphui/tokens' {
21
+ interface GlyphComponentRegistry {
22
+ AlertDialog: StylesEntry<typeof alertDialogStyles>
23
+ }
24
+ }
@@ -0,0 +1,118 @@
1
+ import { AlertDialog as AlertDialogPrimitive } from 'radix-ui'
2
+ import type { ComponentProps } from 'react'
3
+ import { useComponentConfig } from '../../hooks/use-component-config'
4
+ import { Button, type ButtonProps } from '../button'
5
+ import { alertDialogStyles } from './alert-dialog.styles'
6
+
7
+ function useParts() {
8
+ const [, slots] = useComponentConfig('AlertDialog', {})
9
+ return { s: alertDialogStyles(), slots }
10
+ }
11
+
12
+ /**
13
+ * A modal that interrupts to ask for a decision, e.g. confirming a destructive action.
14
+ * Unlike Dialog it can't be dismissed by clicking outside. For a one-line confirm, use `useConfirm()`.
15
+ */
16
+ export const AlertDialog = AlertDialogPrimitive.Root
17
+ export const AlertDialogTrigger = AlertDialogPrimitive.Trigger
18
+
19
+ export interface AlertDialogContentProps
20
+ extends ComponentProps<typeof AlertDialogPrimitive.Content> {
21
+ overlayClassName?: string
22
+ }
23
+
24
+ export function AlertDialogContent(inProps: AlertDialogContentProps) {
25
+ const [props, slots] = useComponentConfig('AlertDialog', inProps)
26
+ const { overlayClassName, className, ...rest } = props
27
+ const s = alertDialogStyles()
28
+ return (
29
+ <AlertDialogPrimitive.Portal>
30
+ <AlertDialogPrimitive.Overlay
31
+ data-slot="alert-dialog-overlay"
32
+ className={s.overlay({ class: [slots.overlay, overlayClassName] })}
33
+ />
34
+ <AlertDialogPrimitive.Content
35
+ data-slot="alert-dialog-content"
36
+ className={s.content({ class: [slots.content, className] })}
37
+ {...rest}
38
+ />
39
+ </AlertDialogPrimitive.Portal>
40
+ )
41
+ }
42
+
43
+ export function AlertDialogHeader({ className, ...props }: ComponentProps<'div'>) {
44
+ const { s, slots } = useParts()
45
+ return (
46
+ <div
47
+ data-slot="alert-dialog-header"
48
+ className={s.header({ class: [slots.header, className] })}
49
+ {...props}
50
+ />
51
+ )
52
+ }
53
+
54
+ export function AlertDialogTitle({
55
+ className,
56
+ ...props
57
+ }: ComponentProps<typeof AlertDialogPrimitive.Title>) {
58
+ const { s, slots } = useParts()
59
+ return (
60
+ <AlertDialogPrimitive.Title
61
+ data-slot="alert-dialog-title"
62
+ className={s.title({ class: [slots.title, className] })}
63
+ {...props}
64
+ />
65
+ )
66
+ }
67
+
68
+ export function AlertDialogDescription({
69
+ className,
70
+ ...props
71
+ }: ComponentProps<typeof AlertDialogPrimitive.Description>) {
72
+ const { s, slots } = useParts()
73
+ return (
74
+ <AlertDialogPrimitive.Description
75
+ data-slot="alert-dialog-description"
76
+ className={s.description({ class: [slots.description, className] })}
77
+ {...props}
78
+ />
79
+ )
80
+ }
81
+
82
+ export function AlertDialogFooter({ className, ...props }: ComponentProps<'div'>) {
83
+ const { s, slots } = useParts()
84
+ return (
85
+ <div
86
+ data-slot="alert-dialog-footer"
87
+ className={s.footer({ class: [slots.footer, className] })}
88
+ {...props}
89
+ />
90
+ )
91
+ }
92
+
93
+ export interface AlertDialogActionProps extends Omit<ButtonProps, 'variant' | 'asChild'> {
94
+ /** `danger` for destructive actions. */
95
+ tone?: 'default' | 'danger'
96
+ }
97
+
98
+ /** The confirming button. Closes the dialog. */
99
+ export function AlertDialogAction({ tone = 'default', ...props }: AlertDialogActionProps) {
100
+ return (
101
+ <AlertDialogPrimitive.Action asChild>
102
+ <Button
103
+ data-slot="alert-dialog-action"
104
+ variant={tone === 'danger' ? 'danger' : 'primary'}
105
+ {...props}
106
+ />
107
+ </AlertDialogPrimitive.Action>
108
+ )
109
+ }
110
+
111
+ /** The dismissing button. Receives focus when the dialog opens, so Enter never confirms by accident. */
112
+ export function AlertDialogCancel(props: Omit<ButtonProps, 'variant' | 'asChild'>) {
113
+ return (
114
+ <AlertDialogPrimitive.Cancel asChild>
115
+ <Button data-slot="alert-dialog-cancel" variant="outline" {...props} />
116
+ </AlertDialogPrimitive.Cancel>
117
+ )
118
+ }
@@ -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'