diff --git a/.changeset/type-scale-tokens.md b/.changeset/type-scale-tokens.md new file mode 100644 index 0000000..acd150a --- /dev/null +++ b/.changeset/type-scale-tokens.md @@ -0,0 +1,5 @@ +--- +"@variablelab/brand": minor +--- + +The type scale is tokens. `tokens.css` defines `--vlab-type--size`, `-line-height`, `-weight`, `-tracking` and `-family` for each step of the scale in `typography.md`, `theme.css` adds each step to Tailwind as `text-`, and `tokens.json` holds the scale under `type`. The step `inline code` is now spelled `inline-code`, as its tokens are. diff --git a/AGENTS.md b/AGENTS.md index db301ac..61f16ad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ Variable Lab's brand guideline, design tokens and shared assets, the package `@v ## Layout - `skills/vlab-brand/`: the guideline, as an Agent Skill. `SKILL.md` routes each task to one chapter; `glossary.md` holds the terms. -- `tokens/tokens.json`: the only source of colors and fonts. +- `tokens/tokens.json`: the only source of colors, fonts and the type scale. - `assets/`: the Logo and the Class stamps. No Project's Sign lives here. - `templates/project/brand/`: what a Project copies into its own `brand/` folder. - `.github/readme/`: the images in the README. Generated. @@ -23,7 +23,7 @@ Edit the source, then run `pnpm build`. `pnpm check` exits 1 when a generated fi | Source | Generated | | --- | --- | -| `tokens/tokens.json` | `tokens/tokens.css`, `tokens/theme.css`, the tables in `skills/vlab-brand/color.md`, the palette in `README.md` and its swatches in `.github/readme/`, `templates/project/brand/sign-readme.svg` | +| `tokens/tokens.json` | `tokens/tokens.css`, `tokens/theme.css`, the tables in `skills/vlab-brand/color.md`, the scale in `skills/vlab-brand/typography.md`, the palette in `README.md` and its swatches in `.github/readme/`, `templates/project/brand/sign-readme.svg` | | `assets/stamps/*.svg` | The stamp previews in `.github/readme/` | | `assets/logo.svg` | `.github/readme/logo.svg`, the Logo on `ground` for the README | diff --git a/scripts/build.ts b/scripts/build.ts index d582eef..accc570 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -1,6 +1,7 @@ // Writes every generated file in this repo from its source: // tokens/tokens.json → tokens/tokens.css, tokens/theme.css, -// the tables in skills/vlab-brand/color.md (an installed skill can't read tokens.json), +// the tables in skills/vlab-brand/color.md and the scale in typography.md +// (an installed skill can't read tokens.json), // the palette in README.md and its swatches in .github/readme/, // templates/project/brand/sign-readme.svg // assets/stamps/*.svg → the stamp previews in .github/readme/ @@ -12,7 +13,9 @@ import { readdirSync, readFileSync, writeFileSync } from "node:fs"; type Token = { value: string; use: string; vesper?: string }; type Group = "color" | "syntax" | "font"; -type Tokens = Record>; +// A step of the type scale. A step with no line height keeps the one it sits in. +type Step = { size: string; "line-height"?: string; weight: string; tracking: string; family: "sans" | "mono" }; +type Tokens = Record> & { type: Record }; const root = new URL("../", import.meta.url); const read = (path: string) => readFileSync(new URL(path, root), "utf8"); @@ -29,10 +32,29 @@ const each = (render: (group: Group, name: string, token: Token) => string) => Object.entries(tokens[group]).map(([name, token]) => render(group, name, token)), ); +const steps = Object.entries(tokens.type); +// Each property of a step is --vlab-type--, its family a var() of the font token. +const stepProperties = (name: string, step: Step) => + Object.entries({ ...step, family: `var(--vlab-font-${step.family})` }).map( + ([property, value]) => ` --vlab-type-${name}-${property}: ${value};`, + ); +// In Tailwind a step is text-: the size, with its line height, tracking and weight. The family goes beside it. +const tailwindText = { size: "", "line-height": "--line-height", tracking: "--letter-spacing", weight: "--font-weight" }; +const stepTheme = (name: string, step: Step) => + Object.entries(tailwindText) + .filter(([property]) => property in step) + .map(([property, suffix]) => ` --text-${name}${suffix}: var(--vlab-type-${name}-${property});`); + outputs["tokens/tokens.css"] = - `${cssHeader}:root {\n${each((group, name, token) => ` --vlab-${cssName(group, name)}: ${token.value};`).join("\n")}\n}\n`; + `${cssHeader}:root {\n${[ + ...each((group, name, token) => ` --vlab-${cssName(group, name)}: ${token.value};`), + ...steps.flatMap(([name, step]) => stepProperties(name, step)), + ].join("\n")}\n}\n`; outputs["tokens/theme.css"] = - `${cssHeader}@import "./tokens.css";\n\n@theme inline {\n${each((group, name) => ` --${themeName(group, name)}: var(--vlab-${cssName(group, name)});`).join("\n")}\n}\n`; + `${cssHeader}@import "./tokens.css";\n\n@theme inline {\n${[ + ...each((group, name) => ` --${themeName(group, name)}: var(--vlab-${cssName(group, name)});`), + ...steps.flatMap(([name, step]) => stepTheme(name, step)), + ].join("\n")}\n}\n`; // README images: a swatch per color, and each Class stamp in ink on a ground square @@ -83,6 +105,18 @@ const blocks: Record> = { rows("syntax", (name, t) => [name, `\`${t.value}\``, `\`${t.vesper}\``, t.use]), ), }, + "skills/vlab-brand/typography.md": { + scale: table( + ["Step", "Size / line height", "Weight", "Tracking", "Family"], + steps.map(([name, s]) => [ + name, + s["line-height"] ? `${s.size} / ${s["line-height"]}` : s.size, + s.weight, + s.tracking === "0" ? "0" : `\`${s.tracking}\``, + s.family, + ]), + ), + }, "README.md": { color: table(["", "Token", "Value", "Use"], rows("color", (name, t) => [swatch("color", name), `\`${name}\``, `\`${t.value}\``, t.use])), syntax: table(["", "Token", "Value", "Use"], rows("syntax", (name, t) => [swatch("syntax", name), `\`${name}\``, `\`${t.value}\``, t.use])), diff --git a/skills/vlab-brand/color.md b/skills/vlab-brand/color.md index c16bf6b..e48e931 100644 --- a/skills/vlab-brand/color.md +++ b/skills/vlab-brand/color.md @@ -38,6 +38,6 @@ Code takes its roles and its two accents, peach and mint, from [Vesper](https:// ## In code -- CSS: `@variablelab/brand/tokens.css` defines `--vlab-`, `--vlab-syntax-` and `--vlab-font-` on `:root`. +- CSS: `@variablelab/brand/tokens.css` defines `--vlab-`, `--vlab-syntax-` and `--vlab-font-` on `:root`, and the type scale's `--vlab-type-*` ([typography.md](typography.md)). - Tailwind v4: `@import "@variablelab/brand/theme.css"` adds the colors `ground`, `ink`, `syntax-keyword` and the rest, and the fonts `sans` and `mono` (`bg-ground`, `text-dim`, `font-mono`). - Anything else, such as a server-rendered image: read `@variablelab/brand/tokens.json`. diff --git a/skills/vlab-brand/typography.md b/skills/vlab-brand/typography.md index 6447d2f..f499e2f 100644 --- a/skills/vlab-brand/typography.md +++ b/skills/vlab-brand/typography.md @@ -11,6 +11,7 @@ The files come from Fontsource (`@fontsource/ibm-plex-sans`, `@fontsource/ibm-pl ## Scale + | Step | Size / line height | Weight | Tracking | Family | | --- | --- | --- | --- | --- | | display | 42px / 1 | 500 | `-0.035em` | sans | @@ -19,14 +20,25 @@ The files come from Fontsource (`@fontsource/ibm-plex-sans`, `@fontsource/ibm-pl | lead | 20px / 1.3 | 400 | `-0.02em` | sans | | body | 16px / 1.55 | 400 | 0 | sans | | small | 14px / 1.5 | 400 | 0 | sans | -| ui | 13px | 400, 500 for a name | 0 | sans | -| inline code | 13px | 400 | 0 | mono, in `ink` | +| ui | 13px | 400 | 0 | sans | +| inline-code | 13px | 400 | 0 | mono | | code | 12.5px / 20px | 400 | 0 | mono | | meta | 12px | 400 | 0 | mono | | label | 11px | 400 | 0 | mono | + + +- `ui` sets 500 for a name. +- `inline-code` is set in `ink`. +- A step with no line height (`ui`, `inline-code`, `meta`, `label`) keeps the one of the text it sits in. The bigger the size, the tighter the tracking: each step carries its own value. +## In code + +- CSS: `@variablelab/brand/tokens.css` defines five properties per step on `:root`: `--vlab-type--size`, `-line-height`, `-weight`, `-tracking` and `-family`, such as `--vlab-type-title-size`. A step with no line height has no `-line-height`. `-family` is `var(--vlab-font-sans)` or `var(--vlab-font-mono)`. +- Tailwind v4: `@import "@variablelab/brand/theme.css"` adds each step as a font size with its line height, tracking and weight (`text-title`, `text-meta`). The family goes beside it: `text-meta font-mono`. +- Anything else, such as a server-rendered image: read `type` in `@variablelab/brand/tokens.json`. + ## Text flow - Titles and one-line statements: `text-wrap: balance`. Paragraphs: `text-wrap: pretty`. No block ends on a single word. diff --git a/tokens/theme.css b/tokens/theme.css index 3ec54dc..603a797 100644 --- a/tokens/theme.css +++ b/tokens/theme.css @@ -18,4 +18,44 @@ --color-syntax-gutter: var(--vlab-syntax-gutter); --font-sans: var(--vlab-font-sans); --font-mono: var(--vlab-font-mono); + --text-display: var(--vlab-type-display-size); + --text-display--line-height: var(--vlab-type-display-line-height); + --text-display--letter-spacing: var(--vlab-type-display-tracking); + --text-display--font-weight: var(--vlab-type-display-weight); + --text-title: var(--vlab-type-title-size); + --text-title--line-height: var(--vlab-type-title-line-height); + --text-title--letter-spacing: var(--vlab-type-title-tracking); + --text-title--font-weight: var(--vlab-type-title-weight); + --text-heading: var(--vlab-type-heading-size); + --text-heading--line-height: var(--vlab-type-heading-line-height); + --text-heading--letter-spacing: var(--vlab-type-heading-tracking); + --text-heading--font-weight: var(--vlab-type-heading-weight); + --text-lead: var(--vlab-type-lead-size); + --text-lead--line-height: var(--vlab-type-lead-line-height); + --text-lead--letter-spacing: var(--vlab-type-lead-tracking); + --text-lead--font-weight: var(--vlab-type-lead-weight); + --text-body: var(--vlab-type-body-size); + --text-body--line-height: var(--vlab-type-body-line-height); + --text-body--letter-spacing: var(--vlab-type-body-tracking); + --text-body--font-weight: var(--vlab-type-body-weight); + --text-small: var(--vlab-type-small-size); + --text-small--line-height: var(--vlab-type-small-line-height); + --text-small--letter-spacing: var(--vlab-type-small-tracking); + --text-small--font-weight: var(--vlab-type-small-weight); + --text-ui: var(--vlab-type-ui-size); + --text-ui--letter-spacing: var(--vlab-type-ui-tracking); + --text-ui--font-weight: var(--vlab-type-ui-weight); + --text-inline-code: var(--vlab-type-inline-code-size); + --text-inline-code--letter-spacing: var(--vlab-type-inline-code-tracking); + --text-inline-code--font-weight: var(--vlab-type-inline-code-weight); + --text-code: var(--vlab-type-code-size); + --text-code--line-height: var(--vlab-type-code-line-height); + --text-code--letter-spacing: var(--vlab-type-code-tracking); + --text-code--font-weight: var(--vlab-type-code-weight); + --text-meta: var(--vlab-type-meta-size); + --text-meta--letter-spacing: var(--vlab-type-meta-tracking); + --text-meta--font-weight: var(--vlab-type-meta-weight); + --text-label: var(--vlab-type-label-size); + --text-label--letter-spacing: var(--vlab-type-label-tracking); + --text-label--font-weight: var(--vlab-type-label-weight); } diff --git a/tokens/tokens.css b/tokens/tokens.css index 3e7f25c..801c4d7 100644 --- a/tokens/tokens.css +++ b/tokens/tokens.css @@ -16,4 +16,55 @@ --vlab-syntax-gutter: #5f5a52; --vlab-font-sans: 'IBM Plex Sans', 'Helvetica Neue', Helvetica, sans-serif; --vlab-font-mono: 'IBM Plex Mono', ui-monospace, Menlo, monospace; + --vlab-type-display-size: 42px; + --vlab-type-display-line-height: 1; + --vlab-type-display-weight: 500; + --vlab-type-display-tracking: -0.035em; + --vlab-type-display-family: var(--vlab-font-sans); + --vlab-type-title-size: 26px; + --vlab-type-title-line-height: 1.2; + --vlab-type-title-weight: 500; + --vlab-type-title-tracking: -0.025em; + --vlab-type-title-family: var(--vlab-font-sans); + --vlab-type-heading-size: 22px; + --vlab-type-heading-line-height: 1.15; + --vlab-type-heading-weight: 500; + --vlab-type-heading-tracking: -0.025em; + --vlab-type-heading-family: var(--vlab-font-sans); + --vlab-type-lead-size: 20px; + --vlab-type-lead-line-height: 1.3; + --vlab-type-lead-weight: 400; + --vlab-type-lead-tracking: -0.02em; + --vlab-type-lead-family: var(--vlab-font-sans); + --vlab-type-body-size: 16px; + --vlab-type-body-line-height: 1.55; + --vlab-type-body-weight: 400; + --vlab-type-body-tracking: 0; + --vlab-type-body-family: var(--vlab-font-sans); + --vlab-type-small-size: 14px; + --vlab-type-small-line-height: 1.5; + --vlab-type-small-weight: 400; + --vlab-type-small-tracking: 0; + --vlab-type-small-family: var(--vlab-font-sans); + --vlab-type-ui-size: 13px; + --vlab-type-ui-weight: 400; + --vlab-type-ui-tracking: 0; + --vlab-type-ui-family: var(--vlab-font-sans); + --vlab-type-inline-code-size: 13px; + --vlab-type-inline-code-weight: 400; + --vlab-type-inline-code-tracking: 0; + --vlab-type-inline-code-family: var(--vlab-font-mono); + --vlab-type-code-size: 12.5px; + --vlab-type-code-line-height: 20px; + --vlab-type-code-weight: 400; + --vlab-type-code-tracking: 0; + --vlab-type-code-family: var(--vlab-font-mono); + --vlab-type-meta-size: 12px; + --vlab-type-meta-weight: 400; + --vlab-type-meta-tracking: 0; + --vlab-type-meta-family: var(--vlab-font-mono); + --vlab-type-label-size: 11px; + --vlab-type-label-weight: 400; + --vlab-type-label-tracking: 0; + --vlab-type-label-family: var(--vlab-font-mono); } diff --git a/tokens/tokens.json b/tokens/tokens.json index c2078e7..7645d6d 100644 --- a/tokens/tokens.json +++ b/tokens/tokens.json @@ -20,5 +20,18 @@ "font": { "sans": { "value": "'IBM Plex Sans', 'Helvetica Neue', Helvetica, sans-serif", "use": "Text." }, "mono": { "value": "'IBM Plex Mono', ui-monospace, Menlo, monospace", "use": "Code, commands, URLs, package and executable names, labels and meta." } + }, + "type": { + "display": { "size": "42px", "line-height": "1", "weight": "500", "tracking": "-0.035em", "family": "sans" }, + "title": { "size": "26px", "line-height": "1.2", "weight": "500", "tracking": "-0.025em", "family": "sans" }, + "heading": { "size": "22px", "line-height": "1.15", "weight": "500", "tracking": "-0.025em", "family": "sans" }, + "lead": { "size": "20px", "line-height": "1.3", "weight": "400", "tracking": "-0.02em", "family": "sans" }, + "body": { "size": "16px", "line-height": "1.55", "weight": "400", "tracking": "0", "family": "sans" }, + "small": { "size": "14px", "line-height": "1.5", "weight": "400", "tracking": "0", "family": "sans" }, + "ui": { "size": "13px", "weight": "400", "tracking": "0", "family": "sans" }, + "inline-code": { "size": "13px", "weight": "400", "tracking": "0", "family": "mono" }, + "code": { "size": "12.5px", "line-height": "20px", "weight": "400", "tracking": "0", "family": "mono" }, + "meta": { "size": "12px", "weight": "400", "tracking": "0", "family": "mono" }, + "label": { "size": "11px", "weight": "400", "tracking": "0", "family": "mono" } } }