diff --git a/packages/propel/.storybook/preview.css b/packages/propel/.storybook/preview.css
index bc4cabd7..9d76e0bb 100644
--- a/packages/propel/.storybook/preview.css
+++ b/packages/propel/.storybook/preview.css
@@ -4,7 +4,11 @@
@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). */
+ visually meaningful (bg + text follow light/dark/contrast).
+
+ Use `layer-2` (not `canvas`): in light mode `--bg-canvas` === `--bg-layer-3`, so
+ tertiary button rest fills (and anything else keyed to `layer-3`) vanish on the
+ default Storybook page — Figma button sheets sit on white/`layer-2`. */
body {
- @apply bg-canvas text-primary;
+ @apply bg-layer-2 text-primary;
}
diff --git a/packages/propel/AGENTS.md b/packages/propel/AGENTS.md
index 391cf703..a408c238 100644
--- a/packages/propel/AGENTS.md
+++ b/packages/propel/AGENTS.md
@@ -197,9 +197,10 @@ existed).
`[&>svg]:size-*`. A family gets its OWN icon part only for genuinely distinct styling (toast/
banner/alert-dialog tones, avatar's scale, autocomplete/combobox focus-brightening); an icon slot
that is only a tint/size of the shared chrome is the internal `Icon` (`tint`/`magnitude` axes,
- defaulting to `inherit`), composed by the `components` ready-made — icon-shaped controls
- (`IconButton`, `Toggle`, `ToolbarButton`, `ToolbarToggle`) wrap their bare svg children in it
- themselves, so consumers pass a bare glyph.
+ defaulting to `inherit`). Ready-mades expose that slot as an `icon` / `startIcon` / `endIcon`
+ **node prop** — consumers pass the public `` (or another already-wrapped
+ node). Icon-shaped controls (`IconButton`, `Toggle`, `ToolbarButton`, `ToolbarToggle`) render the
+ provided node as-is; they do **not** wrap bare lucide glyphs themselves.
- **`Indicator`** — a state-reflecting part. Keep Base UI's own names where they exist
(`ItemIndicator` for a selected marker, `Tabs`/`Progress`/`Meter`/`Slider` `Indicator` for
geometry). Disclosure carets are ONE internal `DisclosureIndicator` (pass a **chevron-down**;
@@ -217,7 +218,8 @@ not a part at all — compose the internal `NodeSlot` in `components` instead (`
Consumer-provided node **props** follow the same convention: name the prop for the slot it fills,
Base UI style (`placeholder`, `title`, `description`), never a `Node` suffix — `icon` fills the
family's single `Icon` slot; `startIcon`/`endIcon` fill dual `Icon` slots (bare `start`/`end` is
-the logical-direction vocabulary, like Drawer's `side`); `trailing` fills `MenuItemTrailing`;
+the logical-direction vocabulary, like Drawer's `side`). Pass the public ``
+into those props (ready-mades do not wrap bare glyphs). `trailing` fills `MenuItemTrailing`;
`meta` fills `MenuLabelMeta`; a consumer-provided control is named for its role (`decrement`,
`increment`).
diff --git a/packages/propel/src/components/anchor-button/anchor-button.stories.tsx b/packages/propel/src/components/anchor-button/anchor-button.stories.tsx
index 7dfa459d..3c990ff0 100644
--- a/packages/propel/src/components/anchor-button/anchor-button.stories.tsx
+++ b/packages/propel/src/components/anchor-button/anchor-button.stories.tsx
@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { ArrowUpRight, Plus } from "lucide-react";
-import { expect, fn } from "storybook/test";
+import * as React from "react";
+import { expect, fn, userEvent as baseUserEvent, waitFor } from "storybook/test";
import { Icon } from "../icon";
import { AnchorButton, type AnchorButtonMagnitude, type AnchorButtonProminence } from "./index";
@@ -115,8 +116,146 @@ export const WithIcons: Story = {
),
};
-/** The `loading` spinner replaces the leading icon and dims the label. */
+/** The `loading` spinner trails the label and mutes via the link chrome. */
export const Loading: Story = {
args: { prominence: "primary", magnitude: "md", loading: true },
render: (args) => ,
};
+
+/**
+ * An action that enters `loading` after click: spinner + `aria-busy`, blocks re-fires, keeps
+ * keyboard focus while soft-disabled, then settles once the work resolves.
+ */
+export const AsyncSubmit: Story = {
+ parameters: { controls: { disable: true } },
+ render: function Render(args) {
+ const [submitting, setSubmitting] = React.useState(false);
+ return (
+ {
+ setSubmitting(true);
+ window.setTimeout(() => setSubmitting(false), 1500);
+ }}
+ />
+ );
+ },
+};
+
+/**
+ * Tab moves focus onto the control, then **Enter** activates it (fires `onClick`). Native
+ * `