diff --git a/apps/design-system/__registry__/index.tsx b/apps/design-system/__registry__/index.tsx index 1a6766deeab..b88ab4709bb 100644 --- a/apps/design-system/__registry__/index.tsx +++ b/apps/design-system/__registry__/index.tsx @@ -148,6 +148,17 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "alert-dialog-close-only": { + name: "alert-dialog-close-only", + type: "components:example", + registryDependencies: ["alert-dialog","button"], + component: React.lazy(() => import("@/registry/default/example/alert-dialog-close-only")), + source: "", + files: ["registry/default/example/alert-dialog-close-only.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "aspect-ratio-demo": { name: "aspect-ratio-demo", type: "components:example", @@ -159,6 +170,28 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "alert-dialog-destructive": { + name: "alert-dialog-destructive", + type: "components:example", + registryDependencies: ["alert-dialog","button"], + component: React.lazy(() => import("@/registry/default/example/alert-dialog-destructive")), + source: "", + files: ["registry/default/example/alert-dialog-destructive.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "alert-dialog-warning": { + name: "alert-dialog-warning", + type: "components:example", + registryDependencies: ["alert-dialog","button"], + component: React.lazy(() => import("@/registry/default/example/alert-dialog-warning")), + source: "", + files: ["registry/default/example/alert-dialog-warning.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "avatar-demo": { name: "avatar-demo", type: "components:example", @@ -1237,6 +1270,17 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "sheet-nonmodal": { + name: "sheet-nonmodal", + type: "components:example", + registryDependencies: ["sheet"], + component: React.lazy(() => import("@/registry/default/example/sheet-nonmodal")), + source: "", + files: ["registry/default/example/sheet-nonmodal.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "sheet-side": { name: "sheet-side", type: "components:example", @@ -1820,39 +1864,6 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, - "text-confirm-dialog-with-info-alert": { - name: "text-confirm-dialog-with-info-alert", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/text-confirm-dialog-with-info-alert")), - source: "", - files: ["registry/default/example/text-confirm-dialog-with-info-alert.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "text-confirm-dialog-with-warning-alert": { - name: "text-confirm-dialog-with-warning-alert", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/text-confirm-dialog-with-warning-alert")), - source: "", - files: ["registry/default/example/text-confirm-dialog-with-warning-alert.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "text-confirm-dialog-with-destructive-alert": { - name: "text-confirm-dialog-with-destructive-alert", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/text-confirm-dialog-with-destructive-alert")), - source: "", - files: ["registry/default/example/text-confirm-dialog-with-destructive-alert.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, "text-confirm-dialog-with-size": { name: "text-confirm-dialog-with-size", type: "components:example", @@ -2458,46 +2469,13 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, - "modal-demo": { - name: "modal-demo", + "confirmation-modal-demo": { + name: "confirmation-modal-demo", type: "components:example", registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/modal-demo")), + component: React.lazy(() => import("@/registry/default/example/confirmation-modal-demo")), source: "", - files: ["registry/default/example/modal-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "modal-aligned-footer": { - name: "modal-aligned-footer", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/modal-aligned-footer")), - source: "", - files: ["registry/default/example/modal-aligned-footer.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "modal-custom-footer": { - name: "modal-custom-footer", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/modal-custom-footer")), - source: "", - files: ["registry/default/example/modal-custom-footer.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "modal-hide-footer": { - name: "modal-hide-footer", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/modal-hide-footer")), - source: "", - files: ["registry/default/example/modal-hide-footer.tsx"], + files: ["registry/default/example/confirmation-modal-demo.tsx"], category: "undefined", subcategory: "undefined", chunks: [] @@ -3005,7 +2983,7 @@ export const Index: Record = { source: "", files: ["registry/default/example/copy-error-messages.tsx"], category: "Getting Started", - subcategory: "Copywriting", + subcategory: "Copywriting", chunks: [] }, "copy-success-messages": { @@ -3060,7 +3038,7 @@ export const Index: Record = { source: "", files: ["registry/default/example/copy-confirmations.tsx"], category: "Getting Started", - subcategory: "Copywriting", + subcategory: "Copywriting", chunks: [] }, }, diff --git a/apps/design-system/app/(app)/page.tsx b/apps/design-system/app/(app)/page.tsx index 9540c812a46..377ca6bdd09 100644 --- a/apps/design-system/app/(app)/page.tsx +++ b/apps/design-system/app/(app)/page.tsx @@ -1,7 +1,7 @@ import { HomepageSvgHandler } from '@/components/homepage-svg-handler' -import Link from 'next/link' +import { Auth, Database, Realtime } from 'icons/src/icons' import { Paintbrush } from 'lucide-react' -import { Realtime, Database, Auth } from 'icons/src/icons' +import Link from 'next/link' export default function Home() { return ( @@ -15,41 +15,6 @@ export default function Home() {
- -
-
- -
-
-

Atom components

-

Building blocks of User interfaces

-
-
- - -
-
- -
-
-

Fragment components

-

Components assembled from Atoms

-
-
- - -
-
- -
-
-

UI Patterns components

-

- Components assembled from Atoms & Fragments -

-
-
-
@@ -57,19 +22,7 @@ export default function Home() {

Colors

-

Building blocks of User interfaces

-
-
- - - -
-
- -
-
-

Theming

-

Simple extensible theming system

+

Custom color palette for Supabase

@@ -87,6 +40,55 @@ export default function Home() {
+ + +
+
+ +
+
+

Theming

+

Simple extensible theming system

+
+
+ + +
+
+ +
+
+

UI patterns

+

+ Design guidelines for common interface patterns +

+
+
+ + + +
+
+ +
+
+

Fragment components

+

Components assembled from atoms

+
+
+ + + +
+
+ +
+
+

Atom components

+

Building blocks of user interfaces

+
+
+ diff --git a/apps/design-system/components/component-preview.tsx b/apps/design-system/components/component-preview.tsx index 4dcfe8c56c0..a16b7377c0c 100644 --- a/apps/design-system/components/component-preview.tsx +++ b/apps/design-system/components/component-preview.tsx @@ -1,8 +1,11 @@ 'use client' -import * as React from 'react' import { Index } from '@/__registry__' +import * as React from 'react' +import { useConfig } from '@/hooks/use-config' +import { styles } from '@/registry/styles' +import { ChevronRight, Expand } from 'lucide-react' import { Button, CollapsibleContent_Shadcn_, @@ -10,9 +13,6 @@ import { Collapsible_Shadcn_, cn, } from 'ui' -import { useConfig } from '@/hooks/use-config' -import { styles } from '@/registry/styles' -import { ChevronRight, Expand } from 'lucide-react' interface ComponentPreviewProps extends React.HTMLAttributes { name: string @@ -70,7 +70,7 @@ export function ComponentPreview({ return ( <>
## Installation @@ -85,3 +88,32 @@ import { ``` + +## Behavior + +Unlike a generic [Dialog](../components/dialog), an Alert Dialog cannot be dismissed by clicking outside the modal. The user must take an explicit action by confirming, cancelling, or pressing Escape. + +This enforced decision helps prevent accidental dismissal of critical warnings or destructive actions. + +## Guidelines + +- **Keep content concise:** AlertDialogDescription renders as a single paragraph and must not contain block-level elements such as lists, multiple paragraphs, or complex layouts. +- **Use for critical decisions only:** Reserve Alert Dialog for destructive or irreversible actions, or for warnings that require explicit acknowledgement. +- **Always provide a cancel action:** Include AlertDialogCancel so users can safely back out, in addition to supporting the Escape key. +- **Avoid rich content:** If the dialog requires detailed explanations, callouts, or form inputs, use [Confirmation Modal](../fragments/confirmation-modal) or [Dialog](../components/dialog) instead. + +See [Modality](../ui-patterns/modality) for guidance on choosing the appropriate dialog pattern. + +## Examples + +### Close only + + + +### Warning + + + +### Destructive + + diff --git a/apps/design-system/content/docs/components/dialog.mdx b/apps/design-system/content/docs/components/dialog.mdx index b8f9ae549d6..108d9852e05 100644 --- a/apps/design-system/content/docs/components/dialog.mdx +++ b/apps/design-system/content/docs/components/dialog.mdx @@ -1,7 +1,6 @@ --- title: Dialog -description: A window overlaid on either the primary window or another dialog window, rendering the content underneath inert. -featured: true +description: A general-purpose modal for non-critical flows, forms, and custom interactions. component: true links: doc: https://www.radix-ui.com/docs/primitives/components/dialog @@ -11,6 +10,20 @@ source: shadcn: true --- +Dialog is a flexible, general-purpose modal used for bespoke interactions such as forms, pickers, multi-step flows, or displaying non-urgent information. Unlike confirmation-focused dialogs, it is designed to be safely dismissible and does not force an explicit decision. + +Dialog can be closed by clicking outside the modal or pressing the Escape key, making it suitable for workflows where cancellation is expected and low-risk. + +Use Dialog when you need full control over layout, content, and behavior, and the interaction does not involve a critical or destructive action. + +For confirmations or warnings, try to use an existing component: + +- Use [Alert Dialog](../components/alert-dialog) for critical confirmations that require an explicit decision +- Use [Confirmation Modal](../fragments/confirmation-modal) when additional context is needed for a confirmation +- Use [Text Confirm Dialog](../fragments/text-confirm-dialog) for highly destructive actions that require typed intent + +See [Modality](../ui-patterns/modality) for guidance on choosing the appropriate dialog pattern. + ## Usage @@ -31,23 +44,31 @@ import { Open - Are you absolutely sure? + Project settings - This action cannot be undone. This will permanently delete your account and remove your data - from our servers. + Update configuration options for this project. Changes can be discarded at any time. + {/* Custom content goes here */} ``` +## Guidelines + +- **Use for non-critical interactions:** Dialog is appropriate when dismissal has no serious consequences. +- **Design for cancellation**: Assume users may close the dialog without completing the action. +- **Keep focus contained**: Dialog content should remain scoped to a single task or flow. +- **Avoid destructive confirmations**: If the dialog’s primary purpose is to confirm a risky action, use a confirmation-focused pattern instead. +- **Compose freely**: Dialog is intentionally unopinionated. Build custom layouts, forms, or step-based flows as needed. + ## Examples ### Custom close button -### Centered behaviour +### Centered behavior You can control whether the dialog is centered by passing `centered={false}` to the `DialogContent` component. @@ -69,30 +90,27 @@ You can control whether the dialog is centered by passing `centered={false}` to To activate the `Dialog` component from within a `Context Menu` or `Dropdown Menu`, you must encase the `Context Menu` or `Dropdown Menu` component in the `Dialog` component. For more information, refer to the linked issue [here](https://github.com/radix-ui/primitives/issues/1836). -```tsx {14-25} +```tsx {7-11, 14-23} - Dialog not centered + Show Menu Open Download - Delete + Show Dialog - Are you absolutely sure? - - This action cannot be undone. Are you sure you want to permanently delete this file from our - servers? - + Edit profile + Make changes to your profile here. - + diff --git a/apps/design-system/content/docs/components/atom-components.mdx b/apps/design-system/content/docs/components/introduction.mdx similarity index 100% rename from apps/design-system/content/docs/components/atom-components.mdx rename to apps/design-system/content/docs/components/introduction.mdx diff --git a/apps/design-system/content/docs/components/sheet.mdx b/apps/design-system/content/docs/components/sheet.mdx index 96220e63eb5..75edbb53860 100644 --- a/apps/design-system/content/docs/components/sheet.mdx +++ b/apps/design-system/content/docs/components/sheet.mdx @@ -12,6 +12,22 @@ source: +1. **Use for side panels** + + - Forms with multiple fields + - Settings panels + - Detailed editors + +2. **Consider screen size** + + - Sheets work well on desktop + - On mobile, consider full-screen or bottom sheet variants + +3. **Structure content clearly** + - Use `SheetHeader` and `SheetTitle` for context + - Use `SheetSection` to group related fields + - Use `SheetFooter` for actions + ## Installation @@ -80,11 +96,21 @@ import { ## Examples -### Side +### Nonmodal -Use the `side` property to `` to indicate the edge of the screen where the component will appear. The values can be `top`, `right`, `bottom` or `left`. +This sheet is nonmodal, meaning it does not block the underlying content. It’s useful when +you want to display content that complements the main content of the screen. - + + +To have the underlying content resize to fit the sheet (so nothing is overlapping) use the Sidebar component +or build a custom panel. You can refer to the following Studio components for guidance: + +- `AIAssistant` +- `EditorPanel` +- `AdvisorPanel` + +See [`LayoutSidebarProvider`](https://github.com/supabase/supabase/blob/master/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider.tsx) for more. ### Size @@ -104,3 +130,11 @@ You can adjust the size of the sheet using CSS classes: ``` + +### Side + +Use the `side` property to `` to indicate the edge of the screen where the component will appear. The values can be `top`, `right`, `bottom` or `left`. + + + +That said, stick to the default `right` unless you have a strong reason not to. diff --git a/apps/design-system/content/docs/fragments/confirmation-modal.mdx b/apps/design-system/content/docs/fragments/confirmation-modal.mdx new file mode 100644 index 00000000000..dfe727fa171 --- /dev/null +++ b/apps/design-system/content/docs/fragments/confirmation-modal.mdx @@ -0,0 +1,71 @@ +--- +title: Confirmation Modal +description: A modal dialog for confirmations that require additional context or simple interaction. +component: true +--- + +Confirmation Modal is a convenience wrapper for confirmation flows that are more complex than a single paragraph but do not warrant a full custom dialog. It is built on top of [Dialog](../components/dialog) and provides a prop-based API for consistent confirmation patterns. + +Use Confirmation Modal when the user needs extra context to make a decision, such as explanatory copy, callouts, or small form elements, and the action is not so destructive that it requires typed confirmation. + +If the confirmation can be expressed as a single short paragraph, use [Alert Dialog](../components/alert-dialog). If the action is highly destructive and requires explicit typed intent, use [Text Confirm Dialog](../fragments/text-confirm-dialog). See [Modality](../ui-patterns/modality) for broader guidance on choosing the appropriate pattern. + + + +## Usage + +```tsx +'use client' + +import { useState } from 'react' +import { Button } from 'ui' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +``` + +```tsx +export default function ConfirmationModalDemo() { + const [visible, setVisible] = useState(false) + + return ( + <> + + + { + setVisible(false) + }} + onCancel={() => { + setVisible(false) + }} + > + This will resume the project and restart any paused processes. + + + ) +} +``` + +## Guidelines + +- **Use for moderate complexity:** Suitable when the confirmation requires more than a single sentence but does not need typed intent. +- **Avoid critical destruction:** Do not use for irreversible or high-risk actions that could benefit from stronger safeguards. +- **Keep content focused:** Include only the context needed to make the decision. If the dialog becomes a full flow, use a custom [Dialog](../components/dialog) instead. +- **Provide clear actions:** Ensure confirm and cancel labels clearly describe the outcome of each choice. + +## Props + +- `visible`: Controls open state +- `title`: Dialog title +- `description`: Optional description +- `variant`: 'default' | 'destructive' | 'warning' +- `loading`: Loading state +- `onConfirm`: Confirm handler +- `onCancel`: Cancel handler +- `alert`: Optional callout (see [Admonition](../fragments/admonition)) +- `children`: Additional content diff --git a/apps/design-system/content/docs/fragments/empty-state-presentational.mdx b/apps/design-system/content/docs/fragments/empty-state-presentational.mdx index e0936a16104..361be6250cb 100644 --- a/apps/design-system/content/docs/fragments/empty-state-presentational.mdx +++ b/apps/design-system/content/docs/fragments/empty-state-presentational.mdx @@ -1,5 +1,5 @@ --- -title: EmptyStatePresentational +title: Empty State Presentational description: An empty state for encouraging action. component: true fragment: true @@ -17,7 +17,7 @@ All text should be written using active language. The title should prompt the us ### Icon -Supports both Lucide icons and [custom icons](../icons) via the `icons` package. If neither are passed, EmptyStatePresentational falls back to Lucide’s `SquarePlus`. +Supports both Lucide icons and [custom icons](../icons) via the `icons` package. If neither are passed, Empty State Presentational falls back to Lucide’s `SquarePlus`. @@ -25,10 +25,10 @@ See also [Empty States](../ui-patterns/empty-states). ## Examples -It’s okay to repeat buttons inside of EmptyStatePresentational that are also available outside of it. The alternative is to conditionally determine button placement whilst polling for list length (to determine whether to show an empty state or not). This is problematic for two reasons: +It’s okay to repeat buttons inside of Empty State Presentational that are also available outside of it. The alternative is to conditionally determine button placement whilst polling for list length (to determine whether to show an empty state or not). This is problematic for two reasons: 1. Rendering after client-side polling often leads to confusing layout shift. This layout shift becomes exacerbated when buttons are stacked against other objects. -2. Consistent entry points outside of EmptyStatePresentational also teach a pattern that will continue to exist post initial object creation. +2. Consistent entry points outside of Empty State Presentational also teach a pattern that will continue to exist post initial object creation. When repeating buttons, set the `type` to `default` so the original `primary`, button remains the only `primary` action on display. diff --git a/apps/design-system/content/docs/fragments/fragment-components.mdx b/apps/design-system/content/docs/fragments/introduction.mdx similarity index 100% rename from apps/design-system/content/docs/fragments/fragment-components.mdx rename to apps/design-system/content/docs/fragments/introduction.mdx diff --git a/apps/design-system/content/docs/fragments/modal.mdx b/apps/design-system/content/docs/fragments/modal.mdx deleted file mode 100644 index 2bbd59837cf..00000000000 --- a/apps/design-system/content/docs/fragments/modal.mdx +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: Modal -description: A window overlaid on either the primary window or another dialog window, rendering the content underneath inert. -fragment: true -links: - doc: https://www.radix-ui.com/docs/primitives/components/dialog - api: https://www.radix-ui.com/docs/primitives/components/dialog#api-reference ---- - - - -## Examples - -### Aligned footer - - - -### Hide footer - - - -### Custom footer - - diff --git a/apps/design-system/content/docs/fragments/text-confirm-dialog.mdx b/apps/design-system/content/docs/fragments/text-confirm-dialog.mdx index f9127a0ebe7..4b812ead978 100644 --- a/apps/design-system/content/docs/fragments/text-confirm-dialog.mdx +++ b/apps/design-system/content/docs/fragments/text-confirm-dialog.mdx @@ -1,57 +1,73 @@ --- title: Text Confirm Dialog -description: A modal dialog that interrupts the user with important content and expects a response. +description: A modal dialog that adds a deliberate confirmation step for highly destructive actions. component: true --- - +Text Confirm Dialog adds a deliberate “speed bump” before a highly destructive action by requiring the user to type an exact confirmation string before the confirm action is enabled. It wraps the Shadcn [Dialog](../components/dialog) component and is intended for actions that must not be triggered accidentally. + +Use Text Confirm Dialog for irreversible operations such as deleting buckets, projects, or other critical resources where an explicit signal of user intent is required beyond a button click. + +For non-destructive or less critical confirmations, use [Alert Dialog](../components/alert-dialog) or [Confirmation Modal](../fragments/confirmation-modal) instead. See [Modality](../ui-patterns/modality) for guidance on choosing the appropriate dialog pattern. + + + +## Usage + +```tsx +'use client' + +import { useState } from 'react' +import { Button } from 'ui' +import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal' +``` + +```tsx +export default function TextConfirmDialogDemo() { + const [visible, setVisible] = useState(false) + const bucketName = 'profile-pictures' + + return ( + <> + + + setVisible(false)} + onCancel={() => setVisible(false)} + > + {/* Optional body content */} + + + ) +} +``` + +## Props + +- `confirmString`: The exact string the user must type to enable the confirm action +- `confirmPlaceholder`: Placeholder text shown in the confirmation input +- `variant`: Visual intent of the dialog (`default`, `destructive`, or `warning`) +- Other standard modal props inherited from the underlying [Dialog](../components/dialog) component ## Examples -### With Info Alert +### Cancel button - + -### With warning Alert +### Children - + -### With destructive Alert +### Size - - -### With cancel button - - - -### With children - - - -### With size - - + diff --git a/apps/design-system/content/docs/ui-patterns/empty-states.mdx b/apps/design-system/content/docs/ui-patterns/empty-states.mdx index 851fda3068e..f4dc21628e5 100644 --- a/apps/design-system/content/docs/ui-patterns/empty-states.mdx +++ b/apps/design-system/content/docs/ui-patterns/empty-states.mdx @@ -3,28 +3,30 @@ title: Empty states description: Convey the absence of data and provide clear instruction for what to do about it. --- -Empty states convey the fact that there is nothing to list, perform, or display on the current page. **Ideally**, they also provide a clear action for the user to take. +Empty states convey the fact that there is nothing to list, perform, or display on the current page. Ideally, they also provide a clear action for the user to take. -## No data +## Best practices + +### No data There are two ways an empty state may be displayed in cases where there is no data: - **Initial state**: no data to begin with - **Zero results**: no data after a search or filter -### Initial state +#### Initial state Perhaps the user has not yet created any data. The presentation of this empty state depends on the context of the list and the type of data it contains. Be mindful of the journey to rendering an empty state and any possible layout shift along the way. -#### Presentational +##### Presentational -The user may be learning about a feature for the first time, and could benefit from lightweight feature education or onboarding. Use the dedicated [EmptyStatePresentational](../fragments/empty-state-presentational) component in this case, putting emphasis on an action the user can take. +The user may be learning about a feature for the first time, and could benefit from lightweight feature education or onboarding. Use the dedicated [Empty State Presentational](../fragments/empty-state-presentational) component in this case, putting emphasis on an action the user can take. Remember to use active language in presentational empty states. For example: “Create a vector bucket” instead of “No vector buckets found”. The latter is more appropriate in table-based presentations, as described below. -#### Informational +##### Informational Or perhaps the list type is data-heavy or does not benefit from additional information. In these cases, the empty state should provide show the initial state in the same presentation as the list when there is data, much like the [zero results](#zero-results) scenario. @@ -32,11 +34,11 @@ Or perhaps the list type is data-heavy or does not benefit from additional infor Keep in mind that empty states will likely appear after a visual loading state. Consider layout shift and button placement during and after the transition. -### Zero results +#### Zero results Data-heavy presentations without results should have an empty state that broadly matches the state when there is data. This makes the transition between the two states more seamless. -#### Table +##### Table A [Table](../components/table) instance with zero results should display a single row. Dulling the TableHead text color and removing the TableCell hover state can further reinforce the lack of usable data. @@ -47,7 +49,7 @@ Studio contains two pre-built components to handle these cases consistently: - No Filter Results - No Search Results -#### Data Grid +##### Data Grid [Data Grid](../ui-patterns/tables#data-grid) and [Data Table](../ui-patterns/tables#data-table) component patterns typically span the full height and width of a container. A classic example is [Users](https://supabase.com/dashboard/project/_/auth/users), which (as it sounds) displays a list of the project’s registered users. Any instance with zero results should display a more prominent empty with a clear title, description, and supporting illustration. @@ -55,7 +57,7 @@ Studio contains two pre-built components to handle these cases consistently: Other Data Grid instances include [Cron Jobs](https://supabase.com/dashboard/project/_/integrations/cron/jobs) and [Queues](https://supabase.com/dashboard/project/_/integrations/queues). -## Missing route +### Missing route Users may accidentally navigate to a non-existent dynamic route, such as a non-existent bucket in [Storage](https://supabase.com/dashboard/project/_/storage) or a non-existent table in the [Table Editor](https://supabase.com/dashboard/project/_/editor). In these cases, follow the pattern of a centered [Admonition](../fragments/admonition) as shown below. @@ -63,6 +65,6 @@ Users may accidentally navigate to a non-existent dynamic route, such as a non-e ## Components -For presentational empty states (initial states with value propositions and actions), use the [EmptyStatePresentational](../fragments/empty-state-presentational) component from `ui-patterns`. This component provides a consistent structure with support for icons, titles, descriptions, and action buttons. +For presentational empty states (initial states with value propositions and actions), use the [Empty State Presentational](../fragments/empty-state-presentational) component from `ui-patterns`. This component provides a consistent structure with support for icons, titles, descriptions, and action buttons. For other empty state scenarios (zero results, missing routes, etc), custom components may still be appropriate as the context and needs for each placement can differ significantly. diff --git a/apps/design-system/content/docs/ui-patterns/ui-patterns.mdx b/apps/design-system/content/docs/ui-patterns/introduction.mdx similarity index 100% rename from apps/design-system/content/docs/ui-patterns/ui-patterns.mdx rename to apps/design-system/content/docs/ui-patterns/introduction.mdx diff --git a/apps/design-system/content/docs/ui-patterns/modality.mdx b/apps/design-system/content/docs/ui-patterns/modality.mdx new file mode 100644 index 00000000000..3da04913adf --- /dev/null +++ b/apps/design-system/content/docs/ui-patterns/modality.mdx @@ -0,0 +1,76 @@ +--- +title: Modality +description: Present ephemeral information and demand action. +--- + +Modal elements interrupt the user’s current task to ask for input, a decision, or focused attention. They appear at the top of the visual stack and (by default) render everything beneath them inactive. + +Given their highly interruptive nature, modal elements should be used sparingly. Common use cases include: + +- Requiring confirmation from the user +- Requiring an ephemeral form submission from the user before an action can be completed +- Alerting or slowing the user down before a destructive action + +We have two main ways of handling modality: + +- [Dialogs](#dialogs) +- [Sheets](#sheets) + +As a general rule: use dialogs for short, focused tasks and use sheets for longer forms or more detailed views. + +## Dialogs + +Dialogs are centered overlays used for short, focused tasks. All dialogs should follow these best practices: + +- **Reiterative:** Dialog header and confirmation button text and should match the action and flow on from the entry point. +- **Simple:** No layered elements like subtitles or admonitions unless necessary. Put all the focus on the actions to get out of the dialog. +- **Accessible:** Always provide clear labels and descriptions via semantic HTML and the correct ARIA attributes. Ensure keyboard navigation works correctly. + +### Components + +There are quite a few dialog components, each suited to a different task or context: + +- [Alert Dialog](../components/alert-dialog) contains a single, short paragraph and an explicit action. +- [Text Confirm Dialog](../fragments/text-confirm-dialog) requires a textual response before the action is enabled. +- [Confirmation Modal](../fragments/confirmation-modal) provides more flexible dialog body contents. +- [Dialog](../components/dialog) is a generalized component for bespoke purposes. + +#### Alert Dialog + +[Alert Dialog](../components/alert-dialog) is used to confirm or acknowledge a critical action with a single, short paragraph and a clear decision. + + + +#### Text Confirm Dialog + +[Text Confirm Dialog](../fragments/text-confirm-dialog) adds a deliberate speed bump for highly destructive actions by requiring the user to type an exact confirmation string before proceeding. + + + +#### Confirmation Modal + +[Confirmation Modal](../fragments/confirmation-modal) is a convenience wrapper for less-critical confirmations that require more than a single paragraph, such as additional context, callouts, or simple form elements. + + + +#### Dialog + +[Dialog](../components/dialog) is a general-purpose modal for bespoke flows such as forms, pickers, or non-critical interactions where dismissal is acceptable. + + + +## Sheets + +Sheets are dialogs presented as side panels. Use them for content that is larger than a few fields, or when a centered dialog would feel cramped. + +- **Use for**: multi-field forms, editors, settings panels, and detailed views. +- **Prefer the default**: sheets slide in from the right unless you have a strong reason to use another side. +- **Group content**: use header/sections/footer so the user can scan and act quickly. + +### Components + +#### Sheet + +[Sheet](../components/sheet) is modal by default, blocking interaction with the underlying page. + + diff --git a/apps/design-system/registry/default/example/alert-dialog-close-only.tsx b/apps/design-system/registry/default/example/alert-dialog-close-only.tsx new file mode 100644 index 00000000000..204d3e3b970 --- /dev/null +++ b/apps/design-system/registry/default/example/alert-dialog-close-only.tsx @@ -0,0 +1,33 @@ +import { + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, + Button, +} from 'ui' + +export default function AlertDialogCloseOnly() { + return ( + + + + + + + Application submitted + + Thank you for your submission! Please check your email for a confirmation link to + complete your application. + + + + Close + + + + ) +} diff --git a/apps/design-system/registry/default/example/alert-dialog-demo.tsx b/apps/design-system/registry/default/example/alert-dialog-demo.tsx index 38f358327a4..447be8975eb 100644 --- a/apps/design-system/registry/default/example/alert-dialog-demo.tsx +++ b/apps/design-system/registry/default/example/alert-dialog-demo.tsx @@ -8,26 +8,27 @@ import { AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, + Button, } from 'ui' -import { Button } from 'ui' export default function AlertDialogDemo() { return ( - + - Are you absolutely sure? + Create new API keys - This action cannot be undone. This will permanently delete your account and remove your - data from our servers. + This will create a default publishable key and a default secret key both named{' '} + default. These keys are required to connect + your application to your Supabase project. Cancel - Continue + Create keys diff --git a/apps/design-system/registry/default/example/alert-dialog-destructive.tsx b/apps/design-system/registry/default/example/alert-dialog-destructive.tsx new file mode 100644 index 00000000000..6edea2532a1 --- /dev/null +++ b/apps/design-system/registry/default/example/alert-dialog-destructive.tsx @@ -0,0 +1,37 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, + Button, +} from 'ui' + +export default function AlertDialogDestructive() { + return ( + + + + + + + + Delete hello-world + + + This action cannot be undone. Ensure that you have a backup in case you want to restore + this edge function. + + + + Cancel + Delete + + + + ) +} diff --git a/apps/design-system/registry/default/example/alert-dialog-warning.tsx b/apps/design-system/registry/default/example/alert-dialog-warning.tsx new file mode 100644 index 00000000000..8b95feaa82a --- /dev/null +++ b/apps/design-system/registry/default/example/alert-dialog-warning.tsx @@ -0,0 +1,35 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, + Button, +} from 'ui' + +export default function AlertDialogWarning() { + return ( + + + + + + + Update branch + + This branch has 3 modified edge functions that will be overwritten when updating with + the latest functions from the production branch. This action cannot be undone. + + + + Cancel + Update + + + + ) +} diff --git a/apps/design-system/registry/default/example/confirmation-modal-demo.tsx b/apps/design-system/registry/default/example/confirmation-modal-demo.tsx new file mode 100644 index 00000000000..dc1d91e32d1 --- /dev/null +++ b/apps/design-system/registry/default/example/confirmation-modal-demo.tsx @@ -0,0 +1,75 @@ +import { useState } from 'react' +import { useForm } from 'react-hook-form' +import { + Button, + Form_Shadcn_, + FormControl_Shadcn_, + FormField_Shadcn_, + Select_Shadcn_, + SelectContent_Shadcn_, + SelectItem_Shadcn_, + SelectTrigger_Shadcn_, + SelectValue_Shadcn_, +} from 'ui' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' + +export default function ConfirmationModalDemo() { + const [visible, setVisible] = useState(false) + const form = useForm({ + defaultValues: { + postgresVersion: '17.6.1.054', + }, + }) + + return ( + <> + + setVisible(false)} + onConfirm={() => {}} + loading={false} + confirmLabel="Resume" + confirmLabelLoading="Resuming" + cancelLabel="Cancel" + > + {/* Dialog contents */} +
+ {/* Text content */} +

+ Your project’s data will be restored to when it was initially paused. +

+ {/* Dropdown for Postgres version */} +
+ + ( + + + + + + + + 17.6.1.054 + 17.6.1.055 + + + + + )} + /> + +
+
+
+ + ) +} diff --git a/apps/design-system/registry/default/example/dialog-centered-off.tsx b/apps/design-system/registry/default/example/dialog-centered-off.tsx index 54713321a25..fd32ad1d7a4 100644 --- a/apps/design-system/registry/default/example/dialog-centered-off.tsx +++ b/apps/design-system/registry/default/example/dialog-centered-off.tsx @@ -1,29 +1,31 @@ -import { Button, DialogSection, DialogSectionSeparator } from 'ui' import { + Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, + DialogSection, + DialogSectionSeparator, DialogTitle, DialogTrigger, + Input_Shadcn_, + Label_Shadcn_, } from 'ui' -import { Input_Shadcn_ } from 'ui' -import { Label_Shadcn_ } from 'ui' export default function DialogDemo() { return ( - + - + This dialog is not centered. This dialog is not centered. - +
Name @@ -37,7 +39,7 @@ export default function DialogDemo() {
- +
diff --git a/apps/design-system/registry/default/example/dialog-close-button.tsx b/apps/design-system/registry/default/example/dialog-close-button.tsx index 4c45847ef7b..1194933899b 100644 --- a/apps/design-system/registry/default/example/dialog-close-button.tsx +++ b/apps/design-system/registry/default/example/dialog-close-button.tsx @@ -1,18 +1,20 @@ import { Copy } from 'lucide-react' -import { Button, DialogSection, DialogSectionSeparator } from 'ui' import { + Button, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, + DialogSection, + DialogSectionSeparator, DialogTitle, DialogTrigger, + Input_Shadcn_, + Label_Shadcn_, } from 'ui' -import { Input_Shadcn_ } from 'ui' -import { Label_Shadcn_ } from 'ui' export default function DialogCloseButton() { return ( @@ -21,12 +23,12 @@ export default function DialogCloseButton() { - + Share link Anyone who has this link will be able to view this. - +
@@ -44,7 +46,7 @@ export default function DialogCloseButton() {
- + + - + Edit profile - - Make changes to your profile here. Click save when you're done. - + Make changes to your profile here. - +
- - Name - - + Name +
- - Username - - + Username +
- - + +
diff --git a/apps/design-system/registry/default/example/drawer-dialog.tsx b/apps/design-system/registry/default/example/drawer-dialog.tsx index a89ce2117b8..599cd5bd8b0 100644 --- a/apps/design-system/registry/default/example/drawer-dialog.tsx +++ b/apps/design-system/registry/default/example/drawer-dialog.tsx @@ -2,18 +2,16 @@ import * as React from 'react' -import { cn } from '@/lib/utils' import { useMediaQuery } from '@/hooks/use-media-query' -import { Button } from 'ui' +import { cn } from '@/lib/utils' import { + Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, -} from 'ui' -import { Drawer, DrawerClose, DrawerContent, @@ -22,9 +20,9 @@ import { DrawerHeader, DrawerTitle, DrawerTrigger, + Input_Shadcn_, + Label_Shadcn_, } from 'ui' -import { Input_Shadcn_ } from 'ui' -import { Label_Shadcn_ } from 'ui' export default function DrawerDialogDemo() { const [open, setOpen] = React.useState(false) @@ -34,7 +32,7 @@ export default function DrawerDialogDemo() { return ( - + @@ -52,7 +50,7 @@ export default function DrawerDialogDemo() { return ( - + diff --git a/apps/design-system/registry/default/example/modal-demo.tsx b/apps/design-system/registry/default/example/modal-demo.tsx deleted file mode 100644 index f11c441568b..00000000000 --- a/apps/design-system/registry/default/example/modal-demo.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Link2 } from 'lucide-react' -import { useState } from 'react' -import { Button, Modal } from 'ui' - -export default function ModalDemo() { - const [visible, setVisible] = useState(false) - - return ( - <> - - setVisible(!visible)} - onConfirm={() => setVisible(!visible)} - title="This is the title of the modal" - description="And i am the description" - size="medium" - hideClose={false} - header={ -
-
- -
-
-

This is the title

- This is the title -
-
- } - > - -

- Modal content is inserted here, if you need to insert anything into the Modal you can do - so via `children`. -

-
-
- - ) -} diff --git a/apps/design-system/registry/default/example/sheet-demo.tsx b/apps/design-system/registry/default/example/sheet-demo.tsx index 8dd0044aadb..174eb0eb253 100644 --- a/apps/design-system/registry/default/example/sheet-demo.tsx +++ b/apps/design-system/registry/default/example/sheet-demo.tsx @@ -1,11 +1,14 @@ -import { Button, Input_Shadcn_, Label_Shadcn_ } from 'ui' import { + Button, + Input_Shadcn_, + Label_Shadcn_, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, + SheetSection, SheetTitle, SheetTrigger, } from 'ui' @@ -14,34 +17,34 @@ export default function SheetDemo() { return ( - + - + Edit profile - - Make changes to your profile here. Click save when you re done. - + Make changes to your profile here. -
-
- - Name - - -
-
- - Username - - -
+
+ +
+
+ + Name + + +
+
+ + Username + + +
+
+
- + diff --git a/apps/design-system/registry/default/example/sheet-nonmodal.tsx b/apps/design-system/registry/default/example/sheet-nonmodal.tsx new file mode 100644 index 00000000000..9129bc48ddd --- /dev/null +++ b/apps/design-system/registry/default/example/sheet-nonmodal.tsx @@ -0,0 +1,38 @@ +import { + Button, + Sheet, + SheetClose, + SheetContent, + SheetFooter, + SheetHeader, + SheetSection, + SheetTitle, + SheetTrigger, +} from 'ui' + +export default function SheetNonmodal() { + return ( + + + + + + + Log details + +
+ +

+ This sheet does not block the underlying content, but it does overlap it. +

+
+
+ + + + + +
+
+ ) +} diff --git a/apps/design-system/registry/default/example/sheet-side.tsx b/apps/design-system/registry/default/example/sheet-side.tsx index edbd6b1f4f2..bf2307665d5 100644 --- a/apps/design-system/registry/default/example/sheet-side.tsx +++ b/apps/design-system/registry/default/example/sheet-side.tsx @@ -1,9 +1,9 @@ 'use client' -import { Button } from 'ui' -import { Input_Shadcn_ } from 'ui' -import { Label_Shadcn_ } from 'ui' import { + Button, + Input_Shadcn_, + Label_Shadcn_, Sheet, SheetClose, SheetContent, @@ -24,7 +24,7 @@ export default function SheetSide() { {SHEET_SIDES.map((side) => ( - + diff --git a/apps/design-system/registry/default/example/text-confirm-dialog-demo.tsx b/apps/design-system/registry/default/example/text-confirm-dialog-demo.tsx index 9303fb27aaa..09292b5bfa0 100644 --- a/apps/design-system/registry/default/example/text-confirm-dialog-demo.tsx +++ b/apps/design-system/registry/default/example/text-confirm-dialog-demo.tsx @@ -1,46 +1,36 @@ 'use client' import { useState } from 'react' -import { toast } from 'sonner' - import { Button } from 'ui' import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal' -const TextConfirmModalPrimary = () => { +export default function TextConfirmDialogDemo() { const [visible, setVisible] = useState(false) - const [loading, setLoading] = useState(false) - - function onVisibleChange() { - setVisible(!visible) - } - - function onSubmit() { - setLoading(true) - setTimeout(() => { - setLoading(false) - setVisible(false) - toast('Updated project', { description: 'Friday, February 10, 2023 at 5:57 PM' }) - }, 3000) - } + const bucketName = 'profile-pictures' return ( <> - + + variant="destructive" + title="Delete bucket" + confirmPlaceholder={bucketName} + confirmString={bucketName} + confirmLabel="Delete bucket" + loading={false} + onConfirm={() => setVisible(false)} + onCancel={() => setVisible(false)} + > +

+ Your bucket {bucketName} and all of + its contents will be permanently deleted. This action cannot be undone. +

+
) } - -export default TextConfirmModalPrimary diff --git a/apps/design-system/registry/default/example/text-confirm-dialog-with-cancel-button.tsx b/apps/design-system/registry/default/example/text-confirm-dialog-with-cancel-button.tsx index 99a2328ef19..e629f3e7901 100644 --- a/apps/design-system/registry/default/example/text-confirm-dialog-with-cancel-button.tsx +++ b/apps/design-system/registry/default/example/text-confirm-dialog-with-cancel-button.tsx @@ -25,8 +25,8 @@ const TextConfirmModalWithCancelButton = () => { return ( <> - { return ( <> - { - const [visible, setVisible] = useState(false) - const [loading, setLoading] = useState(false) - - function onVisibleChange() { - setVisible(!visible) - } - - function onSubmit() { - setLoading(true) - setTimeout(() => { - setLoading(false) - setVisible(false) - toast('Updated project', { description: 'Friday, February 10, 2023 at 5:57 PM' }) - }, 3000) - } - - return ( - <> - - - - ) -} - -export default TextConfirmModalWithDestructiveAlert diff --git a/apps/design-system/registry/default/example/text-confirm-dialog-with-info-alert.tsx b/apps/design-system/registry/default/example/text-confirm-dialog-with-info-alert.tsx deleted file mode 100644 index fef97c1db9c..00000000000 --- a/apps/design-system/registry/default/example/text-confirm-dialog-with-info-alert.tsx +++ /dev/null @@ -1,50 +0,0 @@ -'use client' - -import { useState } from 'react' -import { toast } from 'sonner' - -import { Button } from 'ui' -import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal' - -const TextConfirmModalWithInfoAlert = () => { - const [visible, setVisible] = useState(false) - const [loading, setLoading] = useState(false) - - function onVisibleChange() { - setVisible(!visible) - } - - function onSubmit() { - setLoading(true) - setTimeout(() => { - setLoading(false) - setVisible(false) - toast('Updated project', { description: 'Friday, February 10, 2023 at 5:57 PM' }) - }, 3000) - } - - return ( - <> - - - - ) -} - -export default TextConfirmModalWithInfoAlert diff --git a/apps/design-system/registry/default/example/text-confirm-dialog-with-size.tsx b/apps/design-system/registry/default/example/text-confirm-dialog-with-size.tsx index a42f783a102..e0772b02821 100644 --- a/apps/design-system/registry/default/example/text-confirm-dialog-with-size.tsx +++ b/apps/design-system/registry/default/example/text-confirm-dialog-with-size.tsx @@ -25,8 +25,8 @@ const TextConfirmModalWithSize = () => { return ( <> - { - const [visible, setVisible] = useState(false) - const [loading, setLoading] = useState(false) - - function onVisibleChange() { - setVisible(!visible) - } - - function onSubmit() { - setLoading(true) - setTimeout(() => { - setLoading(false) - setVisible(false) - toast('Updated project', { - description: 'Friday, February 10, 2023 at 5:57 PM', - }) - }, 3000) - } - - return ( - <> - - - - ) -} - -export default TextConfirmModalWithWarningAlert diff --git a/apps/design-system/registry/examples.ts b/apps/design-system/registry/examples.ts index 6824a045953..699d249b682 100644 --- a/apps/design-system/registry/examples.ts +++ b/apps/design-system/registry/examples.ts @@ -43,6 +43,24 @@ export const examples: Registry = [ registryDependencies: ['alert-dialog', 'button'], files: ['example/alert-dialog-demo.tsx'], }, + { + name: 'alert-dialog-close-only', + type: 'components:example', + registryDependencies: ['alert-dialog', 'button'], + files: ['example/alert-dialog-close-only.tsx'], + }, + { + name: 'alert-dialog-destructive', + type: 'components:example', + registryDependencies: ['alert-dialog', 'button'], + files: ['example/alert-dialog-destructive.tsx'], + }, + { + name: 'alert-dialog-warning', + type: 'components:example', + registryDependencies: ['alert-dialog', 'button'], + files: ['example/alert-dialog-warning.tsx'], + }, { name: 'aspect-ratio-demo', type: 'components:example', @@ -731,6 +749,12 @@ export const examples: Registry = [ registryDependencies: ['sheet'], files: ['example/sheet-demo.tsx'], }, + { + name: 'sheet-nonmodal', + type: 'components:example', + registryDependencies: ['sheet'], + files: ['example/sheet-nonmodal.tsx'], + }, { name: 'sheet-side', type: 'components:example', @@ -1039,21 +1063,6 @@ export const examples: Registry = [ type: 'components:example', files: ['example/text-confirm-dialog-demo.tsx'], }, - { - name: 'text-confirm-dialog-with-info-alert', - type: 'components:example', - files: ['example/text-confirm-dialog-with-info-alert.tsx'], - }, - { - name: 'text-confirm-dialog-with-warning-alert', - type: 'components:example', - files: ['example/text-confirm-dialog-with-warning-alert.tsx'], - }, - { - name: 'text-confirm-dialog-with-destructive-alert', - type: 'components:example', - files: ['example/text-confirm-dialog-with-destructive-alert.tsx'], - }, { name: 'text-confirm-dialog-with-size', type: 'components:example', @@ -1360,24 +1369,9 @@ export const examples: Registry = [ files: ['example/tree-view-multi-select.tsx'], }, { - name: 'modal-demo', + name: 'confirmation-modal-demo', type: 'components:example', - files: ['example/modal-demo.tsx'], - }, - { - name: 'modal-aligned-footer', - type: 'components:example', - files: ['example/modal-aligned-footer.tsx'], - }, - { - name: 'modal-custom-footer', - type: 'components:example', - files: ['example/modal-custom-footer.tsx'], - }, - { - name: 'modal-hide-footer', - type: 'components:example', - files: ['example/modal-hide-footer.tsx'], + files: ['example/confirmation-modal-demo.tsx'], }, { name: 'assistant-chat-demo',