@glyphui/ui 0.1.0 → 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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
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.
|
|
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.
|
|
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,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,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'
|