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/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/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..a41647a58c --- /dev/null +++ b/.changeset/token-refresh-class-swaps.md @@ -0,0 +1,35 @@ +--- +"@cloudflare/kumo": minor +--- + +Align internal typography class tokens with the refreshed scale for `Badge`, +`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 + 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} )} /> @@ -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 c93facbe66..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"; @@ -25,10 +24,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 +153,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 ( -
{children}
- - - + {children} ); } diff --git a/packages/kumo/src/components/badge/badge.tsx b/packages/kumo/src/components/badge/badge.tsx index 92aeaa7863..19203b38f6 100644 --- a/packages/kumo/src/components/badge/badge.tsx +++ b/packages/kumo/src/components/badge/badge.tsx @@ -4,7 +4,7 @@ import { resolveVariant } from "../../utils/resolve-variant"; /** Base styles applied to all badge variants. */ export const KUMO_BADGE_BASE_STYLES = - "inline-flex w-fit flex-none shrink-0 items-center justify-self-start rounded-full px-2 py-0.5 text-xs font-medium whitespace-nowrap"; + "inline-flex w-fit flex-none shrink-0 items-center justify-self-start rounded-full px-2 py-0.5 text-sm font-medium whitespace-nowrap"; /** Badge variant definitions mapping variant names to their Tailwind classes and descriptions. */ export const KUMO_BADGE_VARIANTS = { diff --git a/packages/kumo/src/components/banner/banner-action.tsx b/packages/kumo/src/components/banner/banner-action.tsx index 4df98f62ca..759e590cb6 100644 --- a/packages/kumo/src/components/banner/banner-action.tsx +++ b/packages/kumo/src/components/banner/banner-action.tsx @@ -26,10 +26,10 @@ export type BannerActionVariant = Extract< /** * Size of a `Banner.Action`, matching the equivalent `Button` size specs. - * - `"xs"` — extra small for dense/compact banners. - * - `"sm"` — small (default), the standard banner CTA size. + * - `"sm"` — small (26px), the standard banner CTA size used by both + * `size="base"` and `size="sm"` banners. */ -export type BannerActionSize = Extract; +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..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"; @@ -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 = { @@ -376,7 +385,7 @@ export const Button = React.forwardRef( titleLabel && { "aria-label": titleLabel }), }; const iconNode = loading ? ( - + ) : ( renderIconNode(IconComponent) ); @@ -431,7 +440,7 @@ export const RefreshButton = ({ ...props }: RefreshButtonProps) => (
@@ -506,7 +528,7 @@ function Chip({ "flex bg-transparent", )} > - + ); @@ -516,8 +538,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/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.test.tsx b/packages/kumo/src/components/select/select.test.tsx index 0926e899a1..cddf41a243 100644 --- a/packages/kumo/src/components/select/select.test.tsx +++ b/packages/kumo/src/components/select/select.test.tsx @@ -5,7 +5,9 @@ import { Select } from "./select"; describe("Select", () => { describe("size", () => { - it("applies size classes to the trigger", () => { + it("applies size classes to the trigger (deprecated xs)", () => { + // xs is deprecated but still functional; assertion covers the legacy path. + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); const { container } = render( + Option A + , + ); + + const trigger = container.querySelector('[role="combobox"]'); + expect(trigger?.className).toContain("h-8"); + expect(trigger?.className).toContain("text-base"); }); }); diff --git a/packages/kumo/src/components/select/select.tsx b/packages/kumo/src/components/select/select.tsx index df2c6864f2..311ae6257e 100644 --- a/packages/kumo/src/components/select/select.tsx +++ b/packages/kumo/src/components/select/select.tsx @@ -28,13 +28,13 @@ export const KUMO_SELECT_DEFAULT_VARIANTS = { */ export const KUMO_SELECT_STYLING = { trigger: { - height: 36, // h-9 + height: 32, // h-8 paddingX: 12, // px-3 borderRadius: 8, // rounded-lg background: "bg-kumo-elevated", text: "text-color-surface", ring: "color-border", - fontSize: 16, // text-base + fontSize: 13, // text-base fontWeight: 400, // font-normal }, stateTokens: { @@ -42,8 +42,8 @@ export const KUMO_SELECT_STYLING = { disabled: { opacity: 0.5 }, }, icons: { - caret: { name: "ph-caret-up-down", size: 20 }, - check: { name: "ph-check", size: 20 }, + caret: { name: "ph-caret-up-down", size: 14 }, + check: { name: "ph-check", size: 14 }, }, popup: { background: "bg-kumo-elevated", @@ -55,7 +55,7 @@ export const KUMO_SELECT_STYLING = { paddingX: 8, // px-2 paddingY: 6, // py-1.5 borderRadius: 4, // rounded - fontSize: 16, // text-base + fontSize: 13, // text-base highlightBackground: "color-surface-secondary", }, } as const; @@ -66,10 +66,10 @@ export type KumoSelectSize = keyof typeof KUMO_SELECT_VARIANTS.size; export interface KumoSelectVariantsProps { /** * Size of the select 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?: KumoSelectSize; @@ -85,14 +85,11 @@ export function selectVariants({ ); } -const triggerIconStyles: Record< - KumoInputSize, - { iconSize: number; className: string } -> = { - xs: { iconSize: 12, className: "text-kumo-subtle" }, - sm: { iconSize: 14, className: "text-kumo-subtle" }, - base: { iconSize: 16, className: "text-kumo-subtle" }, - lg: { iconSize: 18, 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" }, }; /** @@ -379,6 +376,13 @@ export function Select({ ); } + if (process.env.NODE_ENV !== "production" && size === "xs") { + console.warn( + '[Kumo Select]: size="xs" is deprecated. Use size="sm" instead. ' + + "The xs size will be removed in a future major version.", + ); + } + // New behavior: label presence determines Field wrapper visibility (like Input) // hideLabel is only respected for backward compatibility when explicitly set to true const useFieldWrapper = label && hideLabel !== true; @@ -479,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 a33f489b93..dfe870f766 100644 --- a/packages/kumo/src/components/sidebar/sidebar.tsx +++ b/packages/kumo/src/components/sidebar/sidebar.tsx @@ -1014,7 +1014,7 @@ const SidebarGroupLabel = forwardRef<
( if (!IconProp) return null; if (React.isValidElement(IconProp)) return IconProp; const Comp = IconProp as React.ComponentType<{ className?: string }>; - return ( - - ); + return ; })(); const content = ( @@ -1241,8 +1234,8 @@ const SidebarMenuButton = forwardRef( "group/menu-button relative flex w-full min-w-0 cursor-pointer items-center gap-2.5 rounded-lg outline-none", "before:absolute before:inset-x-0 before:-inset-y-px", // Sizing - size === "base" && "min-h-8.5 px-3 py-0 text-sm font-medium", - size === "sm" && "min-h-7 px-2 py-0 text-sm", + size === "base" && "min-h-8.5 px-3 py-0 text-base font-medium", + size === "sm" && "min-h-7 px-2 py-0 text-base", "text-kumo-default", "transition-[color,box-shadow,outline] duration-(--sidebar-animation-duration)", !active && "hover:bg-(--sidebar-active-bg)", @@ -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, @@ -1467,7 +1460,7 @@ const SidebarMenuSubButton = forwardRef< const isInsideMenuSubItem = useContext(MenuSubItemContext); const buttonClasses = cn( - "group/menu-button relative flex min-h-8.5 w-full min-w-0 cursor-pointer items-center gap-2 rounded-lg px-3 py-0 text-sm font-medium outline-none", + "group/menu-button relative flex min-h-8.5 w-full min-w-0 cursor-pointer items-center gap-2 rounded-lg px-3 py-0 text-base font-medium outline-none", "before:absolute before:inset-x-0 before:-inset-y-px", "text-kumo-default transition-[color] duration-150", !active && "hover:bg-(--sidebar-active-bg)", @@ -2167,7 +2160,7 @@ function SidebarMenuChevron({ className }: { className?: string }) { return ( ; @@ -186,7 +186,7 @@ const TableOfContentsGroup = forwardRef< className={cn("-ml-0.5 flex flex-col gap-2", className)} {...props} > -

+

{label}

    {children}
diff --git a/packages/kumo/src/components/table/table.tsx b/packages/kumo/src/components/table/table.tsx index 57019b04d1..26d5ca7c35 100644 --- a/packages/kumo/src/components/table/table.tsx +++ b/packages/kumo/src/components/table/table.tsx @@ -135,7 +135,7 @@ const TableRoot = forwardRef< ).classes, "[&_td]:border-b [&_td]:border-kumo-fill [&_tr:last-child_td]:border-b-0", // Row border "[&_td]:p-3", // Cell padding - "[&_th]:border-b [&_th]:border-kumo-fill [&_th]:p-3 [&_th]:text-base [&_th]:font-semibold", // Header styles + "[&_th]:border-b [&_th]:border-kumo-fill [&_th]:p-3 [&_th]:text-base [&_th]:font-medium", // Header styles (heading role: 13px medium) "[&_th]:bg-kumo-base", // Header background color "text-left text-base text-kumo-default", props.className, @@ -159,7 +159,7 @@ const TableHeader = forwardRef< const isCompact = variant === "compact"; const className = cn( "group/header", - isCompact && "text-xs text-kumo-strong [&_th]:bg-kumo-elevated [&_th]:py-2", + isCompact && "text-sm text-kumo-strong [&_th]:bg-kumo-elevated [&_th]:py-2", sticky && "[&_th]:sticky [&_th]:top-0 [&_th]:z-1", props.className, ); diff --git a/packages/kumo/src/components/text/text.test.tsx b/packages/kumo/src/components/text/text.test.tsx index e3035b5055..0b6ba4e0d5 100644 --- a/packages/kumo/src/components/text/text.test.tsx +++ b/packages/kumo/src/components/text/text.test.tsx @@ -1,17 +1,49 @@ -import { describe, expect, it } from "vite-plus/test"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; import { render } from "@testing-library/react"; import { Text } from "./text"; describe("Text", () => { - it("renders heading variant with the required `as` element", () => { + it("renders display variant with the required `as` element", () => { const { container } = render( - - Page Title + + Welcome , ); expect(container.querySelector("h1")).toBeTruthy(); }); + it("renders page-title variant", () => { + const { container } = render( + + Account settings + , + ); + expect(container.querySelector("h1")).toBeTruthy(); + expect(container.querySelector("h1")?.className).toContain("text-xl"); + }); + + it("renders section-title variant", () => { + const { container } = render( + + General + , + ); + expect(container.querySelector("h2")).toBeTruthy(); + expect(container.querySelector("h2")?.className).toContain("text-lg"); + }); + + it("renders heading variant (the small inline one)", () => { + const { container } = render( + + API tokens + , + ); + const el = container.querySelector("h3"); + expect(el).toBeTruthy(); + expect(el?.className).toContain("text-base"); + expect(el?.className).toContain("font-medium"); + }); + it("renders body variant as

by default", () => { const { container } = render(Body copy); expect(container.querySelector("p")).toBeTruthy(); @@ -29,7 +61,7 @@ describe("Text", () => { it("allows heading variants to opt out of semantic heading via as='span'", () => { const { container } = render( - + Decorative big text , ); @@ -71,6 +103,82 @@ describe("Text", () => { expect(container.querySelector("pre")).toBeTruthy(); }); + describe("deprecated heading aliases", () => { + let warnSpy: ReturnType; + + beforeEach(() => { + warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + }); + + afterEach(() => { + warnSpy.mockRestore(); + }); + + it("heading1 still renders (same classes as display) and warns", () => { + const { container } = render( + + Legacy + , + ); + const el = container.querySelector("h1"); + expect(el).toBeTruthy(); + expect(el?.className).toContain("text-2xl"); + expect(el?.className).toContain("font-semibold"); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('variant="heading1" is deprecated'), + ); + }); + + it("heading2 warns and maps to page-title classes", () => { + const { container } = render( + + Legacy + , + ); + const el = container.querySelector("h2"); + expect(el?.className).toContain("text-xl"); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("page-title"), + ); + }); + + it("heading3 warns and maps to section-title classes", () => { + const { container } = render( + + Legacy + , + ); + const el = container.querySelector("h3"); + expect(el?.className).toContain("text-lg"); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("section-title"), + ); + }); + }); + + describe("bold prop", () => { + it("applies font-medium on default body variant", () => { + const { container } = render(Bumped); + expect(container.querySelector("p")?.className).toContain("font-medium"); + }); + + it("applies font-medium on secondary variant", () => { + const { container } = render( + + Bumped + , + ); + expect(container.querySelector("p")?.className).toContain("font-medium"); + }); + + it("does not apply font-medium when bold is falsy", () => { + const { container } = render(Plain); + expect(container.querySelector("p")?.className).not.toContain( + "font-medium", + ); + }); + }); + // Type-level enforcement of the required `as` prop for heading variants // lives in `text.type-spec.tsx`. That file is included in the regular // tsconfig glob, so `pnpm typecheck` evaluates every `@ts-expect-error` diff --git a/packages/kumo/src/components/text/text.tsx b/packages/kumo/src/components/text/text.tsx index db0cdc1d23..58ac18dd4f 100644 --- a/packages/kumo/src/components/text/text.tsx +++ b/packages/kumo/src/components/text/text.tsx @@ -9,20 +9,52 @@ import { import { cn } from "../../utils/cn"; import { resolveVariant } from "../../utils/resolve-variant"; -/** Text variant and size definitions mapping names to their Tailwind classes. */ +/** + * Text variant and size definitions mapping names to their Tailwind classes. + * + * Heading variants are role-based: + * display 24px semibold — hero / prominent moments + * page-title 19px medium — the single title of a page or dialog + * section-title 15px medium — card / panel / section headings + * heading 13px medium — inline / row / list-item headings + * + * `heading1`, `heading2`, `heading3` are retained as deprecated aliases of + * `display`, `page-title`, `section-title` respectively. + * + * Sizes: sm=12px, base=13px (default). `xs` and `lg` are deprecated. + */ export const KUMO_TEXT_VARIANTS = { variant: { + display: { + classes: "text-2xl font-semibold", + description: "Display heading — hero / prominent moments (24px semibold)", + }, + "page-title": { + classes: "text-xl font-medium", + description: "Page or dialog title (19px medium)", + }, + "section-title": { + classes: "text-lg font-medium", + description: "Card / panel / section heading (15px medium)", + }, + heading: { + classes: "text-base font-medium", + description: + "Inline / row / list-item heading (13px medium). The small, most-used heading.", + }, + // Deprecated aliases — kept for backwards compatibility. Emit a runtime + // warning in dev when used. Same classes as their replacements. heading1: { - classes: "text-3xl font-semibold", - description: "Large heading for page titles", + classes: "text-2xl font-semibold", + description: "@deprecated Use variant=\"display\" instead.", }, heading2: { - classes: "text-2xl font-semibold", - description: "Medium heading for section titles", + classes: "text-xl font-medium", + description: "@deprecated Use variant=\"page-title\" instead.", }, heading3: { - classes: "text-lg font-semibold", - description: "Small heading for subsections", + classes: "text-lg font-medium", + description: "@deprecated Use variant=\"section-title\" instead.", }, body: { classes: "text-kumo-default", @@ -50,21 +82,26 @@ export const KUMO_TEXT_VARIANTS = { }, }, size: { - xs: { - classes: "text-xs", - description: "Extra small text", - }, sm: { classes: "text-sm", - description: "Small text", + description: "Small (12px) — caption / helper text", }, base: { classes: "text-base", - description: "Default text size", + description: "Default body size (13px)", + }, + // Deprecated sizes — kept for backwards compatibility. Emit a runtime + // warning in dev when used. Same classes as before to avoid layout + // shifts on existing usages. + xs: { + classes: "text-xs", + description: + '@deprecated Use `size="sm"` instead. The `xs` size will be removed in a future major version.', }, lg: { classes: "text-lg", - description: "Large text", + description: + '@deprecated Use `size="base"` instead. The `lg` size will be removed in a future major version.', }, }, } as const; @@ -75,24 +112,22 @@ export const KUMO_TEXT_DEFAULT_VARIANTS = { } as const; /** - * KUMO_TEXT_STYLING - Typography metadata for Figma generator + * KUMO_TEXT_STYLING - Typography metadata for Figma generator. * - * This export provides structured styling information extracted from text.tsx - * for use by the Figma plugin generator. It documents font sizes, weights, - * colors, and font families used across all Text variants. + * Documents the actual pixel sizes and weights emitted by each token so the + * Figma plugin can generate matching styles. * * Source of truth chain: * text.tsx (this file) → component-registry.json → text.ts (Figma generator) */ export const KUMO_TEXT_STYLING = { fontSizes: { - xs: 12, - sm: 14, - base: 16, - lg: 18, - xl: 20, + xs: 11, + sm: 12, + base: 13, + lg: 15, + xl: 17, "2xl": 24, - "3xl": 30, }, fontWeights: { normal: 400, @@ -141,13 +176,32 @@ export function textVariants({ ); } -// Legacy types for backwards compatibility -type Heading = "heading1" | "heading2" | "heading3"; +// Heading variants (role-based) + deprecated numeric aliases. +type HeadingRole = "display" | "page-title" | "section-title" | "heading"; +type HeadingDeprecated = "heading1" | "heading2" | "heading3"; +type Heading = HeadingRole | HeadingDeprecated; type Copy = "body" | "secondary" | "success" | "error"; type Monospace = "mono" | "mono-secondary"; type TextSize = KumoTextSize; type TextVariant = KumoTextVariant; +/** Map deprecated heading variant names to their replacements. */ +const DEPRECATED_HEADING_MAP: Record = { + heading1: "display", + heading2: "page-title", + heading3: "section-title", +}; + +const HEADING_VARIANTS = new Set([ + "display", + "page-title", + "section-title", + "heading", + "heading1", + "heading2", + "heading3", +]); + /** Valid HTML elements for the Text component's `as` prop. */ export type TextElement = | "h1" @@ -184,8 +238,15 @@ type TextPropsInternal = BaseTextProps & (Variant extends Copy ? { variant?: Variant; - bold?: boolean; size?: TextSize; + /** + * Bumps the weight of body copy to `font-medium` (500). Only + * applies to copy variants (`body`, `secondary`, `success`, + * `error`); heading variants already carry their role's weight. + * For structural hierarchy inside a document outline reach for + * `variant="heading"` instead; `bold` is for inline emphasis. + */ + bold?: boolean; truncate?: boolean; /** Optional element override. Defaults to `

`. */ as?: TextElement; @@ -193,8 +254,9 @@ type TextPropsInternal = BaseTextProps & : Variant extends Monospace ? { variant?: Variant; - bold?: never; + /** @deprecated `size="lg"` is deprecated. Monospace text always renders at 12px. */ size?: "lg"; + bold?: never; truncate?: boolean; /** Optional element override. Defaults to ``. */ as?: TextElement; @@ -202,18 +264,14 @@ type TextPropsInternal = BaseTextProps & : Variant extends Heading ? { variant: Variant; - bold?: never; size?: never; + bold?: never; truncate?: boolean; /** * Required for heading variants. Pick the element that reflects * this text's place in the document outline (`"h1"` for a page * title, `"h2"` for a section title, etc.) or `"span"` for * decorative heading-styled text that is NOT a section heading. - * - * Previously optional (defaulted to ``), which silently - * excluded real section headings from the document outline. - * Making it required surfaces the decision at the type level. */ as: TextElement; } @@ -224,7 +282,10 @@ type TextPropsInternal = BaseTextProps & * * @example * ```tsx - * Page Title + * Welcome + * Account settings + * General + * API tokens * Default paragraph text. * Muted helper text * Something went wrong @@ -233,29 +294,47 @@ type TextPropsInternal = BaseTextProps & */ export interface TextProps { /** - * Text style variant. Determines color, font, and weight. - * - `"heading1"` — Large page title (30px, semibold) - * - `"heading2"` — Section title (24px, semibold) - * - `"heading3"` — Subsection title (18px, semibold) - * - `"body"` — Default body text + * Text style variant. + * + * Heading variants (role-based, weight-first hierarchy): + * - `"display"` — Hero / prominent moments (24px semibold) + * - `"page-title"` — The single title of a page or dialog (19px medium) + * - `"section-title"` — Card / panel / section heading (15px medium) + * - `"heading"` — Inline / row / list-item heading (13px medium) + * + * Body variants: + * - `"body"` — Default body text (13px) * - `"secondary"` — Muted text for secondary information * - `"success"` — Success state text * - `"error"` — Error state text * - `"mono"` — Monospace text for code * - `"mono-secondary"` — Muted monospace text + * + * Deprecated (use the role-based names above): + * - `"heading1"` → use `"display"` + * - `"heading2"` → use `"page-title"` + * - `"heading3"` → use `"section-title"` + * * @default "body" */ variant?: KumoTextVariant; /** * Text size (only applies to body/secondary/success/error variants). - * - `"xs"` — 12px - * - `"sm"` — 14px - * - `"base"` — 16px - * - `"lg"` — 18px + * - `"sm"` — 12px (caption / helper text) + * - `"base"` — 13px (default body) + * - `"xs"` — **Deprecated.** Use `"sm"` instead. + * - `"lg"` — **Deprecated.** Use `"base"` instead. * @default "base" */ size?: KumoTextSize; - /** Whether to use bold font weight (only applies to body variants). */ + /** + * Bumps body copy weight to `font-medium` (500). Only applies to copy + * variants (`body`, `secondary`, `success`, `error`); heading variants + * already carry their role's weight and disallow this prop at the type + * level. For structural hierarchy inside a document outline reach for + * `variant="heading"` instead — `bold` is for inline emphasis. + * @default false + */ bold?: boolean; /** Whether to truncate overflowing text with an ellipsis. Adds `truncate min-w-0` classes. */ truncate?: boolean; @@ -265,10 +344,10 @@ export interface TextProps { * `"small"`, `"abbr"`, `"time"`), form-related (`"label"`, `"legend"`), * list/definition (`"dt"`, `"dd"`, `"li"`), and `"figcaption"`. * - * - **Required** for heading variants (`"heading1"`, `"heading2"`, - * `"heading3"`) — pick the element that reflects this text's place in - * the document outline, or `"span"` for decorative heading-styled text - * that is not a section heading. + * - **Required** for heading variants (`"display"`, `"page-title"`, + * `"section-title"`, `"heading"`) — pick the element that reflects this + * text's place in the document outline, or `"span"` for decorative + * heading-styled text that is not a section heading. * - **Optional** for body variants (defaults to `"p"`) and monospace * variants (defaults to `"span"`). */ @@ -284,16 +363,18 @@ export interface TextProps { * * @example * ```tsx - * Page Title - * Section Title + * Welcome + * Account settings + * General + * API tokens * Default body text * ``` */ function _Text( { variant = "body" as Variant, - bold = false, size = "base", + bold, truncate = false, children, DANGEROUS_className, @@ -306,16 +387,42 @@ function _Text( const isCopy = ["body", "secondary", "success", "error"].includes(variant); const isMono = ["mono", "mono-secondary"].includes(variant); + // Deprecation warning for legacy heading variant names. + if ( + process.env.NODE_ENV !== "production" && + (variant === "heading1" || variant === "heading2" || variant === "heading3") + ) { + const replacement = DEPRECATED_HEADING_MAP[variant]; + console.warn( + `[Kumo Text]: variant="${variant}" is deprecated. Use variant="${replacement}" instead.`, + ); + } + + // Deprecation warnings for legacy sizes on Copy variants. + // Mono variants intentionally still accept `size="lg"` as a step-up hint + // but render at the same 12px — no warning there. + if (process.env.NODE_ENV !== "production" && isCopy) { + if (size === "xs") { + console.warn( + '[Kumo Text]: size="xs" is deprecated. Use size="sm" instead. The xs size will be removed in a future major version.', + ); + } else if (size === "lg") { + console.warn( + '[Kumo Text]: size="lg" is deprecated. Use size="base" instead. The lg size will be removed in a future major version.', + ); + } + } + // Heading variants no longer auto-select h1/h2/h3 to avoid coupling visual // presentation to semantic HTML. Use the `as` prop to set the appropriate // heading level for your document outline (e.g., as="h2"). const Component = useMemo(() => { if (as) return as; - if (["mono", "mono-secondary"].includes(variant)) return "span"; - // Headings and body text default to span; use `as` for semantic elements - if (["heading1", "heading2", "heading3"].includes(variant)) return "span"; + if (isMono) return "span"; + // Headings default to span; use `as` for semantic elements + if (HEADING_VARIANTS.has(variant)) return "span"; return "p"; - }, [variant, as]); + }, [variant, as, isMono]); return ( ( KUMO_TEXT_DEFAULT_VARIANTS.size, ).classes : "", - isCopy && bold ? "font-medium" : "", - // Monospace fonts need to be 1pt smaller than body text to optically match - isMono && - (size === "lg" - ? KUMO_TEXT_VARIANTS.size.base.classes - : KUMO_TEXT_VARIANTS.size.sm.classes), + // Monospace fonts render one size step down from body text so they + // optically match — always text-sm (12px). + isMono && KUMO_TEXT_VARIANTS.size.sm.classes, + // `bold` on copy variants bumps body copy from the default 400 to + // font-medium (500). Type-narrowed to `never` on headings and mono, + // so this only ever fires on copy. + isCopy && bold && "font-medium", truncate && "min-w-0 truncate", DANGEROUS_className, )} diff --git a/packages/kumo/src/components/text/text.type-spec.tsx b/packages/kumo/src/components/text/text.type-spec.tsx index 3adeb31440..59502c95e6 100644 --- a/packages/kumo/src/components/text/text.type-spec.tsx +++ b/packages/kumo/src/components/text/text.type-spec.tsx @@ -18,7 +18,29 @@ import { Text } from "./text"; // Positive cases — these MUST compile cleanly. // --------------------------------------------------------------------------- -// Heading variant with required `as`. +// Role-based heading variants with required `as`. +const _display = ( + + Welcome + +); +const _pageTitle = ( + + Account settings + +); +const _sectionTitle = ( + + General + +); +const _heading = ( + + API tokens + +); + +// Deprecated numeric aliases still compile (soft deprecation). const _headingH1 = ( Page Title @@ -37,7 +59,7 @@ const _headingH3 = ( // Heading variant using `as="span"` for decorative (non-section) usage. const _decorativeHeading = ( - + Big bold label ); @@ -90,7 +112,7 @@ const _small = ( ); const _time = 2026-04-27; const _headingAsLabel = ( - + Form heading ); @@ -101,21 +123,80 @@ const _headingAsLabel = ( // tsc itself fails the typecheck with "Unused '@ts-expect-error' directive". // --------------------------------------------------------------------------- -// Missing `as` on heading1 → type error. +// Missing `as` on new role-based heading variants → type error. // @ts-expect-error — heading variants require `as` -const _missingAsH1 = Missing as; +const _missingAsDisplay = Missing as; +// @ts-expect-error — heading variants require `as` +const _missingAsPageTitle = Missing as; +// @ts-expect-error — heading variants require `as` +const _missingAsSectionTitle = Missing as; +// @ts-expect-error — heading variants require `as` +const _missingAsHeading = Missing as; -// Missing `as` on heading2 → type error. +// Missing `as` on deprecated aliases still errors. +// @ts-expect-error — heading variants require `as` +const _missingAsH1 = Missing as; // @ts-expect-error — heading variants require `as` const _missingAsH2 = Missing as; - -// Missing `as` on heading3 → type error. // @ts-expect-error — heading variants require `as` const _missingAsH3 = Missing as; +// `bold` prop is allowed on copy variants (body, secondary, success, error) +// where it bumps weight to font-medium. Reject on headings (already carry +// their role's weight) and mono (design decision — mono stays regular). +const _boldBody = Bold body; +const _boldSecondary = ( + + Bold secondary + +); +const _boldSuccess = ( + + Bold success + +); +const _boldError = ( + + Bold error + +); +const _boldHeading = ( + + Bold heading + +); +const _boldSectionTitle = ( + + Bold section title + +); +const _boldMono = ( + + Bold mono + +); + // Silence unused-variable warnings for all the sentinels above. // This file is never executed; it exists purely for type checking. export const __typeSpec = { + _display, + _pageTitle, + _sectionTitle, + _heading, _headingH1, _headingH2, _headingH3, @@ -141,7 +222,18 @@ export const __typeSpec = { _small, _time, _headingAsLabel, + _missingAsDisplay, + _missingAsPageTitle, + _missingAsSectionTitle, + _missingAsHeading, _missingAsH1, _missingAsH2, _missingAsH3, + _boldBody, + _boldSecondary, + _boldSuccess, + _boldError, + _boldHeading, + _boldSectionTitle, + _boldMono, }; diff --git a/packages/kumo/src/components/toast/toast.tsx b/packages/kumo/src/components/toast/toast.tsx index 29e1b2f5ad..921fd01998 100644 --- a/packages/kumo/src/components/toast/toast.tsx +++ b/packages/kumo/src/components/toast/toast.tsx @@ -30,12 +30,13 @@ export const KUMO_TOAST_VARIANTS = { description: "Toast container with background, border, and shadow", }, title: { - classes: "text-[0.975rem] leading-5 font-medium text-kumo-default", - description: "Toast title with primary text color", + classes: "text-lg leading-5 font-medium text-kumo-default", + description: + "Toast title — role: section-title (15px medium). Pairs with body-lg description.", }, description: { - classes: "text-[0.925rem] leading-5 text-kumo-subtle", - description: "Toast description with muted text color", + classes: "text-lg leading-5 text-kumo-subtle", + description: "Toast description — body-lg (15px regular), muted color.", }, close: { classes: @@ -93,7 +94,7 @@ export const KUMO_TOAST_STYLING = { gap: 4, }, title: { - fontSize: 16, + fontSize: 15, fontWeight: 500, color: "text-color-surface", }, @@ -213,7 +214,8 @@ function wrapManagerMethods< add: (options: KumoToastManagerAddOptions) => { 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); @@ -370,9 +372,9 @@ function ToastList() {

- + {!!toast.actions && (
@@ -398,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={} /> } /> @@ -442,7 +444,5 @@ function ToastIcon({ variant }: { variant?: KumoToastVariant }) { ); if (!("icon" in variantConfig)) return null; const Icon = variantConfig.icon; - return ( - - ); + return ; } diff --git a/packages/kumo/src/components/toolbar/toolbar.test.tsx b/packages/kumo/src/components/toolbar/toolbar.test.tsx index 1b7e756ece..2db317c728 100644 --- a/packages/kumo/src/components/toolbar/toolbar.test.tsx +++ b/packages/kumo/src/components/toolbar/toolbar.test.tsx @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vite-plus/test"; +import { describe, expect, it, vi } from "vite-plus/test"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { Input } from "../input/input"; @@ -19,7 +19,7 @@ describe("Toolbar", () => { expect(toolbarInput.className).toContain("h-6.5"); expect(toolbarInput.className).toContain("rounded-none"); - expect(directInput.className).toContain("h-10"); + expect(directInput.className).toContain("h-9"); expect(directInput.className).not.toContain("rounded-none"); }); @@ -94,4 +94,46 @@ describe("Toolbar", () => { await user.keyboard("{ArrowRight}"); expect(document.activeElement).toBe(visit); }); + + describe("deprecated xs size", () => { + it("emits a single deprecation warning at the Toolbar level", () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + render( + + + , + ); + + const toolbarWarnings = warn.mock.calls.filter((args) => + String(args[0]).includes('[Kumo Toolbar]: size="xs" is deprecated'), + ); + expect(toolbarWarnings).toHaveLength(1); + warn.mockRestore(); + }); + + it("does not emit child Button/Input xs deprecation warnings (remapped to sm)", () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + render( + + + A + , + ); + + const childWarnings = warn.mock.calls.filter((args) => { + const msg = String(args[0]); + return ( + msg.includes('[Kumo Button]: size="xs" is deprecated') || + msg.includes('[Kumo Input]: size="xs" is deprecated') + ); + }); + expect(childWarnings).toHaveLength(0); + + // Child Input should render at sm height (26px), not xs (20px). + const input = screen.getByRole("textbox", { name: "Toolbar input" }); + expect(input.className).toContain("h-6.5"); + expect(input.className).not.toContain("h-5"); + warn.mockRestore(); + }); + }); }); diff --git a/packages/kumo/src/components/toolbar/toolbar.tsx b/packages/kumo/src/components/toolbar/toolbar.tsx index e5b4dec84a..ef73d0c59b 100644 --- a/packages/kumo/src/components/toolbar/toolbar.tsx +++ b/packages/kumo/src/components/toolbar/toolbar.tsx @@ -15,10 +15,11 @@ export const KUMO_TOOLBAR_VARIANTS = { size: { xs: { classes: "text-xs", - description: "Extra small toolbar for compact UIs", + description: + '@deprecated Use `size="sm"` instead. Toolbar `xs` warns once at the Toolbar level and remaps to `sm` internally, so child Button/Input never see the deprecated size.', }, sm: { - classes: "text-xs", + classes: "text-sm", description: "Small toolbar for secondary controls", }, base: { @@ -41,7 +42,14 @@ export type ToolbarSize = keyof typeof KUMO_TOOLBAR_VARIANTS.size; export interface ToolbarProps extends Omit { /** Toolbar controls rendered as one grouped card. */ children: React.ReactNode; - /** Locks every toolbar item to this size. */ + /** + * Locks every toolbar item to this size. + * - `"sm"` — Small toolbar for secondary controls + * - `"base"` — Default toolbar size + * - `"lg"` — Large toolbar for prominent controls + * - `"xs"` — **Deprecated.** Use `"sm"` instead. + * @default "base" + */ size?: ToolbarSize; } @@ -101,6 +109,18 @@ const Root = React.forwardRef( }, ref, ) => { + if (process.env.NODE_ENV !== "production" && size === "xs") { + console.warn( + '[Kumo Toolbar]: size="xs" is deprecated. Use size="sm" instead. ' + + "The xs size will be removed in a future major version.", + ); + } + + // Remap deprecated `xs` to `sm` internally so child Button/Input/InputGroup + // don't emit their own duplicate deprecation warnings. Toolbar renders the + // single, coherent warning above; children receive the migrated value. + const forwardedSize = size === "xs" ? "sm" : size; + return ( ( )} {...props} > - + {children} diff --git a/packages/kumo/src/primitives/index.ts b/packages/kumo/src/primitives/index.ts index 85d5eaa7c3..a20d526488 100644 --- a/packages/kumo/src/primitives/index.ts +++ b/packages/kumo/src/primitives/index.ts @@ -37,10 +37,7 @@ export * from "@base-ui/react/meter"; export * from "@base-ui/react/navigation-menu"; export * from "@base-ui/react/number-field"; export * from "@base-ui/react/otp-field"; -export { - OTPField, - OTPField as OTPFieldPreview, -} from "@base-ui/react/otp-field"; +export { OTPField, OTPField as OTPFieldPreview } from "@base-ui/react/otp-field"; export * from "@base-ui/react/popover"; export * from "@base-ui/react/preview-card"; export * from "@base-ui/react/progress"; diff --git a/packages/kumo/src/primitives/otp-field.ts b/packages/kumo/src/primitives/otp-field.ts index 547b5218a7..c7f9cf2dfa 100644 --- a/packages/kumo/src/primitives/otp-field.ts +++ b/packages/kumo/src/primitives/otp-field.ts @@ -11,7 +11,4 @@ */ export * from "@base-ui/react/otp-field"; -export { - OTPField, - OTPField as OTPFieldPreview, -} from "@base-ui/react/otp-field"; +export { OTPField, OTPField as OTPFieldPreview } from "@base-ui/react/otp-field"; diff --git a/packages/kumo/src/styles/theme-kumo.css b/packages/kumo/src/styles/theme-kumo.css index 356e3daf6b..758f634e33 100644 --- a/packages/kumo/src/styles/theme-kumo.css +++ b/packages/kumo/src/styles/theme-kumo.css @@ -279,14 +279,18 @@ @theme { /* Typography - text sizes and line heights */ - --text-xs: 12px; - --text-xs--line-height: calc(1 / 0.75); - --text-sm: 13px; - --text-sm--line-height: calc(1 / 0.85); - --text-base: 14px; - --text-base--line-height: 1.5; - --text-lg: 16px; - --text-lg--line-height: 1.5; + --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.35; + --text-base: calc(13px * var(--font-scale, 1)); + --text-base--line-height: 1.4; + --text-lg: calc(15px * var(--font-scale, 1)); + --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)); + --text-2xl--line-height: 1.3; } @layer base {