feat(design-system): add persistent Normal and Compact density

Signed-off-by: Arjun Mahanti <arjun@squareup.com>
This commit is contained in:
Arjun Mahanti
2026-09-09 11:22:22 -07:00
parent 22605ed388
commit 8e18061562
19 changed files with 468 additions and 28 deletions

View File

@@ -62,7 +62,7 @@ Buzz is a place where people build together and bring their agents into the room
- **Never judge a value against a surface it will not be used on.** A swatch on a grey fill, or a type specimen in a tinted box, is being evaluated in a context the product will never reproduce. Samples sit on the page. The one exception is a value that needs a backdrop to exist at all — translucency needs something behind it, and a white surface swatch needs a hairline or it renders as nothing.
- **Cards are for widgets, galleries, and settings groups.** A card is a bordered, padded region on the page, not a different depth.
- **Pick the frame before the content.** Decide what the surface is — a list, a reading column, a workspace — before filling it.
- **Whitespace is generous by default.** Crowding reads as a different product.
- **Whitespace is generous in Normal density.** Compact is an explicit user preference for a denser workspace: reduce shared geometry and layout rhythm, keeping readable type and essential interaction targets independent. Components inherit density from the root; never branch their markup by density.
## Motion

View File

@@ -13,6 +13,16 @@ pnpm --filter buzz-desktop-next dev
Open `http://localhost:1430/design`. Use another port when needed:
`pnpm --filter buzz-desktop-next dev --port 5188`.
## Appearance
The navigation contains independent theme and Normal / Compact controls. Density
applies to every page, shared component, composition, and portaled overlay. It is
saved locally, restored before mounting, and synchronized across open tabs. An
unavailable storage write leaves the current visit usable and displays a retry
message. Normal preserves the original dimensions; Compact retains text sizes
while reducing spacing and control sizes. The Spacing and Radius pages show the
values for the selected density.
## Packages and portability
- `packages/design-tokens`: semantic registry, CSS roles, typography, geometry,

View File

@@ -43,3 +43,8 @@ Composer coverage exercises empty drafts, multiline and modifier handling, IME
composition, pointer/keyboard send and stop, retained failed drafts and retry,
attachment removal and limits, model controls, transcript preview, and 390/884/1440
layouts. Card header spacing is checked against the production component.
Density coverage checks pointer/keyboard switching, root and portal geometry,
unchanged text size, retained drafts, reload and cross-tab persistence, invalid
stored values, save failure and retry, live foundation values, compositions,
Glass tab selection, and all 55 examples at 390/884/1440 in both themes.

View File

@@ -2,7 +2,7 @@
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
column-gap: var(--space-page-inset);
row-gap: 3rem;
row-gap: var(--space-catalog-gap);
}
.catalog-entry {
display: flex;
@@ -13,7 +13,7 @@
}
.catalog-preview {
padding-block: var(--space-panel-inset);
min-height: 10rem;
min-height: var(--size-catalog-preview);
display: flex;
align-items: center;
}

View File

@@ -0,0 +1,43 @@
import { Button } from "@buzz/ui";
import { useColorScheme } from "@/shared/theme/useColorScheme";
import { useDensity } from "@/shared/theme/DensityProvider";
/** Independent appearance preferences for every design-system page. */
export function AppearanceControls() {
const { scheme, toggle } = useColorScheme();
const { density, setDensity, saveError } = useDensity();
return (
<div className="mx-3 flex flex-col items-start gap-3">
<Button
size="sm"
variant="secondary"
onClick={toggle}
aria-label={`Switch to ${scheme === "light" ? "dark" : "light"} mode`}
>
{scheme === "light" ? "Dark mode" : "Light mode"}
</Button>
<fieldset className="min-w-0">
<legend className="mb-2 text-meta text-tertiary">Density</legend>
<div className="flex flex-wrap gap-1">
{(["normal", "compact"] as const).map((value) => (
<Button
key={value}
size="sm"
variant={density === value ? "primary" : "ghost"}
aria-pressed={density === value}
onClick={() => setDensity(value)}
>
{value === "normal" ? "Normal" : "Compact"}
</Button>
))}
</div>
</fieldset>
{saveError && (
<p role="status" className="text-caption text-secondary">
{saveError}
</p>
)}
</div>
);
}

View File

@@ -2,7 +2,7 @@ import "../catalog/catalog.css";
import { Link, Outlet } from "@tanstack/react-router";
import { Fragment, type ReactNode } from "react";
import { useColorScheme } from "@/shared/theme/useColorScheme";
import { AppearanceControls } from "./AppearanceControls";
/** A nav entry, optionally with children shown indented beneath it. */
type NavItem = [label: string, to: string, children?: Array<[string, string]>];
@@ -66,8 +66,6 @@ function NavLink({
}
export function DesignSystemLayout() {
const { scheme, toggle } = useColorScheme();
return (
/* Narrow: the nav stacks above the content as a wrapped list, because a
256px column beside a reading column leaves neither enough room. From lg
@@ -75,7 +73,7 @@ export function DesignSystemLayout() {
<div className="flex min-h-screen flex-col bg-panel lg:flex-row">
<nav
aria-label="Design system"
className="flex shrink-0 flex-col gap-8 px-4 py-8 lg:sticky lg:top-0 lg:h-screen lg:w-64 lg:overflow-y-auto"
className="flex shrink-0 flex-col gap-8 px-4 py-8 lg:sticky lg:top-0 lg:h-screen lg:w-[16rem] lg:overflow-y-auto"
>
<div className="px-3">
<Link to="/design" className="text-label text-primary">
@@ -122,14 +120,7 @@ export function DesignSystemLayout() {
))}
</div>
<button
type="button"
onClick={toggle}
aria-label={`Switch to ${scheme === "light" ? "dark" : "light"} mode`}
className="mx-3 self-start rounded-lg bg-inset px-3 py-2 text-label text-secondary transition-colors hover:bg-hover hover:text-primary"
>
{scheme === "light" ? "Dark mode" : "Light mode"}
</button>
<AppearanceControls />
</nav>
<main className="min-w-0 flex-1 px-6 py-10 sm:px-10 lg:px-16">

View File

@@ -1,11 +1,16 @@
import type { CSSProperties } from "react";
import { FOUNDATION_GROUPS } from "@buzz/design-tokens/foundations";
import {
COMPACT_FOUNDATIONS,
FOUNDATION_GROUPS,
} from "@buzz/design-tokens/foundations";
import { useDensity } from "@/shared/theme/DensityProvider";
/** Render geometry from the actual public custom properties, alongside registry values. */
export function FoundationRoles({
name,
}: {
name: (typeof FOUNDATION_GROUPS)[number]["name"];
}) {
const { density } = useDensity();
const group = FOUNDATION_GROUPS.find((group) => group.name === name);
if (!group) throw new Error("Missing foundation group");
return (
@@ -33,7 +38,11 @@ export function FoundationRoles({
style={style}
/>
)}
<span className="text-body text-secondary">{value}</span>
<span className="text-body text-secondary">
{density === "compact"
? (COMPACT_FOUNDATIONS[token] ?? value)
: value}
</span>
</dd>
<dd className="text-caption text-secondary">{description}</dd>
</div>

View File

@@ -7,6 +7,12 @@ import { RouterProvider, createRouter } from "@tanstack/react-router";
import "./shared/styles/globals.css";
import { routeTree } from "./app/routeTree.gen";
import {
DensityProvider,
initializeDensity,
} from "./shared/theme/DensityProvider";
const initialDensity = initializeDensity();
const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
@@ -20,6 +26,8 @@ if (!root) throw new Error("Missing #root element");
createRoot(root).render(
<StrictMode>
<RouterProvider router={router} />
<DensityProvider initialDensity={initialDensity}>
<RouterProvider router={router} />
</DensityProvider>
</StrictMode>,
);

View File

@@ -0,0 +1,84 @@
import {
createContext,
type ReactNode,
useContext,
useEffect,
useLayoutEffect,
useState,
} from "react";
/** Layout density is independent of color scheme and readable text size. */
export type Density = "normal" | "compact";
const STORAGE_KEY = "buzz-next-density";
const normalize = (value: string | null): Density =>
value === "compact" ? "compact" : "normal";
/** Apply the saved density before mounting, including portal geometry. */
export function initializeDensity(): Density {
let density: Density = "normal";
try {
density = normalize(localStorage.getItem(STORAGE_KEY));
} catch {
// Storage is optional. A blocked read cannot supply a saved preference.
}
document.documentElement.dataset.density = density;
return density;
}
const DensityContext = createContext<{
density: Density;
setDensity: (density: Density) => void;
saveError: string | null;
} | null>(null);
/** One root preference survives route changes and synchronizes browser tabs. */
export function DensityProvider({
initialDensity,
children,
}: {
initialDensity: Density;
children: ReactNode;
}) {
const [density, updateDensity] = useState(initialDensity);
const [saveError, setSaveError] = useState<string | null>(null);
useLayoutEffect(() => {
document.documentElement.dataset.density = density;
}, [density]);
useEffect(() => {
const onStorage = (event: StorageEvent) => {
if (event.storageArea !== localStorage) return;
if (event.key !== STORAGE_KEY && event.key !== null) return;
updateDensity(normalize(event.newValue));
setSaveError(null);
};
window.addEventListener("storage", onStorage);
return () => window.removeEventListener("storage", onStorage);
}, []);
const setDensity = (next: Density) => {
updateDensity(next);
try {
localStorage.setItem(STORAGE_KEY, next);
setSaveError(null);
} catch {
setSaveError(
"Density changed for this visit. Your browser couldn’t save it. Select it again to retry.",
);
}
};
return (
<DensityContext.Provider value={{ density, setDensity, saveError }}>
{children}
</DensityContext.Provider>
);
}
/** Read or change the application's shared density preference. */
export function useDensity() {
const value = useContext(DensityContext);
if (!value) throw new Error("useDensity requires DensityProvider");
return value;
}

View File

@@ -0,0 +1,170 @@
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";
import { waitForAnimations } from "../../desktop/tests/helpers/animations";
test("density changes geometry throughout without changing type or losing a draft", async ({
page,
context,
}) => {
await page.goto("/design/components");
const normal = page.getByRole("button", { name: "Normal", exact: true });
const compact = page.getByRole("button", { name: "Compact", exact: true });
const button = page.getByRole("button", { name: "Continue", exact: true });
const input = page.getByRole("textbox", {
name: "Project name",
exact: true,
});
const draft = page.getByRole("textbox", { name: "Prompt the assistant" });
await expect(normal).toHaveAttribute("aria-pressed", "true");
await expect(button).toHaveCSS("height", "40px");
await expect(input).toHaveCSS("font-size", "14px");
await draft.fill("Keep my draft while changing density");
const anotherTab = await context.newPage();
await anotherTab.goto("/design/glass");
await compact.focus();
await compact.press("Space");
await expect(compact).toHaveAttribute("aria-pressed", "true");
await expect(page.locator("html")).toHaveAttribute("data-density", "compact");
await expect(anotherTab.locator("html")).toHaveAttribute(
"data-density",
"compact",
);
await expect(button).toHaveCSS("height", "32px");
await expect(input).toHaveCSS("height", "32px");
await expect(input).toHaveCSS("font-size", "14px");
await expect(draft).toHaveValue("Keep my draft while changing density");
await expect(page.locator("#card .bui-card")).toHaveCSS("padding", "16px");
await expect(page.locator("#table td").first()).toHaveCSS("padding", "8px");
await expect(page.locator(".catalog-grid")).toHaveCSS("row-gap", "32px");
await expect(page.locator("main header").first()).toHaveCSS(
"margin-bottom",
"42px",
);
await expect(draft).toHaveCSS("min-height", "64px");
// Portals are outside the catalog but inherit the same root preference.
await page.getByRole("button", { name: "Edit project", exact: true }).click();
await expect(page.getByRole("dialog")).toHaveCSS("padding", "16px");
await expect(
page.getByRole("button", { name: "Done", exact: true }),
).toHaveCSS("height", "32px");
await page.keyboard.press("Escape");
await page.getByRole("combobox", { name: "Digest frequency" }).click();
await expect(page.getByRole("option").first()).toHaveCSS(
"padding",
"4px 8px",
);
await page.keyboard.press("Escape");
await page.getByRole("button", { name: "Switch to dark mode" }).click();
await expect(page.locator("html")).toHaveClass(/dark/);
await expect(button).toHaveCSS("height", "32px");
await page.reload();
await expect(compact).toHaveAttribute("aria-pressed", "true");
await expect(page.locator("html")).toHaveClass(/dark/);
await page
.getByRole("navigation", { name: "Design system", exact: true })
.getByRole("link", { name: "Spacing", exact: true })
.click();
const token = page
.locator("dl > div")
.filter({ has: page.getByText("control-md", { exact: true }) });
await expect(token).toContainText("2rem");
await page
.getByRole("navigation", { name: "Design system", exact: true })
.getByRole("link", { name: "Compositions", exact: true })
.click();
await expect(page.locator(".bui-card").first()).toHaveCSS("padding", "16px");
await normal.click();
await expect(page.locator(".bui-card").first()).toHaveCSS("padding", "24px");
await expect(anotherTab.locator("html")).toHaveAttribute(
"data-density",
"normal",
);
await expect(page.locator("html")).toHaveClass(/dark/);
await anotherTab.close();
});
test("compact works at narrow and wide sizes in both themes, including Glass tabs", async ({
page,
}, testInfo) => {
await page.addInitScript(() =>
localStorage.setItem("buzz-next-density", "compact"),
);
await page.goto("/design/components");
for (const width of [390, 884, 1440]) {
await page.setViewportSize({ width, height: 960 });
for (const scheme of ["light", "dark"] as const) {
const changeTheme = page.getByRole("button", {
name: `Switch to ${scheme} mode`,
});
if (await changeTheme.count()) await changeTheme.click();
await expect(page.locator(".catalog-entry")).toHaveCount(55);
expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= innerWidth,
),
).toBe(true);
const composer = page.locator("#ai-composer .bui-composer");
expect(
await composer.evaluate((el) => el.scrollWidth <= el.clientWidth),
).toBe(true);
await composer.scrollIntoViewIfNeeded();
await waitForAnimations(page);
await composer.screenshot({
path: testInfo.outputPath(`compact-composer-${width}-${scheme}.png`),
});
}
}
const axe = await new AxeBuilder({ page })
.disableRules(["color-contrast"])
.analyze();
expect(axe.violations).toEqual([]);
await page.goto("/design/glass");
const tabs = page.getByRole("tablist");
await tabs.getByRole("tab", { name: "Me", exact: true }).click();
await page.keyboard.press("ArrowRight");
await page.keyboard.press("Enter");
await expect(
tabs.getByRole("tab", { name: "Messages", exact: true }),
).toHaveAttribute("aria-selected", "true");
await expect(tabs.getByRole("tab").first()).toHaveCSS("padding-left", "12px");
await page.getByRole("button", { name: "Normal", exact: true }).click();
await expect(tabs.getByRole("tab").first()).toHaveCSS("padding-left", "20px");
await expect(
tabs.getByRole("tab", { name: "Messages", exact: true }),
).toHaveAttribute("aria-selected", "true");
});
test("invalid saved density falls back and a failed save stays usable with retry", async ({
page,
}) => {
await page.addInitScript(() => {
localStorage.setItem("buzz-next-density", "invalid");
const original = Storage.prototype.setItem;
let shouldFail = true;
Storage.prototype.setItem = function (key, value) {
if (key === "buzz-next-density" && shouldFail) {
shouldFail = false;
throw new DOMException("Storage unavailable", "QuotaExceededError");
}
return original.call(this, key, value);
};
});
await page.goto("/design/components");
await expect(
page.getByRole("button", { name: "Normal", exact: true }),
).toHaveAttribute("aria-pressed", "true");
const compact = page.getByRole("button", { name: "Compact", exact: true });
await compact.click();
await expect(page.getByText(/Your browser couldn’t save it/)).toBeVisible();
await expect(
page.getByRole("button", { name: "Continue", exact: true }),
).toHaveCSS("height", "32px");
await compact.click();
await expect(page.getByText(/Your browser couldn’t save it/)).toHaveCount(0);
expect(
await page.evaluate(() => localStorage.getItem("buzz-next-density")),
).toBe("compact");
});

View File

@@ -5,3 +5,24 @@ Portable, locally owned design foundations. Color and typography CSS hold the au
Import the CSS exports with Tailwind v4. The UI package will also provide compiled CSS for applications without Tailwind. Switch color scheme with a dark class on the root. Fonts are supplied by the consuming application; the reference uses Inter Variable and JetBrains Mono from public packages.
Run the repository-pinned Node against build.mjs to regenerate foundation CSS; --check detects drift.
## Density
Normal is the default. Set `data-density="compact"` on the root `<html>` element
for Compact; remove it or set `data-density="normal"` to restore Normal. The root
scope includes portaled menus, dialogs, and toasts. This works with both the
Tailwind source exports and the UI package's compiled CSS.
`FOUNDATION_GROUPS` holds Normal values; `COMPACT_FOUNDATIONS` holds the proposed
Compact overrides in the same source file. The generator emits both. Controls,
padding, grouped spacing, multiline fields, chips, and surface radii use these
roles. Tailwind's `--spacing` layout rhythm changes from 0.25rem to 0.1875rem so
utility-based layouts participate as well. That includes numeric width/height
utilities: use an explicit semantic dimension when a region must keep its size
(the catalog's navigation rail does). Content-driven and reading widths remain
independent of density.
Typography, icons, color, motion, scrollbars, checkable control marks, and resize
hit areas keep their values. Root font-size still scales both profiles with zoom;
`--type-scale` remains the separate text preference. Consumer applications own
persistence; no browser storage is accessed by the token or component packages.

View File

@@ -1,11 +1,16 @@
import { readFileSync, writeFileSync } from "node:fs";
import { FOUNDATION_GROUPS } from "./src/foundations.ts";
import { COMPACT_FOUNDATIONS, FOUNDATION_GROUPS } from "./src/foundations.ts";
const declarations = FOUNDATION_GROUPS.flatMap((g) =>
g.roles.map(([name, value]) => " --" + name + ": " + value + ";"),
).join("\n");
const compact = Object.entries(COMPACT_FOUNDATIONS)
.map(([name, value]) => ` --${name}: ${value};`)
.join("\n");
const css =
"/* Generated by packages/design-tokens/build.mjs. */\n@theme {\n" +
declarations +
'\n}\n:root[data-density="compact"] {\n' +
compact +
"\n}\n@media (prefers-reduced-motion: reduce) {\n :root {\n --duration-state: 0ms;\n --duration-enter: 0ms;\n --duration-exit: 0ms;\n --press-scale: 1;\n }\n}\n";
const path = new URL("./src/foundations.css", import.meta.url);
if (process.argv.includes("--check")) {

View File

@@ -3,18 +3,26 @@
--control-sm: 2rem;
--control-md: 2.5rem;
--control-lg: 3.25rem;
--control-calendar: 2.25rem;
--control-textarea-min: 7rem;
--control-multiline-min: 5rem;
--control-multiline-max: 16rem;
--size-scrollbar: 0.375rem;
--size-grip-length: 1rem;
--size-grip-thickness: 0.25rem;
--size-resize-target: 1.5rem;
--space-action-inset: 1.25rem;
--space-tooltip-block: 0.125rem;
--layer-dialog: 80;
--layer-floating: 100;
--layer-toast: 120;
--overlay-scrim: rgb(0 0 0 / 0.5);
--spacing: 0.25rem;
--space-control-gap: 0.5rem;
--space-chip-block: 0.375rem;
--space-chip-inline: 0.625rem;
--space-catalog-gap: 3rem;
--size-catalog-preview: 10rem;
--space-control-inset: 0.75rem;
--space-heading-gap: 1rem;
--space-section-gap: 1.5rem;
@@ -33,6 +41,32 @@
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
--press-scale: 0.97;
}
:root[data-density="compact"] {
--control-sm: 1.75rem;
--control-md: 2rem;
--control-lg: 2.5rem;
--control-calendar: 1.75rem;
--control-textarea-min: 5rem;
--control-multiline-min: 4rem;
--space-action-inset: 0.75rem;
--spacing: 0.1875rem;
--space-control-gap: 0.25rem;
--space-control-inset: 0.5rem;
--space-heading-gap: 0.5rem;
--space-section-gap: 1rem;
--space-panel-inset: 1rem;
--space-page-inset: 1.5rem;
--space-chip-block: 0.25rem;
--space-chip-inline: 0.5rem;
--space-catalog-gap: 2rem;
--size-catalog-preview: 7rem;
--radius-control: 0.375rem;
--radius-notice: 0.5rem;
--radius-menu: 0.75rem;
--radius-container: 1rem;
--radius-overlay: 1.5rem;
--radius-sheet: 2rem;
}
@media (prefers-reduced-motion: reduce) {
:root {
--duration-state: 0ms;

View File

@@ -7,6 +7,12 @@ export const FOUNDATION_GROUPS = [
["control-sm", "2rem", "Small actions."],
["control-md", "2.5rem", "Standard actions and inputs."],
["control-lg", "3.25rem", "Prominent actions."],
["control-calendar", "2.25rem", "Calendar day and navigation targets."],
[
"control-textarea-min",
"7rem",
"Minimum height for longer text fields.",
],
[
"control-multiline-min",
"5rem",
@@ -28,6 +34,11 @@ export const FOUNDATION_GROUPS = [
"Visible resize grip length, independent of its hit area.",
],
["size-grip-thickness", "0.25rem", "Visible resize grip thickness."],
[
"size-resize-target",
"1.5rem",
"Resize hit area preserved at either density.",
],
["space-action-inset", "1.25rem", "Standard button horizontal padding."],
["space-tooltip-block", "0.125rem", "Compact tooltip vertical padding."],
["layer-dialog", "80", "Modal surfaces."],
@@ -39,9 +50,18 @@ export const FOUNDATION_GROUPS = [
{
name: "Spacing",
description:
"A four-pixel rhythm, expressed in rem so the entire interface follows zoom.",
"A four-pixel layout rhythm in Normal and a three-pixel rhythm in Compact, with named component insets. Rem units keep both responsive to zoom.",
roles: [
[
"spacing",
"0.25rem",
"Base layout rhythm for Tailwind spacing utilities.",
],
["space-control-gap", "0.5rem", "Space between a label and its icon."],
["space-chip-block", "0.375rem", "Chip and badge vertical inset."],
["space-chip-inline", "0.625rem", "Chip and badge horizontal inset."],
["space-catalog-gap", "3rem", "Space between catalog rows."],
["size-catalog-preview", "10rem", "Minimum catalog preview height."],
["space-control-inset", "0.75rem", "Compact control padding."],
[
"space-heading-gap",
@@ -84,3 +104,34 @@ export const FOUNDATION_GROUPS = [
],
},
] as const;
/** Proposed compact density. Unlisted roles retain Normal values. Text, icons,
* motion, and fine interaction geometry are deliberately independent of density. */
export const COMPACT_FOUNDATIONS: Partial<
Record<(typeof FOUNDATION_GROUPS)[number]["roles"][number][0], string>
> = {
"control-sm": "1.75rem",
"control-md": "2rem",
"control-lg": "2.5rem",
"control-calendar": "1.75rem",
"control-textarea-min": "5rem",
"control-multiline-min": "4rem",
"space-action-inset": "0.75rem",
spacing: "0.1875rem",
"space-control-gap": "0.25rem",
"space-control-inset": "0.5rem",
"space-heading-gap": "0.5rem",
"space-section-gap": "1rem",
"space-panel-inset": "1rem",
"space-page-inset": "1.5rem",
"space-chip-block": "0.25rem",
"space-chip-inline": "0.5rem",
"space-catalog-gap": "2rem",
"size-catalog-preview": "7rem",
"radius-control": "0.375rem",
"radius-notice": "0.5rem",
"radius-menu": "0.75rem",
"radius-container": "1rem",
"radius-overlay": "1.5rem",
"radius-sheet": "2rem",
};

View File

@@ -132,3 +132,12 @@ Invalid input renders an error and an optional host-provided retry callback.
The host owns streaming assembly, action authorization, persistence, retries, and
network errors. Pass a new immutable complete snapshot when it changes. The renderer
is a presentation boundary, not an agent execution engine or an authorization system.
## Normal and Compact
All components inherit density from the root `<html data-density="compact">`.
Use `data-density="normal"` (or omit it) for original sizing. Density changes
shared geometry without remounting controls or replacing their behavior. Both
profiles work in light and dark mode and follow root zoom. Text sizing is
independent. Keep the density attribute on the document root so portals inherit
it too. See the design-token package for the full profile and layout rhythm.

View File

@@ -79,8 +79,8 @@
.bui-calendar button {
cursor: pointer;
border-radius: var(--radius-pill);
min-width: 2.25rem;
min-height: 2.25rem;
min-width: var(--control-calendar);
min-height: var(--control-calendar);
display: inline-flex;
justify-content: center;
align-items: center;
@@ -125,7 +125,7 @@
display: flex;
align-items: center;
justify-content: center;
width: var(--space-section-gap);
width: var(--size-resize-target);
background: transparent;
}
.bui-resize-handle::after {
@@ -137,7 +137,7 @@
}
.bui-resize-handle[aria-orientation="horizontal"] {
width: auto;
height: var(--space-section-gap);
height: var(--size-resize-target);
}
.bui-resize-handle[aria-orientation="horizontal"]::after {
width: var(--size-grip-length);
@@ -160,7 +160,7 @@
}
.bui-chart {
width: 100%;
border-spacing: 0 0.5rem;
border-spacing: 0 var(--space-control-gap);
border-collapse: separate;
}
.bui-chart th {

View File

@@ -25,7 +25,7 @@
cursor: not-allowed;
}
.bui-textarea {
min-height: 7rem;
min-height: var(--control-textarea-min);
resize: vertical;
}
.bui-input-group {

View File

@@ -30,7 +30,7 @@
height: 1rem;
}
.bui-otp-input {
width: 2.5rem;
width: var(--control-md);
text-align: center;
padding: var(--space-control-gap);
}

View File

@@ -23,7 +23,7 @@
display: inline-flex;
align-items: center;
gap: var(--space-control-gap);
padding: 0.375rem 0.625rem;
padding: var(--space-chip-block) var(--space-chip-inline);
border-radius: var(--radius-pill);
background: var(--bg-inset);
color: var(--text-secondary);