diff --git a/packages/propel/src/components/toolbar/index.tsx b/packages/propel/src/components/toolbar/index.tsx index 2bc7ac6f..c3dbf594 100644 --- a/packages/propel/src/components/toolbar/index.tsx +++ b/packages/propel/src/components/toolbar/index.tsx @@ -17,42 +17,35 @@ import { type DropdownSeparatorProps, } from "../dropdown/index"; -// The Figma "Toolbar" component has a single `variant` axis describing where the -// toolbar is placed (Figma: Floater / Pages - Topbar / Comments bottom bar). The -// variants differ in two ways: their *surface* and their *density*. Only the -// floater is a self-contained surface — a white `surface-1` card with a subtle -// border, `radius/lg` corners and an `Overlay-100` shadow — so it can hover over -// content. Topbar and bottom-bar sit flush inside an existing bar, so they're flat -// (no surface, no shadow) and are therefore visually identical to each other. +// A toolbar is described by two orthogonal axes — `elevation` and `density` — named +// for what they actually express rather than where the toolbar is placed (placement +// is the consumer's job, not a primitive's). The Figma "Toolbar" component models +// these as placements (Floater / Pages - Topbar / Comments bottom bar), but those +// collapse to combinations of the two axes here. // -// Density differs too: the floater packs controls tighter (24px hit targets, a -// 14px chevron) while the topbar/bottom-bar are roomier (28px hit targets, a 16px -// chevron). Every placement shares the root `p-1.5` (6px) padding and `gap-2` (8px) -// item gap; clusters inside `ToolbarGroup`/`ToolbarToggleGroup` keep a tight -// `gap-0.5` (2px). +// `elevation`: whether the toolbar draws its own surface. `raised` is a +// self-contained card — a white `surface-1` fill with a subtle border, `radius/lg` +// corners and an `Overlay-100` shadow — so it can hover over content (the Figma +// floater). `flat` draws no surface and sits flush inside an existing bar (the Figma +// topbar / bottom-bar, which were visually identical). const toolbarVariants = cva("flex w-fit items-center gap-2 p-1.5 text-secondary", { variants: { - variant: { - floater: surfaceVariants({ elevation: "raised", radius: "lg" }), - topbar: "", - "bottom-bar": "", + elevation: { + raised: surfaceVariants({ elevation: "raised", radius: "lg" }), + flat: "", }, }, }); -export type ToolbarVariant = NonNullable["variant"]>; +export type ToolbarElevation = NonNullable["elevation"]>; -// The density of a toolbar follows its placement: the `floater` is compact (24px -// hit targets), while `topbar`/`bottom-bar` are comfortable (28px). It's derived -// from `variant` and shared with the child controls (buttons, toggles, dropdown -// trigger) through context so they size themselves to match the root. -type ToolbarDensity = "compact" | "comfortable"; - -const DENSITY_BY_VARIANT: Record = { - floater: "compact", - topbar: "comfortable", - "bottom-bar": "comfortable", -}; +// `density`: how tightly the controls pack. `compact` is 24px hit targets (a 14px +// chevron), `comfortable` is 28px (a 16px chevron). It's shared with the child +// controls (buttons, toggles, dropdown trigger) through context so they size +// themselves to match the root. Both axes share the root `p-1.5` (6px) padding and +// `gap-2` (8px) item gap; clusters inside `ToolbarGroup`/`ToolbarToggleGroup` keep a +// tight `gap-0.5` (2px). +export type ToolbarDensity = "compact" | "comfortable"; const ToolbarDensityContext = React.createContext("compact"); @@ -61,12 +54,16 @@ export type ToolbarProps = Omit< "className" | "render" | "style" > & { /** - * Where the toolbar is placed, which controls its surface and density. `floater` - * is a self-contained card with a border + shadow that hovers over content and - * packs its controls tightly (24px). `topbar` and `bottom-bar` are flat, sit flush - * inside an existing bar, and use the roomier 28px density. + * Whether the toolbar draws its own surface. `raised` is a self-contained card + * with a border + shadow that hovers over content; `flat` draws no surface and + * sits flush inside an existing bar. Independent of `density`. + */ + elevation: ToolbarElevation; + /** + * How tightly the controls pack. `compact` is 24px hit targets, `comfortable` is + * 28px. Independent of `elevation`, so a flat bar can still be compact. */ - variant: ToolbarVariant; + density: ToolbarDensity; }; /** @@ -76,10 +73,10 @@ export type ToolbarProps = Omit< * carries `role="toolbar"`. Compose it from `ToolbarGroup`, `ToolbarButton`, * `ToolbarToggle`, `ToolbarSeparator` and `ToolbarDropdown`. */ -export function Toolbar({ variant, ...props }: ToolbarProps) { +export function Toolbar({ elevation, density, ...props }: ToolbarProps) { return ( - - + + ); } @@ -121,9 +118,6 @@ const itemVariants = cva( comfortable: "size-7 [&_svg]:size-4", }, }, - defaultVariants: { - density: "compact", - }, }, ); @@ -226,9 +220,6 @@ const dropdownTriggerVariants = cva( comfortable: "h-7", }, }, - defaultVariants: { - density: "compact", - }, }, ); @@ -239,9 +230,6 @@ const dropdownChevronVariants = cva("shrink-0 text-icon-secondary", { comfortable: "size-4", }, }, - defaultVariants: { - density: "compact", - }, }); /** diff --git a/packages/propel/src/components/toolbar/toolbar.stories.tsx b/packages/propel/src/components/toolbar/toolbar.stories.tsx index bc2b5031..900c8305 100644 --- a/packages/propel/src/components/toolbar/toolbar.stories.tsx +++ b/packages/propel/src/components/toolbar/toolbar.stories.tsx @@ -143,7 +143,7 @@ const meta = { ToolbarDropdownItem, ToolbarDropdownSeparator, }, - args: { variant: "floater" }, + args: { elevation: "raised", density: "compact" }, render: (args) => , parameters: { design: { @@ -156,21 +156,77 @@ const meta = { export default meta; type Story = StoryObj; -/** The default `floater`: a self-contained card with a border + shadow. */ +/** The default: a `raised`, `compact` floater — a self-contained card with a border + shadow. */ export const Default: Story = {}; -/** The three placements: a floating card, a flat topbar, and a flat bottom bar. */ -export const Variants: Story = { - parameters: { controls: { disable: true } }, - render: () => ( +/** + * The `elevation` axis: `raised` draws its own card (border + shadow) so it can hover + * over content; `flat` draws no surface and sits flush inside an existing bar. + * Independent of `density` — both rows keep the story's current density. + */ +export const Elevations: Story = { + argTypes: { elevation: { control: false } }, + render: (args) => ( +
+ + +
+ ), +}; + +/** + * The `density` axis: `compact` packs the controls to 24px hit targets, `comfortable` + * gives them 28px. Independent of `elevation` — both rows keep the story's current + * elevation. + */ +export const Densities: Story = { + argTypes: { density: { control: false } }, + render: (args) => (
- - - + +
), }; +/** + * `elevation` and `density` are orthogonal: a `flat` bar can still be `compact`. This + * is Figma's "fixed + compact" bar — a non-floating surface at the tight 24px density + * the raised floater also uses. + */ +export const FlatCompact: Story = { + args: { elevation: "flat", density: "compact" }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/ioN74zM1xMGbcPemsxs4J1/Global-components?node-id=2842-3905", + }, + }, +}; + +/** + * Density wiring check that runs in the browser: `density` drives the child controls' + * size through context, independent of `elevation`, so a `flat` + `compact` toolbar + * renders 24px controls. Tagged out of the sidebar/docs/manifest — it's a test, not a + * designer- or agent-facing example — but still runs under the default `test` tag. + */ +export const DensityDrivesControlSize: Story = { + tags: ["!dev", "!autodocs", "!manifest"], + render: () => ( + + + + + + ), + play: async ({ canvas }) => { + // density="compact" sizes the controls to 24px regardless of the flat elevation. + const bold = canvas.getByRole("button", { name: "Bold" }); + await expect(bold).toHaveClass("size-6"); + await expect(getComputedStyle(bold).height).toBe("24px"); + }, +}; + /** * Because `ToolbarDropdown` composes propel's `Dropdown`, a toolbar menu can hold * richer rows than the old `items[]` config allowed: per-row leading icons, a @@ -240,7 +296,7 @@ export const Behavior: Story = { export const KeyboardRovingFocus: Story = { tags: ["!dev", "!autodocs", "!manifest"], render: () => ( - + diff --git a/packages/propel/src/patterns/comment.stories.tsx b/packages/propel/src/patterns/comment.stories.tsx index 920b822b..9b884520 100644 --- a/packages/propel/src/patterns/comment.stories.tsx +++ b/packages/propel/src/patterns/comment.stories.tsx @@ -70,7 +70,7 @@ const bodyVariants = cva( // editor; here they just compose propel's Toolbar parts. function FormattingToolbar() { return ( - + @@ -248,7 +248,7 @@ const RECIPE_SOURCE = `function CommentComposer() { className="min-h-10 w-full resize-none bg-transparent p-3 text-14 leading-snug text-primary outline-none placeholder:text-placeholder" />
- +