From 49d717575af46ea3989817f5d9b474417c83df96 Mon Sep 17 00:00:00 2001 From: Hunter Garrett Date: Tue, 8 Sep 2026 21:47:53 -0400 Subject: [PATCH 01/10] feat(docs): add the Copy React split button and menu to the component header The header becomes a two-column row with the mock's split button on the right. The menu is Base UI's Menu, styled after the updated Figma frame, and the floating table of contents takes the same 10px padding and 6px corners so the two popups match. Nothing is wired yet. --- .../src/app/components/[slug]/page.module.css | 15 ++ apps/docs/src/app/components/[slug]/page.tsx | 11 +- .../page-actions/page-actions.module.css | 165 ++++++++++++++++++ .../components/page-actions/page-actions.tsx | 72 ++++++++ .../components/page-toc/page-toc.module.css | 13 +- 5 files changed, 267 insertions(+), 9 deletions(-) create mode 100644 apps/docs/src/components/page-actions/page-actions.module.css create mode 100644 apps/docs/src/components/page-actions/page-actions.tsx diff --git a/apps/docs/src/app/components/[slug]/page.module.css b/apps/docs/src/app/components/[slug]/page.module.css index f6fdb074..df416237 100644 --- a/apps/docs/src/app/components/[slug]/page.module.css +++ b/apps/docs/src/app/components/[slug]/page.module.css @@ -15,11 +15,26 @@ margin: var(--spacing-4) 0 0; } +/* The header is two columns: the title and description on the left, the + copy actions on the right, bottom-aligned so the button sits on the + description's line as in the mock. The text column wraps below a + comfortable reading width (the sm container) rather than squeezing, which + drops the actions onto their own line under it on a narrow viewport. */ .header { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + justify-content: space-between; + gap: var(--spacing-4) var(--spacing-6); max-width: var(--container-4xl); margin: var(--spacing-6) 0 0; } +.heading { + flex: 1 1 var(--container-sm); + min-width: 0; +} + .title { font-size: var(--font-size-4xl); font-weight: var(--font-weight-semibold); diff --git a/apps/docs/src/app/components/[slug]/page.tsx b/apps/docs/src/app/components/[slug]/page.tsx index dd2d2e26..b9dea5bd 100644 --- a/apps/docs/src/app/components/[slug]/page.tsx +++ b/apps/docs/src/app/components/[slug]/page.tsx @@ -1,7 +1,8 @@ /** * Shared shell for every converted component page, in the mock's section * order: breadcrumbs and header above the demo, Usage and API Reference - * below it, then prev/next pagination. Titles and descriptions come from the + * below it, then prev/next pagination. The header carries the page's copy + * actions beside the title (page-actions/). Titles and descriptions come from the * catalog (content/components.ts) and page order from its taxonomy tree; the * interactive demo and Usage content come from the demo registry, which * every component page has an entry in. @@ -18,6 +19,7 @@ import { notFound } from 'next/navigation'; import { Breadcrumbs } from '@/components/breadcrumbs/breadcrumbs'; import { CodeBlock } from '@/components/code-block/code-block'; import { ChevronDownIcon } from '@/components/icons/chevron-down'; +import { PageActions } from '@/components/page-actions/page-actions'; import { PageToc, type PageTocSection } from '@/components/page-toc/page-toc'; import { PropsTable } from '@/components/props-table/props-table'; import { getComponentsCatalog, getComponentsTree } from '@/content/catalog'; @@ -88,8 +90,11 @@ export default async function ComponentPage({ params }: PageProps) {
-

{record.label}

-

{record.description}

+
+

{record.label}

+

{record.description}

+
+
diff --git a/apps/docs/src/components/page-actions/page-actions.module.css b/apps/docs/src/components/page-actions/page-actions.module.css new file mode 100644 index 00000000..220d3987 --- /dev/null +++ b/apps/docs/src/components/page-actions/page-actions.module.css @@ -0,0 +1,165 @@ +/* The header's split "Copy React" button and the menu it opens. The box, + its two buttons, and the menu follow the Figma mock; the button's hover + and press feel copy the control panel's text button, and the menu's + panel and rows copy the floating table of contents, so the site's three + popups read as one family. */ + +/* ---- The box ---- */ + +/* One bordered 32px box around two buttons: the mock's darkest gray with + the hairline and 6px corners. The buttons carry no border of their own, + so the box's hairline is the only line around them. */ +.actions { + display: inline-flex; + flex: none; + align-items: stretch; + height: var(--spacing-8); + background: var(--gray-900); + border: var(--border-width-1) solid var(--white-a5); + border-radius: var(--radius-md); + font-size: var(--font-size-sm); + line-height: var(--leading-5); +} + +/* Shared by both buttons: the label gray on a transparent fill, hover to + the faint white fill under full-white text, and a press felt through a + slight scale rather than seen. Each button rounds only its outer corners, + so the hover fill follows the box's shape without clipping the focus + ring. */ +.copy, +.more { + display: inline-flex; + align-items: center; + margin: 0; + padding: 0; + background: transparent; + color: var(--gray-50); + border: 0; + font: inherit; + cursor: pointer; + transition: + color var(--fade-xs) var(--ease-hover), + background-color var(--fade-xs) var(--ease-hover), + transform var(--duration-sm) var(--ease-out); +} + +.copy svg, +.more svg { + flex: none; + transition: color var(--fade-xs) var(--ease-hover); +} + +@media (hover: hover) and (pointer: fine) { + .copy:hover, + .more:hover { + background: var(--white-a5); + color: var(--fg); + } + + .copy:hover svg, + .more:hover svg { + color: var(--fg); + } +} + +.copy:active, +.more:active { + transform: scale(0.97); +} + +.copy:focus-visible, +.more:focus-visible { + outline: 1px solid var(--accent); + outline-offset: 1px; +} + +/* The copy action: a 16px glyph and the label, 8px apart, with 8px on each + side. The mock draws the glyph one step lighter than the chevron. */ +.copy { + gap: var(--spacing-2); + padding: 0 var(--spacing-2); + border-radius: var(--radius-md) 0 0 var(--radius-md); +} + +.copy svg { + color: var(--gray-100); +} + +/* The chevron cell: a 32px square behind a hairline, with the glyph + centered. The rule is drawn as a left border so the cell and its divider + are one box, and it stops 1px short of the top and bottom as in the mock + because the box's own border already covers those pixels. */ +.more { + justify-content: center; + width: var(--spacing-8); + border-left: var(--border-width-1) solid var(--white-a5); + border-radius: 0 var(--radius-md) var(--radius-md) 0; + color: var(--gray-200); +} + +/* ---- The menu ---- */ + +/* Portaled to , so it sets its own type. The mock's panel, shared + with the table of contents: 256px wide, 10px of padding, the darkest + gray with a hairline and 6px corners. It scales in from the edge nearest + the chevron, the site's popup pattern; Base UI sets --transform-origin + from where the popup was placed. */ +.popup { + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: var(--spacing-0-5); + width: var(--container-3xs); + padding: var(--spacing-2-5); + background: var(--gray-900); + color: var(--fg); + border: var(--border-width-1) solid var(--white-a5); + border-radius: var(--radius-md); + box-shadow: 0 8px 24px rgb(0 0 0 / 0.35); + font-size: var(--font-size-sm); + line-height: var(--leading-5); + transform-origin: var(--transform-origin); + transition: + transform var(--duration-sm) var(--ease-out), + opacity var(--fade-sm) var(--ease-out); +} + +.popup[data-starting-style], +.popup[data-ending-style] { + opacity: 0; + transform: scale(0.95); +} + +/* A row is a title over a one-line description: 8px of padding with 12px + on the left, 6px corners, and the two lines 2px apart. Base UI marks the + row under the pointer or the keyboard cursor as highlighted, which the + mock draws as the darker gray fill, the same fill the table of contents + gives its current row. */ +.row { + display: flex; + flex-direction: column; + gap: var(--spacing-0-5); + padding: var(--spacing-2) var(--spacing-2) var(--spacing-2) var(--spacing-3); + border-radius: var(--radius-md); + color: var(--fg); + text-decoration: none; + cursor: pointer; + outline: none; + transition: background-color var(--fade-xs) var(--ease-hover); +} + +.row[data-highlighted] { + background: var(--gray-800); + text-decoration: none; +} + +.rowTitle { + font-size: var(--font-size-sm); + line-height: var(--leading-5); +} + +.rowDescription { + color: var(--gray-100); + font-size: var(--font-size-xs); + line-height: var(--leading-4); +} diff --git a/apps/docs/src/components/page-actions/page-actions.tsx b/apps/docs/src/components/page-actions/page-actions.tsx new file mode 100644 index 00000000..02e0326c --- /dev/null +++ b/apps/docs/src/components/page-actions/page-actions.tsx @@ -0,0 +1,72 @@ +'use client'; + +/** + * The split "Copy React" button in a component page's header, after the + * Figma mock: a bordered box holding the copy action on the left and a + * chevron cell on the right that opens a menu of three actions. Copy React + * copies the demo's current props as JSX, and the two markdown rows copy + * and open the page's markdown export (SHA-115). The shared + * components/[slug] template renders it beside the title and description. + * The menu is Base UI's Menu rather than the controls' Select, because the + * rows are actions and not a value, but it borrows the select's popup + * pattern: portaled, offset from its trigger, and scaled in from the edge + * nearest it. + */ +import { Menu } from '@base-ui/react/menu'; + +import { ChevronDownIcon } from '@/components/icons/chevron-down'; +import { CopyIcon } from '@/components/icons/copy'; + +import styles from './page-actions.module.css'; + +interface PageActionsProps { + /** The component page's slug, which names its markdown export. */ + slug: string; +} + +// The 12-unit copy glyph drawn on a 16px box, which is exactly the mock's +// 16px export of the same icon (every coordinate is the 12-unit path times +// four thirds). +const COPY_ICON_SIZE = 16; + +export function PageActions({ slug }: PageActionsProps) { + const markdownHref = `/components/${slug}/index.md`; + + return ( +
+ + + + + + + + + + Copy React + Copy this component as React + + + Copy as markdown + Copy this page as markdown for LLMs + + + View as markdown + View this page as plain text + + + + + +
+ ); +} diff --git a/apps/docs/src/components/page-toc/page-toc.module.css b/apps/docs/src/components/page-toc/page-toc.module.css index bba9728c..bf3ddd8d 100644 --- a/apps/docs/src/components/page-toc/page-toc.module.css +++ b/apps/docs/src/components/page-toc/page-toc.module.css @@ -114,18 +114,19 @@ /* ---- The menu ---- */ -/* Portaled to , so it sets its own type. The mock's panel: 256px - wide, 12px of padding, the darkest gray with a hairline and 12px corners. - It scales in from the edge nearest the lines, the site's popup pattern; - Base UI sets --transform-origin from where the popup was placed. */ +/* Portaled to , so it sets its own type. The mock's panel, shared + with the header's copy menu (page-actions/): 256px wide, 10px of padding, + the darkest gray with a hairline and 6px corners. It scales in from the + edge nearest the lines, the site's popup pattern; Base UI sets + --transform-origin from where the popup was placed. */ .popup { box-sizing: border-box; width: var(--container-3xs); - padding: var(--spacing-3); + padding: var(--spacing-2-5); background: var(--gray-900); color: var(--fg); border: var(--border-width-1) solid var(--white-a5); - border-radius: var(--radius-xl); + border-radius: var(--radius-md); box-shadow: 0 8px 24px rgb(0 0 0 / 0.35); font-size: var(--font-size-sm); line-height: var(--leading-5); From 6f2fb4475504e1737dc233993c459f4eb12decd4 Mon Sep 17 00:00:00 2001 From: Hunter Garrett Date: Tue, 8 Sep 2026 22:49:36 -0400 Subject: [PATCH 02/10] feat(docs): give both popups the mock's 12px panel and 8px offset --- .../components/page-actions/page-actions.module.css | 12 ++++++------ .../src/components/page-actions/page-actions.tsx | 2 +- .../docs/src/components/page-toc/page-toc.module.css | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/docs/src/components/page-actions/page-actions.module.css b/apps/docs/src/components/page-actions/page-actions.module.css index 220d3987..2bb939fa 100644 --- a/apps/docs/src/components/page-actions/page-actions.module.css +++ b/apps/docs/src/components/page-actions/page-actions.module.css @@ -100,21 +100,21 @@ /* ---- The menu ---- */ /* Portaled to , so it sets its own type. The mock's panel, shared - with the table of contents: 256px wide, 10px of padding, the darkest - gray with a hairline and 6px corners. It scales in from the edge nearest - the chevron, the site's popup pattern; Base UI sets --transform-origin - from where the popup was placed. */ + with the table of contents: 256px wide, 12px of padding, the darkest + gray with a hairline and 12px corners, 8px below the chevron. It scales + in from the edge nearest the chevron, the site's popup pattern; Base UI + sets --transform-origin from where the popup was placed. */ .popup { box-sizing: border-box; display: flex; flex-direction: column; gap: var(--spacing-0-5); width: var(--container-3xs); - padding: var(--spacing-2-5); + padding: var(--spacing-3); background: var(--gray-900); color: var(--fg); border: var(--border-width-1) solid var(--white-a5); - border-radius: var(--radius-md); + border-radius: var(--radius-xl); box-shadow: 0 8px 24px rgb(0 0 0 / 0.35); font-size: var(--font-size-sm); line-height: var(--leading-5); diff --git a/apps/docs/src/components/page-actions/page-actions.tsx b/apps/docs/src/components/page-actions/page-actions.tsx index 02e0326c..090be92f 100644 --- a/apps/docs/src/components/page-actions/page-actions.tsx +++ b/apps/docs/src/components/page-actions/page-actions.tsx @@ -43,7 +43,7 @@ export function PageActions({ slug }: PageActionsProps) { - + Copy React diff --git a/apps/docs/src/components/page-toc/page-toc.module.css b/apps/docs/src/components/page-toc/page-toc.module.css index bf3ddd8d..f3deeb02 100644 --- a/apps/docs/src/components/page-toc/page-toc.module.css +++ b/apps/docs/src/components/page-toc/page-toc.module.css @@ -115,18 +115,18 @@ /* ---- The menu ---- */ /* Portaled to , so it sets its own type. The mock's panel, shared - with the header's copy menu (page-actions/): 256px wide, 10px of padding, - the darkest gray with a hairline and 6px corners. It scales in from the - edge nearest the lines, the site's popup pattern; Base UI sets - --transform-origin from where the popup was placed. */ + with the header's copy menu (page-actions/): 256px wide, 12px of padding, + the darkest gray with a hairline and 12px corners, 8px from the lines. It + scales in from the edge nearest the lines, the site's popup pattern; Base + UI sets --transform-origin from where the popup was placed. */ .popup { box-sizing: border-box; width: var(--container-3xs); - padding: var(--spacing-2-5); + padding: var(--spacing-3); background: var(--gray-900); color: var(--fg); border: var(--border-width-1) solid var(--white-a5); - border-radius: var(--radius-md); + border-radius: var(--radius-xl); box-shadow: 0 8px 24px rgb(0 0 0 / 0.35); font-size: var(--font-size-sm); line-height: var(--leading-5); From 22ae814ec874d9f86ddbe3935d2f2b74d7003a03 Mon Sep 17 00:00:00 2001 From: Hunter Garrett Date: Wed, 9 Sep 2026 06:21:09 -0400 Subject: [PATCH 03/10] feat(docs): shape both popups like the header button Panels keep the 12px corners but drop to 6px of padding and sit 6px from their triggers. Rows are the button's 32px box with 12px on the left and 8px on the right. The copy menu is 208px wide and single-line, its descriptions gone from the mock. --- .../page-actions/page-actions.module.css | 41 ++++++++----------- .../components/page-actions/page-actions.tsx | 15 ++----- .../components/page-toc/page-toc.module.css | 23 ++++++----- .../docs/src/components/page-toc/page-toc.tsx | 2 +- 4 files changed, 33 insertions(+), 48 deletions(-) diff --git a/apps/docs/src/components/page-actions/page-actions.module.css b/apps/docs/src/components/page-actions/page-actions.module.css index 2bb939fa..da3dee4a 100644 --- a/apps/docs/src/components/page-actions/page-actions.module.css +++ b/apps/docs/src/components/page-actions/page-actions.module.css @@ -100,17 +100,19 @@ /* ---- The menu ---- */ /* Portaled to , so it sets its own type. The mock's panel, shared - with the table of contents: 256px wide, 12px of padding, the darkest - gray with a hairline and 12px corners, 8px below the chevron. It scales - in from the edge nearest the chevron, the site's popup pattern; Base UI - sets --transform-origin from where the popup was placed. */ + with the table of contents: the darkest gray with a hairline and 12px + corners, 6px of padding, 6px below the chevron. This one is 208px wide, + the three labels' width with room to spare, where the table of contents + takes the 3xs container. It scales in from the edge nearest the chevron, + the site's popup pattern; Base UI sets --transform-origin from where the + popup was placed. */ .popup { box-sizing: border-box; display: flex; flex-direction: column; gap: var(--spacing-0-5); - width: var(--container-3xs); - padding: var(--spacing-3); + width: var(--spacing-52); + padding: var(--spacing-1-5); background: var(--gray-900); color: var(--fg); border: var(--border-width-1) solid var(--white-a5); @@ -130,16 +132,16 @@ transform: scale(0.95); } -/* A row is a title over a one-line description: 8px of padding with 12px - on the left, 6px corners, and the two lines 2px apart. Base UI marks the - row under the pointer or the keyboard cursor as highlighted, which the - mock draws as the darker gray fill, the same fill the table of contents - gives its current row. */ +/* A row is the button's shape: a 14px label in a 32px box with 12px on the + left and 8px on the right, 6px corners, 2px from the next row. Base UI + marks the row under the pointer or the keyboard cursor as highlighted, + which the mock draws as the darker gray fill, the same fill the table of + contents gives its current row. */ .row { display: flex; - flex-direction: column; - gap: var(--spacing-0-5); - padding: var(--spacing-2) var(--spacing-2) var(--spacing-2) var(--spacing-3); + align-items: center; + height: var(--spacing-8); + padding: 0 var(--spacing-2) 0 var(--spacing-3); border-radius: var(--radius-md); color: var(--fg); text-decoration: none; @@ -152,14 +154,3 @@ background: var(--gray-800); text-decoration: none; } - -.rowTitle { - font-size: var(--font-size-sm); - line-height: var(--leading-5); -} - -.rowDescription { - color: var(--gray-100); - font-size: var(--font-size-xs); - line-height: var(--leading-4); -} diff --git a/apps/docs/src/components/page-actions/page-actions.tsx b/apps/docs/src/components/page-actions/page-actions.tsx index 090be92f..f980b001 100644 --- a/apps/docs/src/components/page-actions/page-actions.tsx +++ b/apps/docs/src/components/page-actions/page-actions.tsx @@ -43,16 +43,10 @@ export function PageActions({ slug }: PageActionsProps) { - + - - Copy React - Copy this component as React - - - Copy as markdown - Copy this page as markdown for LLMs - + Copy React + Copy as markdown - View as markdown - View this page as plain text + View as markdown diff --git a/apps/docs/src/components/page-toc/page-toc.module.css b/apps/docs/src/components/page-toc/page-toc.module.css index f3deeb02..94ee7cf1 100644 --- a/apps/docs/src/components/page-toc/page-toc.module.css +++ b/apps/docs/src/components/page-toc/page-toc.module.css @@ -115,14 +115,14 @@ /* ---- The menu ---- */ /* Portaled to , so it sets its own type. The mock's panel, shared - with the header's copy menu (page-actions/): 256px wide, 12px of padding, - the darkest gray with a hairline and 12px corners, 8px from the lines. It + with the header's copy menu (page-actions/): 256px wide, 6px of padding, + the darkest gray with a hairline and 12px corners, 6px from the lines. It scales in from the edge nearest the lines, the site's popup pattern; Base UI sets --transform-origin from where the popup was placed. */ .popup { box-sizing: border-box; width: var(--container-3xs); - padding: var(--spacing-3); + padding: var(--spacing-1-5); background: var(--gray-900); color: var(--fg); border: var(--border-width-1) solid var(--white-a5); @@ -151,15 +151,16 @@ list-style: none; } -/* A row is the sidebar's row: 14px muted text in a 36px box with 12px on - the left and 8px elsewhere, 6px corners. Hover borrows the sidebar's - faint white fill. The current section is the mock's darker gray fill - under full-white text, rather than the sidebar's lime, because it marks - where the reader is on the page and not a page they chose. */ +/* A row is the header button's shape: 14px muted text in a 32px box with + 12px on the left and 8px on the right, 6px corners. Hover borrows the + sidebar's faint white fill. The current section is the mock's darker + gray fill under full-white text, rather than the sidebar's lime, because + it marks where the reader is on the page and not a page they chose. */ .row { - display: block; - height: var(--spacing-9); - padding: var(--spacing-2) var(--spacing-2) var(--spacing-2) var(--spacing-3); + display: flex; + align-items: center; + height: var(--spacing-8); + padding: 0 var(--spacing-2) 0 var(--spacing-3); border-radius: var(--radius-md); color: var(--fg-muted); text-decoration: none; diff --git a/apps/docs/src/components/page-toc/page-toc.tsx b/apps/docs/src/components/page-toc/page-toc.tsx index b1ad2204..e0956fab 100644 --- a/apps/docs/src/components/page-toc/page-toc.tsx +++ b/apps/docs/src/components/page-toc/page-toc.tsx @@ -137,7 +137,7 @@ export function PageToc({ sections }: { sections: PageTocSection[] }) { the lines with a document-coordinate update one frame behind the scroll, which reads as jitter. */} - +