Files
Danny White 5bfbae22a9 chore(studio + design-system): more flexible Admonition (#45302)
## What kind of change does this PR introduce?

Feature and design-system cleanup. Resolves DEPR-551.

## What is the current behavior?

Admonition supports several overlapping content shapes, but it
previously did not support a first-class success state or
description-only usage cleanly. Title-only usage was also possible,
which made some callouts read like floating headings without body copy.

Docs MDX Admonitions could also pick up prose spacing around rich
children, while the design-system Tailwind config emitted an
ESM/CommonJS warning in the design-system app.

## What is the new behavior?

Adds a `success` Admonition type, description-only support, and a
stricter content contract: `title` or legacy `label` now requires either
`description` or `children`. Existing title-only Studio callsites have
been converted to description-only callouts.

The design-system docs now include examples for description-only and
success Admonitions, plus guidance for `title`, `description`,
`children`, and legacy `label` usage.

This also tightens Admonition body spacing so rich MDX children keep
docs link/code styling without inheriting excessive prose margins, and
renames the design-system Tailwind config to `tailwind.config.cjs` so it
matches its CommonJS syntax.

Warning and destructive alerts now explicitly set `text-foreground`,
preventing nested Admonition titles from inheriting muted
form-description colour after the Tailwind v4 cascade changes.

| Before | After |
| --- | --- |
| <img width="1818" height="388" alt="Image"
src="https://github.com/user-attachments/assets/283a1853-348a-4d74-a408-013957350e5e"
/> | <img width="1380" height="462" alt="Image"
src="https://github.com/user-attachments/assets/e5761e8e-3697-423b-805b-45110205099a"
/> |
| <img width="1398" height="550" alt="CleanShot 2026-04-28 at 15 12
41@2x"
src="https://github.com/user-attachments/assets/982694d9-5461-4362-8bae-a6e2b4c60e8b"
/> | <img width="1402" height="450" alt="CleanShot 2026-04-28 at 15 13
09@2x"
src="https://github.com/user-attachments/assets/0b1257c4-6b58-4c39-a182-4861a9e378ee"
/> |
| <img width="1640" height="716" alt="CleanShot 2026-04-28 at 15 17
25@2x"
src="https://github.com/user-attachments/assets/a5be4d5f-2bf7-4dc2-b396-56129fe64ec9"
/> | <img width="1630" height="716" alt="CleanShot 2026-04-28 at 15 16
00@2x"
src="https://github.com/user-attachments/assets/0d589252-aaf8-4efc-9d81-15ec4f99ec61"
/> |

| Design System Docs |
| --- |
| <img width="1646" height="1864" alt="CleanShot 2026-04-28 at 14 59
15@2x"
src="https://github.com/user-attachments/assets/12d13595-8972-4fb2-a04a-fb916388ebb6"
/> |


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a "success" admonition variant and new example previews
demonstrating success and description-only usages.

* **Documentation**
* Clarified admonition guidance: when to use title vs description vs
children; added example sections for short callouts and success
messages.

* **Refactor**
* Standardized UI by moving short/advisory text into description across
the app and harmonized trailing punctuation.

* **Style**
* Ensured warning/destructive admonitions use consistent foreground text
styling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-01 07:15:00 -06:00

152 lines
5.4 KiB
TypeScript

import { useIsLoggedIn, useParams } from 'common'
import { CheckSquare } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { toast } from 'sonner'
import { Button, cn } from 'ui'
import { Admonition, GenericSkeletonLoader } from 'ui-patterns'
import { OrganizationInviteError } from './OrganizationInviteError'
import { useOrganizationAcceptInvitationMutation } from '@/data/organization-members/organization-invitation-accept-mutation'
import { useOrganizationInvitationTokenQuery } from '@/data/organization-members/organization-invitation-token-query'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { useProfile } from '@/lib/profile'
export const OrganizationInvite = () => {
const router = useRouter()
const isLoggedIn = useIsLoggedIn()
const { profile, isLoading: isLoadingProfile } = useProfile()
const { slug, token } = useParams()
const isSignUpEnabled = useIsFeatureEnabled('dashboard_auth:sign_up')
const {
data,
error,
isSuccess: isSuccessInvitation,
isError: isErrorInvitation,
isPending: isLoadingInvitation,
} = useOrganizationInvitationTokenQuery(
{ slug, token },
{
retry: false,
refetchOnWindowFocus: false,
enabled: !!profile,
}
)
const hasError =
isErrorInvitation ||
(isSuccessInvitation && (data.token_does_not_exist || data.expired_token || !data.email_match))
const inviteIsNoLongerValid =
error?.code === 401 && error?.message.includes('Failed to retrieve organization')
const organizationName = isSuccessInvitation ? data?.organization_name : 'An organization'
const loginRedirectLink = `/sign-in?returnTo=${encodeURIComponent(`/join?token=${token}&slug=${slug}`)}`
const signupRedirectLink = `/sign-up?returnTo=${encodeURIComponent(`/join?token=${token}&slug=${slug}`)}`
const { mutate: joinOrganization, isPending: isJoining } =
useOrganizationAcceptInvitationMutation({
onSuccess: () => {
router.push('/organizations')
},
onError: (error) => {
toast.error(`Failed to join organization: ${error.message}`)
},
})
async function handleJoinOrganization() {
if (!slug) return console.error('Slug is required')
if (!token) return console.error('Token is required')
joinOrganization({ slug, token })
}
return (
<div
className={cn(
'mx-auto overflow-hidden rounded-md border',
'border-muted bg-alternative text-center shadow-sm',
'md:w-[400px]'
)}
>
{!isLoggedIn || (!profile && !isLoadingProfile) ? (
<>
<Admonition
showIcon={false}
type="default"
description={`Sign in${isSignUpEnabled ? ' or create an account' : ''} first to view this invitation.`}
className="border-0 rounded-none text-left"
/>
<div className="p-4 border-muted border-t flex gap-x-3 justify-center">
<Button asChild type="default">
<Link href={loginRedirectLink}>Sign in</Link>
</Button>
{isSignUpEnabled && (
<Button asChild type="default">
<Link href={signupRedirectLink}>Create an account</Link>
</Button>
)}
</div>
</>
) : isLoadingProfile || isLoadingInvitation ? (
<div className="p-5">
<GenericSkeletonLoader />
</div>
) : inviteIsNoLongerValid ? (
<>
<Admonition
type="default"
title="Invalid invitation"
description="This organization invite is no longer valid as it has either been accepted or declined"
className="border-0 rounded-none text-left"
/>
<div className="p-4 border-muted border-t">
<Button type="default" asChild>
<Link href="/">Back to dashboard</Link>
</Button>
</div>
</>
) : (
<>
<div className="flex flex-col gap-y-1 px-6 py-6">
<p className="text-sm text-foreground-light">You have been invited to join </p>
<p className="text-foreground text-2xl">{organizationName}</p>
{isSuccessInvitation && (
<p className="text-xs text-foreground-lighter">{`Organization slug: ${slug}`}</p>
)}
</div>
<div
className={cn('border-t border-muted', hasError ? 'bg-alternative' : 'bg-transparent')}
>
<div
className={cn(
'flex flex-col gap-4',
!isLoadingInvitation && !hasError && 'px-6 py-4'
)}
>
{hasError && (
<OrganizationInviteError data={data} error={error} isError={isErrorInvitation} />
)}
{isSuccessInvitation && !hasError && (
<div className="flex flex-row items-center justify-center gap-3">
<Button type="default" disabled={isJoining} asChild>
<Link href="/projects">Decline</Link>
</Button>
<Button
type="primary"
loading={isJoining}
disabled={isJoining}
onClick={handleJoinOrganization}
icon={<CheckSquare />}
>
Join organization
</Button>
</div>
)}
</div>
</div>
</>
)}
</div>
)
}