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
5 changes: 5 additions & 0 deletions .changeset/type-scale-tokens.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@variablelab/brand": minor
---

The type scale is tokens. `tokens.css` defines `--vlab-type-<step>-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-<step>`, and `tokens.json` holds the scale under `type`. The step `inline code` is now spelled `inline-code`, as its tokens are.
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 |

Expand Down
42 changes: 38 additions & 4 deletions scripts/build.ts
Original file line number Diff line number Diff line change
@@ -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/
Expand All @@ -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<Group, Record<string, Token>>;
// 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<Group, Record<string, Token>> & { type: Record<string, Step> };

const root = new URL("../", import.meta.url);
const read = (path: string) => readFileSync(new URL(path, root), "utf8");
Expand All @@ -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-<step>-<property>, 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-<step>: 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

Expand Down Expand Up @@ -83,6 +105,18 @@ const blocks: Record<string, Record<string, string>> = {
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])),
Expand Down
2 changes: 1 addition & 1 deletion skills/vlab-brand/color.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<token>`, `--vlab-syntax-<token>` and `--vlab-font-<token>` on `:root`.
- CSS: `@variablelab/brand/tokens.css` defines `--vlab-<token>`, `--vlab-syntax-<token>` and `--vlab-font-<token>` 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`.
16 changes: 14 additions & 2 deletions skills/vlab-brand/typography.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The files come from Fontsource (`@fontsource/ibm-plex-sans`, `@fontsource/ibm-pl

## Scale

<!-- build:scale -->
| Step | Size / line height | Weight | Tracking | Family |
| --- | --- | --- | --- | --- |
| display | 42px / 1 | 500 | `-0.035em` | sans |
Expand All @@ -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 |
<!-- /build:scale -->

- `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-<step>-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.
Expand Down
40 changes: 40 additions & 0 deletions tokens/theme.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 51 additions & 0 deletions tokens/tokens.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions tokens/tokens.json
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
}
}
Loading