diff --git a/docs/adr/0001-documentation-site-information-architecture.md b/docs/adr/0001-documentation-site-information-architecture.md new file mode 100644 index 0000000000..280703cb07 --- /dev/null +++ b/docs/adr/0001-documentation-site-information-architecture.md @@ -0,0 +1,128 @@ +# ADR 0001: Information architecture for the new Gamut documentation site + +- **Status:** Proposed +- **Date:** 2026-08-18 +- **Ticket:** GMT-1727 +- **Deciders:** Gamut maintainers + +## Context + +Gamut's documentation lives in a Storybook (`packages/styleguide`) whose structure has grown organically. An audit against the [Diátaxis framework](https://diataxis.fr) identified structural problems: + +1. **Navigation is organized by Gamut's internal taxonomy, not user need.** The sidebar (Atoms / Molecules / Organisms / Foundations / Layouts / Typography) follows Atomic Design. Readers must know how Gamut classifies a component before they can find it, and cross-cutting tasks (theming an app, building a form, supporting dark mode) have no home. +2. **Documentation modes are interleaved.** Component pages mix design guidance, task recipes, and API reference in inconsistent orders (e.g., `Alert` documents `closeButtonProps` as prose bullets between example sections; `Menu` buries accessibility-critical role guidance mid-page). Section vocabularies diverge per page ("Specifications", "HTML element", "Roles"). +3. **There are no tutorials.** Nothing walks a newcomer from zero to a working page. `Installation` — the most task-critical page — is buried mid-list in a grab-bag "Meta" section. +4. **"Meta" is a junk drawer** spanning how-to (Installation, Contributing), reference (ESLint rules), and explanation (Brand, Best practices, FAQs), organized by authors' filing convenience rather than reader need. + +We are building a new documentation website. This is the moment to fix the information architecture rather than port the current structure. +This document will serve as a blueprint for any framework to use and generate a website from. + +Diátaxis distinguishes four documentation modes by user need — **tutorials** (learning), **how-to guides** (working toward a goal), **reference** (looking up facts), and **explanation** (understanding) — and holds that each unit of content should serve one mode, with navigation letting readers stay in their mode. It applies fractally: at site scale and again within a topic area such as a single component. + +## Decision + +### 1. Top-level navigation follows the Diátaxis modes + +The sidebar's top level maps to the reader's lifecycle — learn it, do a task, look something up, understand it deeper: + +```text +Getting started TUTORIALS — Installation, Build your first page, Using this site +Guides HOW-TO — Theming your app, Building forms, Supporting dark + mode, Writing UX copy, Migrating to logical + properties, Contributing to Gamut +Components REFERENCE — all components, grouped by function (see §2) +Reference REFERENCE — design tokens, themes, system props, icon/asset + catalogs, ESLint rules, tooling +Concepts EXPLANATION — architecture of the system, theming model, color + modes, brand, best practices, voice & tone, FAQs +``` + +The "Meta" section is dissolved: its pages distribute into Getting started (Installation, Usage guide), Guides (Contributing), Reference (ESLint rules, Deep Controls), and Concepts (Brand, Best practices, FAQs). "Foundations" splits along the same line: token values and theme palettes are Reference; the rationale behind them is Concepts. + +### 2. Components are grouped by function, not by Atomic Design tier + +Readers arrive with a need ("I have to tell the user something went wrong"), not knowledge of Gamut's composition hierarchy. The Atoms/Molecules/Organisms tiers are retired from navigation (the tier may survive as a metadata badge on component pages). Components are grouped by what they do: + +- **Actions** — Button, CTAButton, FillButton, StrokeButton, TextButton, IconButton, Menu, Tag +- **Containers** — Box, FlexBox, GridBox, Card, ContentContainer, LayoutGrid, Disclosure, Drawer +- **Inputs & forms** — Input, TextArea, Checkbox, Radio, Toggle, Select, SelectDropdown, DatePicker; Form scaffolding (Form, FormGroup, FormGroupLabel, FormGroupDescription, FormRequiredText); ConnectedForm (ConnectedForm, ConnectedFormGroup, ConnectedFormInputs, SubmitButton); GridForm +- **Navigation** — Anchor, Breadcrumbs, Pagination, Tabs, SkipToContent +- **Feedback** — Alert, Toast, Toaster, Coachmark, Tips (ToolTip, InfoTip, PreviewTip) +- **Status** — Badge, ProgressBar, RadialProgress, Loaders (Spinner, Shimmer), FeatureShimmer +- **Overlays** — Modal, Dialog, Overlay, Popover, PopoverContainer, Flyout +- **Data display** — DataTable, DataList, List, BarChart, Markdown +- **Typography** — Text +- **Media & assets** — Icons (Regular, Mini), Illustrations, Patterns, Animations (ExpandInCollapseOut, FadeInSlideOut, Rotation), Video +- **Utilities** — FocusTrap, DelayedRenderWrapper + +Placement rulings for components that could live in two categories (the losing category's landing page cross-links them): + +| Component | Ruling | Rationale | +| --------- | ---------- | ------------------------------------------------------------------------------------------------------------- | +| Menu | Actions | The action-list role is the richer half of its API; nav role is cross-linked | +| Tag | Actions | Docs emphasize interactive selection/removal; read-only overlap with Badge is arbitrated by "When NOT to use" | +| Tips | Feedback | Readers think "the system explaining something"; Overlays is reserved for things you open and dismiss | +| Drawer | Containers | Collapses within page flow, unlike Flyout which floats above it (Overlays) | +| Anchor | Navigation | Functionally navigation despite its typography implementation | + +Existing multi-component group index pages (Buttons, Tips, Modals, Loaders, etc.) survive as collapsible group nodes within their category. + +### 3. Component pages re-apply Diátaxis at page scale + +Every component page follows one template with a fixed section order grouped by mode — explanation, then how-to, then reference — so a reader in any mode can jump to their zone and stay there: + +```text +Header title, subtitle, status, Figma link, source link +── EXPLANATION ── +Usage when to reach for it + Best practices design/UX guidance (not API guidance) + When NOT to use every bullet names and links the alternative component + Anatomy labeled diagram of the component's parts +── HOW-TO ── +Patterns goal-first recipes requiring real wiring (state, callbacks, + composition) — one intent sentence + one live example each +── REFERENCE ── +Variants exhaustive: one rendered example per prop value, no persuasion +Accessibility keyboard behavior, ARIA, focus handling, RTL +Props the canonical API table — prose prop documentation is banned + elsewhere on the page +Playground live editable example +``` + +Template rules: + +1. **Required always:** Header, Usage (with When NOT to use), Variants, Props, Playground. **Optional but fixed-position:** Anatomy (skip for non-visual utilities), Patterns (skip for stateless components), Accessibility (skip only when nothing exceeds native semantics). +2. **The heading vocabulary is closed.** No page invents new top-level sections ("Specifications", "HTML element"); new headings require updating the template first. +3. **Patterns vs. Variants dividing line:** if the interesting part of an example is the code _around_ the component (state, handlers, composition), it is a Pattern; if it is a prop value, it is a Variant. +4. **Tutorials never appear on component pages** — learning-oriented material lives only in Getting started. + +If a component outgrows a single page (heavy guidelines plus a large API, e.g. GridForm), it splits into per-mode child pages along the template's section boundaries. + +## Consequences + +### Positive + +- Readers navigate by need: newcomers land in Getting started, task-driven developers in Guides, fact-lookups in Components/Reference — without knowing Gamut's internal taxonomy. +- Predictable component pages: a fixed section order and closed heading vocabulary make every page scannable the same way, and give doc authors an unambiguous checklist. +- The migration is mostly mechanical: nearly every existing page has a destination in the new structure. Only two pages are net-new ("Build your first page" tutorial; "Architecture of the system" concept, largely extractable from existing Foundations prose). +- Cross-cutting guides (theming, forms, dark mode) finally have a home instead of being fragmented under component folders. + +### Negative / risks + +- **Categorization is a judgment call.** Functional categories have ambiguous edges (see placement rulings). Mitigation: cross-links from the losing category, plus site search for readers who already know a component's name. +- **Deep links break.** Every existing Storybook URL path changes. Mitigation: redirect map from old story IDs to new site URLs. +- **Mixed-mode pages persist by design.** A component page containing explanation, how-to, and reference on one page bends strict Diátaxis (one mode per page). We accept this: the practical harm Diátaxis targets is _interleaving_, which the fixed section order eliminates, and one-page-per-component matches how design-system consumers work. +- **Template enforcement needs tooling.** Without a check, heading vocabularies will drift again. Mitigation: lint page headings against the allowed list in CI. + +## Alternatives considered + +1. **Port the current Atomic Design structure to the new site.** Rejected: reproduces every audited problem; the migration is the cheapest moment to fix IA. +2. **Flat alphabetical component list** (early proposal). Rejected in favor of functional grouping: alphabetical requires knowing the component's name, functional matches need-driven arrival; search covers name-based lookup. Industry precedent (Adobe Spectrum's Actions/Containers/Feedback/... grouping) favors functional. +3. **Strict Diátaxis: one page per mode per component** (à la Carbon/Material per-component tabs). Rejected for now: quadruples page count and maintenance for many small components. The template's section boundaries are the designated split points if a component outgrows one page. +4. **Organize top-level by audience (designers vs. engineers).** Rejected: most Gamut readers wear both hats within a single task; mode-based navigation serves the actual switching behavior. + +## References + +- Diátaxis framework: +- Adobe Spectrum component grouping (functional-category precedent): +- Current styleguide source: `packages/styleguide/src/lib` diff --git a/package.json b/package.json index c09a7f053d..3ab1231358 100644 --- a/package.json +++ b/package.json @@ -136,6 +136,7 @@ "prepare": "husky", "prettier": "prettier --ignore-path .prettierignore \"./**/*.{mdx,js,ts,tsx,json,css,scss}\"", "start": "yarn && yarn start:storybook", + "start:nextra-site": "yarn workspace @codecademy/nextra-site dev", "start:storybook": "nx storybook styleguide", "test": "nx run-many --target=test --all", "test:storybook": "nx run styleguide:storybook-test", diff --git a/packages/nextra-site/.gitignore b/packages/nextra-site/.gitignore new file mode 100644 index 0000000000..efcfe8b397 --- /dev/null +++ b/packages/nextra-site/.gitignore @@ -0,0 +1,7 @@ +# next.js +/.next/ +/out/ + +# typescript +next-env.d.ts +*.tsbuildinfo diff --git a/packages/nextra-site/app/[[...mdxPath]]/page.tsx b/packages/nextra-site/app/[[...mdxPath]]/page.tsx new file mode 100644 index 0000000000..9914102fb8 --- /dev/null +++ b/packages/nextra-site/app/[[...mdxPath]]/page.tsx @@ -0,0 +1,29 @@ +import { generateStaticParamsFor, importPage } from 'nextra/pages'; + +import { useMDXComponents as getMDXComponents } from '../../mdx-components'; + +export const generateStaticParams = generateStaticParamsFor('mdxPath'); + +type PageProps = { + params: Promise<{ mdxPath?: string[] }>; +}; + +export async function generateMetadata(props: PageProps) { + const params = await props.params; + const { metadata } = await importPage(params.mdxPath); + return metadata; +} + +const Wrapper = getMDXComponents().wrapper!; + +export default async function Page(props: PageProps) { + const params = await props.params; + const result = await importPage(params.mdxPath); + const { default: MDXContent, toc, metadata } = result; + + return ( + + + + ); +} diff --git a/packages/nextra-site/app/layout.tsx b/packages/nextra-site/app/layout.tsx new file mode 100644 index 0000000000..86d8e46389 --- /dev/null +++ b/packages/nextra-site/app/layout.tsx @@ -0,0 +1,54 @@ +import 'nextra-theme-docs/style.css'; + +import type { Metadata } from 'next'; +import { Head } from 'nextra/components'; +import { getPageMap } from 'nextra/page-map'; +import { Footer, Layout, Navbar } from 'nextra-theme-docs'; + +export const metadata: Metadata = { + title: { + default: 'Gamut', + template: '%s – Gamut', + }, + description: "Codecademy's design system documentation", +}; + +const navbar = ( + Gamut} + projectLink="https://github.com/Codecademy/gamut" + /> +); + +const footer = ( +
+ MIT {new Date().getFullYear()} © Codecademy. Built from{' '} + + ADR 0001 + + . +
+); + +export default async function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + + {children} + + + + ); +} diff --git a/packages/nextra-site/app/not-found.tsx b/packages/nextra-site/app/not-found.tsx new file mode 100644 index 0000000000..641fc674d2 --- /dev/null +++ b/packages/nextra-site/app/not-found.tsx @@ -0,0 +1,10 @@ +export default function NotFound() { + return ( +
+

404: Page not found

+

+ Try the home page or the sidebar navigation. +

+
+ ); +} diff --git a/packages/nextra-site/components/ButtonAnatomy.tsx b/packages/nextra-site/components/ButtonAnatomy.tsx new file mode 100644 index 0000000000..3859197870 --- /dev/null +++ b/packages/nextra-site/components/ButtonAnatomy.tsx @@ -0,0 +1,33 @@ +const part = { + border: '1px dashed #9ca3af', + borderRadius: 6, + padding: '.5rem .75rem', + fontSize: '.8rem', + color: '#4b5563', +}; + +/** + * A labeled diagram of the component's parts (ADR 0001 §3 Anatomy). + * Buttons in this family are visual, so unlike a stateless utility + * this section isn't skipped. + */ +export const ButtonAnatomy = () => ( +
+
+ icon (optional) + Label +
+ + ← container: sets variant color, size, and focus/hover states + +
+); diff --git a/packages/nextra-site/components/ButtonPlayground.tsx b/packages/nextra-site/components/ButtonPlayground.tsx new file mode 100644 index 0000000000..235fa16570 --- /dev/null +++ b/packages/nextra-site/components/ButtonPlayground.tsx @@ -0,0 +1,198 @@ +'use client'; + +import * as icons from '@codecademy/gamut-icons'; +import { type ComponentType, useId, useState } from 'react'; + +import { GamutSandbox } from './GamutSandbox'; + +const variants = ['primary', 'secondary', 'danger', 'interface'] as const; +const sizes = ['small', 'normal', 'large'] as const; +const iconOptions = { + None: undefined, + MiniArrowRightIcon: icons.MiniArrowRightIcon, + MiniAddIcon: icons.MiniAddIcon, + MiniStarIcon: icons.MiniStarIcon, + MiniCheckCircleIcon: icons.MiniCheckCircleIcon, + MiniCalendarIcon: icons.MiniCalendarIcon, +} as const; +type IconOptionName = keyof typeof iconOptions; + +export interface ButtonPlaygroundProps { + button: ComponentType; + componentName: string; +} + +/** + * The Playground section (ADR 0001 §3): a live, editable example — + * equivalent to Storybook's Canvas + Controls. + */ +export const ButtonPlayground = ({ + button: Button, + componentName, +}: ButtonPlaygroundProps) => { + const formId = useId(); + const [label, setLabel] = useState('Click me'); + const [variant, setVariant] = useState<(typeof variants)[number]>('primary'); + const [size, setSize] = useState<(typeof sizes)[number]>('normal'); + const [disabled, setDisabled] = useState(false); + const [iconName, setIconName] = useState('None'); + const [iconPosition, setIconPosition] = useState<'left' | 'right'>('left'); + + const icon = iconOptions[iconName]; + + const codeLines = [ + `<${componentName}`, + ` variant="${variant}"`, + ` size="${size}"`, + ...(disabled ? [' disabled'] : []), + ...(icon + ? [` icon={${iconName}}`, ` iconPosition="${iconPosition}"`] + : []), + `>`, + ` ${label}`, + ``, + ].join('\n'); + + return ( +
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + setLabel(event.target.value)} + /> +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + setDisabled(event.target.checked)} + /> +
+
+          {codeLines}
+        
+
+
+ ); +}; diff --git a/packages/nextra-site/components/ButtonVariantsGrid.tsx b/packages/nextra-site/components/ButtonVariantsGrid.tsx new file mode 100644 index 0000000000..60621e8a99 --- /dev/null +++ b/packages/nextra-site/components/ButtonVariantsGrid.tsx @@ -0,0 +1,57 @@ +'use client'; + +import type { ComponentType } from 'react'; + +import { GamutSandbox } from './GamutSandbox'; + +const variants = ['primary', 'secondary', 'danger', 'interface'] as const; +const sizes = ['small', 'normal', 'large'] as const; + +export interface ButtonVariantsGridProps { + button: ComponentType; +} + +/** + * The Variants section (ADR 0001 §3) is exhaustive — one rendered + * example per prop value, no persuasion copy. + */ +export const ButtonVariantsGrid = ({ + button: Button, +}: ButtonVariantsGridProps) => ( + +
+ + + + + {sizes.map((size) => ( + + ))} + + + + + {variants.map((variant) => ( + + + {sizes.map((size) => ( + + ))} + + + ))} + +
variantsize="{size}"disabled
+ {variant} + + + + +
+
+
+); diff --git a/packages/nextra-site/components/ComponentHeader.tsx b/packages/nextra-site/components/ComponentHeader.tsx new file mode 100644 index 0000000000..7d6f0b9971 --- /dev/null +++ b/packages/nextra-site/components/ComponentHeader.tsx @@ -0,0 +1,91 @@ +export type ComponentStatus = 'current' | 'deprecated' | 'experimental'; + +export interface ComponentHeaderProps { + title: string; + subtitle: string; + status: ComponentStatus; + figmaUrl?: string; + githubUrl: string; + tier?: 'Atom' | 'Molecule' | 'Organism'; +} + +const statusLabel: Record = { + current: 'Current', + deprecated: 'Deprecated', + experimental: 'Experimental', +}; + +const statusColor: Record = { + current: '#0d7d3f', + deprecated: '#b3261e', + experimental: '#8a5300', +}; + +/** + * Header block required on every component page (ADR 0001 §3): title, + * subtitle, status, Figma link, source link. + */ +export const ComponentHeader = ({ + title, + subtitle, + status, + figmaUrl, + githubUrl, + tier, +}: ComponentHeaderProps) => ( +
+
+

{title}

+ + {statusLabel[status]} + + {tier && ( + + {tier} + + )} +
+

+ {subtitle} +

+ +
+); diff --git a/packages/nextra-site/components/GamutSandbox.tsx b/packages/nextra-site/components/GamutSandbox.tsx new file mode 100644 index 0000000000..685de4aadd --- /dev/null +++ b/packages/nextra-site/components/GamutSandbox.tsx @@ -0,0 +1,15 @@ +'use client'; + +import { coreTheme, GamutProvider } from '@codecademy/gamut-styles'; +import type { ReactNode } from 'react'; + +/** + * Live component examples render inside this so they get Gamut's theme + * context (colors, tokens) without the docs site inheriting Gamut's + * global Reboot/Typography resets (`useGlobals` stays off). + */ +export const GamutSandbox = ({ children }: { children: ReactNode }) => ( + + {children} + +); diff --git a/packages/nextra-site/components/PropsTable.tsx b/packages/nextra-site/components/PropsTable.tsx new file mode 100644 index 0000000000..40bb365632 --- /dev/null +++ b/packages/nextra-site/components/PropsTable.tsx @@ -0,0 +1,42 @@ +export interface PropRow { + name: string; + type: string; + default?: string; + required?: boolean; + description: string; +} + +/** + * The canonical API table (ADR 0001 §3): prose prop documentation is + * banned elsewhere on the page, so this is the single source of truth + * for a component's props. + */ +export const PropsTable = ({ rows }: { rows: PropRow[] }) => ( + + + + + + + + + + + {rows.map((row) => ( + + + + + + + ))} + +
PropTypeDefaultDescription
+ + {row.name} + {row.required ? '*' : ''} + + + {row.type} + {row.default ? {row.default} : '—'}{row.description}
+); diff --git a/packages/nextra-site/components/buttonProps.ts b/packages/nextra-site/components/buttonProps.ts new file mode 100644 index 0000000000..c63e4ffa24 --- /dev/null +++ b/packages/nextra-site/components/buttonProps.ts @@ -0,0 +1,70 @@ +import type { PropRow } from './PropsTable'; + +/** + * FillButton, StrokeButton, and TextButton all resolve to + * `InlineIconButtonProps`, so they share one prop shape + * (packages/gamut/src/Button/shared/types.ts, styles.ts, variants.ts). + */ +export const buttonCommonProps: PropRow[] = [ + { + name: 'children', + type: 'ReactNode', + required: true, + description: 'The button label.', + }, + { + name: 'variant', + type: "'primary' | 'secondary' | 'danger' | 'interface'", + default: 'primary', + description: + 'The semantic intent of the action. Drives color: primary for the main action, secondary for an alternate, danger for destructive actions, interface for chrome-level controls.', + }, + { + name: 'size', + type: "'small' | 'normal' | 'large'", + default: 'normal', + description: 'Controls height, padding, and font size.', + }, + { + name: 'icon', + type: 'ComponentType', + description: + 'A mini icon component (from @codecademy/gamut-icons) rendered alongside the label.', + }, + { + name: 'iconPosition', + type: "'left' | 'right'", + default: 'left', + description: 'Where the icon renders relative to the label.', + }, + { + name: 'disabled', + type: 'boolean', + default: 'false', + description: + 'Disables the button. When combined with href, the element renders as a non-interactive element instead of an anchor.', + }, + { + name: 'href', + type: 'string', + description: + 'If defined (and the button is not disabled), the button renders as an element instead of a + {status === 'done' && {resultLabel}} + + + ); +}; diff --git a/packages/nextra-site/components/patterns/ConfirmPattern.tsx b/packages/nextra-site/components/patterns/ConfirmPattern.tsx new file mode 100644 index 0000000000..ee8df4ef52 --- /dev/null +++ b/packages/nextra-site/components/patterns/ConfirmPattern.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { type ComponentType, useState } from 'react'; + +import { GamutSandbox } from '../GamutSandbox'; + +export interface ConfirmPatternProps { + button: ComponentType; + idleLabel: string; + confirmLabel: string; + resultLabel: string; +} + +/** + * A Pattern (ADR 0001 §3): a two-step confirm requires composing the + * button with local state and a timeout to reset the confirm window — + * not just a different prop value, so it belongs here rather than in + * Variants. + */ +export const ConfirmPattern = ({ + button: Button, + idleLabel, + confirmLabel, + resultLabel, +}: ConfirmPatternProps) => { + const [awaitingConfirm, setAwaitingConfirm] = useState(false); + const [done, setDone] = useState(false); + + const handleClick = () => { + if (!awaitingConfirm) { + setAwaitingConfirm(true); + return; + } + setAwaitingConfirm(false); + setDone(true); + }; + + return ( + +
+ + {done && {resultLabel}} +
+
+ ); +}; diff --git a/packages/nextra-site/components/wrappers/FillButtonDemo.tsx b/packages/nextra-site/components/wrappers/FillButtonDemo.tsx new file mode 100644 index 0000000000..4c6f7435ca --- /dev/null +++ b/packages/nextra-site/components/wrappers/FillButtonDemo.tsx @@ -0,0 +1,29 @@ +'use client'; + +import { FillButton } from '@codecademy/gamut'; + +import { ButtonPlayground } from '../ButtonPlayground'; +import { ButtonVariantsGrid } from '../ButtonVariantsGrid'; +import { + type AsyncActionPatternProps, + AsyncActionPattern, +} from '../patterns/AsyncActionPattern'; + +/** + * `@codecademy/gamut`'s barrel export pulls in components that use + * client-only hooks (Alert, BarChart, ...). Importing it must happen + * inside a "use client" module — never at the top level of a page.mdx, + * which Next treats as a Server Component — so these thin wrappers are + * the only place FillButton is imported. + */ +export const FillButtonVariants = () => ( + +); + +export const FillButtonPlayground = () => ( + +); + +export const FillButtonAsyncPattern = ( + props: Omit +) => ; diff --git a/packages/nextra-site/components/wrappers/StrokeButtonDemo.tsx b/packages/nextra-site/components/wrappers/StrokeButtonDemo.tsx new file mode 100644 index 0000000000..a3b7f80ece --- /dev/null +++ b/packages/nextra-site/components/wrappers/StrokeButtonDemo.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { StrokeButton } from '@codecademy/gamut'; + +import { ButtonPlayground } from '../ButtonPlayground'; +import { ButtonVariantsGrid } from '../ButtonVariantsGrid'; +import { + type ConfirmPatternProps, + ConfirmPattern, +} from '../patterns/ConfirmPattern'; + +/** + * See FillButtonDemo.tsx for why the `@codecademy/gamut` import must + * live inside a "use client" module rather than at the top of a + * page.mdx. + */ +export const StrokeButtonVariants = () => ( + +); + +export const StrokeButtonPlayground = () => ( + +); + +export const StrokeButtonConfirmPattern = ( + props: Omit +) => ; diff --git a/packages/nextra-site/components/wrappers/TextButtonDemo.tsx b/packages/nextra-site/components/wrappers/TextButtonDemo.tsx new file mode 100644 index 0000000000..865a1b8d62 --- /dev/null +++ b/packages/nextra-site/components/wrappers/TextButtonDemo.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { TextButton } from '@codecademy/gamut'; + +import { ButtonPlayground } from '../ButtonPlayground'; +import { ButtonVariantsGrid } from '../ButtonVariantsGrid'; +import { + type AsyncActionPatternProps, + AsyncActionPattern, +} from '../patterns/AsyncActionPattern'; + +/** + * See FillButtonDemo.tsx for why the `@codecademy/gamut` import must + * live inside a "use client" module rather than at the top of a + * page.mdx. + */ +export const TextButtonVariants = () => ( + +); + +export const TextButtonPlayground = () => ( + +); + +export const TextButtonAsyncPattern = ( + props: Omit +) => ; diff --git a/packages/nextra-site/content/_meta.ts b/packages/nextra-site/content/_meta.ts new file mode 100644 index 0000000000..554058f3e7 --- /dev/null +++ b/packages/nextra-site/content/_meta.ts @@ -0,0 +1,21 @@ +export default { + index: { + title: 'Home', + display: 'hidden', + }, + 'getting-started': { + title: 'Getting started', + }, + guides: { + title: 'Guides', + }, + components: { + title: 'Components', + }, + reference: { + title: 'Reference', + }, + concepts: { + title: 'Concepts', + }, +}; diff --git a/packages/nextra-site/content/components/_meta.ts b/packages/nextra-site/content/components/_meta.ts new file mode 100644 index 0000000000..55f91c551e --- /dev/null +++ b/packages/nextra-site/content/components/_meta.ts @@ -0,0 +1,14 @@ +export default { + index: 'Overview', + actions: 'Actions', + containers: 'Containers', + 'inputs-and-forms': 'Inputs & forms', + navigation: 'Navigation', + feedback: 'Feedback', + status: 'Status', + overlays: 'Overlays', + 'data-display': 'Data display', + typography: 'Typography', + 'media-and-assets': 'Media & assets', + utilities: 'Utilities', +}; diff --git a/packages/nextra-site/content/components/actions/_meta.ts b/packages/nextra-site/content/components/actions/_meta.ts new file mode 100644 index 0000000000..6156840892 --- /dev/null +++ b/packages/nextra-site/content/components/actions/_meta.ts @@ -0,0 +1,6 @@ +export default { + index: 'Overview', + 'fill-button': 'FillButton', + 'stroke-button': 'StrokeButton', + 'text-button': 'TextButton', +}; diff --git a/packages/nextra-site/content/components/actions/fill-button.mdx b/packages/nextra-site/content/components/actions/fill-button.mdx new file mode 100644 index 0000000000..f664df4802 --- /dev/null +++ b/packages/nextra-site/content/components/actions/fill-button.mdx @@ -0,0 +1,112 @@ +--- +title: FillButton +description: A button with a solid background color, used for primary actions. +--- + +import { Callout } from 'nextra/components'; + +import { ButtonAnatomy } from '../../../components/ButtonAnatomy'; +import { buttonCommonProps } from '../../../components/buttonProps'; +import { ComponentHeader } from '../../../components/ComponentHeader'; +import { PropsTable } from '../../../components/PropsTable'; +import { + FillButtonAsyncPattern, + FillButtonPlayground, + FillButtonVariants, +} from '../../../components/wrappers/FillButtonDemo'; + + + + + Use the `/gamut-buttons` skill in Cursor or Claude Code for AI-assisted + guidance on button selection, variants, disabled patterns, and ToolTip focus + management. + + +## Usage + +Reach for `FillButton` for the primary action in a group — the one thing +you most want the reader to do (submit, save, continue). A screen should +rarely show more than one `FillButton` in the same context; competing +solid buttons undercut the sense of a single primary action. Add a +leading or trailing icon from the mini icon set when it clarifies the +action (for example, a download icon on a "Download" button). + +### When NOT to use + +- The action is secondary to another action on screen — use + [StrokeButton](/components/actions/stroke-button) instead. +- The action is tertiary, or you need a lower-emphasis inline action — + use [TextButton](/components/actions/text-button). +- You're building a marketing/hero call-to-action with its own bespoke + treatment — use `CTAButton`. +- There's no visible label, only an icon — use `IconButton` so the + accessible name comes from an explicit label prop instead of visible + text. +- Clicking reveals a list of further actions rather than performing one + directly — use `Menu`. + +### Anatomy + + + +## Patterns + +### Prevent duplicate submissions + +Disable the button for the duration of the async action it triggers, and +swap its label to a pending state. This is the standard shape for any +`FillButton` that kicks off a network request. + + + +```tsx +const [status, setStatus] = useState<'idle' | 'pending' | 'done'>('idle'); + + { + setStatus('pending'); + saveChanges().then(() => setStatus('done')); + }} +> + {status === 'pending' ? 'Saving…' : 'Save changes'} +; +``` + +## Variants + + + +## Accessibility + +- Renders a native `