From bfe3ea06df7af0f145388ae8b885deaea43f7fce Mon Sep 17 00:00:00 2001 From: "kemal.earth" <606977+kemaldotearth@users.noreply.github.com> Date: Mon, 15 Dec 2025 10:44:08 +0000 Subject: [PATCH] feat(design-system): add initial copy writing guide (#41307) * feat: create entry points for copy writing docs * feat: basic page generated using the supabase writing style guide * chore: editing page * feat: more components * feat: remaining example components * chore: remove duplicate registry entry * chore: add aria label to example * chore: run prettier * fix: alphabetise getting started section * chore: rename copy writing to copywriting --- apps/design-system/__registry__/index.tsx | 88 +++++++ .../components/component-preview.tsx | 79 +++--- apps/design-system/config/docs.ts | 31 ++- .../content/docs/copywriting.mdx | 240 ++++++++++++++++++ apps/design-system/registry/copy-writing.ts | 68 +++++ .../default/example/copy-button-verbs.tsx | 22 ++ .../default/example/copy-confirmations.tsx | 54 ++++ .../default/example/copy-empty-states.tsx | 34 +++ .../default/example/copy-error-messages.tsx | 24 ++ .../default/example/copy-form-labels.tsx | 29 +++ .../default/example/copy-loading-states.tsx | 38 +++ .../default/example/copy-success-messages.tsx | 24 ++ .../default/example/copy-tooltips.tsx | 51 ++++ apps/design-system/registry/registry.ts | 3 +- 14 files changed, 736 insertions(+), 49 deletions(-) create mode 100644 apps/design-system/content/docs/copywriting.mdx create mode 100644 apps/design-system/registry/copy-writing.ts create mode 100644 apps/design-system/registry/default/example/copy-button-verbs.tsx create mode 100644 apps/design-system/registry/default/example/copy-confirmations.tsx create mode 100644 apps/design-system/registry/default/example/copy-empty-states.tsx create mode 100644 apps/design-system/registry/default/example/copy-error-messages.tsx create mode 100644 apps/design-system/registry/default/example/copy-form-labels.tsx create mode 100644 apps/design-system/registry/default/example/copy-loading-states.tsx create mode 100644 apps/design-system/registry/default/example/copy-success-messages.tsx create mode 100644 apps/design-system/registry/default/example/copy-tooltips.tsx diff --git a/apps/design-system/__registry__/index.tsx b/apps/design-system/__registry__/index.tsx index b668b042da0..1a6766deeab 100644 --- a/apps/design-system/__registry__/index.tsx +++ b/apps/design-system/__registry__/index.tsx @@ -2975,5 +2975,93 @@ export const Index: Record = { subcategory: "Composed", chunks: [] }, + "copy-button-verbs": { + name: "copy-button-verbs", + type: "components:example", + registryDependencies: ["button"], + component: React.lazy(() => import("@/registry/default/example/copy-button-verbs")), + source: "", + files: ["registry/default/example/copy-button-verbs.tsx"], + category: "Getting Started", + subcategory: "Copywriting", + chunks: [] + }, + "copy-form-labels": { + name: "copy-form-labels", + type: "components:example", + registryDependencies: ["form"], + component: React.lazy(() => import("@/registry/default/example/copy-form-labels")), + source: "", + files: ["registry/default/example/copy-form-labels.tsx"], + category: "Getting Started", + subcategory: "Copywriting", + chunks: [] + }, + "copy-error-messages": { + name: "copy-error-messages", + type: "components:example", + registryDependencies: ["form"], + component: React.lazy(() => import("@/registry/default/example/copy-error-messages")), + source: "", + files: ["registry/default/example/copy-error-messages.tsx"], + category: "Getting Started", + subcategory: "Copywriting", + chunks: [] + }, + "copy-success-messages": { + name: "copy-success-messages", + type: "components:example", + registryDependencies: ["form"], + component: React.lazy(() => import("@/registry/default/example/copy-success-messages")), + source: "", + files: ["registry/default/example/copy-success-messages.tsx"], + category: "Getting Started", + subcategory: "Copywriting", + chunks: [] + }, + "copy-tooltips": { + name: "copy-tooltips", + type: "components:example", + registryDependencies: ["tooltip"], + component: React.lazy(() => import("@/registry/default/example/copy-tooltips")), + source: "", + files: ["registry/default/example/copy-tooltips.tsx"], + category: "Getting Started", + subcategory: "Copywriting", + chunks: [] + }, + "copy-loading-states": { + name: "copy-loading-states", + type: "components:example", + registryDependencies: ["loading-state"], + component: React.lazy(() => import("@/registry/default/example/copy-loading-states")), + source: "", + files: ["registry/default/example/copy-loading-states.tsx"], + category: "Getting Started", + subcategory: "Copywriting", + chunks: [] + }, + "copy-empty-states": { + name: "copy-empty-states", + type: "components:example", + registryDependencies: ["empty-state"], + component: React.lazy(() => import("@/registry/default/example/copy-empty-states")), + source: "", + files: ["registry/default/example/copy-empty-states.tsx"], + category: "Getting Started", + subcategory: "Copywriting", + chunks: [] + }, + "copy-confirmations": { + name: "copy-confirmations", + type: "components:example", + registryDependencies: ["confirmation"], + component: React.lazy(() => import("@/registry/default/example/copy-confirmations")), + source: "", + files: ["registry/default/example/copy-confirmations.tsx"], + category: "Getting Started", + subcategory: "Copywriting", + chunks: [] + }, }, } diff --git a/apps/design-system/components/component-preview.tsx b/apps/design-system/components/component-preview.tsx index 278dddbcfd0..4dcfe8c56c0 100644 --- a/apps/design-system/components/component-preview.tsx +++ b/apps/design-system/components/component-preview.tsx @@ -23,6 +23,7 @@ interface ComponentPreviewProps extends React.HTMLAttributes { showGrid?: boolean showDottedGrid?: boolean wide?: boolean + hideCode?: boolean } export function ComponentPreview({ @@ -36,6 +37,7 @@ export function ComponentPreview({ showGrid = false, showDottedGrid = true, wide = false, + hideCode = false, ...props }: ComponentPreviewProps) { const [config] = useConfig() @@ -136,7 +138,10 @@ export function ComponentPreview({ return (
{showGrid && (
@@ -146,42 +151,44 @@ export function ComponentPreview({ )}
{ComponentPreview}
- - - - View code - - -
+ - {Code} -
-
-
+ + View code + + +
+ {Code} +
+
+ + )}
) } diff --git a/apps/design-system/config/docs.ts b/apps/design-system/config/docs.ts index 8979ca8bfc1..a877949c492 100644 --- a/apps/design-system/config/docs.ts +++ b/apps/design-system/config/docs.ts @@ -9,31 +9,28 @@ export const docsConfig: DocsConfig = { sidebarNav: [ { title: 'Getting Started', - sortOrder: 'manual', + sortOrder: 'alphabetical', items: [ { title: 'Introduction', href: '/docs', + priority: true, items: [], }, { - title: 'Tailwind Classes', - href: '/docs/tailwind-classes', + title: 'Accessibility', + href: '/docs/accessibility', items: [], }, + { title: 'Color Usage', href: '/docs/color-usage', items: [], }, { - title: 'Typography', - href: '/docs/typography', - items: [], - }, - { - title: 'Theming', - href: '/docs/theming', + title: 'Copywriting', + href: '/docs/copywriting', items: [], }, { @@ -42,8 +39,18 @@ export const docsConfig: DocsConfig = { items: [], }, { - title: 'Accessibility', - href: '/docs/accessibility', + title: 'Tailwind Classes', + href: '/docs/tailwind-classes', + items: [], + }, + { + title: 'Theming', + href: '/docs/theming', + items: [], + }, + { + title: 'Typography', + href: '/docs/typography', items: [], }, ], diff --git a/apps/design-system/content/docs/copywriting.mdx b/apps/design-system/content/docs/copywriting.mdx new file mode 100644 index 00000000000..751cf4e0497 --- /dev/null +++ b/apps/design-system/content/docs/copywriting.mdx @@ -0,0 +1,240 @@ +--- +title: Copywriting +description: A concise guide for writing UI copy in Supabase. +--- + +Write UI copy that helps developers complete tasks quickly. Be direct, action-oriented, and respectful of developer time. + +## Voice and tone + +Supabase UI copy is: + +- **Direct**: Say what something does, not what it "enables" you to do. +- **Action-oriented**: Focus on what happens, not what we built. +- **Technical without jargon**: Use precise terms but explain when necessary. +- **Pragmatic**: Acknowledge tradeoffs and limitations when relevant. + +## Buttons and actions + +### Use verbs, not nouns + + + +### Be specific about outcomes + +| Bad | Good | +| ----------- | ---------------- | +| "Remove" | "Delete project" | +| "Change" | "Revoke access" | +| "Configure" | "Enable RLS" | + +### Match button text to the action + +| Action | Bad | Good | +| ----------------- | --------- | -------------- | +| Primary action: | "Submit" | "Create table" | +| Secondary action: | "Go back" | "Cancel" | + +## Form labels and descriptions + +### Labels describe the field, not the feature + + + +| Action | Bad | Good | +| ------------ | ------------------------------------------------------------------------------------------------ | ---------------------------------------- | +| Label: | "Name your table" | "Table name" | +| Description: | "This field allows you to specify a name for your table using letters, numbers, and underscores" | "Letters, numbers, and underscores only" | + +### Descriptions explain constraints, not concepts + +| Bad | Good | +| --------------------------------------------------------------- | ---------------------------------- | +| "This ensures your table name is unique" | "Must be unique within the schema" | +| "You can enter up to 255 characters here" | "Maximum 255 characters" | +| "This field is required when using Row Level Security policies" | "Required for RLS policies" | + +### Use present tense + +| Bad | Good | +| -------------------------------------- | --------------------------------- | +| "Will store connection pool settings" | "Stores connection pool settings" | +| "This will limit query execution time" | "Limits query execution time" | + +## Error messages + +### State what went wrong, then how to fix it + + + +| Bad | Good | +| ----------------------------------------- | ----------------------------------------------------- | +| "An error occurred" | "Table name already exists. Choose a different name." | +| "Something went wrong. Please try again." | "Invalid API key. Check your project settings." | + +### Be specific about the problem + +| Bad | Good | +| ------------------ | --------------------------------------------- | +| "Invalid input" | "Password must be at least 8 characters" | +| "Connection error" | "Connection failed: timeout after 30 seconds" | + +### Avoid blame or apology + +| Bad | Good | +| ---------------------------- | ------------------------------------------------ | +| "Sorry, we couldn't connect" | "Unable to connect to database" | +| "Oops! Something went wrong" | "Table creation failed: column name is reserved" | + +## Success messages + +### Confirm what happened + + + +| Bad | Good | +| --------------------- | ---------------------------- | +| "Success!" | "Table created successfully" | +| "Done" | "API key revoked" | +| "Operation completed" | "Changes saved" | + +### Keep it brief + +| Bad | Good | +| ------------------------------------------------------------- | ------------------- | +| "Your backup has been successfully restored to your database" | "Backup restored" | +| "The migration has been applied successfully to your project" | "Migration applied" | + +## Tooltips and help text + +### Explain why, not what + + + +| Bad | Good | +| ------------------------- | ------------------------------------------------ | +| "This is a toggle switch" | "Enables real-time subscriptions for this table" | +| "Click to delete" | "Prevents accidental deletions" | + +### One sentence maximum + +| Bad | Good | +| ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------ | +| "Row Level Security restricts access based on user policies. When enabled, users can only access rows that match their policy conditions." | "Restricts access based on user policies" | +| "This setting controls the maximum number of concurrent connections that can be established to your database at any given time." | "Maximum number of concurrent connections" | + +## Navigation and headings + +### Use sentence case + +| Bad | Good | +| ----------------------- | ----------------------- | +| "Set Up Authentication" | "Set up authentication" | +| "Database Settings" | "Database settings" | +| "Create New Project" | "Create new project" | + +### Headings describe the page, not the feature + +| Bad | Good | +| ------------------------------ | -------------------- | +| "Manage your API keys" | "API keys" | +| "Configure connection pooling" | "Connection pooling" | +| "Edit your tables" | "Table editor" | + +## Empty states + +### Explain what's missing, then how to add it + + + +| Bad | Good | +| --------------------------------- | ---------------------------------------------------------- | +| "You don't have any tables" | "No tables yet. Create your first table to get started." | +| "There are no API keys available" | "No API keys. Generate a key to connect your application." | + +### Include the action + +| Bad | Good | +| ------------------------ | ------------------------------------------------- | +| "No buckets found" | "No buckets yet. [Create bucket] button" | +| "No functions available" | "No functions deployed. [Deploy function] button" | + +## Loading states + +### Describe what's happening + + + +| Bad | Good | +| ---------------- | ----------------------- | +| "Please wait..." | "Creating table..." | +| "Loading..." | "Loading schema..." | +| "Processing..." | "Applying migration..." | + +### Match the action verb + +| Action | Bad | Good | +| --------------------------- | ---------------- | --------------------- | +| "Delete project" → Loading: | "Processing..." | "Deleting project..." | +| "Save changes" → Loading: | "Please wait..." | "Saving changes..." | + +## Confirmations and dialogs + +### State consequences clearly + + + +| Bad | Good | +| ------------------------------------- | ----------------------------------------------------------------------------------------- | +| "Are you sure?" | "Delete this project? This action cannot be undone and will permanently delete all data." | +| "This action is permanent. Continue?" | "Revoke this API key? Applications using this key will stop working immediately." | + +### Use active voice + +| Bad | Good | +| ------------------------------------------------------------ | --------------------------------------------------- | +| "All data will be removed if this project is deleted" | "Deleting this project will remove all data" | +| "Existing connections will be broken if this key is revoked" | "Revoking this key will break existing connections" | + +## Words to avoid + +### Marketing language + +| Bad | Good | +| ---------------------------- | -------------------- | +| "Easily create tables" | "Create tables" | +| "Simply configure settings" | "Configure settings" | +| "Powerful database features" | "Database features" | + +### Vague verbs + +| Bad | Good | +| ---------------- | -------------------------------- | +| "Manage tables" | "Create, edit, or delete tables" | +| "Handle errors" | "View and resolve errors" | +| "Work with data" | "Query and update data" | + +## Capitalization + +- **Sentence case** for all UI text (buttons, labels, headings) +- **Product names:** Database, Auth, Storage, Edge Functions, Realtime, Vector +- **Postgres**, not PostgreSQL +- **Supabase** (capitalize except in code) + +## Formatting + +- **Bold for emphasis** only when necessary +- **Inline code** for technical terms: `RLS`, `API key`, `supabase init` +- **No italics** for emphasis +- **No exclamation marks** unless critical (e.g., destructive actions) + +## Quick checklist + +Before publishing UI copy, ask: + +- Does it use an action verb? +- Is it specific about what happens? +- Can a developer complete the task without reading more? +- Does it avoid marketing language? +- Is it in sentence case? +- Is it one sentence or less (for labels, buttons, tooltips)? diff --git a/apps/design-system/registry/copy-writing.ts b/apps/design-system/registry/copy-writing.ts new file mode 100644 index 00000000000..33afc906d80 --- /dev/null +++ b/apps/design-system/registry/copy-writing.ts @@ -0,0 +1,68 @@ +import { Registry } from './schema' + +export const copyWriting: Registry = [ + { + name: 'copy-button-verbs', + type: 'components:example', + files: ['example/copy-button-verbs.tsx'], + registryDependencies: ['button'], + category: 'Getting Started', + subcategory: 'Copywriting', + }, + { + name: 'copy-form-labels', + type: 'components:example', + files: ['example/copy-form-labels.tsx'], + registryDependencies: ['form'], + category: 'Getting Started', + subcategory: 'Copywriting', + }, + { + name: 'copy-error-messages', + type: 'components:example', + files: ['example/copy-error-messages.tsx'], + registryDependencies: ['form'], + category: 'Getting Started', + subcategory: 'Copywriting', + }, + { + name: 'copy-success-messages', + type: 'components:example', + files: ['example/copy-success-messages.tsx'], + registryDependencies: ['form'], + category: 'Getting Started', + subcategory: 'Copywriting', + }, + { + name: 'copy-tooltips', + type: 'components:example', + files: ['example/copy-tooltips.tsx'], + registryDependencies: ['tooltip'], + category: 'Getting Started', + subcategory: 'Copywriting', + }, + { + name: 'copy-loading-states', + type: 'components:example', + files: ['example/copy-loading-states.tsx'], + registryDependencies: ['loading-state'], + category: 'Getting Started', + subcategory: 'Copywriting', + }, + { + name: 'copy-empty-states', + type: 'components:example', + files: ['example/copy-empty-states.tsx'], + registryDependencies: ['empty-state'], + category: 'Getting Started', + subcategory: 'Copywriting', + }, + { + name: 'copy-confirmations', + type: 'components:example', + files: ['example/copy-confirmations.tsx'], + registryDependencies: ['confirmation'], + category: 'Getting Started', + subcategory: 'Copywriting', + }, +] diff --git a/apps/design-system/registry/default/example/copy-button-verbs.tsx b/apps/design-system/registry/default/example/copy-button-verbs.tsx new file mode 100644 index 00000000000..3cf3c071531 --- /dev/null +++ b/apps/design-system/registry/default/example/copy-button-verbs.tsx @@ -0,0 +1,22 @@ +'use client' + +import { Button } from 'ui' + +export default function CopyButtonVerbs() { + return ( +
+
+ Bad Example + + + +
+
+ Good Example + + + +
+
+ ) +} diff --git a/apps/design-system/registry/default/example/copy-confirmations.tsx b/apps/design-system/registry/default/example/copy-confirmations.tsx new file mode 100644 index 00000000000..ce9406cbfc9 --- /dev/null +++ b/apps/design-system/registry/default/example/copy-confirmations.tsx @@ -0,0 +1,54 @@ +'use client' + +import { Button } from 'ui' +import { AlertTriangle } from 'lucide-react' + +export default function CopyConfirmations() { + return ( +
+
+ Bad Example +
+
+
+

Are you sure?

+

+ All data will be removed if this project is deleted +

+
+
+
+ + +
+
+
+
+ Good Example +
+
+
+

Delete this project?

+

+ This action cannot be undone and will permanently delete all data. Deleting this + project will remove all data. +

+
+
+
+ + +
+
+
+
+ ) +} diff --git a/apps/design-system/registry/default/example/copy-empty-states.tsx b/apps/design-system/registry/default/example/copy-empty-states.tsx new file mode 100644 index 00000000000..1315f8e42b6 --- /dev/null +++ b/apps/design-system/registry/default/example/copy-empty-states.tsx @@ -0,0 +1,34 @@ +'use client' + +import { Button } from 'ui' +import { EmptyStatePresentational } from 'ui-patterns' +import { Key } from 'lucide-react' + +export default function CopyEmptyStates() { + return ( +
+
+ Bad Example +
+ +
+
+
+ Good Example +
+ + + +
+
+
+ ) +} diff --git a/apps/design-system/registry/default/example/copy-error-messages.tsx b/apps/design-system/registry/default/example/copy-error-messages.tsx new file mode 100644 index 00000000000..7a19c25ca31 --- /dev/null +++ b/apps/design-system/registry/default/example/copy-error-messages.tsx @@ -0,0 +1,24 @@ +'use client' + +import { CircleAlert } from 'lucide-react' + +export default function CopyErrorMessages() { + return ( +
+
+ Bad Example +
+ +

Something went wrong. Please try again.

+
+
+
+ Good Example +
+ +

Invalid API key. Check your project settings.

+
+
+
+ ) +} diff --git a/apps/design-system/registry/default/example/copy-form-labels.tsx b/apps/design-system/registry/default/example/copy-form-labels.tsx new file mode 100644 index 00000000000..099afe5b906 --- /dev/null +++ b/apps/design-system/registry/default/example/copy-form-labels.tsx @@ -0,0 +1,29 @@ +'use client' + +import { Input_Shadcn_, Label_Shadcn_ } from 'ui' + +export default function CopyFormLabels() { + return ( +
+
+ Bad Example +
+ Name your table + +

+ This field allows you to specify a name for your table using letters, numbers, and + underscores +

+
+
+
+ Good Example +
+ Table name + +

Letters, numbers, and underscores only

+
+
+
+ ) +} diff --git a/apps/design-system/registry/default/example/copy-loading-states.tsx b/apps/design-system/registry/default/example/copy-loading-states.tsx new file mode 100644 index 00000000000..db1259dc196 --- /dev/null +++ b/apps/design-system/registry/default/example/copy-loading-states.tsx @@ -0,0 +1,38 @@ +'use client' + +import { Button } from 'ui' + +export default function CopyLoadingStates() { + return ( +
+
+ Bad Example +
+ + + +
+
+
+ Good Example +
+ + + +
+
+
+ ) +} diff --git a/apps/design-system/registry/default/example/copy-success-messages.tsx b/apps/design-system/registry/default/example/copy-success-messages.tsx new file mode 100644 index 00000000000..d5589fede23 --- /dev/null +++ b/apps/design-system/registry/default/example/copy-success-messages.tsx @@ -0,0 +1,24 @@ +'use client' + +import { CheckCircle } from 'lucide-react' + +export default function CopySuccessMessages() { + return ( +
+
+ Bad Example +
+ +

Success!

+
+
+
+ Good Example +
+ +

Table created successfully

+
+
+
+ ) +} diff --git a/apps/design-system/registry/default/example/copy-tooltips.tsx b/apps/design-system/registry/default/example/copy-tooltips.tsx new file mode 100644 index 00000000000..594c5622533 --- /dev/null +++ b/apps/design-system/registry/default/example/copy-tooltips.tsx @@ -0,0 +1,51 @@ +'use client' + +import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from 'ui' +import { Info } from 'lucide-react' + +export default function CopyTooltips() { + return ( +
+
+ Bad Example +
+ + + +
+
+
+ Good Example +
+ + + +
+
+
+ ) +} diff --git a/apps/design-system/registry/registry.ts b/apps/design-system/registry/registry.ts index e03e39fe31b..27f60ea8459 100644 --- a/apps/design-system/registry/registry.ts +++ b/apps/design-system/registry/registry.ts @@ -2,5 +2,6 @@ import { Registry } from '@/registry/schema' import { examples } from '@/registry//examples' import { fragments } from '@/registry/fragments' import { charts } from '@/registry/charts' +import { copyWriting } from '@/registry/copy-writing' -export const registry: Registry = [...fragments, ...examples, ...charts] +export const registry: Registry = [...fragments, ...examples, ...charts, ...copyWriting]