mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 20:05:06 +03:00
## 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 -->
231 lines
8.0 KiB
TypeScript
231 lines
8.0 KiB
TypeScript
// End of third-party imports
|
||
|
||
import { useParams } from 'common'
|
||
import { AnimatePresence, motion } from 'framer-motion'
|
||
import { Check, ChevronsUpDown, ExternalLink } from 'lucide-react'
|
||
import Link from 'next/link'
|
||
import type { UseFormReturn } from 'react-hook-form'
|
||
import { toast } from 'sonner'
|
||
import { Button, cn, CommandGroup_Shadcn_, CommandItem_Shadcn_, FormControl, FormField } from 'ui'
|
||
import { Admonition } from 'ui-patterns'
|
||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
|
||
|
||
import type { ExtendedSupportCategories } from './Support.constants'
|
||
import type { SupportFormValues } from './SupportForm.schema'
|
||
import { NO_ORG_MARKER, NO_PROJECT_MARKER } from './SupportForm.utils'
|
||
import CopyButton from '@/components/ui/CopyButton'
|
||
import { OrganizationProjectSelector } from '@/components/ui/OrganizationProjectSelector'
|
||
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
||
|
||
interface ProjectAndPlanProps {
|
||
form: UseFormReturn<SupportFormValues>
|
||
orgSlug: string | null
|
||
projectRef: string | null
|
||
category: ExtendedSupportCategories
|
||
subscriptionPlanId: string | undefined
|
||
showPlanExpectationInfo?: boolean
|
||
}
|
||
|
||
export function ProjectAndPlanInfo({
|
||
form,
|
||
orgSlug,
|
||
projectRef,
|
||
category,
|
||
subscriptionPlanId,
|
||
showPlanExpectationInfo = true,
|
||
}: ProjectAndPlanProps) {
|
||
const hasProjectSelected = projectRef && projectRef !== NO_PROJECT_MARKER
|
||
|
||
return (
|
||
<div className="flex flex-col gap-y-2">
|
||
<ProjectSelector form={form} orgSlug={orgSlug} projectRef={projectRef} />
|
||
<ProjectRefHighlighted projectRef={projectRef} />
|
||
|
||
{!hasProjectSelected && (
|
||
<Admonition type="default" description="No project has been selected." />
|
||
)}
|
||
|
||
{showPlanExpectationInfo &&
|
||
orgSlug &&
|
||
subscriptionPlanId !== 'enterprise' &&
|
||
subscriptionPlanId !== 'platform' &&
|
||
category !== 'Login_issues' && (
|
||
<PlanExpectationInfoBox orgSlug={orgSlug} planId={subscriptionPlanId} />
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
interface ProjectSelectorProps {
|
||
form: UseFormReturn<SupportFormValues>
|
||
orgSlug: string | null
|
||
projectRef: string | null
|
||
}
|
||
|
||
function ProjectSelector({ form, orgSlug, projectRef }: ProjectSelectorProps) {
|
||
const { projectRef: urlProjectRef } = useParams()
|
||
|
||
return (
|
||
<FormField
|
||
name="projectRef"
|
||
control={form.control}
|
||
render={({ field }) => (
|
||
<FormItemLayout hideMessage layout="vertical" label="Which project is affected?">
|
||
<FormControl>
|
||
<OrganizationProjectSelector
|
||
key={orgSlug}
|
||
sameWidthAsTrigger
|
||
fetchOnMount
|
||
checkPosition="left"
|
||
slug={!orgSlug || orgSlug === NO_ORG_MARKER ? undefined : orgSlug}
|
||
selectedRef={field.value}
|
||
onInitialLoad={(projects) => {
|
||
if (!urlProjectRef && (!projectRef || projectRef === NO_PROJECT_MARKER))
|
||
field.onChange(projects[0]?.ref ?? NO_PROJECT_MARKER)
|
||
}}
|
||
onSelect={(project) => field.onChange(project.ref)}
|
||
renderTrigger={({ isLoading, project, listboxId, open }) => {
|
||
return (
|
||
<Button
|
||
block
|
||
type="default"
|
||
role="combobox"
|
||
aria-label="Select a project"
|
||
aria-expanded={open}
|
||
aria-controls={listboxId}
|
||
size="small"
|
||
className="justify-between"
|
||
iconRight={<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />}
|
||
>
|
||
{!!orgSlug && isLoading ? (
|
||
<ShimmeringLoader className="w-44 py-2" />
|
||
) : !field.value || field.value === NO_PROJECT_MARKER ? (
|
||
'No specific project'
|
||
) : (
|
||
(project?.name ?? 'Unknown project')
|
||
)}
|
||
</Button>
|
||
)
|
||
}}
|
||
renderActions={(setOpen) => (
|
||
<CommandGroup_Shadcn_>
|
||
<CommandItem_Shadcn_
|
||
className="w-full gap-x-2"
|
||
onSelect={() => {
|
||
field.onChange(NO_PROJECT_MARKER)
|
||
setOpen(false)
|
||
}}
|
||
>
|
||
{field.value === NO_PROJECT_MARKER && <Check size={16} />}
|
||
<p className={cn(field.value !== NO_PROJECT_MARKER && 'ml-6')}>
|
||
No specific project
|
||
</p>
|
||
</CommandItem_Shadcn_>
|
||
</CommandGroup_Shadcn_>
|
||
)}
|
||
/>
|
||
</FormControl>
|
||
</FormItemLayout>
|
||
)}
|
||
/>
|
||
)
|
||
}
|
||
|
||
interface ProjectRefHighlightedProps {
|
||
projectRef: string | null
|
||
}
|
||
|
||
function ProjectRefHighlighted({ projectRef }: ProjectRefHighlightedProps) {
|
||
const isVisible = !!projectRef && projectRef !== NO_PROJECT_MARKER
|
||
|
||
return (
|
||
<AnimatePresence>
|
||
{isVisible && (
|
||
<motion.div
|
||
initial={{ opacity: 0, height: 0 }}
|
||
animate={{ opacity: 1, height: 'auto' }}
|
||
exit={{ opacity: 0, height: 0 }}
|
||
transition={{ duration: 0.3 }}
|
||
className="flex items-center gap-x-1"
|
||
>
|
||
<p className="text-sm transition text-foreground-lighter">
|
||
Project ID:{' '}
|
||
<code className="text-code-inline text-foreground-light!">{projectRef}</code>
|
||
</p>
|
||
<CopyButton
|
||
iconOnly
|
||
type="text"
|
||
text={projectRef}
|
||
onClick={() => toast.success('Copied project ID to clipboard')}
|
||
/>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
)
|
||
}
|
||
|
||
interface PlanExpectationInfoBoxProps {
|
||
orgSlug: string
|
||
planId?: string
|
||
}
|
||
|
||
const PlanExpectationInfoBox = ({ orgSlug, planId }: PlanExpectationInfoBoxProps) => {
|
||
const { billingAll } = useIsFeatureEnabled(['billing:all'])
|
||
const shouldShowUpgradeActions = billingAll && planId !== 'enterprise'
|
||
|
||
return (
|
||
<Admonition
|
||
type="default"
|
||
title="Expected response times are based on your organization’s plan"
|
||
description={
|
||
<>
|
||
{planId === 'free' && (
|
||
<p>
|
||
Support on the Free plan is provided through the community and by the team on a
|
||
best-effort basis. For a guaranteed response time, we recommend upgrading to the Pro
|
||
plan. Enhanced support SLAs are available on the Enterprise plan.
|
||
</p>
|
||
)}
|
||
|
||
{planId === 'pro' && (
|
||
<p>
|
||
The Pro plan includes email support. In most cases, you can expect a response within 1
|
||
business day for all severities. For prioritized ticketing on all issues and
|
||
prioritized escalation to product engineering, we recommend upgrading to the Team
|
||
plan. Enhanced support SLAs are available on the Enterprise plan.
|
||
</p>
|
||
)}
|
||
|
||
{planId === 'team' && (
|
||
<p>
|
||
The Team plan includes email support with prioritized ticketing and escalation to
|
||
product engineering. Low, normal, and high-severity tickets are typically handled
|
||
within 1 business day. Urgent issues are handled within 1 day, 365 days a year.
|
||
Enhanced support SLAs are available on the Enterprise plan.
|
||
</p>
|
||
)}
|
||
</>
|
||
}
|
||
actions={
|
||
shouldShowUpgradeActions && (
|
||
<>
|
||
<Button asChild>
|
||
<Link
|
||
href={`/org/${orgSlug}/billing?panel=subscriptionPlan&source=planSupportExpectationInfoBox`}
|
||
>
|
||
Upgrade plan
|
||
</Link>
|
||
</Button>
|
||
<Button asChild type="default" icon={<ExternalLink />}>
|
||
<Link href="https://supabase.com/contact/enterprise" target="_blank" rel="noreferrer">
|
||
Enquire about Enterprise
|
||
</Link>
|
||
</Button>
|
||
</>
|
||
)
|
||
}
|
||
/>
|
||
)
|
||
}
|