Skip to content

Commit 04c9fb7

Browse files
authored
chore(design-system): redo safer Admonition updates (supabase#45618)
## What kind of change does this PR introduce? Feature and design-system update. Resolves DEPR-551. This is a narrower redo of supabase#45302 after the revert in supabase#45535. ## What is the current behaviour? The reverted implementation made Admonition more flexible, but it also changed Studio callsites, touched shared Alert styling, renamed the design-system Tailwind config, and changed Docs-facing content/API assumptions in a way that broke production docs static generation. ## What is the new behaviour? Admonition now supports description-only content, children-only content, optional `title`, legacy `label`, and `type="success"` without touching `apps/docs/content/**` or shared Alert styling. `title` wins over `label` when both are provided. The runtime component props stay backwards-compatible for existing MDX and Studio usage, while `AdmonitionStrictProps` captures the stricter new-usage contract for tests and future callsites. The design-system docs and registry include description-only and success examples, and the Admonition tests cover the rendering paths that broke production previously. | After | | --- | | <img width="1668" height="1768" alt="CleanShot 2026-05-06 at 17 35 13@2x" src="https://github.com/user-attachments/assets/1c00ea7f-e3ca-45eb-8af9-3536b657c341" /> | ## Additional context These things that were in supabase#45302 have been left out (unless checked): - [ ] Studio callsite rewrites from title/label to description - [ ] Shared Alert text-colour changes - [ ] Design-system Tailwind config rename - [ ] Design-system global CSS changes - [ ] Any docs content migration or label deprecation - [ ] Any production docs workaround <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added success admonition variant with dedicated styling and icon. * Introduced description-only admonition example. * **Documentation** * Expanded admonition guidance on title vs description usage and best practices. * Added example sections showcasing description-only and success variants. * **Tests** * Added comprehensive tests covering admonition variants and rendering/precedence behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent e6b81c4 commit 04c9fb7

9 files changed

Lines changed: 275 additions & 45 deletions

File tree

apps/design-system/__registry__/index.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,17 @@ export const Index: Record<string, any> = {
126126
subcategory: "undefined",
127127
chunks: []
128128
},
129+
"admonition-description-only": {
130+
name: "admonition-description-only",
131+
type: "components:example",
132+
registryDependencies: ["admonition"],
133+
component: React.lazy(() => import("@/registry/default/example/admonition-description-only")),
134+
source: "",
135+
files: ["registry/default/example/admonition-description-only.tsx"],
136+
category: "undefined",
137+
subcategory: "undefined",
138+
chunks: []
139+
},
129140
"admonition-warning": {
130141
name: "admonition-warning",
131142
type: "components:example",
@@ -137,6 +148,17 @@ export const Index: Record<string, any> = {
137148
subcategory: "undefined",
138149
chunks: []
139150
},
151+
"admonition-success": {
152+
name: "admonition-success",
153+
type: "components:example",
154+
registryDependencies: ["admonition"],
155+
component: React.lazy(() => import("@/registry/default/example/admonition-success")),
156+
source: "",
157+
files: ["registry/default/example/admonition-success.tsx"],
158+
category: "undefined",
159+
subcategory: "undefined",
160+
chunks: []
161+
},
140162
"admonition-destructive": {
141163
name: "admonition-destructive",
142164
type: "components:example",

apps/design-system/content/docs/fragments/admonition.mdx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ Admonition provides focus for situations that require a callout.
1515

1616
Use Admonition instead of [Alert](../components/alert) unless you specifically need the primitives. If in doubt, stick with Admonition.
1717

18+
Use `title` for the heading slot when the callout needs its own heading. `title` is optional: short callouts may use `description` without a title when nearby content already provides enough context.
19+
20+
Avoid title-only Admonitions in new code. A callout with `title` should also include `description` or `children` so it does not read like an incomplete heading.
21+
22+
`label` is still supported for older Docs content, but new code should use `title`.
23+
1824
## Usage
1925

2026
```tsx
@@ -72,6 +78,24 @@ There are several components that wrap the `warning` Admonition type with reusab
7278

7379
AlertError for example rolls up consistent error handling and support contact methods.
7480

81+
### Description only
82+
83+
Use description-only Admonitions for short callouts that sit near a heading or form label with enough context.
84+
85+
<ComponentPreview
86+
name="admonition-description-only"
87+
className="[&_.preview>[data-orientation=vertical]]:sm:max-w-[70%]"
88+
/>
89+
90+
### Success
91+
92+
Use `success` for positive, completed states where the user does not need to take corrective action.
93+
94+
<ComponentPreview
95+
name="admonition-success"
96+
className="[&_.preview>[data-orientation=vertical]]:sm:max-w-[70%]"
97+
/>
98+
7599
### Destructive
76100

77101
<ComponentPreview
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Admonition } from 'ui-patterns/admonition'
2+
3+
export default function AdmonitionDescriptionOnly() {
4+
return (
5+
<Admonition
6+
type="default"
7+
description="Changes to these settings can take a few minutes to appear across all projects."
8+
/>
9+
)
10+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Admonition } from 'ui-patterns/admonition'
2+
3+
export default function AdmonitionSuccess() {
4+
return (
5+
<Admonition
6+
type="success"
7+
title="Connection confirmed"
8+
description="You can now close this tab."
9+
/>
10+
)
11+
}

apps/design-system/registry/examples.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,24 @@ export const examples: Registry = [
2525
registryDependencies: ['admonition'],
2626
files: ['example/admonition-button.tsx'],
2727
},
28+
{
29+
name: 'admonition-description-only',
30+
type: 'components:example',
31+
registryDependencies: ['admonition'],
32+
files: ['example/admonition-description-only.tsx'],
33+
},
2834
{
2935
name: 'admonition-warning',
3036
type: 'components:example',
3137
registryDependencies: ['admonition'],
3238
files: ['example/admonition-warning.tsx'],
3339
},
40+
{
41+
name: 'admonition-success',
42+
type: 'components:example',
43+
registryDependencies: ['admonition'],
44+
files: ['example/admonition-success.tsx'],
45+
},
3446
{
3547
name: 'admonition-destructive',
3648
type: 'components:example',

packages/ui-patterns/src/Dialogs/ConfirmationModal.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ export const ConfirmationModal = forwardRef<
8585
if (loading_ !== undefined) setLoading(loading_)
8686
}, [loading_])
8787

88+
const { title: _alertBaseTitle, children: _alertBaseChildren, ...alertBase } = alert?.base ?? {}
89+
const alertTitleProps = alert?.title ? { label: alert.title } : {}
90+
8891
return (
8992
<Dialog
9093
open={visible}
@@ -108,10 +111,10 @@ export const ConfirmationModal = forwardRef<
108111
{alert && (
109112
<Admonition
110113
type={variant as 'default' | 'destructive' | 'warning'}
111-
label={alert.title}
112114
description={alert.description}
115+
{...alertTitleProps}
113116
className="border-x-0 rounded-none -mt-px"
114-
{...alert?.base}
117+
{...alertBase}
115118
/>
116119
)}
117120
{children && (

packages/ui-patterns/src/Dialogs/TextConfirmModal.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ export const TextConfirmModal = forwardRef<
128128
return () => clearTimeout(timer)
129129
}, [showCopied])
130130

131+
const { title: _alertBaseTitle, children: _alertBaseChildren, ...alertBase } = alert?.base ?? {}
132+
const alertTitleProps = alert?.title ? { label: alert.title } : {}
133+
131134
return (
132135
<Dialog
133136
open={visible}
@@ -145,10 +148,10 @@ export const TextConfirmModal = forwardRef<
145148
{alert && (
146149
<Admonition
147150
type={variant as 'default' | 'destructive' | 'warning'}
148-
label={alert.title}
149151
description={alert.description}
152+
{...alertTitleProps}
150153
className="border-x-0 rounded-none -mt-px"
151-
{...alert?.base}
154+
{...alertBase}
152155
/>
153156
)}
154157
{children && (
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { render, screen, within } from '@testing-library/react'
2+
import { describe, expect, it } from 'vitest'
3+
4+
import { Admonition, type AdmonitionProps } from './admonition'
5+
6+
const stringDescriptionProps = {
7+
description: 'Description-only copy.',
8+
} satisfies AdmonitionProps
9+
10+
void stringDescriptionProps
11+
12+
describe('Admonition', () => {
13+
it('renders description-only content', () => {
14+
render(<Admonition type="default" description="Changes can take a few minutes to apply." />)
15+
16+
expect(screen.getByRole('alert')).toHaveTextContent('Changes can take a few minutes to apply.')
17+
})
18+
19+
it('renders children-only rich MDX-like content', () => {
20+
render(
21+
<Admonition type="note">
22+
<p>
23+
This is a Postgres{' '}
24+
<a href="/docs/guides/database/postgres/row-level-security">SECURITY DEFINER</a> function.
25+
</p>
26+
<ul>
27+
<li>Keep privileges scoped.</li>
28+
</ul>
29+
</Admonition>
30+
)
31+
32+
const alert = screen.getByRole('alert')
33+
expect(within(alert).getByText('SECURITY DEFINER')).toHaveAttribute(
34+
'href',
35+
'/docs/guides/database/postgres/row-level-security'
36+
)
37+
expect(within(alert).getByText('Keep privileges scoped.')).toBeVisible()
38+
})
39+
40+
it('renders title and description together', () => {
41+
render(
42+
<Admonition
43+
type="warning"
44+
title="Manual approval required"
45+
description="Review the pending changes before continuing."
46+
/>
47+
)
48+
49+
const alert = screen.getByRole('alert')
50+
expect(alert).toHaveTextContent('Manual approval required')
51+
expect(alert).toHaveTextContent('Review the pending changes before continuing.')
52+
})
53+
54+
it('renders title and children together', () => {
55+
render(
56+
<Admonition type="caution" title="Security definer function">
57+
<p>Review ownership before exposing this function.</p>
58+
</Admonition>
59+
)
60+
61+
const alert = screen.getByRole('alert')
62+
expect(alert).toHaveTextContent('Security definer function')
63+
expect(alert).toHaveTextContent('Review ownership before exposing this function.')
64+
})
65+
66+
it('renders success state with success styling', () => {
67+
render(
68+
<Admonition
69+
type="success"
70+
title="Connection confirmed"
71+
description="You can now close this tab."
72+
/>
73+
)
74+
75+
const alert = screen.getByRole('alert')
76+
expect(alert).toHaveTextContent('Connection confirmed')
77+
expect(alert).toHaveTextContent('You can now close this tab.')
78+
expect(alert).toHaveClass('bg-brand-400/15')
79+
expect(alert).toHaveClass('border-brand-400')
80+
expect(alert.querySelector('svg path')?.getAttribute('d')).toContain('M10.5 19.5')
81+
})
82+
83+
it('does not render the destructive icon when showIcon is false', () => {
84+
render(
85+
<Admonition
86+
type="destructive"
87+
showIcon={false}
88+
title="Deletion blocked"
89+
description="Resolve dependent resources before retrying."
90+
/>
91+
)
92+
93+
const alert = screen.getByRole('alert')
94+
expect(alert).toHaveTextContent('Deletion blocked')
95+
expect(alert.querySelector('svg')).not.toBeInTheDocument()
96+
})
97+
98+
it('prefers title over legacy label when both are provided', () => {
99+
render(
100+
<Admonition
101+
type="note"
102+
label="Legacy heading"
103+
title="Preferred heading"
104+
description="Body copy."
105+
/>
106+
)
107+
108+
expect(screen.getByText('Preferred heading')).toBeVisible()
109+
expect(screen.queryByText('Legacy heading')).not.toBeInTheDocument()
110+
})
111+
})

0 commit comments

Comments
 (0)