Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/editorial-typography-tokens.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@launchpad-ui/tokens': minor
'@launchpad-ui/box': patch
---

Add a Sora-based editorial typography token family for brand-themed surfaces (empty states, hero moments, announcements). Adds `fontFamily.sora` (with an Inter fallback), pixel-named `fontSize.editorial.*`, `fontWeight.editorial.*`, unitless `lineHeight.editorial.*`, a new top-level `letterSpacing.editorial.*` group, seven `text.editorial.*` compositions, and `size.30`/`size.50` primitives. All values mirror the `ai-refresh` `_typography.scss` source of truth. Existing utility `text.*` compositions are unchanged.

Box's `rainbow-sprinkles` now flattens the `fontSize`, `fontWeight`, and `lineHeight` token groups (matching how the `font` typography group is already handled) so the nested `editorial.*` sub-namespaces don't break `defineProperties`' flat-record typing.
8 changes: 8 additions & 0 deletions .storybook/preview-head.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preload" href="./static/assets/Audimat3000-Regulier.var-subset.woff2" as="font" crossorigin />
<link rel="preload" href="./static/assets/Inter.var-subset.woff2" as="font" crossorigin />

<!-- Sora (editorial type family) loaded from Google Fonts for Storybook specimens only.
The in-product @font-face/preload ships separately in Phase 2 (gonfalon). -->
<link
href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>

<link href="./styles.css" rel="stylesheet" />
<link href="./static/fonts.css" rel="stylesheet" />
14 changes: 10 additions & 4 deletions packages/box/src/styles/rainbow-sprinkles.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createRainbowSprinkles, defineProperties } from 'rainbow-sprinkles';

// biome-ignore lint/correctness/noUnusedVariables: ignore
const { bg, border, fill, shadow, text, ...global } = vars.color;
const { text: typography, gradient } = vars;
const { text: typography, gradient, fontSize, fontWeight, lineHeight } = vars;

type FlattenObjectKeys<T extends Record<string, unknown>, Key = keyof T> = Key extends string
? T[Key] extends Record<string, unknown>
Expand All @@ -19,13 +19,19 @@ type BorderKeys = FlattenObjectKeys<typeof border>;
type FillKeys = FlattenObjectKeys<typeof fill>;
type TextKeys = FlattenObjectKeys<typeof text>;
type TypographyKeys = FlattenObjectKeys<typeof typography>;
type FontSizeKeys = FlattenObjectKeys<typeof fontSize>;
type FontWeightKeys = FlattenObjectKeys<typeof fontWeight>;
type LineHeightKeys = FlattenObjectKeys<typeof lineHeight>;

const colors = flatten<typeof global, Record<GlobalKeys, string>>(global);
const backgrounds = flatten<typeof bg, Record<BackgroundKeys, string>>(bg);
const borders = flatten<typeof border, Record<BorderKeys, string>>(border);
const fills = flatten<typeof fill, Record<FillKeys, string>>(fill);
const texts = flatten<typeof text, Record<TextKeys, string>>(text);
const typographies = flatten<typeof typography, Record<TypographyKeys, string>>(typography);
const fontSizes = flatten<typeof fontSize, Record<FontSizeKeys, string>>(fontSize);
const fontWeights = flatten<typeof fontWeight, Record<FontWeightKeys, string>>(fontWeight);
const lineHeights = flatten<typeof lineHeight, Record<LineHeightKeys, string>>(lineHeight);

const responsiveProperties = defineProperties({
conditions: {
Expand Down Expand Up @@ -70,9 +76,9 @@ const responsiveProperties = defineProperties({
borderWidth: vars.borderWidth,
font: typographies,
fontFamily: vars.fontFamily,
fontSize: vars.fontSize,
fontWeight: vars.fontWeight,
lineHeight: vars.lineHeight,
fontSize: fontSizes,
fontWeight: fontWeights,
lineHeight: lineHeights,
width: vars.size,
height: vars.size,
maxHeight: vars.size,
Expand Down
86 changes: 81 additions & 5 deletions packages/tokens/stories/typography.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { vars } from '@launchpad-ui/vars';
import { Description, Stories, Title } from '@storybook/addon-docs/blocks';

import { Button } from '../../components/src/Button';
import { ToastRegion, toastQueue } from '../../components/src/Toast';
Expand All @@ -11,8 +12,15 @@ export default {
docs: {
description: {
component:
'Typography tokens for the LaunchPad design system. For components using these tokens, see [Text](/docs/components-content-text--docs), [Heading](/docs/components-content-heading--docs), [Label](/docs/components-content-label--docs), and [Code](/docs/components-content-code--docs).',
'Typography tokens for the LaunchPad design system. Our typography is split between two type sets: Utility and Editorial. For components using these tokens, see [Text](/docs/components-content-text--docs), [Heading](/docs/components-content-heading--docs), [Label](/docs/components-content-label--docs), and [Code](/docs/components-content-code--docs). For the full framework, see the [Utility & Editorial Type System for LaunchPad](https://launchdarkly.atlassian.net/wiki/spaces/~712020490f77e4363240f1888e975e52e895be/pages/4939022523/) proposal.',
},
page: () => (
<>
<Title />
<Description />
<Stories title="" includePrimary />
</>
),
},
},
};
Expand All @@ -33,15 +41,33 @@ const flatten = (obj: Record<string, unknown>, prefix = ''): Record<string, stri
return result;
};

const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);

const getDisplayText = (key: string) => {
const parts = key.split('-');

if (parts[0] === 'editorial') {
const variant = parts[1]; // display, h1, h2, h3
const modifiers = parts.slice(2); // alt, medium, etc. all treated as variant suffixes

let base: string;
if (variant === 'display') {
base = 'Display';
} else if (/^h\d+$/.test(variant)) {
base = `Heading ${variant.slice(1)}`;
} else {
base = capitalize(variant);
}

return modifiers.length ? `${base} ${modifiers.map(capitalize).join(' ')}` : base;
}

const category = parts[0]; // heading, body, etc.
const size = parts[1]; // 1, 2, etc.
const weight = parts[2]; // medium, semibold, etc.

if (weight) {
return `${category.charAt(0).toUpperCase() + category.slice(1)} ${size} - ${weight.charAt(0).toUpperCase() + weight.slice(1)}`;
return `${capitalize(category)} ${size} - ${capitalize(weight)}`;
Comment thread
cursor[bot] marked this conversation as resolved.
}
if (category === 'display') {
return `Display ${size}`;
Expand All @@ -55,7 +81,24 @@ const getSemanticElement = (token: string, font: string) => {
const category = parts[0]; // heading, body, etc.
const size = parts[1]; // 1, 2, etc.

console.log({ category, token });
// Editorial tokens are keyed as editorial-<variant>[-modifier] (e.g. editorial-h1-alt),
// so the heading level lives in parts[1]. Map it to the matching tag instead of falling
// through to the generic div below.
if (category === 'editorial') {
const variant = parts[1]; // display, h1, h2, h3
const match = /^h(\d+)$/.exec(variant);
if (match) {
const Tag = `h${match[1]}` as 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
return {
element: Tag,
render: (text: string) => <Tag style={{ font }}>{text}</Tag>,
};
}
return {
element: 'div',
render: (text: string) => <div style={{ font }}>{text}</div>,
};
}

switch (category) {
case 'display':
Expand Down Expand Up @@ -167,6 +210,39 @@ const TokenTable = ({ tokens }: { tokens: Record<string, string> }) => {
);
};

export const Typography = {
render: () => <TokenTable tokens={flatten(vars.text)} />,
const UTILITY_DESCRIPTION = `The foundation of the LaunchDarkly product. Inter for text, SF Mono for numeric content where vertical alignment matters. Designed to feel invisible: when it's working correctly, users don't notice it. Used across navigation, forms, tables, modals, settings, and any task-driven surface.`;

const EDITORIAL_DESCRIPTION =
'Carries the LaunchDarkly brand voice into the product by using Sora for display headings. Used on first-time onboarding empty states, in-app banners, feature announcements, and educational moments where the user has paused. Should feel intentional and slightly different from the rest of the UI.';

const utilityTokens = () =>
Object.fromEntries(
Object.entries(flatten(vars.text)).filter(([key]) => !key.startsWith('editorial-')),
);

const editorialTokens = () =>
Object.fromEntries(
Object.entries(flatten(vars.text)).filter(([key]) => key.startsWith('editorial-')),
);

export const Utility = {
parameters: {
docs: {
description: {
story: UTILITY_DESCRIPTION,
},
},
},
render: () => <TokenTable tokens={utilityTokens()} />,
};

export const Editorial = {
parameters: {
docs: {
description: {
story: EDITORIAL_DESCRIPTION,
},
},
},
render: () => <TokenTable tokens={editorialTokens()} />,
};
111 changes: 111 additions & 0 deletions packages/tokens/tokens/font.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
},
"display": {
"$value": "Audimat\\ 3000 Regulier"
},
"sora": {
"$value": "Sora, Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, Helvetica, Arial, sans-serif"
}
},
"fontSize": {
Expand Down Expand Up @@ -50,6 +53,47 @@
},
"800": {
"$value": 60
},
"editorial": {
"14": {
"$value": "{size.14}"
},
"16": {
"$value": "{size.16}"
},
"18": {
"$value": "{size.18}"
},
"20": {
"$value": "{size.20}"
},
"24": {
"$value": "{size.24}"
},
"30": {
"$value": "{size.30}"
},
"32": {
"$value": "{size.32}"
},
"40": {
"$value": "{size.40}"
},
"48": {
"$value": "{size.48}"
},
"50": {
"$value": "{size.50}"
},
"56": {
"$value": "{size.56}"
},
"64": {
"$value": "{size.64}"
},
"80": {
"$value": "{size.80}"
}
}
},
"fontWeight": {
Expand Down Expand Up @@ -78,6 +122,17 @@
},
"extrabold": {
"$value": 800
},
"editorial": {
"medium": {
"$value": 500
},
"semibold": {
"$value": 600
},
"bold": {
"$value": 700
}
}
},
"lineHeight": {
Expand All @@ -103,6 +158,62 @@
},
"500": {
"$value": "{size.72}"
},
"editorial": {
"tight": {
"$value": 1
},
"snug": {
"$value": 1.05
},
"close": {
"$value": 1.1
},
"default": {
"$value": 1.15
},
"relaxed": {
"$value": 1.5
}
}
},
"letterSpacing": {
"$type": "dimension",
"$extensions": {
"com.figma": {
"hiddenFromPublishing": false,
"scopes": ["LETTER_SPACING"],
"codeSyntax": {}
}
},
"editorial": {
"tight-2px": {
"$value": "-2px"
},
"tight-1px": {
"$value": "-1px"
},
"tight-em-05": {
"$value": "-0.05em"
},
"tight-em-03": {
"$value": "-0.03em"
},
"tight-px-065": {
"$value": "-0.65px"
},
"tight-px-050": {
"$value": "-0.5px"
},
"tight-px-025": {
"$value": "-0.25px"
},
"wide-em-06": {
"$value": "0.06em"
},
"normal": {
"$value": 0
}
}
}
}
6 changes: 6 additions & 0 deletions packages/tokens/tokens/size.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@
"28": {
"$value": 28
},
"30": {
"$value": 30
},
"32": {
"$value": 32
},
Expand All @@ -77,6 +80,9 @@
"48": {
"$value": 48
},
"50": {
"$value": 50
},
"56": {
"$value": 56
},
Expand Down
Loading
Loading