From 4f705d1ad2d20fee2701eb2aae6dae301ee5bc48 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 29 Aug 2026 13:50:51 -0700 Subject: [PATCH 1/4] refactor(website): move the docs rules from global.css into docs.css (pure cut-paste) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relocates every .docs-prose rule, the code-figure rules ([data-rehype-pretty-code-*], .shiki), .docs-diagram/heading-anchor rules, the .ag-ui-arch-grid block, and the --docs-* local-constants block from global.css into docs.css. Zero edits — verified byte-identical via git diff HEAD~1 (pure relocation, no content change). Part of the inline-style substrate migration (Batch 3). --- apps/website/src/app/global.css | 249 ------------------------------- apps/website/src/styles/docs.css | 248 ++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+), 249 deletions(-) diff --git a/apps/website/src/app/global.css b/apps/website/src/app/global.css index 417bc6d4a..455f1f8fa 100644 --- a/apps/website/src/app/global.css +++ b/apps/website/src/app/global.css @@ -15,36 +15,6 @@ @import "../styles/marketing.css"; @import "../styles/pages.css"; -/* - * Local, non-token constants. - * - * These are deliberately NOT design tokens. Promoting them to - * @threadplane/design-tokens would imply the design system owns the syntax - * theme and the docs figure treatment. It does not. - * - * The --docs-code-* group is coupled to `rehypeOptions.theme` ('tokyo-night') - * in components/docs/MdxRenderer.tsx. Change the shiki theme and these must - * change with it. - * --docs-code-title-fg is here rather than on --color-text-muted for the same - * reason: it sits on a dark surface, where the light-theme muted token drops - * to 3.6:1 contrast. - */ -:root { - --docs-code-bg: #1a1b26; - --docs-code-title-fg: #8b8fa3; - --docs-code-border: rgba(0, 0, 0, 0.1); - --docs-code-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); - --docs-code-title-rule: rgba(255, 255, 255, 0.06); - --docs-figure-shadow: 0 4px 16px rgba(0, 32, 72, 0.1); - --docs-figure-shadow-bare: 0 4px 16px rgba(0, 32, 72, 0.08); - /* Accent tints in the 3.5–10% range, around --color-accent-surface (6%). - * Derived rather than hardcoded so they track the accent. */ - --docs-accent-tint-faint: color-mix(in srgb, var(--color-accent) 3.5%, transparent); - --docs-accent-tint-soft: color-mix(in srgb, var(--color-accent) 8%, transparent); - --docs-accent-tint-line: color-mix(in srgb, var(--color-accent) 10%, transparent); -} - - * { box-sizing: border-box; } @@ -95,222 +65,3 @@ h1, h2, h3, h4, h5, h6 { html { scroll-behavior: smooth; } - -/* Shiki code blocks — tokyo-night theme */ -.shiki { - padding: 1.5rem; - background: var(--docs-code-bg) !important; - overflow-x: auto; -} -.shiki code { - font-family: var(--font-mono), monospace; - font-size: 0.75rem; - line-height: 1.7; -} - -/* rehype-pretty-code — docs code blocks */ -.docs-prose [data-rehype-pretty-code-figure] { - margin: 1.5rem 0; - max-width: 100%; - overflow: hidden; -} - -.docs-prose [data-rehype-pretty-code-figure] pre { - padding: 1.25rem 1.5rem; - border-radius: 0.75rem; - border: 1px solid var(--docs-code-border); - box-shadow: var(--docs-code-shadow); - overflow-x: auto; - font-size: 0.8rem; - line-height: 1.7; -} - -.docs-prose [data-rehype-pretty-code-figure] code { - font-family: var(--font-mono), monospace; - font-size: inherit; - background: none !important; - padding: 0 !important; - border-radius: 0 !important; - color: inherit !important; -} - -.docs-prose [data-rehype-pretty-code-figure] [data-line] { - padding: 0 0.25rem; -} - -.docs-prose [data-rehype-pretty-code-figure] [data-rehype-pretty-code-title] { - font-family: var(--font-mono), monospace; - font-size: 0.7rem; - color: var(--docs-code-title-fg); - padding: 0.5rem 1.5rem; - background: var(--docs-code-bg); - border-bottom: 1px solid var(--docs-code-title-rule); - border-radius: 0.75rem 0.75rem 0 0; -} - -.docs-prose [data-rehype-pretty-code-figure]:has([data-rehype-pretty-code-title]) pre { - border-radius: 0 0 0.75rem 0.75rem; -} - -.docs-prose :not(pre) > code { - font-family: var(--font-mono), monospace; - font-size: 0.85em; - background: var(--color-accent-surface); - color: var(--color-accent); - padding: 0.15rem 0.4rem; - border-radius: 0.25rem; - font-weight: 400; -} - -.docs-prose { overflow-wrap: break-word; word-break: break-word; max-width: 100%; } -.docs-prose > * { max-width: 100%; } -.docs-prose h1 { font-size: 1.875rem; font-weight: 700; margin-top: 0; margin-bottom: 1rem; font-family: var(--font-garamond); } -.docs-prose h2 { font-size: 1.5rem; font-weight: 600; margin-top: 2.5rem; margin-bottom: 1rem; font-family: var(--font-garamond); } -.docs-prose h3 { font-size: 1.25rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.75rem; font-family: var(--font-garamond); } -.docs-prose p { line-height: 1.75; margin-bottom: 1.25rem; } -.docs-prose ul, .docs-prose ol { - margin-bottom: 1.25rem; - padding-left: 1.5rem; - list-style-position: outside; -} -/* Use longhand `list-style-type` to dodge Lightning CSS shorthand rewriting - `list-style: disc` to `list-style: outside` (which drops the bullet type - and lets Tailwind's `ol, ul, menu { list-style: none }` preflight win). */ -.docs-prose ul { list-style-type: disc; } -.docs-prose ol { list-style-type: decimal; } -.docs-prose li { margin-bottom: 0.5rem; line-height: 1.6; } -.docs-prose li > p { margin-bottom: 0.5rem; } -.docs-prose li::marker { color: var(--color-text-muted); } -.docs-prose ul ul, .docs-prose ol ol, .docs-prose ul ol, .docs-prose ol ul { - margin-top: 0.5rem; - margin-bottom: 0.5rem; -} - -/* Figures (screenshots + caption). - * Goal: lift the screenshot off the page so it reads as a distinct artifact - * rather than an inline asset. Tinted backdrop + soft shadow + clean caption. - * Scoped to image figures via :has(> img) so it doesn't collide with the - * code-block figures from rehype-pretty-code or component figures (e.g. - * AgUiArchDiagram). */ -.docs-prose figure:has(> img) { - margin: 2.5rem 0; - padding: 0.75rem 0.75rem 0; - background: var(--docs-accent-tint-faint); - border: 1px solid var(--docs-accent-tint-line); - border-radius: 0.75rem; -} -.docs-prose figure:has(> img) > img { - display: block; - width: 100%; - height: auto; - border-radius: 0.5rem; - box-shadow: var(--docs-figure-shadow); -} -.docs-prose figure:has(> img) > figcaption { - margin: 0; - padding: 0.875rem 0.5rem 0.625rem; - font-size: 0.875rem; - line-height: 1.5; - color: var(--color-text-muted); - text-align: center; - font-style: italic; -} -/* Bare images (no figure wrapper) — give them the same lift treatment. */ -.docs-prose > p > img, -.docs-prose > img { - display: block; - max-width: 100%; - height: auto; - margin: 2rem auto; - border-radius: 0.5rem; - box-shadow: var(--docs-figure-shadow-bare); -} - -/* Architecture diagrams are authored at exactly the width `.docs-prose` - * computes to (70ch = ~706px), so they render 1:1 and their labels stay at - * their authored size. Below that, scaling the whole figure down would take - * 11.5px labels under 5px, so the paragraph scrolls instead — the same - * treatment `.docs-table-scroll` gives a wide table. */ -.docs-prose > p:has(> .docs-diagram) { overflow-x: auto; } -.docs-prose > p > img.docs-diagram { max-width: none; } - -.docs-table-scroll { max-width: 100%; overflow-x: auto; margin: 1.5rem 0; } -.docs-prose table { width: 100%; border-collapse: collapse; font-size: 0.875rem; margin: 0; } -.docs-prose th { text-align: left; padding: 0.5rem 0.75rem; font-family: var(--font-mono); font-size: 0.75rem; text-transform: uppercase; color: var(--color-text-muted); border-bottom: 1px solid var(--color-accent-border); } -.docs-prose td { padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--docs-accent-tint-soft); color: var(--color-text-secondary); } -.docs-prose td code { font-size: 0.8em; } - -/* AG-UI architecture diagram */ -.ag-ui-arch-grid { - display: grid; - grid-template-columns: 1fr auto 1fr auto 1fr; - align-items: stretch; - gap: 0; -} -@media (max-width: 720px) { - .ag-ui-arch-grid { - grid-template-columns: 1fr; - } - .ag-ui-arch-arrow { - transform: rotate(90deg); - padding: 12px 0; - margin: 4px auto; - } -} - -/* Docs — readable column max-width */ -.docs-prose { - max-width: 70ch; -} -@media (max-width: 768px) { - .docs-prose { - max-width: 100%; - } -} - -/* Docs — hash-anchor affordance on H2/H3 */ -.docs-prose h2, -.docs-prose h3 { - position: relative; -} -.docs-prose h2 .heading-anchor, -.docs-prose h3 .heading-anchor { - position: absolute; - left: -1.25em; - top: 0; - font-family: var(--font-mono); - font-weight: 400; - color: var(--color-text-muted); - opacity: 0; - text-decoration: none; - transition: opacity 120ms ease; -} -/* - * The `#` glyph is generated content, never a text node: that keeps it out of - * the heading's textContent so extracted headings ("Prerequisites") stay clean - * for search snippets, page outlines, and agents reading the DOM. - */ -.docs-prose h2 .heading-anchor::before, -.docs-prose h3 .heading-anchor::before { - content: '#'; -} -.docs-prose h2:hover .heading-anchor, -.docs-prose h3:hover .heading-anchor, -.docs-prose h2 .heading-anchor:focus-visible, -.docs-prose h3 .heading-anchor:focus-visible { - opacity: 0.6; -} -.docs-prose h2 .heading-anchor:hover, -.docs-prose h3 .heading-anchor:hover { - opacity: 1; - color: var(--color-accent); -} -@media (max-width: 768px) { - /* On narrow viewports, drop the absolute positioning so the hash doesn't overlap the page edge. */ - .docs-prose h2 .heading-anchor, - .docs-prose h3 .heading-anchor { - position: static; - margin-right: 6px; - display: none; - } -} diff --git a/apps/website/src/styles/docs.css b/apps/website/src/styles/docs.css index e4ef38d5f..a476d4982 100644 --- a/apps/website/src/styles/docs.css +++ b/apps/website/src/styles/docs.css @@ -8,3 +8,251 @@ * * Migration: docs/superpowers/plans/2026-08-29-inline-style-substrate-migration.md */ + +/* + * Local, non-token constants. + * + * These are deliberately NOT design tokens. Promoting them to + * @threadplane/design-tokens would imply the design system owns the syntax + * theme and the docs figure treatment. It does not. + * + * The --docs-code-* group is coupled to `rehypeOptions.theme` ('tokyo-night') + * in components/docs/MdxRenderer.tsx. Change the shiki theme and these must + * change with it. + * --docs-code-title-fg is here rather than on --color-text-muted for the same + * reason: it sits on a dark surface, where the light-theme muted token drops + * to 3.6:1 contrast. + */ +:root { + --docs-code-bg: #1a1b26; + --docs-code-title-fg: #8b8fa3; + --docs-code-border: rgba(0, 0, 0, 0.1); + --docs-code-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); + --docs-code-title-rule: rgba(255, 255, 255, 0.06); + --docs-figure-shadow: 0 4px 16px rgba(0, 32, 72, 0.1); + --docs-figure-shadow-bare: 0 4px 16px rgba(0, 32, 72, 0.08); + /* Accent tints in the 3.5–10% range, around --color-accent-surface (6%). + * Derived rather than hardcoded so they track the accent. */ + --docs-accent-tint-faint: color-mix(in srgb, var(--color-accent) 3.5%, transparent); + --docs-accent-tint-soft: color-mix(in srgb, var(--color-accent) 8%, transparent); + --docs-accent-tint-line: color-mix(in srgb, var(--color-accent) 10%, transparent); +} + +/* Shiki code blocks — tokyo-night theme */ +.shiki { + padding: 1.5rem; + background: var(--docs-code-bg) !important; + overflow-x: auto; +} +.shiki code { + font-family: var(--font-mono), monospace; + font-size: 0.75rem; + line-height: 1.7; +} + +/* rehype-pretty-code — docs code blocks */ +.docs-prose [data-rehype-pretty-code-figure] { + margin: 1.5rem 0; + max-width: 100%; + overflow: hidden; +} + +.docs-prose [data-rehype-pretty-code-figure] pre { + padding: 1.25rem 1.5rem; + border-radius: 0.75rem; + border: 1px solid var(--docs-code-border); + box-shadow: var(--docs-code-shadow); + overflow-x: auto; + font-size: 0.8rem; + line-height: 1.7; +} + +.docs-prose [data-rehype-pretty-code-figure] code { + font-family: var(--font-mono), monospace; + font-size: inherit; + background: none !important; + padding: 0 !important; + border-radius: 0 !important; + color: inherit !important; +} + +.docs-prose [data-rehype-pretty-code-figure] [data-line] { + padding: 0 0.25rem; +} + +.docs-prose [data-rehype-pretty-code-figure] [data-rehype-pretty-code-title] { + font-family: var(--font-mono), monospace; + font-size: 0.7rem; + color: var(--docs-code-title-fg); + padding: 0.5rem 1.5rem; + background: var(--docs-code-bg); + border-bottom: 1px solid var(--docs-code-title-rule); + border-radius: 0.75rem 0.75rem 0 0; +} + +.docs-prose [data-rehype-pretty-code-figure]:has([data-rehype-pretty-code-title]) pre { + border-radius: 0 0 0.75rem 0.75rem; +} + +.docs-prose :not(pre) > code { + font-family: var(--font-mono), monospace; + font-size: 0.85em; + background: var(--color-accent-surface); + color: var(--color-accent); + padding: 0.15rem 0.4rem; + border-radius: 0.25rem; + font-weight: 400; +} + +.docs-prose { overflow-wrap: break-word; word-break: break-word; max-width: 100%; } +.docs-prose > * { max-width: 100%; } +.docs-prose h1 { font-size: 1.875rem; font-weight: 700; margin-top: 0; margin-bottom: 1rem; font-family: var(--font-garamond); } +.docs-prose h2 { font-size: 1.5rem; font-weight: 600; margin-top: 2.5rem; margin-bottom: 1rem; font-family: var(--font-garamond); } +.docs-prose h3 { font-size: 1.25rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.75rem; font-family: var(--font-garamond); } +.docs-prose p { line-height: 1.75; margin-bottom: 1.25rem; } +.docs-prose ul, .docs-prose ol { + margin-bottom: 1.25rem; + padding-left: 1.5rem; + list-style-position: outside; +} +/* Use longhand `list-style-type` to dodge Lightning CSS shorthand rewriting + `list-style: disc` to `list-style: outside` (which drops the bullet type + and lets Tailwind's `ol, ul, menu { list-style: none }` preflight win). */ +.docs-prose ul { list-style-type: disc; } +.docs-prose ol { list-style-type: decimal; } +.docs-prose li { margin-bottom: 0.5rem; line-height: 1.6; } +.docs-prose li > p { margin-bottom: 0.5rem; } +.docs-prose li::marker { color: var(--color-text-muted); } +.docs-prose ul ul, .docs-prose ol ol, .docs-prose ul ol, .docs-prose ol ul { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +/* Figures (screenshots + caption). + * Goal: lift the screenshot off the page so it reads as a distinct artifact + * rather than an inline asset. Tinted backdrop + soft shadow + clean caption. + * Scoped to image figures via :has(> img) so it doesn't collide with the + * code-block figures from rehype-pretty-code or component figures (e.g. + * AgUiArchDiagram). */ +.docs-prose figure:has(> img) { + margin: 2.5rem 0; + padding: 0.75rem 0.75rem 0; + background: var(--docs-accent-tint-faint); + border: 1px solid var(--docs-accent-tint-line); + border-radius: 0.75rem; +} +.docs-prose figure:has(> img) > img { + display: block; + width: 100%; + height: auto; + border-radius: 0.5rem; + box-shadow: var(--docs-figure-shadow); +} +.docs-prose figure:has(> img) > figcaption { + margin: 0; + padding: 0.875rem 0.5rem 0.625rem; + font-size: 0.875rem; + line-height: 1.5; + color: var(--color-text-muted); + text-align: center; + font-style: italic; +} +/* Bare images (no figure wrapper) — give them the same lift treatment. */ +.docs-prose > p > img, +.docs-prose > img { + display: block; + max-width: 100%; + height: auto; + margin: 2rem auto; + border-radius: 0.5rem; + box-shadow: var(--docs-figure-shadow-bare); +} + +/* Architecture diagrams are authored at exactly the width `.docs-prose` + * computes to (70ch = ~706px), so they render 1:1 and their labels stay at + * their authored size. Below that, scaling the whole figure down would take + * 11.5px labels under 5px, so the paragraph scrolls instead — the same + * treatment `.docs-table-scroll` gives a wide table. */ +.docs-prose > p:has(> .docs-diagram) { overflow-x: auto; } +.docs-prose > p > img.docs-diagram { max-width: none; } + +.docs-table-scroll { max-width: 100%; overflow-x: auto; margin: 1.5rem 0; } +.docs-prose table { width: 100%; border-collapse: collapse; font-size: 0.875rem; margin: 0; } +.docs-prose th { text-align: left; padding: 0.5rem 0.75rem; font-family: var(--font-mono); font-size: 0.75rem; text-transform: uppercase; color: var(--color-text-muted); border-bottom: 1px solid var(--color-accent-border); } +.docs-prose td { padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--docs-accent-tint-soft); color: var(--color-text-secondary); } +.docs-prose td code { font-size: 0.8em; } + +/* AG-UI architecture diagram */ +.ag-ui-arch-grid { + display: grid; + grid-template-columns: 1fr auto 1fr auto 1fr; + align-items: stretch; + gap: 0; +} +@media (max-width: 720px) { + .ag-ui-arch-grid { + grid-template-columns: 1fr; + } + .ag-ui-arch-arrow { + transform: rotate(90deg); + padding: 12px 0; + margin: 4px auto; + } +} + +/* Docs — readable column max-width */ +.docs-prose { + max-width: 70ch; +} +@media (max-width: 768px) { + .docs-prose { + max-width: 100%; + } +} + +/* Docs — hash-anchor affordance on H2/H3 */ +.docs-prose h2, +.docs-prose h3 { + position: relative; +} +.docs-prose h2 .heading-anchor, +.docs-prose h3 .heading-anchor { + position: absolute; + left: -1.25em; + top: 0; + font-family: var(--font-mono); + font-weight: 400; + color: var(--color-text-muted); + opacity: 0; + text-decoration: none; + transition: opacity 120ms ease; +} +/* + * The `#` glyph is generated content, never a text node: that keeps it out of + * the heading's textContent so extracted headings ("Prerequisites") stay clean + * for search snippets, page outlines, and agents reading the DOM. + */ +.docs-prose h2 .heading-anchor::before, +.docs-prose h3 .heading-anchor::before { + content: '#'; +} +.docs-prose h2:hover .heading-anchor, +.docs-prose h3:hover .heading-anchor, +.docs-prose h2 .heading-anchor:focus-visible, +.docs-prose h3 .heading-anchor:focus-visible { + opacity: 0.6; +} +.docs-prose h2 .heading-anchor:hover, +.docs-prose h3 .heading-anchor:hover { + opacity: 1; + color: var(--color-accent); +} +@media (max-width: 768px) { + /* On narrow viewports, drop the absolute positioning so the hash doesn't overlap the page edge. */ + .docs-prose h2 .heading-anchor, + .docs-prose h3 .heading-anchor { + position: static; + margin-right: 6px; + display: none; + } +} From 245cd35b5460e045e8c6ad05f82420d1f4d103e4 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 29 Aug 2026 13:56:01 -0700 Subject: [PATCH 2/4] refactor(website): move mdx component styles to docs.css Migrates Callout, Steps, Card, Tabs, CodeBlock, CodeGroup, and FeatureChips (components/docs/mdx) off inline style props onto data-mdx/.mdx-* classes in docs.css. Callout's tone map becomes data-tone rules on its existing attribute; Tabs/CodeGroup active-tab state and CodeBlock's copied state become data-active/data-copied modifiers; the 5 presentation-only hover handlers across Card, Tabs, CodeGroup, and FeatureChips become :hover rules. headings.tsx has no style sites (skipped); MdxRenderer's --tw-prose-* custom-property object is the documented escape hatch and stays inline. Part of the inline-style substrate migration (Batch 3). --- .../src/components/docs/mdx/Callout.tsx | 72 +--- apps/website/src/components/docs/mdx/Card.tsx | 60 +-- .../src/components/docs/mdx/CodeBlock.tsx | 25 +- .../src/components/docs/mdx/CodeGroup.tsx | 43 +-- .../src/components/docs/mdx/FeatureChips.tsx | 65 +--- .../website/src/components/docs/mdx/Steps.tsx | 43 +-- apps/website/src/components/docs/mdx/Tabs.tsx | 31 +- apps/website/src/styles/docs.css | 342 ++++++++++++++++++ 8 files changed, 389 insertions(+), 292 deletions(-) diff --git a/apps/website/src/components/docs/mdx/Callout.tsx b/apps/website/src/components/docs/mdx/Callout.tsx index 6acd1d4bd..24d100d26 100644 --- a/apps/website/src/components/docs/mdx/Callout.tsx +++ b/apps/website/src/components/docs/mdx/Callout.tsx @@ -1,5 +1,4 @@ import type { ReactNode } from 'react'; -import { tokens } from '@threadplane/design-tokens'; type CalloutType = 'tip' | 'warning' | 'info' | 'danger'; @@ -9,72 +8,23 @@ interface Props { children: ReactNode; } -const TONE: Record = { - tip: { stripe: '#1a7a40', icon: '✓' }, - warning: { stripe: '#D4850F', icon: '!' }, - info: { stripe: tokens.colors.accent, icon: 'i' }, - danger: { stripe: tokens.colors.angularRed, icon: '✕' }, +const ICON: Record = { + tip: '✓', + warning: '!', + info: 'i', + danger: '✕', }; export function Callout({ type = 'info', title, children }: Props) { - const tone = TONE[type]; return ( -
-
-