improvement(docs): align components with the platform design system (#5227)

* improvement(docs): align components with the platform design system

Bring the docs app's chrome in line with the main Sim design system,
validated against the canonical emcn source in apps/sim.

- ask-ai: fix undefined tokens (--text-base x6, --text-link) that broke
  the send-button fill and link/text colors; send button now matches the
  canonical primary fill (text-primary/text-inverse, dark:bg-white); use
  --shadow-medium and chip gap rhythm
- not-found: replace the hand-rolled brand pill with <ChipLink variant='brand'>
  and swap fumadocs tokens for platform tokens
- search-trigger: compose the exported chip chrome constants instead of
  re-spelling them (single source of truth)
- what-you-will-learn, video-chapters: fumadocs fd-* tokens -> platform tokens
- workflow-preview: add --wp-highlight token; route the #33b4ff highlight,
  #ef4444 error dots, and toggle/slider green through tokens
- video-placeholder: tokenize the status pill (bespoke illustration art
  intentionally left as-is)

dropdown-menu, faq, theme-toggle, and page-type-badge were deliberately
left at their canonical values (14px row icons, rounded-md badge) after
validation showed those match the platform, not the chip-pill, standard.

* improvement(docs): neutral primary chip for nav CTA + fix cluster spacing

Align the docs navbar with the main app, which reserves green for
accents/status and uses a neutral high-contrast CTA in nav.

- add a canonical `primary` chip variant (inverse fill: dark in light
  mode, white in dark mode), mirroring the emcn chip's primary action
- "Get started" and the 404 "Go home" now use variant='primary' instead
  of the green brand surface
- retire the now-unused `brand` chip variant (no parallel path left behind)
- fix navbar right-cluster spacing: gap-2 to match the landing navbar and
  drop the asymmetric ml-1 on the CTA
This commit is contained in:
Waleed
2026-06-26 17:24:09 -07:00
committed by GitHub
parent a33c173146
commit 954de0559b
14 changed files with 69 additions and 48 deletions
+6 -9
View File
@@ -1,5 +1,5 @@
import { DocsPage } from 'fumadocs-ui/page'
import Link from 'next/link'
import { ChipLink } from '@/components/ui/chip'
export const metadata = {
title: 'Page Not Found',
@@ -9,19 +9,16 @@ export default function NotFound() {
return (
<DocsPage>
<div className='flex min-h-[70vh] flex-col items-center justify-center gap-4 text-center'>
<h1 className='bg-gradient-to-b from-[#47d991] to-[#33c482] bg-clip-text font-semibold text-8xl text-transparent'>
<h1 className='bg-gradient-to-b from-[var(--brand-accent)] to-[var(--brand-accent-hover)] bg-clip-text font-semibold text-8xl text-transparent'>
404
</h1>
<h2 className='font-semibold text-2xl text-foreground'>Page Not Found</h2>
<p className='text-muted-foreground'>
<h2 className='font-semibold text-2xl text-[var(--text-primary)]'>Page Not Found</h2>
<p className='text-[var(--text-muted)]'>
The page you're looking for doesn't exist or has been moved.
</p>
<Link
href='/'
className='ml-1 flex items-center rounded-[8px] bg-[#33c482] px-2.5 py-1.5 text-[13px] text-white transition-colors duration-200 hover:bg-[#2DAC72]'
>
<ChipLink href='/' variant='primary'>
Go home
</Link>
</ChipLink>
</div>
</DocsPage>
)
+1
View File
@@ -1669,6 +1669,7 @@ main article blockquote {
--wp-chip-text: var(--text-body);
--wp-divider: #ededed; /* --divider */
--wp-edge: #e0e0e0; /* --workflow-edge */
--wp-highlight: #33b4ff; /* traced edge / highlighted output node */
/* text */
--wp-text: var(--text-primary);
+11 -11
View File
@@ -84,7 +84,7 @@ export function AskAI({ locale }: AskAIProps) {
type='button'
aria-label='Ask AI'
onClick={() => setOpen(true)}
className='fixed right-4 bottom-4 z-50 flex h-11 items-center gap-2 rounded-full border border-[var(--border-1)] bg-[var(--surface-5)] px-4 font-season text-[var(--text-base)] text-sm shadow-lg transition-colors hover:bg-[var(--surface-active)] dark:bg-[var(--surface-4)]'
className='fixed right-4 bottom-4 z-50 flex h-11 items-center gap-1.5 rounded-full border border-[var(--border-1)] bg-[var(--surface-5)] px-4 font-season text-[var(--text-body)] text-sm shadow-[var(--shadow-medium)] transition-colors hover:bg-[var(--surface-active)] dark:bg-[var(--surface-4)]'
>
<MessageCircle className='size-[16px] text-[var(--text-icon)]' />
Ask AI
@@ -92,9 +92,9 @@ export function AskAI({ locale }: AskAIProps) {
)}
{open && (
<div className='fixed right-4 bottom-4 z-50 flex h-[600px] max-h-[calc(100vh-2rem)] w-[400px] max-w-[calc(100vw-2rem)] flex-col overflow-hidden rounded-xl border border-[var(--border-1)] bg-[var(--surface-5)] shadow-xl dark:bg-[var(--surface-4)]'>
<div className='fixed right-4 bottom-4 z-50 flex h-[600px] max-h-[calc(100vh-2rem)] w-[400px] max-w-[calc(100vw-2rem)] flex-col overflow-hidden rounded-xl border border-[var(--border-1)] bg-[var(--surface-5)] shadow-[var(--shadow-medium)] dark:bg-[var(--surface-4)]'>
<div className='flex items-center justify-between border-[var(--border-1)] border-b px-4 py-3'>
<span className='flex items-center gap-2 font-season text-[var(--text-base)] text-sm'>
<span className='flex items-center gap-1.5 font-season text-[var(--text-body)] text-sm'>
<MessageCircle className='size-[16px] text-[var(--text-icon)]' />
Ask AI
</span>
@@ -105,7 +105,7 @@ export function AskAI({ locale }: AskAIProps) {
stop()
setOpen(false)
}}
className='flex size-7 items-center justify-center rounded-md text-[var(--text-icon)] transition-colors hover:bg-[var(--surface-active)]'
className='flex size-7 items-center justify-center rounded-lg text-[var(--text-icon)] transition-colors hover:bg-[var(--surface-active)]'
>
<X className='size-[16px]' />
</button>
@@ -132,13 +132,13 @@ export function AskAI({ locale }: AskAIProps) {
)}
>
{message.role === 'user' ? (
<div className='max-w-[90%] whitespace-pre-wrap rounded-lg bg-[var(--surface-active)] px-3 py-2 text-[var(--text-base)] text-sm'>
<div className='max-w-[90%] whitespace-pre-wrap rounded-lg bg-[var(--surface-active)] px-3 py-2 text-[var(--text-body)] text-sm'>
{text}
</div>
) : (
<div className='max-w-[90%] text-[var(--text-base)] text-sm'>
<div className='max-w-[90%] text-[var(--text-body)] text-sm'>
{text ? (
<Streamdown className='space-y-2 text-sm leading-relaxed [&_a]:text-[var(--text-link)] [&_a]:underline [&_li]:my-0.5 [&_ol]:list-decimal [&_ol]:pl-5 [&_ul]:list-disc [&_ul]:pl-5'>
<Streamdown className='space-y-2 text-sm leading-relaxed [&_a]:text-[var(--brand-accent)] [&_a]:underline [&_li]:my-0.5 [&_ol]:list-decimal [&_ol]:pl-5 [&_ul]:list-disc [&_ul]:pl-5'>
{text}
</Streamdown>
) : isStreaming ? (
@@ -156,7 +156,7 @@ export function AskAI({ locale }: AskAIProps) {
href={source.url}
target='_blank'
rel='noopener noreferrer'
className='rounded-md border border-[var(--border-1)] px-2 py-0.5 text-[var(--text-muted)] text-xs transition-colors hover:bg-[var(--surface-active)]'
className='rounded-lg border border-[var(--border-1)] px-2 py-0.5 text-[var(--text-muted)] text-xs transition-colors hover:bg-[var(--surface-active)]'
>
{source.title || source.url}
</a>
@@ -189,7 +189,7 @@ export function AskAI({ locale }: AskAIProps) {
}}
rows={1}
placeholder='Ask a question…'
className='max-h-32 flex-1 resize-none bg-transparent font-season text-[var(--text-base)] text-sm outline-none placeholder:text-[var(--text-muted)]'
className='max-h-32 flex-1 resize-none bg-transparent font-season text-[var(--text-body)] text-sm outline-none placeholder:text-[var(--text-muted)]'
/>
{isBusy ? (
<button
@@ -198,14 +198,14 @@ export function AskAI({ locale }: AskAIProps) {
onClick={() => stop()}
className='flex size-8 shrink-0 items-center justify-center rounded-lg bg-[var(--surface-active)] text-[var(--text-icon)]'
>
<Square className='size-[14px]' />
<Square className='size-[16px]' />
</button>
) : (
<button
type='submit'
aria-label='Send'
disabled={!input.trim()}
className='flex size-8 shrink-0 items-center justify-center rounded-lg bg-[var(--text-base)] text-[var(--surface-5)] transition-opacity disabled:opacity-40 dark:bg-[var(--text-base)]'
className='flex size-8 shrink-0 items-center justify-center rounded-lg bg-[var(--text-primary)] text-[var(--text-inverse)] transition-opacity disabled:opacity-40 dark:bg-white dark:text-[var(--bg)]'
>
<ArrowUp className='size-[16px]' />
</button>
+2 -2
View File
@@ -52,10 +52,10 @@ export function Navbar() {
<SearchTrigger />
</div>
<div className='flex items-center gap-1'>
<div className='flex items-center gap-2'>
<LanguageDropdown />
<ThemeToggle />
<ChipLink href='https://sim.ai' variant='brand' className='ml-1'>
<ChipLink href='https://sim.ai' variant='primary'>
Get started
</ChipLink>
</div>
+10 -7
View File
@@ -26,6 +26,9 @@ export const chipContentIconClass = 'size-[16px] flex-shrink-0 text-[var(--text-
export const chipContentLabelClass = 'min-w-0 truncate text-[var(--text-body)] text-sm'
/** The filled FILL (surface only, no border) — `--surface-5` light / `--surface-4` dark. */
export const chipFilledFillTokens = 'bg-[var(--surface-5)] dark:bg-[var(--surface-4)]'
/** The inverse/primary FILL — dark surface + inverse text in light, white + `--bg` text in dark. */
const chipPrimaryFillTokens =
'bg-[var(--text-primary)] text-[var(--text-inverse)] dark:bg-white dark:text-[var(--bg)]'
/** 1px `--border-1` border applied to chip triggers so they read as controls. */
export const TRIGGER_BORDER_CLASS = 'border border-[var(--border-1)]'
@@ -34,8 +37,9 @@ export const TRIGGER_BORDER_CLASS = 'border border-[var(--border-1)]'
*
* @remarks
* The implicit default variant is the bare pill — transparent, `--surface-active`
* on hover. `filled` adds the filled surface; `brand` is the green CTA surface
* (`--brand-accent`) used for the "Get started" link.
* on hover. `filled` adds the filled surface; `primary` is the canonical inverse
* CTA surface (dark in light mode, white in dark mode) used for the "Get started"
* link.
*/
const chipVariants = cva(
`group cursor-pointer font-season ${chipGeometryClass} transition-colors disabled:cursor-not-allowed disabled:opacity-60`,
@@ -44,8 +48,7 @@ const chipVariants = cva(
variant: {
default: 'hover:bg-[var(--surface-active)]',
filled: `${chipFilledFillTokens} hover:bg-[var(--surface-active)]`,
brand:
'bg-[var(--brand-accent)] text-white hover:bg-[var(--brand-accent-hover)] hover:text-white',
primary: `${chipPrimaryFillTokens} hover:bg-[var(--text-body)] dark:hover:bg-[var(--text-secondary)]`,
},
fullWidth: { true: 'flex', false: 'inline-flex' },
},
@@ -66,7 +69,7 @@ interface ChipBaseProps extends VariantProps<typeof chipVariants> {
}
/**
* `brand` sets text color on the chip itself — its icon and label inherit via
* `primary` sets text color on the chip itself — its icon and label inherit via
* `currentColor`. The default and `filled` chips use explicit icon
* (`--text-icon`) and label (`--text-body`) colors.
*/
@@ -76,7 +79,7 @@ function ChipContent({
rightIcon: RightIcon,
children,
}: ChipBaseProps) {
const isInverse = variant === 'brand'
const isInverse = variant === 'primary'
const iconClass = cn(chipContentIconClass, isInverse && 'text-current')
const labelClass = cn(chipContentLabelClass, 'flex-1', isInverse && 'text-current')
return (
@@ -121,7 +124,7 @@ interface ChipLinkProps
ChipBaseProps {}
/**
* @example <ChipLink href='https://sim.ai' variant='brand'>Get started</ChipLink>
* @example <ChipLink href='https://sim.ai' variant='primary'>Get started</ChipLink>
*/
const ChipLink = forwardRef<HTMLAnchorElement, ChipLinkProps>(function ChipLink(
{ className, variant, fullWidth, leftIcon, rightIcon, children, ...props },
+14 -2
View File
@@ -1,6 +1,13 @@
'use client'
import { Search } from 'lucide-react'
import {
chipContentIconClass,
chipFilledFillTokens,
chipGeometryClass,
TRIGGER_BORDER_CLASS,
} from '@/components/ui/chip'
import { cn } from '@/lib/utils'
export function SearchTrigger() {
const openSearchDialog = () => {
@@ -16,10 +23,15 @@ export function SearchTrigger() {
<button
type='button'
data-search-trigger
className='flex h-[30px] w-[360px] cursor-pointer items-center gap-1.5 rounded-lg border border-[var(--border-1)] bg-[var(--surface-5)] px-2 font-season text-[var(--text-muted)] text-sm transition-colors hover:bg-[var(--surface-active)] dark:bg-[var(--surface-4)]'
className={cn(
chipGeometryClass,
chipFilledFillTokens,
TRIGGER_BORDER_CLASS,
'flex w-[360px] cursor-pointer font-season text-[var(--text-muted)] transition-colors hover:bg-[var(--surface-active)]'
)}
onClick={openSearchDialog}
>
<Search className='size-[14px] text-[var(--text-icon)]' />
<Search className={chipContentIconClass} />
<span>Search&hellip;</span>
<kbd className='ml-auto flex items-center'>
<span className='text-[15px]'>⌘</span>
+7 -4
View File
@@ -43,9 +43,12 @@ export function VideoChapters({ title = 'Chapters', chapters, className }: Video
return (
<aside
className={cn('not-prose rounded-xl border border-fd-border bg-fd-card/40 p-5', className)}
className={cn(
'not-prose rounded-xl border border-[var(--border-1)] bg-[var(--surface-3)] p-5',
className
)}
>
<h2 className='mt-0 mb-3 font-semibold text-fd-foreground text-lg'>{title}</h2>
<h2 className='mt-0 mb-3 font-semibold text-[var(--text-primary)] text-lg'>{title}</h2>
<ul className='m-0 flex list-none flex-col gap-0.5 p-0'>
{chapters.map((chapter) => (
<li key={chapter.title}>
@@ -58,12 +61,12 @@ export function VideoChapters({ title = 'Chapters', chapters, className }: Video
new CustomEvent('academy:seek', { detail: { time: parseTime(chapter.time) } })
)
}}
className='flex w-full cursor-pointer items-start gap-2.5 rounded-lg px-2.5 py-2 text-left text-fd-muted-foreground text-sm transition-colors hover:bg-fd-accent/50 disabled:cursor-default disabled:hover:bg-transparent'
className='flex w-full cursor-pointer items-start gap-2.5 rounded-lg px-2.5 py-2 text-left text-[var(--text-secondary)] text-sm transition-colors hover:bg-[var(--surface-active)] disabled:cursor-default disabled:hover:bg-transparent'
>
<CirclePlay className='mt-0.5 size-4 shrink-0' />
<span className='min-w-0 flex-1 break-words'>{chapter.title}</span>
{chapter.time && (
<span className='mt-0.5 shrink-0 text-fd-muted-foreground text-xs tabular-nums'>
<span className='mt-0.5 shrink-0 text-[var(--text-muted)] text-xs tabular-nums'>
{chapter.time}
</span>
)}
@@ -143,8 +143,8 @@ export function VideoPlaceholder({
{/* Top-right status pill — only until a video is wired up */}
{!hasVideo && (
<span className='absolute top-6 right-6 z-10 inline-flex items-center gap-2 rounded-full border border-[#E6E6E6] bg-white px-4 py-2 font-medium text-[#5F5F5F] text-[12px] uppercase tracking-[0.14em] md:top-8 md:right-8 dark:border-white/12 dark:bg-[#1A1A1A] dark:text-[#E6E6E6]'>
<span className='size-1.5 rounded-full bg-[#1F8A5B]' />
<span className='absolute top-6 right-6 z-10 inline-flex items-center gap-2 rounded-full border border-[var(--border-1)] bg-[var(--surface-2)] px-4 py-2 font-medium text-[12px] text-[var(--text-secondary)] uppercase tracking-[0.14em] md:top-8 md:right-8 dark:bg-[var(--surface-1)]'>
<span className='size-1.5 rounded-full bg-[var(--brand-accent)]' />
{label}
</span>
)}
@@ -14,14 +14,19 @@ interface WhatYouWillLearnProps {
export function WhatYouWillLearn({ items, className }: WhatYouWillLearnProps) {
return (
<div
className={cn('not-prose rounded-xl border border-fd-border bg-fd-card/40 p-6', className)}
className={cn(
'not-prose rounded-xl border border-[var(--border-1)] bg-[var(--surface-3)] p-6',
className
)}
>
<h2 className='mt-0 mb-5 font-semibold text-fd-foreground text-xl'>What you will learn</h2>
<h2 className='mt-0 mb-5 font-semibold text-[var(--text-primary)] text-xl'>
What you will learn
</h2>
<div className='flex flex-col gap-5'>
{items.map((item) => (
<div key={item.title}>
<p className='mb-1 font-semibold text-fd-foreground text-sm'>{item.title}</p>
<p className='m-0 text-fd-muted-foreground text-sm leading-relaxed'>{item.body}</p>
<p className='mb-1 font-semibold text-[var(--text-primary)] text-sm'>{item.title}</p>
<p className='m-0 text-[var(--text-secondary)] text-sm leading-relaxed'>{item.body}</p>
</div>
))}
</div>
@@ -76,7 +76,7 @@ function FieldControl({ field }: { field: InspectorField }) {
<div className='flex items-center gap-2'>
<div
className='flex h-[18px] w-[32px] items-center rounded-full px-[2px] transition-colors'
style={{ background: on ? '#33C482' : 'var(--wp-border-1)' }}
style={{ background: on ? 'var(--brand-accent)' : 'var(--wp-border-1)' }}
>
<div
className='size-[14px] rounded-full bg-white'
@@ -94,7 +94,7 @@ function FieldControl({ field }: { field: InspectorField }) {
<div className='flex w-full items-center gap-3'>
<div className='relative h-[4px] flex-1 rounded-full bg-[var(--wp-border-1)]'>
<div
className='absolute inset-y-0 left-0 rounded-full bg-[#33C482]'
className='absolute inset-y-0 left-0 rounded-full bg-[var(--brand-accent)]'
style={{ width: `${percent}%` }}
/>
<div
@@ -76,7 +76,7 @@ function BlockCard({ spec }: { spec: BlockDisplaySpec }) {
<span className='flex-shrink-0 font-normal text-[14px] text-[var(--wp-text-3)] capitalize'>
error
</span>
<span className={`${DOT} right-[-16px] rounded-r-[2px] bg-[#ef4444]`} />
<span className={`${DOT} right-[-16px] rounded-r-[2px] bg-[var(--text-error)]`} />
</div>
)}
</div>
@@ -73,7 +73,7 @@ function TreeNode({ node, depth = 0 }: { node: OutputNode; depth?: number }) {
<div className='flex min-h-[26px] items-center gap-2 rounded-[6px] px-1'>
<span
className='text-[13px]'
style={{ color: node.highlight ? '#33b4ff' : 'var(--wp-text)' }}
style={{ color: node.highlight ? 'var(--wp-highlight)' : 'var(--wp-text)' }}
>
{node.key}
</span>
@@ -42,7 +42,7 @@ const HANDLE_RIGHT = `${HANDLE_BASE} !right-[-8px] !h-5 !w-[7px] !rounded-l-none
const HANDLE_BRANCH =
'!z-[10] !border-none !right-[-16px] !h-5 !w-[7px] !rounded-l-none !rounded-r-[2px] !bg-[var(--wp-edge)]'
const HANDLE_ERROR =
'!z-[10] !border-none !right-[-16px] !h-5 !w-[7px] !rounded-l-none !rounded-r-[2px] !bg-[#ef4444]'
'!z-[10] !border-none !right-[-16px] !h-5 !w-[7px] !rounded-l-none !rounded-r-[2px] !bg-[var(--text-error)]'
/**
* Static preview block node matching the real WorkflowBlock styling.
@@ -88,7 +88,7 @@ export const PreviewBlockNode = memo(function PreviewBlockNode({
>
<div className='relative z-[20] w-[250px] select-none rounded-[8px] border border-[var(--wp-border-1)] bg-[var(--wp-surface)]'>
{isHighlighted && (
<div className='pointer-events-none absolute inset-0 z-40 rounded-[8px] ring-2 ring-[#33b4ff]' />
<div className='pointer-events-none absolute inset-0 z-40 rounded-[8px] ring-2 ring-[var(--wp-highlight)]' />
)}
{!hideTargetHandle && (
<Handle
@@ -52,7 +52,7 @@ export const BLOCK_STAGGER = 0.12
export const EASE_OUT: [number, number, number, number] = [0.16, 1, 0.3, 1]
const EDGE_STYLE = { stroke: 'var(--wp-edge)', strokeWidth: 1.5 } as const
const EDGE_STYLE_HIGHLIGHT = { stroke: '#33b4ff', strokeWidth: 2.5 } as const
const EDGE_STYLE_HIGHLIGHT = { stroke: 'var(--wp-highlight)', strokeWidth: 2.5 } as const
/** Optional emphasis: light one block or one edge and dim everything else. */
export interface HighlightOptions {