Skip to content
2 changes: 1 addition & 1 deletion packages/propel/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import tailwindcss from "@tailwindcss/vite";
import type { StorybookConfig } from "@storybook/react-vite";
import tailwindcss from "@tailwindcss/vite";

const config: StorybookConfig = {
framework: "@storybook/react-vite",
Expand Down
9 changes: 4 additions & 5 deletions packages/propel/.storybook/preview.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
/* Storybook's Tailwind entry: pull in Tailwind, then propel's tokens + styles.
propel.css's `@source "../"` makes Tailwind scan src/ (components + stories),
so the utility classes propel uses are generated for the preview. */
@import "tailwindcss";
@import "../src/styles/propel.css";
/* Storybook's Tailwind entry: the shared tooling entry (Tailwind + propel's tokens).
propel.css's `@source "../"` makes Tailwind scan src/ (components + stories), so
the utility classes propel uses are generated for the preview. */
@import "../tailwind.css";

/* Make the preview canvas adapt to the active theme so the toolbar toggle is
visually meaningful (bg + text follow light/dark/contrast). */
Expand Down
1 change: 1 addition & 0 deletions packages/propel/.storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DirectionProvider } from "@base-ui/react/direction-provider";
import type { Decorator, Preview } from "@storybook/react-vite";
import { useLayoutEffect } from "react";

import "./preview.css";
import { type Theme, THEMES } from "./themes";

Expand Down
30 changes: 15 additions & 15 deletions packages/propel/src/components/accordion/accordion.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { CircleHelp } from "lucide-react";
import { expect } from "storybook/test";

import { Accordion, AccordionItem, AccordionPanel, AccordionTrigger } from "./index";

// Design-review convention — when to add a pseudo-states "States" story:
Expand Down Expand Up @@ -87,16 +88,16 @@ export const WithIcon: Story = {
};

/**
* The trigger's interaction states side by side. The accordion trigger styles
* interaction purely with CSS utilities (`hover:bg-layer-transparent-hover` and
* `focus-visible:ring-2 focus-visible:ring-accent-strong`), so
* storybook-addon-pseudo-states can force them statically — no real pointer/keyboard.
* The trigger's interaction states side by side. The accordion trigger styles interaction purely
* with CSS utilities (`hover:bg-layer-transparent-hover` and `focus-visible:ring-2
* focus-visible:ring-accent-strong`), so storybook-addon-pseudo-states can force them statically —
* no real pointer/keyboard.
*
* One accordion with a row per state: each trigger `<button>` carries a unique id
* (Base UI forwards it from `AccordionTrigger`), and `parameters.pseudo` maps that id
* to the pseudo-class to force. A single `Accordion` root keeps a single `region`
* landmark (multiple roots would collide on the axe `landmark-unique` rule). The Hover
* row should show the transparent-hover background; the Focus row the accent focus ring.
* One accordion with a row per state: each trigger `<button>` carries a unique id (Base UI forwards
* it from `AccordionTrigger`), and `parameters.pseudo` maps that id to the pseudo-class to force. A
* single `Accordion` root keeps a single `region` landmark (multiple roots would collide on the axe
* `landmark-unique` rule). The Hover row should show the transparent-hover background; the Focus
* row the accent focus ring.
*/
export const States: Story = {
parameters: {
Expand Down Expand Up @@ -141,8 +142,8 @@ export const MultipleItems: Story = {
};

/**
* Behavior test: clicking a collapsed trigger expands its panel and flips
* `aria-expanded` to true (a `region` appears); clicking again collapses it.
* Behavior test: clicking a collapsed trigger expands its panel and flips `aria-expanded` to true
* (a `region` appears); clicking again collapses it.
*/
export const Interaction: Story = {
tags: ["!dev", "!autodocs", "!manifest"],
Expand Down Expand Up @@ -177,10 +178,9 @@ export const Interaction: Story = {
};

/**
* Keyboard ARIA pattern (WAI-ARIA accordion): Tab moves focus to the trigger and
* both **Enter** and **Space** toggle `aria-expanded`, showing/hiding the panel
* region. Tagged out of the sidebar/docs/manifest while still running under the
* default `test` tag.
* Keyboard ARIA pattern (WAI-ARIA accordion): Tab moves focus to the trigger and both **Enter** and
* **Space** toggle `aria-expanded`, showing/hiding the panel region. Tagged out of the
* sidebar/docs/manifest while still running under the default `test` tag.
*/
export const KeyboardToggle: Story = {
tags: ["!dev", "!autodocs", "!manifest"],
Expand Down
20 changes: 10 additions & 10 deletions packages/propel/src/components/accordion/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ export type AccordionProps = Omit<
>;

/**
* Groups a set of `AccordionItem`s. Single-open by default; pass `multiple` to allow
* several panels open at once. Use `defaultValue` (uncontrolled) or `value` +
* `onValueChange` (controlled) to drive which items are expanded.
* Groups a set of `AccordionItem`s. Single-open by default; pass `multiple` to allow several panels
* open at once. Use `defaultValue` (uncontrolled) or `value` + `onValueChange` (controlled) to
* drive which items are expanded.
*/
export function Accordion(props: AccordionProps) {
return <BaseAccordion.Root className="flex w-full flex-col" {...props} />;
Expand All @@ -39,17 +39,17 @@ export type AccordionTriggerProps = Omit<
"className" | "render" | "style"
> & {
/**
* Optional icon shown before the label, matching the Figma header icon. Named
* `leadingIcon` (not `icon`) to match Button/Input, so a `trailingIcon` can be
* added later without a breaking rename.
* Optional icon shown before the label, matching the Figma header icon. Named `leadingIcon` (not
* `icon`) to match Button/Input, so a `trailingIcon` can be added later without a breaking
* rename.
*/
leadingIcon?: React.ReactNode;
};

/**
* The clickable header that opens and closes its panel. Renders an optional
* `leadingIcon`, the label, and a chevron that rotates when the panel is open. Base
* UI sets `aria-expanded` and `aria-controls` for you.
* The clickable header that opens and closes its panel. Renders an optional `leadingIcon`, the
* label, and a chevron that rotates when the panel is open. Base UI sets `aria-expanded` and
* `aria-controls` for you.
*/
export function AccordionTrigger({ leadingIcon, children, ...props }: AccordionTriggerProps) {
return (
Expand Down Expand Up @@ -95,7 +95,7 @@ export function AccordionPanel({ children, ...props }: AccordionPanelProps) {
"h-[var(--accordion-panel-height)] overflow-hidden",
"text-14 text-secondary",
"transition-[height] duration-200 ease-out",
"data-[starting-style]:h-0 data-[ending-style]:h-0",
"data-[ending-style]:h-0 data-[starting-style]:h-0",
)}
{...props}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect } from "storybook/test";

import { Avatar } from "../avatar/index";
import { AvatarGroup } from "./index";

Expand Down Expand Up @@ -50,8 +51,8 @@ export const ThreeMembers: Story = {
};

/**
* Overflow pattern: show a few members as images, then a final avatar with a
* "+N" count for the rest. The counter is just an `Avatar` with a `fallback`.
* Overflow pattern: show a few members as images, then a final avatar with a "+N" count for the
* rest. The counter is just an `Avatar` with a `fallback`.
*/
export const OverflowCount: Story = {
render: (args) => (
Expand Down
1 change: 1 addition & 0 deletions packages/propel/src/components/avatar-group/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type * as React from "react";

import { AvatarGroupContext, type AvatarMagnitude } from "../avatar";

// Figma's "Avatar Groups" component only defines three sizes (Small/Base/Large =
Expand Down
17 changes: 9 additions & 8 deletions packages/propel/src/components/avatar/avatar.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect } from "storybook/test";

import { AVATAR_TONES, Avatar, type AvatarMagnitude } from "./index";

const MAGNITUDES: AvatarMagnitude[] = ["2xs", "xs", "sm", "md", "lg", "xl", "2xl", "3xl"];
Expand Down Expand Up @@ -39,8 +40,8 @@ export const Magnitudes: Story = {
};

/**
* The initials background follows `tone`. When `tone` is omitted it's derived
* from `alt`, so every person gets a stable color automatically.
* The initials background follows `tone`. When `tone` is omitted it's derived from `alt`, so every
* person gets a stable color automatically.
*/
export const Tones: Story = {
args: { src: undefined },
Expand Down Expand Up @@ -69,12 +70,12 @@ export const States: Story = {
};

/**
* The single project-wide CSS check: an `md` avatar is `size-7` (28px) and the
* tone utility resolves to a real color. Concrete computed values prove the shared
* preview actually compiled Tailwind + propel's tokens — a plain render would pass
* even with no styles loaded. Tagged `!dev`/`!autodocs`/`!manifest` so it's hidden
* from the sidebar, the docs page, and the AI/MCP manifest — it's a test canary,
* not a designer- or agent-facing example — but still runs via the default `test` tag.
* The single project-wide CSS check: an `md` avatar is `size-7` (28px) and the tone utility
* resolves to a real color. Concrete computed values prove the shared preview actually compiled
* Tailwind + propel's tokens — a plain render would pass even with no styles loaded. Tagged
* `!dev`/`!autodocs`/`!manifest` so it's hidden from the sidebar, the docs page, and the AI/MCP
* manifest — it's a test canary, not a designer- or agent-facing example — but still runs via the
* default `test` tag.
*/
export const CssCheck: Story = {
tags: ["!dev", "!autodocs", "!manifest"],
Expand Down
10 changes: 5 additions & 5 deletions packages/propel/src/components/avatar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ const avatarVariants = cva(
export type AvatarMagnitude = NonNullable<VariantProps<typeof avatarVariants>["magnitude"]>;

/**
* Set by `AvatarGroup` to give every avatar inside it the same `magnitude`, so a
* group stays consistently sized. An avatar's own `magnitude` prop takes precedence.
* Set by `AvatarGroup` to give every avatar inside it the same `magnitude`, so a group stays
* consistently sized. An avatar's own `magnitude` prop takes precedence.
*/
export const AvatarGroupContext = React.createContext<AvatarMagnitude | undefined>(undefined);

Expand All @@ -47,9 +47,9 @@ export const initialsToneClass: Record<AvatarTone, string> = {
};

/**
* Deterministically pick a tone from a seed (e.g. a name or user id) so the same
* person always gets the same color — the "system picks it" behavior. Used as the
* default when `tone` is not set; pass `tone` to override.
* Deterministically pick a tone from a seed (e.g. a name or user id) so the same person always gets
* the same color — the "system picks it" behavior. Used as the default when `tone` is not set; pass
* `tone` to override.
*/
export function getAvatarTone(seed: string): AvatarTone {
let hash = 0;
Expand Down
5 changes: 3 additions & 2 deletions packages/propel/src/components/badge/badge.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Check } from "lucide-react";

import { iconControl } from "../../storybook/icon-control";
import { Badge, type BadgeMagnitude, type BadgeTone } from "./index";

Expand Down Expand Up @@ -117,8 +118,8 @@ const PLAN_TONES = { paid: "brand", free: "grey" } as const satisfies Record<
>;

/**
* The "Plan Badges" treatment: `paid` reads as a `brand`/upgrade accent, `free`
* as neutral `grey`. These are plain Badge tones, shown here at every magnitude.
* The "Plan Badges" treatment: `paid` reads as a `brand`/upgrade accent, `free` as neutral `grey`.
* These are plain Badge tones, shown here at every magnitude.
*/
export const PlanBadges: Story = {
parameters: {
Expand Down
8 changes: 4 additions & 4 deletions packages/propel/src/components/badge/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as React from "react";
// t-shirt scale by their label text size (S 12px, Base 13px, Large 14px), consistent
// with how Avatar names its steps. Height/padding/radius track Figma per step.
const badgeVariants = cva(
"inline-flex w-fit shrink-0 items-center justify-center gap-1 whitespace-nowrap font-medium leading-none",
"inline-flex w-fit shrink-0 items-center justify-center gap-1 leading-none font-medium whitespace-nowrap",
{
variants: {
magnitude: {
Expand Down Expand Up @@ -56,9 +56,9 @@ export type BadgeProps = Omit<React.ComponentProps<"span">, "className" | "style
/** Size of the badge. */
magnitude: BadgeMagnitude;
/**
* Optional leading icon (e.g. a lucide icon), sized to the magnitude and tinted to
* the tone. Named `leadingIcon` (not `icon`) to match Button/Input and leave room
* for a future `trailingIcon`.
* Optional leading icon (e.g. a lucide icon), sized to the magnitude and tinted to the tone.
* Named `leadingIcon` (not `icon`) to match Button/Input and leave room for a future
* `trailingIcon`.
*/
leadingIcon?: React.ReactNode;
};
Expand Down
18 changes: 9 additions & 9 deletions packages/propel/src/components/banner/banner.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn } from "storybook/test";
import { Button } from "../button/index";

import { iconControl } from "../../storybook/icon-control";
import { Button } from "../button/index";
import { Banner, type BannerTone } from "./index";

const TONES: BannerTone[] = ["neutral", "info", "accent", "warning", "danger"];
Expand Down Expand Up @@ -57,10 +58,9 @@ export const Variants: Story = {
};

/**
* The full page banner from Figma (see the meta's design link): a message with trailing
* actions and a dismiss control. `actions` takes any nodes, so the banner composes propel
* `Button`s, here a ghost, a secondary, and a primary, matching the three buttons plus
* the close in the design.
* The full page banner from Figma (see the meta's design link): a message with trailing actions and
* a dismiss control. `actions` takes any nodes, so the banner composes propel `Button`s, here a
* ghost, a secondary, and a primary, matching the three buttons plus the close in the design.
*/
export const WithActions: Story = {
parameters: { controls: { disable: true } },
Expand Down Expand Up @@ -94,10 +94,10 @@ export const Dismissible: Story = {
};

/**
* Real interaction test: clicking the dismiss button invokes `onDismiss`. The spy
* comes from a Storybook `fn()`; the button is queried by its `aria-label`. Tagged
* `!dev`/`!autodocs`/`!manifest` so it stays out of the sidebar, docs, and AI
* manifest, but still runs under the default `test` tag.
* Real interaction test: clicking the dismiss button invokes `onDismiss`. The spy comes from a
* Storybook `fn()`; the button is queried by its `aria-label`. Tagged
* `!dev`/`!autodocs`/`!manifest` so it stays out of the sidebar, docs, and AI manifest, but still
* runs under the default `test` tag.
*/
export const DismissCallsHandler: Story = {
tags: ["!dev", "!autodocs", "!manifest"],
Expand Down
17 changes: 8 additions & 9 deletions packages/propel/src/components/banner/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ const bannerVariants = cva("flex items-center overflow-clip", {
// Per Figma the neutral page banner sits on the page surface, while the colored
// tones use their soft tone surface; inline neutral uses the layered surface.
compoundVariants: [
{ variant: "page", tone: "neutral", class: "bg-surface-1 border-subtle" },
{ variant: "inline", tone: "neutral", class: "bg-surface-2 border-subtle" },
{ tone: "info", class: "bg-info-subtle border-info-subtle" },
{ tone: "accent", class: "bg-accent-subtle border-accent-subtle" },
{ tone: "warning", class: "bg-warning-subtle border-warning-subtle" },
{ tone: "danger", class: "bg-danger-subtle border-danger-subtle" },
{ variant: "page", tone: "neutral", class: "border-subtle bg-surface-1" },
{ variant: "inline", tone: "neutral", class: "border-subtle bg-surface-2" },
{ tone: "info", class: "border-info-subtle bg-info-subtle" },
{ tone: "accent", class: "border-accent-subtle bg-accent-subtle" },
{ tone: "warning", class: "border-warning-subtle bg-warning-subtle" },
{ tone: "danger", class: "border-danger-subtle bg-danger-subtle" },
],
});

Expand Down Expand Up @@ -84,9 +84,8 @@ export type BannerProps = Omit<React.ComponentProps<"div">, "className" | "style
/** Figma Intent: the banner's meaning/color. */
tone: BannerTone;
/**
* Leading icon. Defaults to a tone-appropriate lucide icon; pass `null` to hide it.
* Named `leadingIcon` (not `icon`) to match Button/Input and leave room for a future
* `trailingIcon`.
* Leading icon. Defaults to a tone-appropriate lucide icon; pass `null` to hide it. Named
* `leadingIcon` (not `icon`) to match Button/Input and leave room for a future `trailingIcon`.
*/
leadingIcon?: React.ReactNode;
/** The banner's headline. Rendered as its own block above any `children` body. */
Expand Down
Loading