admonition.tsx219 lines · main
1import { cva } from 'class-variance-authority'
2import { ComponentProps, forwardRef, ReactNode } from 'react'
3import { Alert, AlertDescription, AlertTitle, cn } from 'ui'
4
5export type AdmonitionType =
6 | 'note'
7 | 'tip'
8 | 'caution'
9 | 'danger'
10 | 'deprecation'
11 | 'default'
12 | 'destructive'
13 | 'success'
14 | 'warning'
15
16export interface AdmonitionProps {
17 type?: AdmonitionType
18 title?: string
19 /** @deprecated Prefer title for new usage. label remains supported for existing MDX content. */
20 label?: string
21 description?: ReactNode
22 children?: ReactNode
23 showIcon?: boolean
24 childProps?: {
25 title?: ComponentProps<typeof AlertTitle>
26 description?: ComponentProps<typeof AlertDescription>
27 }
28 layout?: 'horizontal' | 'vertical' | 'responsive'
29 actions?: ReactNode
30 icon?: ReactNode
31 className?: string
32}
33
34const admonitionToAlertMapping: Record<AdmonitionType, 'default' | 'destructive' | 'warning'> = {
35 note: 'default',
36 tip: 'default',
37 caution: 'warning',
38 danger: 'destructive',
39 deprecation: 'warning',
40 default: 'default',
41 warning: 'warning',
42 destructive: 'destructive',
43 success: 'default',
44}
45
46const InfoIcon = () => (
47 <svg
48 xmlns="http://www.w3.org/2000/svg"
49 viewBox="0 0 21 20"
50 className="w-6 h-6"
51 fill="currentColor"
52 >
53 <path
54 fillRule="evenodd"
55 clipRule="evenodd"
56 d="M0.625 9.8252C0.625 4.44043 4.99023 0.0751953 10.375 0.0751953C15.7598 0.0751953 20.125 4.44043 20.125 9.8252C20.125 15.21 15.7598 19.5752 10.375 19.5752C4.99023 19.5752 0.625 15.21 0.625 9.8252ZM9.3584 4.38135C9.45117 4.28857 9.55518 4.20996 9.66699 4.14648C9.88086 4.02539 10.1245 3.96045 10.375 3.96045C10.5845 3.96045 10.7896 4.00586 10.9766 4.09229C11.1294 4.1626 11.2705 4.26025 11.3916 4.38135C11.6611 4.65088 11.8125 5.0166 11.8125 5.39795C11.8125 5.5249 11.7959 5.6499 11.7637 5.77002C11.6987 6.01172 11.5718 6.23438 11.3916 6.41455C11.1221 6.68408 10.7563 6.83545 10.375 6.83545C9.99365 6.83545 9.62793 6.68408 9.3584 6.41455C9.08887 6.14502 8.9375 5.7793 8.9375 5.39795C8.9375 5.29492 8.94873 5.19287 8.97021 5.09375C9.02783 4.82568 9.16162 4.57812 9.3584 4.38135ZM10.375 15.6899C10.0933 15.6899 9.82275 15.5781 9.62354 15.3789C9.42432 15.1797 9.3125 14.9092 9.3125 14.6274V9.31494C9.3125 9.0332 9.42432 8.7627 9.62354 8.56348C9.82275 8.36426 10.0933 8.25244 10.375 8.25244C10.6567 8.25244 10.9272 8.36426 11.1265 8.56348C11.3257 8.7627 11.4375 9.0332 11.4375 9.31494V14.6274C11.4375 14.7944 11.3979 14.9575 11.3242 15.104C11.2739 15.2046 11.2075 15.2979 11.1265 15.3789C10.9272 15.5781 10.6567 15.6899 10.375 15.6899Z"
57 />
58 </svg>
59)
60
61const SuccessIcon = () => (
62 <svg
63 xmlns="http://www.w3.org/2000/svg"
64 viewBox="0 0 21 20"
65 className="w-6 h-6"
66 fill="currentColor"
67 >
68 <path
69 fillRule="evenodd"
70 clipRule="evenodd"
71 d="M10.5 19.5C5.25329 19.5 1 15.2467 1 10C1 4.75329 5.25329 0.5 10.5 0.5C15.7467 0.5 20 4.75329 20 10C20 15.2467 15.7467 19.5 10.5 19.5ZM14.7803 7.78033C15.0732 7.48744 15.0732 7.01256 14.7803 6.71967C14.4874 6.42678 14.0126 6.42678 13.7197 6.71967L9.25 11.1893L7.28033 9.21967C6.98744 8.92678 6.51256 8.92678 6.21967 9.21967C5.92678 9.51256 5.92678 9.98744 6.21967 10.2803L8.71967 12.7803C9.01256 13.0732 9.48744 13.0732 9.78033 12.7803L14.7803 7.78033Z"
72 />
73 </svg>
74)
75
76export const WarningIcon = ({ className }: { className?: string }) => (
77 <svg
78 xmlns="http://www.w3.org/2000/svg"
79 viewBox="0 0 22 20"
80 className={cn('w-6 h-6', className)}
81 fill="currentColor"
82 >
83 <path
84 fillRule="evenodd"
85 clipRule="evenodd"
86 d="M8.15137 1.95117C9.30615 -0.0488281 12.1943 -0.0488281 13.3481 1.95117L20.7031 14.6992C21.8574 16.6992 20.4131 19.1992 18.104 19.1992H3.39502C1.08594 19.1992 -0.356933 16.6992 0.797364 14.6992L8.15137 1.95117ZM11.7666 16.0083C11.4971 16.2778 11.1313 16.4292 10.75 16.4292C10.3687 16.4292 10.0029 16.2778 9.7334 16.0083C9.46387 15.7388 9.3125 15.373 9.3125 14.9917C9.3125 14.9307 9.31641 14.8706 9.32373 14.811C9.33545 14.7197 9.35547 14.6304 9.38379 14.5439L9.41406 14.4609C9.48584 14.2803 9.59375 14.1147 9.7334 13.9751C10.0029 13.7056 10.3687 13.5542 10.75 13.5542C11.1313 13.5542 11.4971 13.7056 11.7666 13.9751C12.0361 14.2446 12.1875 14.6104 12.1875 14.9917C12.1875 15.373 12.0361 15.7388 11.7666 16.0083ZM10.75 4.69971C11.0317 4.69971 11.3022 4.81152 11.5015 5.01074C11.7007 5.20996 11.8125 5.48047 11.8125 5.76221V11.0747C11.8125 11.3564 11.7007 11.627 11.5015 11.8262C11.3022 12.0254 11.0317 12.1372 10.75 12.1372C10.4683 12.1372 10.1978 12.0254 9.99854 11.8262C9.79932 11.627 9.6875 11.3564 9.6875 11.0747V5.76221C9.6875 5.48047 9.79932 5.20996 9.99854 5.01074C10.1978 4.81152 10.4683 4.69971 10.75 4.69971Z"
87 />
88 </svg>
89)
90
91const admonitionSVG = cva('', {
92 variants: {
93 type: {
94 default: `[&>svg]:bg-foreground-muted`,
95 success: `bg-brand-400/15 dark:bg-brand/10 border-brand-400 dark:border-brand-500 [&>svg]:text-white dark:[&>svg]:text-brand-link [&>svg]:bg-brand dark:[&>svg]:bg-brand-500/50`,
96 warning: ``,
97 destructive: ``,
98 },
99 },
100})
101
102const admonitionBodyClassName =
103 '[&_p]:!mt-0 [&_p]:!mb-1.5 [&_p:last-child]:!mb-0 [&_p:only-child]:!mb-0 [&_ul]:!my-1.5 [&_ol]:!my-1.5 [&_li]:!my-0.5'
104
105export const Admonition = forwardRef<
106 React.ElementRef<typeof Alert>,
107 Omit<React.ComponentPropsWithoutRef<typeof Alert>, keyof AdmonitionProps | 'children'> &
108 AdmonitionProps
109>(
110 (
111 {
112 type = 'note',
113 variant,
114 showIcon = true,
115 label,
116 title,
117 description,
118 children,
119 layout = 'vertical',
120 actions,
121 childProps = {},
122 icon,
123 ...props
124 },
125 ref
126 ) => {
127 const typeMapped = variant ? admonitionToAlertMapping[variant] : admonitionToAlertMapping[type]
128 const typeStyle = type === 'success' ? 'success' : typeMapped
129 const heading = title ?? label
130
131 return (
132 <Alert
133 ref={ref}
134 variant={typeMapped}
135 {...props}
136 className={cn(
137 // Handle occasional background elements
138 'overflow-hidden',
139 // Container query context for responsive layout
140 layout === 'responsive' && '@container',
141 // SVG icon
142 admonitionSVG({ type: typeStyle }),
143 props.className
144 )}
145 >
146 {!!icon ? (
147 icon
148 ) : showIcon && typeStyle === 'success' ? (
149 <SuccessIcon />
150 ) : showIcon && (typeMapped === 'warning' || typeMapped === 'destructive') ? (
151 <WarningIcon />
152 ) : showIcon ? (
153 <InfoIcon />
154 ) : null}
155 <div
156 className={cn(
157 'flex',
158 layout === 'vertical' && 'flex-col',
159 layout === 'horizontal' && 'flex-row items-center justify-between gap-x-6 lg:gap-x-8',
160 layout === 'responsive' &&
161 'flex-col @md:flex-row @md:items-center @md:justify-between @md:gap-x-6 @lg:gap-x-8'
162 )}
163 >
164 <div>
165 {heading && (
166 <AlertTitle
167 {...childProps.title}
168 className={cn(
169 'text mt-0.5 flex gap-3 text-sm',
170 !label && 'flex-col',
171 childProps.title?.className
172 )}
173 >
174 {heading}
175 </AlertTitle>
176 )}
177 {description && (
178 <AlertDescription
179 {...childProps.description}
180 className={cn(
181 admonitionBodyClassName,
182 !heading && 'my-0.5',
183 childProps.description?.className
184 )}
185 >
186 {description}
187 </AlertDescription>
188 )}
189 {/* // children is to handle Docs and MDX issues with children and <p> elements */}
190 {children && (
191 <AlertDescription
192 {...childProps.description}
193 className={cn(
194 admonitionBodyClassName,
195 !heading && !description && 'my-0.5',
196 childProps?.description?.className
197 )}
198 >
199 {children}
200 </AlertDescription>
201 )}
202 </div>
203 {actions && (
204 <div
205 className={cn(
206 'flex flex-row gap-3',
207 layout === 'vertical' && 'mt-3 items-start',
208 layout === 'horizontal' && 'items-center',
209 layout === 'responsive' && 'mt-3 items-start @md:mt-0 @md:items-center'
210 )}
211 >
212 {actions}
213 </div>
214 )}
215 </div>
216 </Alert>
217 )
218 }
219)