Skip to content

Commit df180f2

Browse files
Abeutyclaude
andauthored
feat(landing): shared DS-aligned library landing template + brand/type DS work (#1040)
Replace the copy-pasted per-library landing pages with a single config-driven LibraryLanding component, and convert Query (rich) and Ranger (lean) as the first POC pages. Landing: - Shared LibraryLanding template (hero, why-grid, split sections, testimonials, ecosystem, CTA) driven by a per-library config + demo-panel slots - De-noise: one exclamation-free CTA, drop superlative taglines, small TanStack mark stacked over a large product wordmark - Repackage typography onto DS text-ds-* roles (drop font-black) - ~30% more section padding, gutters, and column gap Design system: - Eyebrow component: mono-caps role, brand-by-category via --color-lib-* tokens, with a /ds/eyebrow style book - Logos brand page (/ds/logos) with per-variant SVG downloads; Brand entry merged into a "Brand & Styles" nav section - Button: add polymorphic `as` prop so it can render as a Link/anchor - Lighten body type tokens (body xl/lg/md -> weight 300) + style-book labels - Navbar library wordmark -> DS heading-4 in the display font Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent dfe24b5 commit df180f2

11 files changed

Lines changed: 1096 additions & 697 deletions

File tree

src/components/ds/ds-nav.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ export interface DsNavSection {
1313
*
1414
* This is the single source of truth for the sidebar. Adding a new page is a
1515
* two-step change: drop a `ds.<name>.tsx` route in `src/routes` and add an
16-
* entry here. Keep `Styles` (design tokens) above `Components` (rendered UI).
16+
* entry here. Keep `Brand & Styles` (tokens & assets) above `Components`
17+
* (rendered UI).
1718
*/
1819
export const dsNav: Array<DsNavSection> = [
1920
{
20-
title: 'Styles',
21+
title: 'Brand & Styles',
2122
items: [
2223
{ label: 'Overview', to: '/ds' },
24+
{ label: 'Logos', to: '/ds/logos' },
2325
{ label: 'Colors', to: '/ds/colors' },
2426
{ label: 'Typography', to: '/ds/typography' },
2527
{ label: 'Iconography', to: '/ds/iconography' },
@@ -40,6 +42,7 @@ export const dsNav: Array<DsNavSection> = [
4042
items: [
4143
{ label: 'Buttons', to: '/ds/buttons' },
4244
{ label: 'Badges', to: '/ds/badges' },
45+
{ label: 'Eyebrow', to: '/ds/eyebrow' },
4346
{ label: 'Inputs', to: '/ds/inputs' },
4447
{ label: 'Dropdown', to: '/ds/dropdown' },
4548
{ label: 'Avatar', to: '/ds/avatar' },

src/components/ds/ui/index.tsx

Lines changed: 136 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
44
import { Link } from '@tanstack/react-router'
55
import { CaretDown, CircleNotch, User } from '@phosphor-icons/react'
66
import type { MarkdownHeading } from '~/utils/markdown'
7+
import type { LibraryId } from '~/libraries/ids'
78

89
/**
910
* New-system DS components — built on the Figma semantic tokens (action-*,
@@ -31,13 +32,29 @@ type ButtonColor =
3132
type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'icon-sm' | 'icon-md'
3233
type ButtonRounded = 'none' | 'md' | 'lg' | 'full'
3334

34-
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
35+
// Polymorphic: defaults to <button>, but `as={Link}` / `as="a"` lets a CTA
36+
// render as a link while keeping the button styling. Mirrors the production
37+
// Button's API (src/ui/Button.tsx) so pages can swap imports 1:1.
38+
type ButtonOwnProps<TElement extends React.ElementType = 'button'> = {
39+
as?: TElement
40+
children: React.ReactNode
3541
variant?: ButtonVariant
3642
color?: ButtonColor
3743
size?: ButtonSize
3844
rounded?: ButtonRounded
45+
className?: string
3946
}
4047

48+
type ButtonProps<TElement extends React.ElementType = 'button'> =
49+
ButtonOwnProps<TElement> &
50+
Omit<React.ComponentPropsWithRef<TElement>, keyof ButtonOwnProps<TElement>>
51+
52+
type ButtonComponent = <TElement extends React.ElementType = 'button'>(
53+
props: ButtonProps<TElement>,
54+
) => React.ReactNode
55+
56+
type ButtonInnerProps = ButtonOwnProps & Record<string, unknown>
57+
4158
// Color set mapped onto the new palette (brand blue = the teal #013e53).
4259
const primaryColorStyles: Record<ButtonColor, string> = {
4360
blue: 'bg-ds-blue-500 text-white border-ds-blue-500 hover:bg-ds-blue-400',
@@ -119,48 +136,49 @@ function getDefaultRounded(size: ButtonSize): ButtonRounded {
119136
return 'lg'
120137
}
121138

122-
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
123-
function Button(
139+
export const Button: ButtonComponent = React.forwardRef<
140+
HTMLElement,
141+
ButtonInnerProps
142+
>(function Button(props, ref) {
143+
const {
144+
as,
145+
children,
146+
variant = 'primary',
147+
color = 'blue',
148+
size,
149+
rounded,
150+
className,
151+
...rest
152+
} = props as ButtonOwnProps & Record<string, unknown>
153+
const Component = as || 'button'
154+
const resolvedSize = size ?? getDefaultSize(variant)
155+
const resolvedRounded = rounded ?? getDefaultRounded(resolvedSize)
156+
const colorStyles =
157+
variant === 'primary'
158+
? primaryColorStyles[color]
159+
: variant === 'icon'
160+
? iconColorStyles[color]
161+
: variant === 'link'
162+
? linkColorStyles[color]
163+
: ''
164+
165+
return React.createElement(
166+
Component,
124167
{
125-
children,
126-
variant = 'primary',
127-
color = 'blue',
128-
size,
129-
rounded,
130-
className,
131-
...rest
168+
ref,
169+
className: twMerge(
170+
baseStyles,
171+
variantStyles[variant],
172+
sizeStyles[resolvedSize],
173+
roundedStyles[resolvedRounded],
174+
colorStyles,
175+
className,
176+
),
177+
...rest,
132178
},
133-
ref,
134-
) {
135-
const resolvedSize = size ?? getDefaultSize(variant)
136-
const resolvedRounded = rounded ?? getDefaultRounded(resolvedSize)
137-
const colorStyles =
138-
variant === 'primary'
139-
? primaryColorStyles[color]
140-
: variant === 'icon'
141-
? iconColorStyles[color]
142-
: variant === 'link'
143-
? linkColorStyles[color]
144-
: ''
145-
146-
return (
147-
<button
148-
ref={ref}
149-
className={twMerge(
150-
baseStyles,
151-
variantStyles[variant],
152-
sizeStyles[resolvedSize],
153-
roundedStyles[resolvedRounded],
154-
colorStyles,
155-
className,
156-
)}
157-
{...rest}
158-
>
159-
{children}
160-
</button>
161-
)
162-
},
163-
)
179+
children,
180+
)
181+
}) as unknown as ButtonComponent
164182

165183
/* ------------------------------------------------------------------ Badge -- */
166184

@@ -207,6 +225,83 @@ export function Badge({
207225
)
208226
}
209227

228+
/* ---------------------------------------------------------------- Eyebrow -- */
229+
230+
type EyebrowTone = 'muted' | 'secondary' | 'accent'
231+
232+
const eyebrowToneStyles: Record<EyebrowTone, string> = {
233+
muted: 'text-text-muted',
234+
secondary: 'text-text-secondary',
235+
accent: 'text-text-accent',
236+
}
237+
238+
// Libraries that ship a `--color-lib-*` brand token. Written as literal classes
239+
// so Tailwind's JIT emits each `text-lib-*` utility. Libraries without a brand
240+
// token (e.g. ranger, config) simply fall back to the neutral tone.
241+
const eyebrowLibraryStyles: Partial<Record<LibraryId, string>> = {
242+
start: 'text-lib-start',
243+
router: 'text-lib-router',
244+
query: 'text-lib-query',
245+
table: 'text-lib-table',
246+
db: 'text-lib-db',
247+
ai: 'text-lib-ai',
248+
form: 'text-lib-form',
249+
virtual: 'text-lib-virtual',
250+
pacer: 'text-lib-pacer',
251+
hotkeys: 'text-lib-hotkeys',
252+
store: 'text-lib-store',
253+
devtools: 'text-lib-devtools',
254+
cli: 'text-lib-cli',
255+
intent: 'text-lib-intent',
256+
}
257+
258+
/**
259+
* Section eyebrow / kicker — the small uppercase label that sits above a
260+
* heading. Locks in the DS `mono-caps` role (IBM Plex Mono, 12px, +1.5px
261+
* tracking) plus the inline icon layout, so every eyebrow across the site stays
262+
* on-system instead of hand-stacking `text-xs font-black uppercase`.
263+
*
264+
* Color resolves in priority order: `library` (brand the eyebrow by the
265+
* category it sits within, via that library's `--color-lib-*` token) →
266+
* `className` override → `tone` (neutral default). Pass a `library` to brand it,
267+
* omit it for a neutral eyebrow.
268+
*/
269+
export function Eyebrow({
270+
children,
271+
icon,
272+
tone = 'secondary',
273+
library,
274+
className,
275+
}: {
276+
children: React.ReactNode
277+
icon?: React.ReactNode
278+
tone?: EyebrowTone
279+
/** Brand the eyebrow by the library/category it's nested within. Falls back
280+
* to `tone` when unset or when the library has no brand token. */
281+
library?: LibraryId
282+
className?: string
283+
}) {
284+
const color =
285+
(library && eyebrowLibraryStyles[library]) ?? eyebrowToneStyles[tone]
286+
287+
// The `mono-caps` type role is a fixed base — never merged away — so the DS
288+
// font/size/tracking can't be overridden. Only color (library / tone /
289+
// className) goes through twMerge, which handles standard color utilities
290+
// cleanly. This also sidesteps twMerge dropping the custom `text-ds-*` size
291+
// when a caller passes a text color.
292+
return (
293+
<p
294+
className={`inline-flex items-center gap-2 font-ds-mono text-ds-mono-caps uppercase ${twMerge(
295+
color,
296+
className,
297+
)}`}
298+
>
299+
{icon}
300+
{children}
301+
</p>
302+
)
303+
}
304+
210305
/* -------------------------------------------------------------- FormInput -- */
211306

212307
type FormInputProps = React.InputHTMLAttributes<HTMLInputElement> & {

0 commit comments

Comments
 (0)