From 1b876ca1154f8c0173ed18975e8227224f9302b1 Mon Sep 17 00:00:00 2001 From: balint Date: Tue, 28 Jul 2026 15:53:42 +0100 Subject: [PATCH 1/4] feat: update font scale --- .changeset/banner-action-sm-size.md | 15 ++ .changeset/banner-typography-weight-first.md | 37 +++ .changeset/button-sizes-refresh.md | 33 +++ .changeset/collapsible-trigger-text-base.md | 17 ++ .changeset/font-scale-multiplier.md | 51 ++++ .changeset/form-controls-size-refresh.md | 37 +++ .changeset/icons-em-relative-sizing.md | 57 +++++ .changeset/label-tooltip-button-sm.md | 9 + .changeset/table-header-weight.md | 16 ++ .changeset/text-role-based-variants.md | 42 ++++ .changeset/toast-typography-role-based.md | 13 + .changeset/token-refresh-class-swaps.md | 30 +++ .changeset/toolbar-deprecate-xs.md | 18 ++ .../src/components/FontScaleToggle.tsx | 141 +++++++++++ .../src/components/Header.astro | 10 +- .../src/components/SidebarNav.tsx | 20 +- .../src/components/demos/AutocompleteDemo.tsx | 14 +- .../src/components/demos/BreadcrumbsDemo.tsx | 6 +- .../src/components/demos/ButtonDemo.tsx | 3 - .../src/components/demos/DialogDemo.tsx | 36 +-- .../src/components/demos/GridDemo.tsx | 20 +- .../src/components/demos/HomeGrid.tsx | 15 +- .../src/components/demos/InputAreaDemo.tsx | 5 - .../src/components/demos/InputDemo.tsx | 1 - .../src/components/demos/InputGroupDemo.tsx | 15 -- .../src/components/demos/LayerCardDemo.tsx | 16 +- .../src/components/demos/PopoverDemo.tsx | 2 +- .../src/components/demos/SelectDemo.tsx | 12 +- .../src/components/demos/TextDemo.tsx | 54 ++-- .../src/components/demos/ToolbarDemo.tsx | 2 +- .../components/docs/ComponentPreview.astro | 2 +- .../src/components/docs/StickyDocHeader.tsx | 10 +- .../components/skill/CollapseSizeExample.tsx | 2 +- .../src/components/skill/design-tips.tsx | 60 ++--- .../src/layouts/BaseLayout.astro | 16 +- .../src/pages/changelog/[...page].astro | 4 +- .../src/pages/components-vs-blocks.mdx | 4 +- .../src/pages/components/autocomplete.mdx | 5 +- .../src/pages/components/combobox.mdx | 4 +- .../src/pages/components/input-area.mdx | 2 +- .../src/pages/components/input.mdx | 2 +- .../src/pages/components/sidebar.astro | 14 +- .../src/pages/components/text.mdx | 79 ++++-- .../kumo-docs-astro/src/styles/global.css | 17 ++ packages/kumo-figma/src/build-theme-data.ts | 21 +- .../kumo/scripts/theme-generator/config.ts | 71 +++++- .../scripts/theme-generator/generate-css.ts | 34 ++- .../kumo/scripts/theme-generator/index.ts | 26 ++ .../kumo/scripts/theme-generator/types.ts | 28 +++ .../components/autocomplete/autocomplete.tsx | 16 +- packages/kumo/src/components/badge/badge.tsx | 2 +- .../src/components/banner/banner-action.tsx | 10 +- .../src/components/banner/banner.test.tsx | 26 +- .../kumo/src/components/banner/banner.tsx | 30 ++- .../kumo/src/components/button/button.tsx | 35 ++- .../components/collapsible/collapsible.tsx | 4 +- .../kumo/src/components/combobox/combobox.tsx | 61 +++-- .../components/input-group/input-group.tsx | 24 +- .../kumo/src/components/input/input-area.tsx | 7 + .../kumo/src/components/input/input.test.tsx | 11 +- packages/kumo/src/components/input/input.tsx | 40 +-- packages/kumo/src/components/label/label.tsx | 2 +- .../src/components/select/select.test.tsx | 17 +- .../kumo/src/components/select/select.tsx | 33 ++- .../kumo/src/components/sidebar/sidebar.tsx | 8 +- .../table-of-contents/table-of-contents.tsx | 4 +- packages/kumo/src/components/table/table.tsx | 4 +- .../kumo/src/components/text/text.test.tsx | 118 ++++++++- packages/kumo/src/components/text/text.tsx | 230 +++++++++++++----- .../src/components/text/text.type-spec.tsx | 108 +++++++- packages/kumo/src/components/toast/toast.tsx | 15 +- .../src/components/toolbar/toolbar.test.tsx | 46 +++- .../kumo/src/components/toolbar/toolbar.tsx | 28 ++- packages/kumo/src/primitives/index.ts | 5 +- packages/kumo/src/primitives/otp-field.ts | 5 +- packages/kumo/src/styles/theme-kumo.css | 16 +- 76 files changed, 1613 insertions(+), 440 deletions(-) create mode 100644 .changeset/banner-action-sm-size.md create mode 100644 .changeset/banner-typography-weight-first.md create mode 100644 .changeset/button-sizes-refresh.md create mode 100644 .changeset/collapsible-trigger-text-base.md create mode 100644 .changeset/font-scale-multiplier.md create mode 100644 .changeset/form-controls-size-refresh.md create mode 100644 .changeset/icons-em-relative-sizing.md create mode 100644 .changeset/label-tooltip-button-sm.md create mode 100644 .changeset/table-header-weight.md create mode 100644 .changeset/text-role-based-variants.md create mode 100644 .changeset/toast-typography-role-based.md create mode 100644 .changeset/token-refresh-class-swaps.md create mode 100644 .changeset/toolbar-deprecate-xs.md create mode 100644 packages/kumo-docs-astro/src/components/FontScaleToggle.tsx diff --git a/.changeset/banner-action-sm-size.md b/.changeset/banner-action-sm-size.md new file mode 100644 index 0000000000..9c88974bd1 --- /dev/null +++ b/.changeset/banner-action-sm-size.md @@ -0,0 +1,15 @@ +--- +"@cloudflare/kumo": patch +--- + +`Banner`: stop rendering `Banner.Action` children at the deprecated +`size="xs"` in compact (`size="sm"`) banners. Both `base` and compact +banners now render actions at `Button`'s `sm` size (26px), which is the +smallest size in the current Button scale. This silences the +`[Kumo Button]: size="xs" is deprecated` warning that was emitted whenever +a compact banner rendered a `Banner.Action`. + +`BannerActionSize` narrows from `"xs" | "sm"` to `"sm"`. Consumers didn't +set this prop directly — `Banner.Action` receives its size via context +from the parent `Banner` — so this is a type-level cleanup with no +behavioral change beyond removing the internal `xs` usage. diff --git a/.changeset/banner-typography-weight-first.md b/.changeset/banner-typography-weight-first.md new file mode 100644 index 0000000000..b85180956f --- /dev/null +++ b/.changeset/banner-typography-weight-first.md @@ -0,0 +1,37 @@ +--- +"@cloudflare/kumo": minor +--- + +`Banner`: align title and description with the weight-first typography scale. +Both now inherit the banner container's text size (`text-base` in +`size="base"`, `text-sm` in `size="sm"`), and the title is distinguished +from the description by `font-medium` alone — matching how +`` relates to ``. + +Previously the description carried an internal `text-sm` override that made +it render one step below the container's `text-base` title. That override +is gone; a banner now reads as one hierarchy level, with weight as the +signal. + +**Rendered pixel changes (as part of the wider font-scale refresh):** + +- `size="base"`: title `text-base` renders at 13px (was 14px on the old + scale). Description was `text-sm` → 13px on both scales; on the new + scale it now inherits `text-base` at 13px. Net: title shrinks 14 → 13px, + description stays at 13px, and the two now sit at identical size + distinguished only by weight. +- `size="sm"`: title and description both render at `text-sm` = 12px (were + both 13px on the old scale). Description already inherited from the + container, so the class change is purely a token rename with the same + net pixel shift the whole scale is undergoing. + +The `text-sm` class is no longer applied directly to the description +element in either size; it lives on the banner container. If you were +relying on that class being present on the description node specifically +(e.g. for selector-based styling), update to select the banner container +instead. + +**Visual regression note:** consumers with Chromatic or screenshot tests on +`Banner` will see diffs — the title shrinks on `size="base"`, and the +whole component picks up the refreshed scale on both sizes. This is +intentional; approve the new baseline. diff --git a/.changeset/button-sizes-refresh.md b/.changeset/button-sizes-refresh.md new file mode 100644 index 0000000000..1d06078467 --- /dev/null +++ b/.changeset/button-sizes-refresh.md @@ -0,0 +1,33 @@ +--- +"@cloudflare/kumo": minor +--- + +`Button`: refresh size scale to align with the refreshed typography. + +**Height changes:** + +| Size | Before | After | +| ---- | ------ | ----- | +| `sm` | 26px | 26px (unchanged) | +| `base` | 36px | 32px | +| `lg` | 40px | 36px | + +**Font size:** `sm` buttons render at `text-sm` (12px) — one step below body +— so a small button reads as secondary next to `base` (13px) copy. `base` +and `lg` both render at `text-base` (13px); `lg` signals prominence via +height and padding, not larger type, so a button-plus-input row keeps its +type on one line without vertical bounce. + +**Optical centering (`sm`):** `sm` now applies `leading-none` to fix vertical +centering. At 12px the default `text-sm` line-height (~17px) combined with +the 26px button height left ~8.6px of slack that sans-serif ascender-heavy +metrics pushed off-center. Collapsing the line-box to glyph height lets flex +centering land the text exactly on the button's optical midline. Other sizes +keep their default `text-base` line-heights. + +**Deprecated:** `size="xs"`. Use `size="sm"` instead. The `xs` variant still +renders and looks the same as before, but emits a `console.warn` in +development and will be removed in a future major version. + +Icon-only (`shape="square"` / `shape="circle"`) buttons shrink in step with +their text siblings — `base` is 32×32, `lg` is 36×36. diff --git a/.changeset/collapsible-trigger-text-base.md b/.changeset/collapsible-trigger-text-base.md new file mode 100644 index 0000000000..3e271c05d5 --- /dev/null +++ b/.changeset/collapsible-trigger-text-base.md @@ -0,0 +1,17 @@ +--- +"@cloudflare/kumo": patch +--- + +`Collapsible.DefaultTrigger`: align tokens with the refreshed typography +scale. + +- Label class swaps `text-sm` → `text-base`. Rendered pixel size is + unchanged (old `text-sm` = 13px, new `text-base` = 13px); only the class + name changes so the trigger reads consistently with the refreshed body + scale. +- Caret icon shrinks from 16×16 (`h-4 w-4`) to 14×14 (`h-3.5 w-3.5`) so it + sits visually flush with the 13px label rather than looming larger than + it. This is the actual visible change. + +Only affects `Collapsible.DefaultTrigger`. Bare `Collapsible.Trigger` still +inherits its label size from its consumer as before. diff --git a/.changeset/font-scale-multiplier.md b/.changeset/font-scale-multiplier.md new file mode 100644 index 0000000000..c7c6a69701 --- /dev/null +++ b/.changeset/font-scale-multiplier.md @@ -0,0 +1,51 @@ +--- +"@cloudflare/kumo": minor +--- + +Expose `--font-scale` as a hookable multiplier on every font-size token. + +**Why:** we want to give products a way to let users bump up type size +without breaking layout. Neither browser zoom nor rem-based font scaling +solves this today: + +- **Browser zoom** scales everything — including the viewport and pixel + values in CSS — so raising font size also enlarges paddings, gaps, + container widths, and cursor targets. The layout you designed at 100% + is not the layout the user sees at 125%. +- **Native browser font-size preferences** would work if the library + reserved `rem` strictly for font-size. It doesn't — and neither does + Tailwind's default theme, which uses `rem` for paddings, margins, and + gaps as well. So changing the browser's base font-size would rescale + spacing along with type, producing the same "zoomed layout" effect. + +`--font-scale` sidesteps both by living inside font-size tokens only. +Every font-size token is now emitted as `calc( * var(--font-scale, 1))`. +Consumers override the variable at any scope; spacing tokens are +untouched, so the layout grid stays put while type scales inside it. + +**Mechanics:** + +- Default multiplier (`1`) lives in the `var()` fallback, so no `:root` + rule is emitted — consumers can override `--font-scale` at any scope + without fighting `:root` specificity. +- Line-heights stay as raw ratios and multiply against the already-scaled + font-size at use time, so they grow naturally. +- No named presets ship with the library. Density modes are an + application-level UX concern; consumers who want a "compact" or + "comfortable" mode declare their own selectors (e.g. + `[data-density="compact"] { --font-scale: 0.875; }`) in their own + stylesheet. +- Recommended multipliers are terminating decimals so every scaled size + is also a terminating decimal — `0.875` (7/8) and `1.125` (9/8) work + cleanly; `12/13` and `14/13` produce infinite decimals like `13.9997px` + that read as floating-point noise in DevTools. + +**Future work (not in this PR):** for `--font-scale` to fully deliver +"scale type without scaling layout", the library needs to move to +`rem`-for-font-size + `px`-for-spacing as a hard convention. Tailwind +default utilities still emit `rem` for spacing, so a follow-up will +either swap the spacing scale to `px` or ship a Tailwind preset that +does. Icon sizing is already `em`-relative (see the `icons-em-relative-sizing` +changeset) so icons come along for the ride automatically. + +No visual regression at the default multiplier. diff --git a/.changeset/form-controls-size-refresh.md b/.changeset/form-controls-size-refresh.md new file mode 100644 index 0000000000..41129f9898 --- /dev/null +++ b/.changeset/form-controls-size-refresh.md @@ -0,0 +1,37 @@ +--- +"@cloudflare/kumo": minor +--- + +Align form control sizes with the refreshed Button scale. Affects `Input`, +`InputArea`, `Select`, `Combobox`, `Autocomplete`, and `InputGroup`. + +**Height changes:** + +| Size | Before | After | +| ------ | ------ | ---------------- | +| `sm` | 26px | 26px (unchanged) | +| `base` | 36px | 32px | +| `lg` | 40px | 36px | + +**Font size:** `sm` form controls now use `text-sm` (12px) instead of +`text-xs` (11px), matching Button's `sm`. `base` and `lg` remain at +`text-base` (13px) — form inputs stay at body-text legibility across +their larger sizes rather than scaling their type up with height. Button +follows the same policy at `lg` (also `text-base`, 13px), so a +button-plus-input row keeps its type on one line; `lg` here signals a +larger touch target and prominence, not larger type. + +**Deprecated:** `size="xs"` on all form controls. Use `size="sm"` instead. +The `xs` variant still renders and looks the same as before, but emits a +`console.warn` in development and will be removed in a future major +version. + +**Trigger icons** (`Select` caret, `Combobox` caret + clear X) no longer +carry per-size hardcoded pixel values. They now inherit their size from +the containing control's text size — see the `icons-em-relative-sizing` +changeset for the pattern. Net effect: at `base` size a caret renders +around 13px (matching the label) instead of the previous 16px, which +read heavy against 13px labels. + +Select's Figma styling metadata is updated accordingly +(`height: 36 → 32`, `fontSize: 16 → 13`). diff --git a/.changeset/icons-em-relative-sizing.md b/.changeset/icons-em-relative-sizing.md new file mode 100644 index 0000000000..8b14d2f1a8 --- /dev/null +++ b/.changeset/icons-em-relative-sizing.md @@ -0,0 +1,57 @@ +--- +"@cloudflare/kumo": minor +--- + +**Icons now scale with the surrounding text.** Hardcoded pixel sizes on +icons across ~20 components were replaced with em-relative sizes (or +dropped entirely so icons inherit Phosphor's `1em` default). Following +Apple's SF Symbols pattern — a UI icon is a glyph in the text's context, +and should shrink and grow with the text it sits alongside. + +**Rationale:** + +Under the old scale we used hardcoded pixel sizes (`size={12}`, `h-4 w-4`) +per component + per size variant, which meant every time the type scale +shifted, dozens of icon sizes across the library had to be re-audited by +hand. Worse, hardcoded sizes broke `--font-scale` (see the +`font-scale-multiplier` changeset): scaling body text up without touching +icons produced surfaces where icons floated at fixed pixel sizes while +their labels grew around them. + +Em-based sizing solves both problems: + +- Icons inside a `text-sm` (12px) label render at 12px automatically; the + same icon inside a `text-base` (13px) label renders at 13px. +- When `--font-scale` shifts the whole type scale, icons come along for + the ride without any per-component intervention. +- Component-internal decisions collapse from "what pixel size at what + variant" to "what ratio to the surrounding text" — expressed in one + place, valid at every scale. + +**Sizing conventions established:** + +| Ratio | Use for | +| ----------------------- | ------------------------------------------------------------ | +| **Bare (`1em`)** | Caret / chevron icons in form controls, buttons, pagination | +| **`0.85em`** | Inline glyphs inside chips, badges, checkboxes (X, check) | +| **`1.15em`** | Leading icons in dropdown items, menu items | +| **`1.25em`** | Sidebar menu icons, larger inline actions | +| **`var(--text-lg)`** | One-offs where the icon size must survive nested type scopes | + +**Affected components** (icon sizing internals only — no API changes): + +`Autocomplete`, `Button` (`RefreshButton`, loader glyph), +`Checkbox` (indicator glyph), `Collapsible` (caret), `Combobox` (caret, +clear X, chip X), `CommandPalette` (leading icons, back-arrow), +`DatePicker` (nav carets, globe), `DateRangePicker` (nav carets), +`Dropdown` (leading icons, submenu caret, check indicator, external-link +glyph), `Empty` (illustration icon), `InputGroup.Addon`, +`InputGroup.Button`, `Menubar` (leading icons via `IconContext`), +`Pagination` (nav carets), `Select` (caret), `SensitiveInput` (eye +toggle), `Sidebar` (menu-button icons, submenu chevron), `Toast` (close +X, variant icon). + +**Visual regression:** icons on nearly every component will shift by 1–3px +per size variant to align with the refreshed type scale. This is +intentional — the previous hardcoded values were tuned for the 14px body +baseline and read heavy against 13px labels. diff --git a/.changeset/label-tooltip-button-sm.md b/.changeset/label-tooltip-button-sm.md new file mode 100644 index 0000000000..2630bd6188 --- /dev/null +++ b/.changeset/label-tooltip-button-sm.md @@ -0,0 +1,9 @@ +--- +"@cloudflare/kumo": patch +--- + +`Label`: bump the inline "More information" help button from `size="xs"` to +`size="sm"` (20px → 26px). Follows the Button `xs` deprecation — the label +tooltip trigger is internal, so this is a size increase rather than a +deprecation. The 26px button sits comfortably next to `text-base` (13px) +label copy without dominating it. diff --git a/.changeset/table-header-weight.md b/.changeset/table-header-weight.md new file mode 100644 index 0000000000..9f55d848ae --- /dev/null +++ b/.changeset/table-header-weight.md @@ -0,0 +1,16 @@ +--- +"@cloudflare/kumo": patch +--- + +`Table`: align header styling with the refreshed typography scale. + +- Column header (``) weight bumped down from `font-semibold` to + `font-medium`. 13px semibold was louder than any Text heading variant; + medium matches the `Text variant="heading"` role, which is what a column + header structurally is. +- Compact table header size bumped up from `text-xs` (11px) to `text-sm` + (12px). 11px is the escape-hatch tier reserved for chart labels and dense + metadata; a compact table header is still a header and reads more + comfortably at 12px. + +Body cell typography is unchanged (`text-base`, 13px). diff --git a/.changeset/text-role-based-variants.md b/.changeset/text-role-based-variants.md new file mode 100644 index 0000000000..3fbe1b9fad --- /dev/null +++ b/.changeset/text-role-based-variants.md @@ -0,0 +1,42 @@ +--- +"@cloudflare/kumo": minor +--- + +`Text` component: introduce role-based heading variants and deprecate raw +size steps on body variants. + +**New variants** (all require the `as` prop for document-outline safety): + +- `variant="display"` — hero / prominent moments (24px semibold) +- `variant="page-title"` — the single title of a page or dialog (19px medium) +- `variant="section-title"` — card / panel / section heading (15px medium) +- `variant="heading"` — inline / row / list-item heading (13px medium) + +**Deprecated (still functional, emits a dev warning):** + +- `variant="heading1"` → use `variant="display"` +- `variant="heading2"` → use `variant="page-title"` +- `variant="heading3"` → use `variant="section-title"` +- `size="xs"` (11px) and `size="lg"` (15px) on body variants → use `size="sm"` + (12px) or `size="base"` (13px), or reach for a heading variant for + hierarchy. Both still render and look the same as before but emit a + `console.warn` in development and will be removed in a future major + version. + +For monospace variants, `size="lg"` remains accepted for backwards +compatibility but no longer changes the rendered size (mono always renders +at 12px, one step below body, for optical parity). + +**`bold` prop (kept, refined):** + +The `bold` prop is retained but its type is now narrowed to copy variants +only (`body`, `secondary`, `success`, `error`), where it bumps weight to +`font-medium` (500). Passing it on heading or monospace variants is a type +error — headings already carry their role's weight, and mono deliberately +stays regular. + +The role-based names make it obvious which variant to reach for based on +what the text **is**, not what size you want. Weight-first hierarchy — +differentiating by weight rather than raw size steps on body text — is the +recommended pattern going forward. Use `bold` for inline emphasis; use +`variant="heading"` for structural hierarchy inside a document outline. diff --git a/.changeset/toast-typography-role-based.md b/.changeset/toast-typography-role-based.md new file mode 100644 index 0000000000..cb6a12065e --- /dev/null +++ b/.changeset/toast-typography-role-based.md @@ -0,0 +1,13 @@ +--- +"@cloudflare/kumo": patch +--- + +Toast: align title and description with the role-based typography scale. + +- Title: `text-[0.975rem]` (15.6px) → `text-lg` (15px medium) — matches `Text variant="section-title"` role. +- Description: `text-[0.925rem]` (14.8px) → `text-lg` (15px regular) — body-lg, muted color. +- Weight-first hierarchy: title and description share the 15px `lg` size; the medium weight on the title carries the differentiation (consistent with Linear's approach). +- `leading-5` retained on both for a compact toast footprint (vs. the default `lg--line-height` of 1.5). +- Figma metadata (`KUMO_TOAST_STYLING`) updated: title fontSize 16 → 15. + +No API changes. diff --git a/.changeset/token-refresh-class-swaps.md b/.changeset/token-refresh-class-swaps.md new file mode 100644 index 0000000000..f8c1894e2b --- /dev/null +++ b/.changeset/token-refresh-class-swaps.md @@ -0,0 +1,30 @@ +--- +"@cloudflare/kumo": minor +--- + +Align internal typography class tokens with the refreshed scale for `Badge`, +`Sidebar`, and `TableOfContents`. **Rendered pixel size is unchanged** in +every case — old `text-sm` / `text-xs` and new `text-base` / `text-sm` +resolve to the same pixel values on their respective scales. Only the +class names change so component internals read consistently with the +refreshed scale (body = `text-base`, caption = `text-sm`, escape hatch = +`text-xs`). + +**Affected surfaces:** + +- `Badge` (`KUMO_BADGE_BASE_STYLES`): `text-xs` → `text-sm` (12px both + scales) +- `Sidebar.GroupLabel`, `Sidebar.MenuButton` (`base` and `sm`), and + `Sidebar.MenuSubButton`: `text-sm` → `text-base` (13px both scales) +- `TableOfContents` items and group-labels: `text-sm` → `text-base` (13px + both scales). `TableOfContents.Title` (uppercase eyebrow) remains + `text-xs`. + +Consumers extending these components via `className` or selecting on the +old class names (e.g. `[class*="text-sm"]` on sidebar rows, custom styling +that extends `KUMO_BADGE_BASE_STYLES`) will need to update their +references. The bump is `minor` (not `patch`) because sidebar internals in +particular ship stringly-typed class contracts that consumers occasionally +lean on. + +No visible change to rendered typography on any of these components. diff --git a/.changeset/toolbar-deprecate-xs.md b/.changeset/toolbar-deprecate-xs.md new file mode 100644 index 0000000000..ac10ea2a22 --- /dev/null +++ b/.changeset/toolbar-deprecate-xs.md @@ -0,0 +1,18 @@ +--- +"@cloudflare/kumo": patch +--- + +`Toolbar`: deprecate `size="xs"`. Use `size="sm"` instead. Toolbar forwards +its `size` to child `Button` and `Input` components, both of which already +deprecated `xs`. To avoid three duplicate warnings for the same underlying +issue, Toolbar now: + +1. Emits a single `[Kumo Toolbar]: size="xs" is deprecated` warning at the + Toolbar level in development, and +2. Silently remaps `xs → sm` internally on the forwarded context so child + `Button`, `Input`, and `InputGroup` render at their `sm` (26px) size and + do **not** emit their own deprecation warnings. + +The container itself still applies the deprecated size's text class for +backwards visual compatibility. The `xs` size will be removed in a future +major version. diff --git a/packages/kumo-docs-astro/src/components/FontScaleToggle.tsx b/packages/kumo-docs-astro/src/components/FontScaleToggle.tsx new file mode 100644 index 0000000000..9c983d41e9 --- /dev/null +++ b/packages/kumo-docs-astro/src/components/FontScaleToggle.tsx @@ -0,0 +1,141 @@ +import { useState, useEffect } from "react"; +import { Button, cn } from "@cloudflare/kumo"; + +/** + * The three available font-scale steps, in the *cycle order* the user + * traverses on repeated clicks. + * + * default → large → small → default + */ +const CYCLE = ["default", "large", "small"] as const; +type Step = (typeof CYCLE)[number]; + +const STORAGE_KEY = "font-scale"; + +/** Human-readable label per step, used for aria + title. */ +const LABELS: Record = { + small: "Small", + default: "Default", + large: "Large", +}; + +function applyStep(step: Step) { + const root = document.documentElement; + if (step === "default") { + root.removeAttribute("data-font-scale"); + } else { + root.setAttribute("data-font-scale", step); + } +} + +function readInitialStep(): Step { + if (typeof window === "undefined") return "default"; + const stored = localStorage.getItem(STORAGE_KEY); + if (stored && (CYCLE as readonly string[]).includes(stored)) { + return stored as Step; + } + return "default"; +} + +/** + * "aA" toggle — cycles the docs site through five font-scale presets. + * + * The lowercase `a` renders at the *previous* step's size, the uppercase `A` + * at the *current* step's size, so the button itself illustrates where you + * are on the scale. On click, the icon briefly pulses in the direction the + * scale moved (up or down) as visual feedback. + */ +export function FontScaleToggle() { + const [step, setStep] = useState("default"); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + const initial = readInitialStep(); + setStep(initial); + applyStep(initial); + }, []); + + const cycle = () => { + const idx = CYCLE.indexOf(step); + const next = CYCLE[(idx + 1) % CYCLE.length]; + setStep(next); + applyStep(next); + localStorage.setItem(STORAGE_KEY, next); + }; + + const nextStep = CYCLE[(CYCLE.indexOf(step) + 1) % CYCLE.length]; + + // Show the icon in its default state during SSR + before hydration to + // avoid a layout shift. + if (!mounted) { + return ( + + ); + } + + return ( + + ); +} + +/** + * "aaA" glyph icon — three letters at fixed pixel sizes (11 / 13 / 15) that + * illustrate the scale. The active step's glyph is rendered in + * `kumo-default`; the two inactive glyphs are in `kumo-subtle`, so the icon + * itself doubles as a state indicator. + * + * All three sizes are hardcoded in px so the icon does NOT scale with the + * `--font-scale` multiplier it controls. + * + * small → left small `a` active + * default → middle medium `a` active + * large → uppercase `A` active + */ +function FontScaleIcon({ step }: { step: Step }) { + const glyphClass = "font-medium transition-colors duration-150"; + return ( + + + a + + + a + + + A + + + ); +} diff --git a/packages/kumo-docs-astro/src/components/Header.astro b/packages/kumo-docs-astro/src/components/Header.astro index 8da1ec2971..017c4f747d 100644 --- a/packages/kumo-docs-astro/src/components/Header.astro +++ b/packages/kumo-docs-astro/src/components/Header.astro @@ -1,5 +1,6 @@ --- import { ThemeToggle } from "./ThemeToggle"; +import { FontScaleToggle } from "./FontScaleToggle"; declare const __KUMO_VERSION__: string; @@ -26,7 +27,12 @@ const kumoVersion = -
- +
+
+ +
+
+ +
diff --git a/packages/kumo-docs-astro/src/components/SidebarNav.tsx b/packages/kumo-docs-astro/src/components/SidebarNav.tsx index 8536742e8c..53006375d0 100644 --- a/packages/kumo-docs-astro/src/components/SidebarNav.tsx +++ b/packages/kumo-docs-astro/src/components/SidebarNav.tsx @@ -8,6 +8,7 @@ import { import { KumoMenuIcon } from "./KumoMenuIcon"; import { SearchDialog } from "./SearchDialog"; import { ThemeToggle } from "./ThemeToggle"; +import { FontScaleToggle } from "./FontScaleToggle"; interface NavItem { label: string; @@ -213,9 +214,9 @@ export function SidebarNav({ currentPath }: SidebarNavProps) { <> @@ -242,7 +243,7 @@ export function SidebarNav({ currentPath }: SidebarNavProps) { {/* Components Section */}

Kumo

- +
+ + +
{/* Mobile slide-out drawer */} @@ -403,7 +407,7 @@ export function SidebarNav({ currentPath }: SidebarNavProps) {
{navContent} @@ -453,7 +457,7 @@ export function SidebarNav({ currentPath }: SidebarNavProps) {
{navContent}
diff --git a/packages/kumo-docs-astro/src/components/demos/AutocompleteDemo.tsx b/packages/kumo-docs-astro/src/components/demos/AutocompleteDemo.tsx index bec1fb4cc4..615cc9136e 100644 --- a/packages/kumo-docs-astro/src/components/demos/AutocompleteDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/AutocompleteDemo.tsx @@ -210,22 +210,10 @@ export function AutocompleteGroupedDemo() { ); } -/** Demonstrates the four size variants: xs, sm, base, and lg. */ +/** Demonstrates the three size variants: sm, base, and lg. */ export function AutocompleteSizesDemo() { return (
- - - - - {(item: string) => ( - - {item} - - )} - - - diff --git a/packages/kumo-docs-astro/src/components/demos/BreadcrumbsDemo.tsx b/packages/kumo-docs-astro/src/components/demos/BreadcrumbsDemo.tsx index bdfb9db2d0..e09daeef39 100644 --- a/packages/kumo-docs-astro/src/components/demos/BreadcrumbsDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/BreadcrumbsDemo.tsx @@ -16,7 +16,7 @@ export function BreadcrumbsDemo() { export function BreadcrumbsWithIconsDemo() { return ( - }> + }> Home @@ -30,7 +30,7 @@ export function BreadcrumbsWithIconsDemo() { export function BreadcrumbsLoadingDemo() { return ( - }> + }> Home @@ -44,7 +44,7 @@ export function BreadcrumbsLoadingDemo() { export function BreadcrumbsRootDemo() { return ( - }> + }> Worker Analytics diff --git a/packages/kumo-docs-astro/src/components/demos/ButtonDemo.tsx b/packages/kumo-docs-astro/src/components/demos/ButtonDemo.tsx index 7fade2087e..df67f88ee9 100644 --- a/packages/kumo-docs-astro/src/components/demos/ButtonDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/ButtonDemo.tsx @@ -42,9 +42,6 @@ export function ButtonSecondaryDestructiveDemo() { export function ButtonSizesDemo() { return (
- diff --git a/packages/kumo-docs-astro/src/components/demos/DialogDemo.tsx b/packages/kumo-docs-astro/src/components/demos/DialogDemo.tsx index 105830716f..244e8224b3 100644 --- a/packages/kumo-docs-astro/src/components/demos/DialogDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/DialogDemo.tsx @@ -15,7 +15,7 @@ export function DialogBasicDemo() { } />
- + Modal Title
- + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. @@ -46,7 +46,7 @@ export function DialogWithActionsDemo() { } />
- + Modal Title
- + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. @@ -99,7 +99,7 @@ export function DialogMaxWidthDemo() { />
- + Max width override
- + This dialog uses className="max-w-lg" and should stay capped around 512px on desktop. @@ -142,11 +142,11 @@ export function DialogConfirmationDemo() {
- + Delete Project?
- + This action cannot be undone. This will permanently delete the project and all associated data. @@ -190,11 +190,11 @@ export function DialogAlertDemo() {
- + Delete Account?
- + This action cannot be undone. All your data will be permanently removed from our servers. Are you sure you want to proceed? @@ -232,7 +232,7 @@ export function DialogWithSelectDemo() { } />
- + Create Resource
- + Select a region for your new resource. diff --git a/packages/kumo-docs-astro/src/components/demos/InputGroupDemo.tsx b/packages/kumo-docs-astro/src/components/demos/InputGroupDemo.tsx index 4adb7052aa..d773c775da 100644 --- a/packages/kumo-docs-astro/src/components/demos/InputGroupDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/InputGroupDemo.tsx @@ -226,21 +226,6 @@ export function InputGroupSuffixDemo() { export function InputGroupSizesDemo() { return (
- - - - - - - - - - diff --git a/packages/kumo-docs-astro/src/components/demos/LayerCardDemo.tsx b/packages/kumo-docs-astro/src/components/demos/LayerCardDemo.tsx index 1df334421e..dec855e261 100644 --- a/packages/kumo-docs-astro/src/components/demos/LayerCardDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/LayerCardDemo.tsx @@ -26,9 +26,7 @@ export function LayerCardBasicDemo() { Getting Started -

- Quick start guide for new users -

+

Quick start guide for new users

); @@ -37,9 +35,7 @@ export function LayerCardBasicDemo() { export function LayerCardSurfaceDemo() { return ( -

- Quick start guide for new users -

+

Quick start guide for new users

); } @@ -52,9 +48,7 @@ export function LayerCardTestIdDemo() { Getting Started -

- Quick start guide for new users -

+

Quick start guide for new users

); @@ -66,13 +60,13 @@ export function LayerCardMultipleDemo() { Components -

Browse all components

+

Browse all components

Examples -

View code examples

+

View code examples

diff --git a/packages/kumo-docs-astro/src/components/demos/PopoverDemo.tsx b/packages/kumo-docs-astro/src/components/demos/PopoverDemo.tsx index b33afd6e6a..8c7dd1f973 100644 --- a/packages/kumo-docs-astro/src/components/demos/PopoverDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/PopoverDemo.tsx @@ -200,7 +200,7 @@ export function PopoverVirtualAnchorDemo() { {row.status}
@@ -460,7 +462,7 @@ export const designTips = [ jsx: (
- API tokens + API tokens
@@ -515,19 +517,19 @@ export const designTips = [ jsx: (
- + Recent Requests
- + Time - + Status - + Query @@ -551,15 +553,15 @@ export const designTips = [ jsx: (
- + Recent Requests
- Time - Status - Query + Time + Status + Query 00:50 UTC diff --git a/packages/kumo-docs-astro/src/layouts/BaseLayout.astro b/packages/kumo-docs-astro/src/layouts/BaseLayout.astro index 15423a8611..111e366ee7 100644 --- a/packages/kumo-docs-astro/src/layouts/BaseLayout.astro +++ b/packages/kumo-docs-astro/src/layouts/BaseLayout.astro @@ -75,6 +75,8 @@ const buildDate = // must be re-applied after each view-transition swap because the // attributes are replaced during navigation. (function () { + var FONT_SCALES = ["small", "default", "large"]; + function setTheme(theme) { localStorage.setItem("theme", theme); document.documentElement.setAttribute("data-mode", theme); @@ -92,16 +94,28 @@ const buildDate = } } + function applyStoredFontScale() { + const stored = localStorage.getItem("font-scale"); + if (stored && FONT_SCALES.indexOf(stored) !== -1 && stored !== "default") { + document.documentElement.setAttribute("data-font-scale", stored); + } else { + document.documentElement.removeAttribute("data-font-scale"); + } + } + // Apply on this (initial or freshly-swapped) document immediately. applyStoredTheme(); + applyStoredFontScale(); // Register global listeners exactly once across soft navigations. if (!window.__kumoThemeInit) { window.__kumoThemeInit = true; - // Reapply theme before each swap paints, so there's no light flash. + // Reapply theme + font-scale before each swap paints, so there's no flash. document.addEventListener("astro:before-swap", applyStoredTheme); document.addEventListener("astro:after-swap", applyStoredTheme); + document.addEventListener("astro:before-swap", applyStoredFontScale); + document.addEventListener("astro:after-swap", applyStoredFontScale); document.addEventListener("keydown", function (event) { if (event.defaultPrevented || event.repeat) return; diff --git a/packages/kumo-docs-astro/src/pages/changelog/[...page].astro b/packages/kumo-docs-astro/src/pages/changelog/[...page].astro index befd7271bf..6f386daf04 100644 --- a/packages/kumo-docs-astro/src/pages/changelog/[...page].astro +++ b/packages/kumo-docs-astro/src/pages/changelog/[...page].astro @@ -161,7 +161,7 @@ const GITHUB_RELEASE_URL = "https://github.com/cloudflare/kumo/releases/tag/%40c > - {v.version} + {v.version} {badgeConfig[v.bump].label} h3]:m-0"> - {sectionLabels[section.type]} + {sectionLabels[section.type]}
    {section.entries.map((entry: ParsedEntry) => ( diff --git a/packages/kumo-docs-astro/src/pages/components-vs-blocks.mdx b/packages/kumo-docs-astro/src/pages/components-vs-blocks.mdx index 3c3214aa52..bf2ba63178 100644 --- a/packages/kumo-docs-astro/src/pages/components-vs-blocks.mdx +++ b/packages/kumo-docs-astro/src/pages/components-vs-blocks.mdx @@ -89,10 +89,10 @@ Consider a `ProductCard`. It's a specific arrangement of components for a specif {imgAlt}
    - + {title} - {description} + {description}
    diff --git a/packages/kumo-docs-astro/src/pages/components/autocomplete.mdx b/packages/kumo-docs-astro/src/pages/components/autocomplete.mdx index b89b224636..1138f48fdd 100644 --- a/packages/kumo-docs-astro/src/pages/components/autocomplete.mdx +++ b/packages/kumo-docs-astro/src/pages/components/autocomplete.mdx @@ -124,8 +124,9 @@ import { ## Sizes

    - The `size` prop on `Autocomplete.InputGroup` supports four variants matching the - Input component: `xs`, `sm`, `base` (default), and `lg`. + The `size` prop on `Autocomplete.InputGroup` supports three variants matching + the Input component: `sm`, `base` (default), and `lg`. (`xs` is deprecated — + use `sm`.)

    diff --git a/packages/kumo-docs-astro/src/pages/components/combobox.mdx b/packages/kumo-docs-astro/src/pages/components/combobox.mdx index ab3a7ff80b..96aeea7769 100644 --- a/packages/kumo-docs-astro/src/pages/components/combobox.mdx +++ b/packages/kumo-docs-astro/src/pages/components/combobox.mdx @@ -95,8 +95,8 @@ export default function Example() { ### Sizes

    - The Combobox supports four size variants that match the Input component: `xs`, - `sm`, `base` (default), and `lg`. + The Combobox supports three size variants that match the Input component: + `sm`, `base` (default), and `lg`. (`xs` is deprecated — use `sm`.)

    diff --git a/packages/kumo-docs-astro/src/pages/components/input-area.mdx b/packages/kumo-docs-astro/src/pages/components/input-area.mdx index 8c1eb3a2c8..2c3db81837 100644 --- a/packages/kumo-docs-astro/src/pages/components/input-area.mdx +++ b/packages/kumo-docs-astro/src/pages/components/input-area.mdx @@ -132,7 +132,7 @@ export default function Example() { ### Sizes -

    Four sizes available: `xs`, `sm`, `base` (default), `lg`.

    +

    Three sizes available: `sm`, `base` (default), `lg`. (`xs` is deprecated — use `sm`.)

    diff --git a/packages/kumo-docs-astro/src/pages/components/input.mdx b/packages/kumo-docs-astro/src/pages/components/input.mdx index 9ee34763ac..dca0ed6e75 100644 --- a/packages/kumo-docs-astro/src/pages/components/input.mdx +++ b/packages/kumo-docs-astro/src/pages/components/input.mdx @@ -136,7 +136,7 @@ export default function Example() { ### Input Sizes -

    Four sizes available: `xs`, `sm`, `base` (default), `lg`.

    +

    Three sizes available: `sm`, `base` (default), `lg`. (`xs` is deprecated — use `sm`.)

    diff --git a/packages/kumo-docs-astro/src/pages/components/sidebar.astro b/packages/kumo-docs-astro/src/pages/components/sidebar.astro index e71b681f59..9d115572f8 100644 --- a/packages/kumo-docs-astro/src/pages/components/sidebar.astro +++ b/packages/kumo-docs-astro/src/pages/components/sidebar.astro @@ -45,9 +45,9 @@ import { Installation - Barrel + Barrel -

    Granular

    + Granular
    @@ -376,7 +376,7 @@ const { state, isPeeking } = useSidebar();
    -

    Sidebar

    + Sidebar

    The main sidebar container. Renders as <aside> on desktop and a navigation drawer on mobile.

    @@ -384,7 +384,7 @@ const { state, isPeeking } = useSidebar();
    -

    Sidebar.Provider

    + Sidebar.Provider

    Context provider managing expand/collapse state and mobile detection.

    @@ -392,14 +392,14 @@ const { state, isPeeking } = useSidebar();
    -

    Sidebar.Content

    + Sidebar.Content

    Scrollable middle section (flex-1 overflow-y-auto). Use Header / Footer to pin content above or below this scroll area.

    -

    Sidebar.MenuButton

    + Sidebar.MenuButton

    Primary interactive element. Supports icons, active state, links, and auto-tooltip when collapsed. Auto-wraps in <li> — no MenuItem wrapper needed unless wrapping a Collapsible. @@ -408,7 +408,7 @@ const { state, isPeeking } = useSidebar();

    -

    Sidebar.MenuSubButton

    + Sidebar.MenuSubButton

    Button inside a sub-menu for nested navigation. Auto-wraps in <li> — no MenuSubItem wrapper needed. diff --git a/packages/kumo-docs-astro/src/pages/components/text.mdx b/packages/kumo-docs-astro/src/pages/components/text.mdx index 1c67d24e86..decfa295d9 100644 --- a/packages/kumo-docs-astro/src/pages/components/text.mdx +++ b/packages/kumo-docs-astro/src/pages/components/text.mdx @@ -57,7 +57,25 @@ export default function Example() { ```

    - Semantic HTML + Heading variants + + Heading variants are role-based. Reach for the one that describes what the + text **is**, not how big you want it: + + + - `variant="display"` — hero / prominent moments (24px semibold) + - `variant="page-title"` — the single title of a page or dialog (19px medium) + - `variant="section-title"` — card / panel / section heading (15px medium) + - `variant="heading"` — inline / row / list-item heading (13px medium) + + + `heading` is the small, most-used one — reach for it before adding extra + weight or size to body text. + +
    + +
    + Semantic HTML The `variant` prop controls visual styling only—it does not determine the HTML element rendered. Heading variants **require** the `as` prop to avoid silently excluding real section headings from @@ -67,17 +85,19 @@ export default function Example() { ```tsx // Heading variants REQUIRE `as` — TypeScript will flag usages missing it -Page Title // Doesn't compile +Welcome // Doesn't compile // Real section headings (contribute to the document outline) -Page Title -Section Title +Welcome +Account settings +General +API tokens // Decorative heading-styled text that is NOT a section heading -Big bold card label +Big card label // Visually one size, semantically another -Visually large, but semantically h3 +Visually large, but semantically h3 ``` @@ -88,42 +108,61 @@ export default function Example() {
    - Restrictions + Restrictions - The `bold` and `size` props are intentionally restricted to the `base`, `secondary`, `success`, and `error` text variants. + The `size` prop is intentionally restricted to the `body`, `secondary`, `success`, and `error` variants. ```tsx -Body -Body secondary -Success +Body +Body secondary +Success Error ``` - Monospace variants (`mono` and `mono-secondary`) can only set `size` to `lg` - and cannot use the `bold` prop: + Monospace variants (`mono` and `mono-secondary`) always render at 12px and + do not accept a `size` prop. ```tsx Monospace -Monospace -Monospace // Doesn't compile ``` - Headings (i.e. `heading1`, `heading2` and `heading3` variants) cannot use - these props at all: + Heading variants (`display`, `page-title`, `section-title`, `heading`) don't + accept `size` — the size is part of the role. + + + + The `bold` prop follows the same shape: it applies only to copy variants + (`body`, `secondary`, `success`, `error`), where it bumps weight to + `font-medium` (500) for inline emphasis. Heading variants already carry + their role's weight, and monospace stays regular by design — both type + `bold` as `never` so consumers get a compile error rather than a silent + no-op. For structural hierarchy inside a document outline reach for + `variant="heading"` instead of `bold`. ```tsx - - Heading 1 - // Doesn't compile +Important detail +Muted but bumped ```
    +
    + Deprecated variants + + The numeric heading names still work but emit a warning in development. + Migrate call sites to the role-based names: + + + - `heading1` → `display` + - `heading2` → `page-title` + - `heading3` → `section-title` +
    + {/* Truncate */} diff --git a/packages/kumo-docs-astro/src/styles/global.css b/packages/kumo-docs-astro/src/styles/global.css index 1d53e5799a..835e691e6c 100644 --- a/packages/kumo-docs-astro/src/styles/global.css +++ b/packages/kumo-docs-astro/src/styles/global.css @@ -12,6 +12,23 @@ "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; } +/* ========================================================================== + Font-scale presets (docs-only) + -------------------------------------------------------------------------- + Kumo exposes `--font-scale` as a hookable multiplier on every font-size + token via `calc( * var(--font-scale, 1))`. The library ships NO + presets — density modes are an application-level UX concern. These + selectors are docs-only and drive the FontScaleToggle in the header. + ========================================================================== */ +@layer base { + [data-font-scale="small"] { + --font-scale: 0.875; + } + [data-font-scale="large"] { + --font-scale: 1.125; + } +} + * { box-sizing: border-box; -webkit-font-smoothing: antialiased; diff --git a/packages/kumo-figma/src/build-theme-data.ts b/packages/kumo-figma/src/build-theme-data.ts index b7c41df4b2..14f5b19180 100644 --- a/packages/kumo-figma/src/build-theme-data.ts +++ b/packages/kumo-figma/src/build-theme-data.ts @@ -114,18 +114,29 @@ function remToPx(remValue: string): number { function parseKumoFontSizes(css: string): Record { const sizes: Record = {}; + // Match `calc( * var(--font-scale, ...))` — extract the unscaled + // px value. Font-size tokens are emitted wrapped in this multiplier so + // consumers can shift the whole scale via `--font-scale`; Figma + // operates at 1× so we take the base value. + const calcMatches = css.matchAll( + /--text-(\w+):\s*calc\(\s*(\d+)px\s*\*\s*var\(--font-scale[^)]*\)\s*\)/g, + ); + for (const match of calcMatches) { + sizes[match[1]] = parseInt(match[2], 10); + } + // Match: --text-xs: 12px; or --text-sm: 13px; - const pxMatches = css.matchAll(/--text-(\w+):\s*(\d+)px/g); + const pxMatches = css.matchAll(/--text-(\w+):\s*(\d+)px\s*;/g); for (const match of pxMatches) { + if (sizes[match[1]] !== undefined) continue; sizes[match[1]] = parseInt(match[2], 10); } // Match rem values: --text-xl: 1.25rem; - const remMatches = css.matchAll(/--text-(\w+):\s*([\d.]+)rem/g); + const remMatches = css.matchAll(/--text-(\w+):\s*([\d.]+)rem\s*;/g); for (const match of remMatches) { - if (!sizes[match[1]]) { - sizes[match[1]] = remToPx(match[2] + "rem"); - } + if (sizes[match[1]] !== undefined) continue; + sizes[match[1]] = remToPx(match[2] + "rem"); } return sizes; diff --git a/packages/kumo/scripts/theme-generator/config.ts b/packages/kumo/scripts/theme-generator/config.ts index 947f803a79..92e7cf6f55 100644 --- a/packages/kumo/scripts/theme-generator/config.ts +++ b/packages/kumo/scripts/theme-generator/config.ts @@ -592,36 +592,49 @@ export const THEME_CONFIG: ThemeConfig = { * * Note: Typography is NOT theme-dependent (no light/dark mode). * Values are the same across color modes but may differ per theme. + * + * Scale rationale: + * - Weight is the primary hierarchy signal; size is used only for large jumps. + * - `base` (13px) is the product default body size — dense-app appropriate. + * - `sm` (12px) is for helper/caption text and descriptions. + * - `xs` (11px) is an escape hatch for chart labels and dense metadata. + * - `lg` (15px) covers long-form body and mid-level section headings. + * - `xl` (19px) is the standard page/dialog title. + * - `2xl` (24px) is reserved for prominent/hero moments. */ typography: { xs: { newName: "", + description: + "Escape hatch: chart labels, dense metadata. Avoid in body copy.", theme: { - kumo: "12px", + kumo: "11px", }, }, "xs--line-height": { newName: "", theme: { - kumo: "calc(1 / 0.75)", + kumo: "1.4", }, }, sm: { newName: "", + description: "Caption — helper text, descriptions, table metadata.", theme: { - kumo: "13px", + kumo: "12px", }, }, "sm--line-height": { newName: "", theme: { - kumo: "calc(1 / 0.85)", + kumo: "1.45", }, }, base: { newName: "", + description: "Body — default product text size.", theme: { - kumo: "14px", + kumo: "13px", }, }, "base--line-height": { @@ -632,8 +645,9 @@ export const THEME_CONFIG: ThemeConfig = { }, lg: { newName: "", + description: "Body-lg / section heading — long-form content, dialogs.", theme: { - kumo: "16px", + kumo: "15px", }, }, "lg--line-height": { @@ -642,6 +656,51 @@ export const THEME_CONFIG: ThemeConfig = { kumo: "1.5", }, }, + xl: { + newName: "", + description: "Page / dialog title — paired with a description.", + theme: { + kumo: "19px", + }, + }, + "xl--line-height": { + newName: "", + theme: { + kumo: "1.4", + }, + }, + "2xl": { + newName: "", + description: "Display — hero / prominent moments.", + theme: { + kumo: "24px", + }, + }, + "2xl--line-height": { + newName: "", + theme: { + kumo: "1.3", + }, + }, + }, + + /** + * Font-scale — exposes `--font-scale` as a hookable multiplier on every + * font-size token. Font-size tokens are emitted as + * `calc( * var(--font-scale, 1))`; consumers can override the + * variable at any scope to shift the whole scale. + * + * The library ships no named presets — that's an application-level UX + * concern, not a token concern. Consumers who want density modes + * declare their own selectors (e.g. `[data-font-scale="compact"]`) in + * their own stylesheet. + * + * Line-heights stay as raw ratios and multiply against the already- + * scaled font-size at use time. + */ + fontScale: { + default: 1, + presets: {}, }, }; diff --git a/packages/kumo/scripts/theme-generator/generate-css.ts b/packages/kumo/scripts/theme-generator/generate-css.ts index 09b54f9f1e..b11313aefe 100644 --- a/packages/kumo/scripts/theme-generator/generate-css.ts +++ b/packages/kumo/scripts/theme-generator/generate-css.ts @@ -160,6 +160,11 @@ export function generateKumoThemeCSS( lines.push("}"); // Typography tokens (font sizes and line heights) + // + // Font-size tokens are multiplied by `--font-scale` so a single knob can + // shift the whole scale. Line-height tokens stay as raw ratios — they'll + // multiply against the already-scaled font-size at use time and grow + // naturally, so scaling them here would double-apply. if (config.typography && Object.keys(config.typography).length > 0) { lines.push(""); lines.push("@theme {"); @@ -168,13 +173,40 @@ export function generateKumoThemeCSS( for (const [tokenName, def] of Object.entries(config.typography)) { const name = useNewNames && def.newName ? def.newName : tokenName; const value = def.theme.kumo; + const isLineHeight = tokenName.endsWith("--line-height"); - lines.push(` --text-${name}: ${value};`); + const emitted = + config.fontScale && !isLineHeight + ? `calc(${value} * var(--font-scale, 1))` + : value; + + lines.push(` --text-${name}: ${emitted};`); } lines.push("}"); } + // Font-scale presets. + // + // The library declares no named presets — the default multiplier lives + // in the `var(--font-scale, 1)` fallback in the typography block above. + // Consumers can override `--font-scale` at any scope (typically on + // `` via a `data-*` attribute) to shift the whole scale. Kept as + // a hook here in case an internal preset is ever justified. + if (config.fontScale && Object.keys(config.fontScale.presets).length > 0) { + lines.push(""); + lines.push("@layer base {"); + lines.push(" /* Font-scale presets — override --font-scale by selector. */"); + const fmt = (n: number) => + Number.isInteger(n) ? n.toString() : n.toFixed(4).replace(/\.?0+$/, ""); + for (const [preset, value] of Object.entries(config.fontScale.presets)) { + lines.push( + ` [data-font-scale="${preset}"] { --font-scale: ${fmt(value)}; }`, + ); + } + lines.push("}"); + } + // Explicit runtime vars avoid transient unresolved light-dark() values // during class/DOM mutations in some browser style recalculation paths. pushModeScopedBaseVariables(lines, config, "kumo", useNewNames, { diff --git a/packages/kumo/scripts/theme-generator/index.ts b/packages/kumo/scripts/theme-generator/index.ts index 2b71ef63e4..22e0a7aaf2 100644 --- a/packages/kumo/scripts/theme-generator/index.ts +++ b/packages/kumo/scripts/theme-generator/index.ts @@ -10,6 +10,7 @@ * pnpm codegen:themes --dry-run # Preview without writing */ +import { execFileSync } from "node:child_process"; import * as fs from "node:fs"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; @@ -146,6 +147,31 @@ async function main() { } if (!args.dryRun) { + // Format generated CSS with oxfmt so codegen output stays consistent with + // the rest of the repo — otherwise minor whitespace / numeric drift from + // the ad-hoc string builder pollutes every regen with unrelated diff. + const filenames = [...files.keys()]; + try { + execFileSync( + "pnpm", + [ + "exec", + "vp", + "fmt", + ...filenames.map((n) => path.join("src/styles", n)), + ], + { + cwd: path.resolve(__dirname, "../.."), + stdio: "inherit", + }, + ); + } catch (err) { + console.warn( + " Warning: oxfmt pass failed; generated CSS may have inconsistent formatting.", + err instanceof Error ? err.message : err, + ); + } + console.log("\nTheme generation complete!"); console.log( "\nTip: Run with --dry-run to preview changes without writing files", diff --git a/packages/kumo/scripts/theme-generator/types.ts b/packages/kumo/scripts/theme-generator/types.ts index d645be8259..bc03384d0c 100644 --- a/packages/kumo/scripts/theme-generator/types.ts +++ b/packages/kumo/scripts/theme-generator/types.ts @@ -65,6 +65,32 @@ export type TypographyTokens = { [tokenName: string]: TypographyTokenDefinition; }; +/** + * Font-scale configuration + * + * Introduces a global multiplier variable (`--font-scale`) applied to every + * font-size token (but NOT to line-heights — those stay ratios and grow + * naturally because they multiply against the already-scaled font-size). + * + * `presets` maps arbitrary preset names to multiplier values. Each preset + * emits a `[data-font-scale="{name}"] { --font-scale: {multiplier}; }` block. + * The `default` multiplier is applied to `:root`. + * + * Example: + * ```ts + * fontScale: { + * default: 1, + * presets: { small: 12 / 13, large: 14 / 13 }, + * } + * ``` + */ +export type FontScaleConfig = { + /** Multiplier applied to `:root` (usually 1). */ + default: number; + /** Named presets → multiplier. */ + presets: Record; +}; + /** Complete theme configuration */ export type ThemeConfig = { /** Text color tokens */ @@ -73,6 +99,8 @@ export type ThemeConfig = { color: ColorTokens; /** Typography tokens (font sizes and line heights) */ typography?: TypographyTokens; + /** Optional font-scale multiplier + named presets */ + fontScale?: FontScaleConfig; }; /** Output options for CSS generation */ diff --git a/packages/kumo/src/components/autocomplete/autocomplete.tsx b/packages/kumo/src/components/autocomplete/autocomplete.tsx index c93facbe66..7da9679815 100644 --- a/packages/kumo/src/components/autocomplete/autocomplete.tsx +++ b/packages/kumo/src/components/autocomplete/autocomplete.tsx @@ -25,10 +25,10 @@ export type KumoAutocompleteSize = keyof typeof KUMO_AUTOCOMPLETE_VARIANTS.size; export interface KumoAutocompleteVariantsProps { /** * Size of the autocomplete input. Matches Input component sizes. - * - `"xs"` — Extra small for compact UIs (h-5 / 20px) - * - `"sm"` — Small for secondary fields (h-6.5 / 26px) - * - `"base"` — Default size (h-9 / 36px) - * - `"lg"` — Large for prominent fields (h-10 / 40px) + * - `"sm"` — Small for secondary fields (26px tall) + * - `"base"` — Default size (32px tall) + * - `"lg"` — Large for prominent fields (36px tall) + * - `"xs"` — **Deprecated.** Use `"sm"` instead. * @default "base" */ size?: KumoAutocompleteSize; @@ -154,6 +154,14 @@ function InputGroup({ placeholder?: string; }) { const { hasError } = useContext(AutocompleteContext); + + if (process.env.NODE_ENV !== "production" && size === "xs") { + console.warn( + '[Kumo Autocomplete]: size="xs" is deprecated. Use size="sm" instead. ' + + "The xs size will be removed in a future major version.", + ); + } + return ( ; +export type BannerActionSize = Extract; /** Value shared from the `Banner` root to its `Banner.Action` children. */ export interface BannerActionContextValue { @@ -43,8 +43,8 @@ export interface BannerActionContextValue { * Propagates the banner's variant and action size to `Banner.Action` * children so each CTA can self-style without prop drilling: * - `variant` — selects the matching accent color. - * - `size` — a compact `size="sm"` banner renders actions at `"xs"`, and a - * `"base"` banner renders them at `"sm"`. + * - `size` — both `"base"` and compact `"sm"` banners render actions at + * `"sm"` (26px), the smallest Button size in the current scale. * * The `Banner` root always overrides these defaults via a Provider; the literals * mirror a default, base-size banner (kept as literals to avoid a runtime import diff --git a/packages/kumo/src/components/banner/banner.test.tsx b/packages/kumo/src/components/banner/banner.test.tsx index 0faa017a28..dc1c4e68db 100644 --- a/packages/kumo/src/components/banner/banner.test.tsx +++ b/packages/kumo/src/components/banner/banner.test.tsx @@ -130,7 +130,8 @@ describe("Banner", () => { const cta = screen.getByTestId("cta"); expect(cta.className).toContain("h-6.5"); expect(cta.className).toContain("px-2"); - expect(cta.className).toContain("text-xs"); + // sm buttons use text-sm (12px) to stay proportional to their 26px height. + expect(cta.className).toContain("text-sm"); }); it("applies compact spacing for the sm banner size", () => { @@ -146,7 +147,7 @@ describe("Banner", () => { expect(className).not.toContain("items-start"); }); - it("defaults Banner.Action children to xs in an sm banner", () => { + it("defaults Banner.Action children to sm in an sm banner", () => { render( { ); const cta = screen.getByTestId("cta"); - // Inherits the banner's size => xs (h-5), not the standalone sm default (h-6.5). - expect(cta.className).toContain("h-5"); - expect(cta.className).toContain("px-1.5"); + // Both base and compact banners now render actions at the sm Button + // size (26px) — xs was removed from the Button scale. + expect(cta.className).toContain("h-6.5"); + expect(cta.className).toContain("px-2"); }); it("matches an icon-only action to the text action height in an sm banner", () => { @@ -179,9 +181,10 @@ describe("Banner", () => { ); const cta = screen.getByTestId("cta"); - expect(cta.className).toContain("h-5"); - expect(cta.className).toContain("px-1.5"); - expect(cta.className).not.toContain("size-3.5"); + // Icon-only Banner.Action inherits the sm Button size (26px tall). It + // doesn't set a compact `shape`, so it stays rectangular. + expect(cta.className).toContain("h-6.5"); + expect(cta.className).toContain("px-2"); }); it("renders title and description inline in an sm banner", () => { @@ -217,7 +220,12 @@ describe("Banner", () => { expect(actionGroup?.parentElement).toBe(description); expect(actionGroup?.className).toContain("ml-1.5"); - expect(description.className).toContain("text-sm"); + // In an sm banner the description inherits `text-sm` (12px) from the + // banner container rather than carrying the class itself. Title and + // description share the container size; only the title's font weight + // distinguishes them. + const banner = description.closest('[class*="text-sm"]'); + expect(banner).not.toBeNull(); }); it("keeps a Banner.Action trailing in an sm banner", () => { diff --git a/packages/kumo/src/components/banner/banner.tsx b/packages/kumo/src/components/banner/banner.tsx index 4e956c7c4f..d0693b741f 100644 --- a/packages/kumo/src/components/banner/banner.tsx +++ b/packages/kumo/src/components/banner/banner.tsx @@ -64,24 +64,28 @@ export type KumoBannerSize = keyof typeof KUMO_BANNER_VARIANTS.size; /** * Per-size render-site classes not carried by `bannerVariants` (which only emits * the container classes). `row` is the title↔action flex gap, `icon` the icon - * wrapper height, `description` the description text size, and `action` the size - * that child `Banner.Action`s inherit via {@link BannerActionContext}. + * wrapper height, and `action` the size that child `Banner.Action`s inherit via + * {@link BannerActionContext}. + * + * The title and description both inherit the container's text size + * (`text-base` for `base`, `text-sm` for `sm`) — hierarchy is expressed via + * font weight (title `font-medium`, description regular), not size. This + * mirrors the `Text` scale's weight-first hierarchy: within a banner, the + * title is a `heading` role and the description is `body`. */ const BANNER_SIZE_PARTS: Record< KumoBannerSize, - { row: string; icon: string; description: string; action: BannerActionSize } + { row: string; icon: string; action: BannerActionSize } > = { base: { row: "gap-3", icon: "h-[1.375em]", - description: "text-sm", action: "sm", }, sm: { row: "gap-2", icon: "h-[1.25em]", - description: "text-sm", - action: "xs", + action: "sm", }, }; @@ -186,10 +190,12 @@ export interface BannerProps extends Omit< */ variant?: KumoBannerVariant; /** - * Size of the banner. A `"sm"` banner uses tighter spacing and `text-sm`, - * renders a Kumo `Link` action inline with the description, and sets its - * `Banner.Action` children to the `"xs"` size — suited to dialogs and other - * tight spaces. + * Size of the banner. A `"sm"` banner uses tighter spacing and `text-sm` + * (12px) and renders a Kumo `Link` action inline with the description — + * suited to dialogs and other tight spaces. In both sizes the title and + * description share the container text size (only the title's + * `font-medium` weight distinguishes them), and `Banner.Action` children + * render at the small (26px) Button size. * @default "base" */ size?: KumoBannerSize; @@ -284,7 +290,7 @@ const BannerRoot = forwardRef(function BannerRoot( )} {description && ( - + {description} {hasInlineLinkAction && ( @@ -298,7 +304,7 @@ const BannerRoot = forwardRef(function BannerRoot(
    {title &&

    {title}

    } {description && ( -
    +
    {isValidElement(description) ? ( description ) : ( diff --git a/packages/kumo/src/components/button/button.tsx b/packages/kumo/src/components/button/button.tsx index b5f59bd1dc..28f66e0dd5 100644 --- a/packages/kumo/src/components/button/button.tsx +++ b/packages/kumo/src/components/button/button.tsx @@ -25,26 +25,27 @@ export const KUMO_BUTTON_VARIANTS = { size: { xs: { classes: "h-5 gap-1 rounded-sm px-1.5 text-xs", - description: "Extra small button for compact UIs", + description: + '@deprecated Use `size="sm"` instead. The `xs` size will be removed in a future major version.', }, sm: { - classes: "h-6.5 gap-1 rounded-md px-2 text-xs", - description: "Small button for secondary actions", + classes: "h-6.5 gap-1 rounded-md px-2 text-sm leading-none", + description: "Small button (26px tall) for secondary actions", }, base: { - classes: "h-9 gap-1.5 rounded-lg px-3 text-base", - description: "Default button size", + classes: "h-8 gap-1.5 rounded-lg px-3 text-base", + description: "Default button size (32px tall)", }, lg: { - classes: "h-10 gap-2 rounded-lg px-4 text-base", - description: "Large button for primary CTAs", + classes: "h-9 gap-2 rounded-lg px-4 text-base", + description: "Large button (36px tall) for primary CTAs", }, }, compactSize: { xs: { classes: "size-3.5" }, sm: { classes: "size-6.5" }, - base: { classes: "size-9" }, - lg: { classes: "size-10" }, + base: { classes: "size-8" }, + lg: { classes: "size-9" }, }, variant: { primary: { @@ -102,10 +103,10 @@ export interface KumoButtonVariantsProps { shape?: KumoButtonShape; /** * Button size. - * - `"xs"` — Extra small for compact UIs - * - `"sm"` — Small for secondary actions - * - `"base"` — Default size - * - `"lg"` — Large for primary CTAs + * - `"sm"` — Small for secondary actions (26px tall) + * - `"base"` — Default size (32px tall) + * - `"lg"` — Large for primary CTAs (36px tall) + * - `"xs"` — **Deprecated.** Use `"sm"` instead. * @default "base" */ size?: KumoButtonSize; @@ -366,6 +367,14 @@ export const Button = React.forwardRef( ref, ) => { const { type, ...restProps } = props; + + if (process.env.NODE_ENV !== "production" && size === "xs") { + console.warn( + '[Kumo Button]: size="xs" is deprecated. Use size="sm" instead. ' + + "The xs size will be removed in a future major version.", + ); + } + const emphasisStyle = getEmphasisStyle(variant); const titleLabel = getTitleLabel(title); const buttonProps = { diff --git a/packages/kumo/src/components/collapsible/collapsible.tsx b/packages/kumo/src/components/collapsible/collapsible.tsx index 331f8d69cd..c62c9544e2 100644 --- a/packages/kumo/src/components/collapsible/collapsible.tsx +++ b/packages/kumo/src/components/collapsible/collapsible.tsx @@ -161,12 +161,12 @@ const CollapsibleDefaultTrigger = forwardRef< // Defensive resets to prevent global button styles from polluting the trigger "m-0 border-none bg-transparent p-0 shadow-none", // Base styles for the trigger - "flex cursor-pointer items-center gap-1 text-sm text-kumo-link select-none", + "flex cursor-pointer items-center gap-1 text-base text-kumo-link select-none", className, )} > {children}{" "} - + ); }); diff --git a/packages/kumo/src/components/combobox/combobox.tsx b/packages/kumo/src/components/combobox/combobox.tsx index 22414692cf..b148169124 100644 --- a/packages/kumo/src/components/combobox/combobox.tsx +++ b/packages/kumo/src/components/combobox/combobox.tsx @@ -54,10 +54,10 @@ export type KumoComboboxInputSide = export interface KumoComboboxVariantsProps { /** * Size of the combobox trigger. Matches Input component sizes. - * - `"xs"` — Extra small for compact UIs (h-5 / 20px) - * - `"sm"` — Small for secondary fields (h-6.5 / 26px) - * - `"base"` — Default size (h-9 / 36px) - * - `"lg"` — Large for prominent fields (h-10 / 40px) + * - `"sm"` — Small for secondary fields (26px tall) + * - `"base"` — Default size (32px tall) + * - `"lg"` — Large for prominent fields (36px tall) + * - `"xs"` — **Deprecated.** Use `"sm"` instead. * @default "base" */ size?: KumoComboboxSize; @@ -166,6 +166,13 @@ function Root({ error?: string | { message: ReactNode; match: FieldErrorMatch }; size?: KumoComboboxSize; }) { + if (process.env.NODE_ENV !== "production" && size === "xs") { + console.warn( + '[Kumo Combobox]: size="xs" is deprecated. Use size="sm" instead. ' + + "The xs size will be removed in a future major version.", + ); + } + const comboboxControl = ( {children} @@ -251,10 +258,10 @@ const triggerValueIconStyles: Record< KumoComboboxSize, { padding: string; iconSize: number; iconRight: string } > = { - xs: { padding: "pr-5", iconSize: 12, iconRight: "right-1" }, - sm: { padding: "pr-6", iconSize: 14, iconRight: "right-1.5" }, - base: { padding: "pr-8", iconSize: 16, iconRight: "right-2" }, - lg: { padding: "pr-10", iconSize: 18, iconRight: "right-3" }, + xs: { padding: "pr-5", iconSize: 10, iconRight: "right-1" }, + sm: { padding: "pr-6", iconSize: 12, iconRight: "right-1.5" }, + base: { padding: "pr-8", iconSize: 14, iconRight: "right-2" }, + lg: { padding: "pr-10", iconSize: 16, iconRight: "right-3" }, }; function TriggerValue({ @@ -290,32 +297,47 @@ function TriggerValue({ ); } -// Size-dependent styles for TriggerInput icons +// Size-dependent styles for TriggerInput icons. +// +// The clear X icon renders one step smaller than the caret at every size. +// Phosphor's `XIcon` has two crossed strokes that carry more optical weight +// than the single-stroke caret at matching numeric sizes, so shrinking the +// X keeps the two glyphs visually balanced next to each other. const triggerInputIconStyles: Record< KumoComboboxSize, - { padding: string; iconSize: number; clearRight: string; caretRight: string } + { + padding: string; + clearIconSize: number; + caretIconSize: number; + clearRight: string; + caretRight: string; + } > = { xs: { padding: "pr-7", - iconSize: 12, + clearIconSize: 8, + caretIconSize: 10, clearRight: "right-5", caretRight: "right-1", }, sm: { padding: "pr-9", - iconSize: 14, + clearIconSize: 10, + caretIconSize: 12, clearRight: "right-6", caretRight: "right-1.5", }, base: { padding: "pr-12", - iconSize: 16, + clearIconSize: 12, + caretIconSize: 14, clearRight: "right-8", caretRight: "right-2", }, lg: { padding: "pr-14", - iconSize: 18, + clearIconSize: 14, + caretIconSize: 16, clearRight: "right-9", caretRight: "right-3", }, @@ -366,7 +388,7 @@ function TriggerInput({ iconStyles.clearRight, )} > - + - +
    @@ -516,8 +541,8 @@ function Chip({ const sizeToMinHeight: Record = { xs: "min-h-5", sm: "min-h-6.5", - base: "min-h-9", - lg: "min-h-10", + base: "min-h-8", + lg: "min-h-9", }; function TriggerMultipleWithInput({ diff --git a/packages/kumo/src/components/input-group/input-group.tsx b/packages/kumo/src/components/input-group/input-group.tsx index 8471897c2d..a912b12c4c 100644 --- a/packages/kumo/src/components/input-group/input-group.tsx +++ b/packages/kumo/src/components/input-group/input-group.tsx @@ -29,20 +29,21 @@ export { type InputGroupSuffixProps } from "./input-group-suffix"; export const KUMO_INPUT_GROUP_VARIANTS = { size: { xs: { - classes: "h-6 text-xs", - description: "Extra small size.", + classes: "h-5 text-xs", + description: + "@deprecated Use `size=\"sm\"` instead. The `xs` size will be removed in a future major version.", }, sm: { - classes: "h-7 text-xs", - description: "Small size.", + classes: "h-6.5 text-sm", + description: "Small size (26px tall).", }, base: { - classes: "h-9 text-base", - description: "Default size.", + classes: "h-8 text-base", + description: "Default size (32px tall).", }, lg: { - classes: "h-11 text-base", - description: "Large size.", + classes: "h-9 text-base", + description: "Large size (36px tall).", }, }, } as const; @@ -103,6 +104,13 @@ const Root = forwardRef< const inputId = useId(); const focusMode = detectFocusMode(children); + if (process.env.NODE_ENV !== "production" && size === "xs") { + console.warn( + '[Kumo InputGroup]: size="xs" is deprecated. Use size="sm" instead. ' + + "The xs size will be removed in a future major version.", + ); + } + const contextValue = useMemo( () => ({ size, diff --git a/packages/kumo/src/components/input/input-area.tsx b/packages/kumo/src/components/input/input-area.tsx index e2273ebf2a..b2b473faaf 100644 --- a/packages/kumo/src/components/input/input-area.tsx +++ b/packages/kumo/src/components/input/input-area.tsx @@ -174,6 +174,13 @@ export const InputArea = React.forwardRef( ); } + if (process.env.NODE_ENV !== "production" && size === "xs") { + console.warn( + '[Kumo InputArea]: size="xs" is deprecated. Use size="sm" instead. ' + + "The xs size will be removed in a future major version.", + ); + } + // Auto-apply error styling when error prop is truthy // Explicit variant prop takes precedence for backwards compatibility const variant = variantProp ?? (error ? "error" : "default"); diff --git a/packages/kumo/src/components/input/input.test.tsx b/packages/kumo/src/components/input/input.test.tsx index 0662b6cd5d..a0c6e8a057 100644 --- a/packages/kumo/src/components/input/input.test.tsx +++ b/packages/kumo/src/components/input/input.test.tsx @@ -59,12 +59,15 @@ describe("Input", () => { // Size variants it("renders with default size 'base'", () => { render(); - expect(screen.getByRole("textbox").className).toContain("h-9"); + expect(screen.getByRole("textbox").className).toContain("h-8"); }); - it("renders with size 'xs'", () => { + it("renders with size 'xs' (deprecated)", () => { + // xs is deprecated but still functional; assertion covers the legacy path. + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); render(); expect(screen.getByRole("textbox").className).toContain("h-5"); + warn.mockRestore(); }); it("renders with size 'sm'", () => { @@ -74,7 +77,7 @@ describe("Input", () => { it("renders with size 'lg'", () => { render(); - expect(screen.getByRole("textbox").className).toContain("h-10"); + expect(screen.getByRole("textbox").className).toContain("h-9"); }); // Variant styles @@ -193,7 +196,7 @@ describe("Input", () => { it("applies size classes from KUMO_INPUT_VARIANTS", () => { const classes = inputVariants({ size: "lg" }); - expect(classes).toContain("h-10"); + expect(classes).toContain("h-9"); expect(classes).toContain("px-4"); }); diff --git a/packages/kumo/src/components/input/input.tsx b/packages/kumo/src/components/input/input.tsx index 9d4cf52acf..41fc819bcd 100644 --- a/packages/kumo/src/components/input/input.tsx +++ b/packages/kumo/src/components/input/input.tsx @@ -17,19 +17,20 @@ export const KUMO_INPUT_VARIANTS = { size: { xs: { classes: "h-5 gap-1 rounded-sm px-1.5 text-xs", - description: "Extra small input for compact UIs", + description: + "@deprecated Use `size=\"sm\"` instead. The `xs` size will be removed in a future major version.", }, sm: { - classes: "h-6.5 gap-1 rounded-md px-2 text-xs", - description: "Small input for secondary fields", + classes: "h-6.5 gap-1 rounded-md px-2 text-sm", + description: "Small input (26px tall) for secondary fields", }, base: { - classes: "h-9 gap-1.5 rounded-lg px-3 text-base", - description: "Default input size", + classes: "h-8 gap-1.5 rounded-lg px-3 text-base", + description: "Default input size (32px tall)", }, lg: { - classes: "h-10 gap-2 rounded-lg px-4 text-base", - description: "Large input for prominent fields", + classes: "h-9 gap-2 rounded-lg px-4 text-base", + description: "Large input (36px tall) for prominent fields", }, }, variant: { @@ -51,16 +52,16 @@ export const KUMO_INPUT_DEFAULT_VARIANTS = { export const KUMO_INPUT_STYLING = { dimensions: { - xs: { height: 20, paddingX: 6, fontSize: 12, borderRadius: 2, width: 160 }, - sm: { height: 26, paddingX: 8, fontSize: 12, borderRadius: 6, width: 200 }, + xs: { height: 20, paddingX: 6, fontSize: 11, borderRadius: 2, width: 160 }, + sm: { height: 26, paddingX: 8, fontSize: 13, borderRadius: 6, width: 200 }, base: { - height: 36, + height: 32, paddingX: 12, - fontSize: 16, + fontSize: 13, borderRadius: 8, width: 280, }, - lg: { height: 40, paddingX: 16, fontSize: 16, borderRadius: 8, width: 320 }, + lg: { height: 36, paddingX: 16, fontSize: 13, borderRadius: 8, width: 320 }, }, baseTokens: { background: "color-secondary", @@ -82,10 +83,10 @@ export type KumoInputVariant = keyof typeof KUMO_INPUT_VARIANTS.variant; export interface KumoInputVariantsProps { /** * Input size. - * - `"xs"` — Extra small for compact UIs - * - `"sm"` — Small for secondary fields - * - `"base"` — Default size - * - `"lg"` — Large for prominent fields + * - `"sm"` — Small for secondary fields (26px tall) + * - `"base"` — Default size (32px tall) + * - `"lg"` — Large for prominent fields (36px tall) + * - `"xs"` — **Deprecated.** Use `"sm"` instead. * @default "base" */ size?: KumoInputSize; @@ -160,6 +161,13 @@ export const Input = forwardRef((props, ref) => { ); } + if (process.env.NODE_ENV !== "production" && size === "xs") { + console.warn( + '[Kumo Input]: size="xs" is deprecated. Use size="sm" instead. ' + + "The xs size will be removed in a future major version.", + ); + } + // Auto-apply error styling when error prop is truthy // Explicit variant prop takes precedence for backwards compatibility const variant = variantProp ?? (error ? "error" : "default"); diff --git a/packages/kumo/src/components/label/label.tsx b/packages/kumo/src/components/label/label.tsx index 4142bc8b2c..1eb4fad808 100644 --- a/packages/kumo/src/components/label/label.tsx +++ b/packages/kumo/src/components/label/label.tsx @@ -104,7 +104,7 @@ export function Label({ render={ )} /> @@ -157,14 +157,9 @@ export function DeleteResource({ {resourceName} {copied ? ( - + ) : ( diff --git a/packages/kumo/src/components/autocomplete/autocomplete.tsx b/packages/kumo/src/components/autocomplete/autocomplete.tsx index 7da9679815..0607d6b58c 100644 --- a/packages/kumo/src/components/autocomplete/autocomplete.tsx +++ b/packages/kumo/src/components/autocomplete/autocomplete.tsx @@ -1,5 +1,4 @@ import { Autocomplete as AutocompleteBase } from "@base-ui/react/autocomplete"; -import { CheckIcon } from "@phosphor-icons/react"; import { createContext, useContext, type ReactNode } from "react"; import { inputVariants, KUMO_INPUT_VARIANTS } from "../input/input"; import { cn } from "../../utils/cn"; @@ -242,12 +241,9 @@ function Item({ children, ...props }: AutocompleteBase.Item.Props) { data-kumo-component="Autocomplete" data-kumo-part="item" {...props} - className="group mx-1.5 grid cursor-pointer grid-cols-[1fr_16px] gap-2 rounded px-2 py-1.5 text-base data-highlighted:bg-kumo-overlay data-selected:font-medium" + className="mx-1.5 cursor-pointer rounded px-2 py-1.5 text-base data-highlighted:bg-kumo-overlay" > -
    {children}
    - - - + {children} ); } diff --git a/packages/kumo/src/components/button/button.tsx b/packages/kumo/src/components/button/button.tsx index 28f66e0dd5..93e7fd2dba 100644 --- a/packages/kumo/src/components/button/button.tsx +++ b/packages/kumo/src/components/button/button.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { ArrowsClockwise, type Icon } from "@phosphor-icons/react"; +import { ArrowsClockwiseIcon, type Icon } from "@phosphor-icons/react"; import { Loader } from "../loader/loader"; import { Tooltip } from "../tooltip/tooltip"; import { cn } from "../../utils/cn"; @@ -385,7 +385,7 @@ export const Button = React.forwardRef( titleLabel && { "aria-label": titleLabel }), }; const iconNode = loading ? ( - + ) : ( renderIconNode(IconComponent) ); @@ -440,7 +440,7 @@ export const RefreshButton = ({ ...props }: RefreshButtonProps) => (
    @@ -531,7 +528,7 @@ function Chip({ "flex bg-transparent", )} > - + ); diff --git a/packages/kumo/src/components/command-palette/command-palette.tsx b/packages/kumo/src/components/command-palette/command-palette.tsx index 64d091115c..8ffcef200d 100644 --- a/packages/kumo/src/components/command-palette/command-palette.tsx +++ b/packages/kumo/src/components/command-palette/command-palette.tsx @@ -255,7 +255,7 @@ function InputHeader({
    {leading ?? ( )} @@ -514,7 +514,7 @@ function ResultItem({ className="text-base text-kumo-default" /> @@ -525,7 +525,7 @@ function ResultItem({ className="text-base text-kumo-default" /> {external && ( - + )} {description && ( <> @@ -538,7 +538,7 @@ function ResultItem({
    {showArrow && !external && !nonInteractive && ( - + )} ); diff --git a/packages/kumo/src/components/date-picker/date-picker.tsx b/packages/kumo/src/components/date-picker/date-picker.tsx index 52c6f5d0d3..9089692af9 100644 --- a/packages/kumo/src/components/date-picker/date-picker.tsx +++ b/packages/kumo/src/components/date-picker/date-picker.tsx @@ -17,7 +17,7 @@ import { cn } from "../../utils/cn"; */ const Chevron: CustomComponents["Chevron"] = ({ orientation, ...props }) => { const Icon = orientation === "left" ? CaretLeftIcon : CaretRightIcon; - return ; + return ; }; /** Base props shared across all DatePicker modes */ diff --git a/packages/kumo/src/components/date-range-picker/date-range-picker.tsx b/packages/kumo/src/components/date-range-picker/date-range-picker.tsx index 39dbf931a4..33b4c2f996 100644 --- a/packages/kumo/src/components/date-range-picker/date-range-picker.tsx +++ b/packages/kumo/src/components/date-range-picker/date-range-picker.tsx @@ -277,7 +277,7 @@ export function DateRangePicker({ className="absolute top-0 left-0 cursor-pointer rounded bg-kumo-interact/85 p-1.5 hover:bg-kumo-interact" onClick={() => adjustMonth(-1)} > - + adjustMonth(1)} > - + - + Timezone: {timezone} diff --git a/packages/kumo/src/components/pagination/pagination.tsx b/packages/kumo/src/components/pagination/pagination.tsx index bd93912d66..7c70b232d4 100644 --- a/packages/kumo/src/components/pagination/pagination.tsx +++ b/packages/kumo/src/components/pagination/pagination.tsx @@ -267,7 +267,7 @@ function PaginationControls({ setEditingPage(1); }} > - + )} - + {controls === "full" && (pageSelector === "dropdown" ? ( @@ -338,7 +338,7 @@ function PaginationControls({ setEditingPage(nextPage); }} > - + {controls === "full" && ( - + )} diff --git a/packages/kumo/src/components/select/select.tsx b/packages/kumo/src/components/select/select.tsx index b17e9e9eca..311ae6257e 100644 --- a/packages/kumo/src/components/select/select.tsx +++ b/packages/kumo/src/components/select/select.tsx @@ -85,14 +85,11 @@ export function selectVariants({ ); } -const triggerIconStyles: Record< - KumoInputSize, - { iconSize: number; className: string } -> = { - xs: { iconSize: 10, className: "text-kumo-subtle" }, - sm: { iconSize: 12, className: "text-kumo-subtle" }, - base: { iconSize: 14, className: "text-kumo-subtle" }, - lg: { iconSize: 16, className: "text-kumo-subtle" }, +const triggerIconStyles: Record = { + xs: { className: "text-kumo-subtle" }, + sm: { className: "text-kumo-subtle" }, + base: { className: "text-kumo-subtle" }, + lg: { className: "text-kumo-subtle" }, }; /** @@ -486,10 +483,7 @@ export function Select({ triggerIconStyles[size].className, )} > - + diff --git a/packages/kumo/src/components/sensitive-input/sensitive-input.tsx b/packages/kumo/src/components/sensitive-input/sensitive-input.tsx index dc68aee19b..5d5fbf4a59 100644 --- a/packages/kumo/src/components/sensitive-input/sensitive-input.tsx +++ b/packages/kumo/src/components/sensitive-input/sensitive-input.tsx @@ -421,7 +421,7 @@ export const SensitiveInput = forwardRef( size === "sm" && "right-2", size === "base" && "right-3", size === "lg" && "right-4", - iconSize, + "size-[1.15em]", !showEyeButton && "pointer-events-none opacity-0", )} > diff --git a/packages/kumo/src/components/sidebar/sidebar.tsx b/packages/kumo/src/components/sidebar/sidebar.tsx index d0ed8fc548..dfe870f766 100644 --- a/packages/kumo/src/components/sidebar/sidebar.tsx +++ b/packages/kumo/src/components/sidebar/sidebar.tsx @@ -1201,14 +1201,7 @@ const SidebarMenuButton = forwardRef( if (!IconProp) return null; if (React.isValidElement(IconProp)) return IconProp; const Comp = IconProp as React.ComponentType<{ className?: string }>; - return ( - - ); + return ; })(); const content = ( @@ -1355,7 +1348,7 @@ const SidebarMenuBadge = forwardRef< data-sidebar="menu-badge" className={cn( "inline-flex shrink-0 items-center rounded-full border border-dashed border-kumo-line", - "px-1.5 py-0.5 text-[11px]/none font-medium text-kumo-strong select-none", + "px-1.5 py-0.5 text-xs/none font-medium text-kumo-strong select-none", // Hidden when collapsed "group-data-[state=collapsed]/sidebar:hidden", className, @@ -2167,7 +2160,7 @@ function SidebarMenuChevron({ className }: { className?: string }) { return ( ) => { if (options.id) { const toasts = (manager as any).toasts as - Array> | undefined; + | Array> + | undefined; if (toasts) { const existingToast = toasts.find((toast) => toast.id === options.id); @@ -399,7 +400,7 @@ function ToastList() { "absolute top-2 right-2 size-5 rounded text-kumo-subtle hover:bg-current/15", toast.variant && TOAST_CLOSE_CLASSES[toast.variant], )} - icon={} + icon={} /> } /> @@ -443,7 +444,5 @@ function ToastIcon({ variant }: { variant?: KumoToastVariant }) { ); if (!("icon" in variantConfig)) return null; const Icon = variantConfig.icon; - return ( - - ); + return ; } From 5a8ba619ad65716fa4286097528b1c08878eb41a Mon Sep 17 00:00:00 2001 From: balint Date: Fri, 31 Jul 2026 14:56:58 +0100 Subject: [PATCH 3/4] fix: tighten sm/base/lg line-heights to preserve dense feel The refreshed typography scale used conventional 1.45-1.5 ratios which loosened text-sm and text-base line-heights by ~2-4px vs. the old scale's tight ratios (1/0.85 for sm, 1.5 with 14px base). Surfaces that swapped from text-sm to text-base (Sidebar, TableOfContents) were most affected. - sm ratio 1.45 -> 1.35 (12px * 1.35 = 16.2px) - base ratio 1.5 -> 1.4 (13px * 1.4 = 18.2px) - lg ratio 1.5 -> 1.45 (15px * 1.45 ~ 21.75px) xs / xl / 2xl unchanged. --- .changeset/line-height-ratios-tighten.md | 22 +++++++++++++++++++ .changeset/token-refresh-class-swaps.md | 7 +++++- .../kumo/scripts/theme-generator/config.ts | 6 ++--- packages/kumo/src/styles/theme-kumo.css | 6 ++--- 4 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 .changeset/line-height-ratios-tighten.md diff --git a/.changeset/line-height-ratios-tighten.md b/.changeset/line-height-ratios-tighten.md new file mode 100644 index 0000000000..f1b3d73a2b --- /dev/null +++ b/.changeset/line-height-ratios-tighten.md @@ -0,0 +1,22 @@ +--- +"@cloudflare/kumo": patch +--- + +Tighten line-height ratios on the refreshed typography scale so dense UI +surfaces (sidebar rows, table of contents, form controls) don't gain +excess leading versus the previous scale: + +- `--text-sm--line-height`: `1.45` → `1.35` (12px × 1.35 = 16.2px) +- `--text-base--line-height`: `1.5` → `1.4` (13px × 1.4 = 18.2px) +- `--text-lg--line-height`: `1.5` → `1.45` (15px × 1.45 ≈ 21.75px) + +Rationale: the old scale used tight ratios (`1/0.85 ≈ 1.176` for sm, +`1/0.75 ≈ 1.333` for xs) that produced roughly constant absolute leading +across the small end of the scale. The refreshed scale switched to +conventional per-size ratios, which loosened `text-sm` and `text-base` +by ~2–4px per line at the same rendered font-size. Components that +swapped from `text-sm` → `text-base` (Sidebar, TableOfContents) were +particularly affected. The new ratios split the difference — still +airier than the old tight scale, but closer to the previous dense feel. + +`xs`, `xl`, and `2xl` ratios are unchanged. diff --git a/.changeset/token-refresh-class-swaps.md b/.changeset/token-refresh-class-swaps.md index f8c1894e2b..a41647a58c 100644 --- a/.changeset/token-refresh-class-swaps.md +++ b/.changeset/token-refresh-class-swaps.md @@ -3,13 +3,18 @@ --- Align internal typography class tokens with the refreshed scale for `Badge`, -`Sidebar`, and `TableOfContents`. **Rendered pixel size is unchanged** in +`Sidebar`, and `TableOfContents`. **Rendered font-size is unchanged** in every case — old `text-sm` / `text-xs` and new `text-base` / `text-sm` resolve to the same pixel values on their respective scales. Only the class names change so component internals read consistently with the refreshed scale (body = `text-base`, caption = `text-sm`, escape hatch = `text-xs`). +Note: rendered *line-height* shifts slightly because the refreshed scale +uses different ratios (see the accompanying line-height tuning changeset). +Sidebar rows in particular gain ~1px of leading vs. the old scale even +though the font-size is identical. + **Affected surfaces:** - `Badge` (`KUMO_BADGE_BASE_STYLES`): `text-xs` → `text-sm` (12px both diff --git a/packages/kumo/scripts/theme-generator/config.ts b/packages/kumo/scripts/theme-generator/config.ts index 92e7cf6f55..517a81a013 100644 --- a/packages/kumo/scripts/theme-generator/config.ts +++ b/packages/kumo/scripts/theme-generator/config.ts @@ -627,7 +627,7 @@ export const THEME_CONFIG: ThemeConfig = { "sm--line-height": { newName: "", theme: { - kumo: "1.45", + kumo: "1.35", }, }, base: { @@ -640,7 +640,7 @@ export const THEME_CONFIG: ThemeConfig = { "base--line-height": { newName: "", theme: { - kumo: "1.5", + kumo: "1.4", }, }, lg: { @@ -653,7 +653,7 @@ export const THEME_CONFIG: ThemeConfig = { "lg--line-height": { newName: "", theme: { - kumo: "1.5", + kumo: "1.45", }, }, xl: { diff --git a/packages/kumo/src/styles/theme-kumo.css b/packages/kumo/src/styles/theme-kumo.css index 24a1779e69..758f634e33 100644 --- a/packages/kumo/src/styles/theme-kumo.css +++ b/packages/kumo/src/styles/theme-kumo.css @@ -282,11 +282,11 @@ --text-xs: calc(11px * var(--font-scale, 1)); --text-xs--line-height: 1.4; --text-sm: calc(12px * var(--font-scale, 1)); - --text-sm--line-height: 1.45; + --text-sm--line-height: 1.35; --text-base: calc(13px * var(--font-scale, 1)); - --text-base--line-height: 1.5; + --text-base--line-height: 1.4; --text-lg: calc(15px * var(--font-scale, 1)); - --text-lg--line-height: 1.5; + --text-lg--line-height: 1.45; --text-xl: calc(19px * var(--font-scale, 1)); --text-xl--line-height: 1.4; --text-2xl: calc(24px * var(--font-scale, 1)); From d75f185ba47a7778273c4cb5b331cfa4ab7776c2 Mon Sep 17 00:00:00 2001 From: balint Date: Mon, 3 Aug 2026 13:02:06 +0100 Subject: [PATCH 4/4] refactor(docs): replace DANGEROUS_className font-medium with Text variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use the new role-based Text API introduced in this branch instead of ad-hoc className overrides: - Body-context font-medium → `` - Heading-context (`as="h3"` + font-medium) → `` - Sized emphasis (`size="sm"` + font-medium) → `` Applies to GridDemo, design-tips (font-weight and layer-card-nesting good/bad examples), and the changelog section headers. Anti-pattern demonstrations (`font-bold`, `uppercase`) intentionally left as DANGEROUS_className since that's the point of those examples. --- .../src/components/demos/GridDemo.tsx | 20 ++++++------- .../src/components/skill/design-tips.tsx | 28 +++++++++---------- .../src/pages/changelog/[...page].astro | 2 +- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/kumo-docs-astro/src/components/demos/GridDemo.tsx b/packages/kumo-docs-astro/src/components/demos/GridDemo.tsx index f11a86b951..5ce625a878 100644 --- a/packages/kumo-docs-astro/src/components/demos/GridDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/GridDemo.tsx @@ -5,7 +5,7 @@ export function GridDemo() { - Item 1 + Item 1
    First grid item
    @@ -13,7 +13,7 @@ export function GridDemo() {
    - Item 2 + Item 2
    Second grid item
    @@ -100,7 +100,7 @@ export function GridAsymmetricDemo() { - Main Content + Main Content
    Two-thirds width
    @@ -108,7 +108,7 @@ export function GridAsymmetricDemo() {
    - Sidebar + Sidebar
    One-third width
    @@ -122,7 +122,7 @@ export function GridAsymmetricDemo() { - Sidebar + Sidebar
    One-third width
    @@ -130,7 +130,7 @@ export function GridAsymmetricDemo() {
    - Main Content + Main Content
    Two-thirds width
    @@ -219,7 +219,7 @@ export function GridMobileDividerDemo() { - Item 1 + Item 1
    Has divider on mobile
    @@ -227,7 +227,7 @@ export function GridMobileDividerDemo() {
    - Item 2 + Item 2
    Has divider on mobile
    @@ -235,7 +235,7 @@ export function GridMobileDividerDemo() {
    - Item 3 + Item 3
    Has divider on mobile
    @@ -243,7 +243,7 @@ export function GridMobileDividerDemo() {
    - Item 4 + Item 4
    Has divider on mobile
    diff --git a/packages/kumo-docs-astro/src/components/skill/design-tips.tsx b/packages/kumo-docs-astro/src/components/skill/design-tips.tsx index ddd3e417bf..8bce9b3dd3 100644 --- a/packages/kumo-docs-astro/src/components/skill/design-tips.tsx +++ b/packages/kumo-docs-astro/src/components/skill/design-tips.tsx @@ -133,7 +133,7 @@ export const designTips = [ { variant: "good", exampleCode: `Account settings -required`, +required`, jsx: (
    @@ -141,7 +141,7 @@ export const designTips = [ This action is{" "} - + required . @@ -235,7 +235,7 @@ export const designTips = [ exampleCode: `...`, jsx: ( - Production + Production ), }, @@ -244,7 +244,7 @@ export const designTips = [ exampleCode: `...`, jsx: ( - Production + Production ), }, @@ -438,7 +438,7 @@ export const designTips = [ jsx: (
    - API tokens + API tokens
    @@ -462,7 +462,7 @@ export const designTips = [ jsx: (
    - API tokens + API tokens
    @@ -517,19 +517,19 @@ export const designTips = [ jsx: (
    - + Recent Requests
    - + Time - + Status - + Query @@ -553,15 +553,15 @@ export const designTips = [ jsx: (
    - + Recent Requests
    - Time - Status - Query + Time + Status + Query 00:50 UTC diff --git a/packages/kumo-docs-astro/src/pages/changelog/[...page].astro b/packages/kumo-docs-astro/src/pages/changelog/[...page].astro index 6f386daf04..dac1ffad65 100644 --- a/packages/kumo-docs-astro/src/pages/changelog/[...page].astro +++ b/packages/kumo-docs-astro/src/pages/changelog/[...page].astro @@ -180,7 +180,7 @@ const GITHUB_RELEASE_URL = "https://github.com/cloudflare/kumo/releases/tag/%40c {v.sections.map((section: ParsedSection) => (
    - {sectionLabels[section.type]} + {sectionLabels[section.type]}
      {section.entries.map((entry: ParsedEntry) => (