From c22fba217577a1009582a342e275535e9c95868b Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 14 Aug 2026 08:35:05 -0400 Subject: [PATCH] refactor(ui): remove the Emotion slot-recipe engine from Mosaic Mosaic is StyleX-only now, so the recipe engine and everything that fed it is gone: defineSlotRecipe/useRecipe/useSlot/slot, the appearance cascade and scope parsing, the MosaicTheme + resolveVariables token layer, the slot registry augmentation seam, and the condition vocabulary. MosaicProvider drops appearance, scope, nonce and cssLayerName along with the Emotion cache and reset. Icon glyph overrides, previously appearance.icons, become their own `icons` prop. The components that still depended on recipes are removed: Box, Dialog, Tabs, Skeleton, SectionSkeleton, Destructive and the organization components, plus their swingset stories. Docs follow: mosaic-architecture.md is rewritten around StyleX, the mosaic skill loses references/styling.md, and swingset's docs drop styleEngine, the sx row, the Emotion pragma rule and the variables panel. --- .changeset/tall-pans-tickle.md | 2 + .claude/skills/mosaic/SKILL.md | 42 +- .claude/skills/mosaic/references/stylex.md | 17 +- .claude/skills/mosaic/references/styling.md | 197 -------- .claude/skills/mosaic/references/testing.md | 5 +- .../src/primitives/dialog/dialog.test.tsx | 6 +- packages/swingset/CLAUDE.md | 99 ++-- packages/swingset/README.md | 29 +- packages/swingset/next.config.mjs | 3 - packages/swingset/package.json | 1 - .../swingset/src/components/Composition.tsx | 6 +- .../swingset/src/components/DocsViewer.tsx | 18 - .../src/components/PlaygroundContext.tsx | 18 +- .../swingset/src/components/PropTable.tsx | 3 +- .../swingset/src/components/StoryPreview.tsx | 50 +- .../src/components/VariablesPanel.tsx | 101 ---- packages/swingset/src/lib/registry.ts | 81 +--- packages/swingset/src/lib/types.ts | 6 +- .../swingset/src/stories/avatar.stories.tsx | 1 - .../swingset/src/stories/badge.stories.tsx | 1 - .../swingset/src/stories/button.stories.tsx | 2 - .../src/stories/card.component.stories.tsx | 2 - packages/swingset/src/stories/destructive.mdx | 15 - .../src/stories/destructive.stories.tsx | 54 --- .../src/stories/dialog.component.stories.tsx | 2 - .../src/stories/field.component.stories.tsx | 1 - .../swingset/src/stories/heading.stories.tsx | 2 - packages/swingset/src/stories/icon.mdx | 4 +- .../swingset/src/stories/icon.stories.tsx | 38 +- .../swingset/src/stories/input.stories.tsx | 1 - .../swingset/src/stories/item.stories.tsx | 1 - .../swingset/src/stories/menu.component.mdx | 2 +- .../src/stories/menu.component.stories.tsx | 1 - .../organization-profile-api-keys-panel.mdx | 17 - ...ization-profile-api-keys-panel.stories.tsx | 97 ---- .../organization-profile-delete-section.mdx | 16 - ...ization-profile-delete-section.stories.tsx | 29 -- .../organization-profile-domains-section.mdx | 18 - ...zation-profile-domains-section.stories.tsx | 113 ----- .../organization-profile-general-panel.mdx | 27 -- ...nization-profile-general-panel.stories.tsx | 26 - .../organization-profile-leave-section.mdx | 16 - ...nization-profile-leave-section.stories.tsx | 29 -- .../organization-profile-members-panel.mdx | 17 - ...nization-profile-members-panel.stories.tsx | 73 --- .../organization-profile-profile-section.mdx | 15 - ...zation-profile-profile-section.stories.tsx | 31 -- .../src/stories/organization-profile.mdx | 23 - .../stories/organization-profile.stories.tsx | 24 - .../src/stories/popover.component.mdx | 2 +- .../src/stories/popover.component.stories.tsx | 2 - .../src/stories/scroll-area.stories.tsx | 61 ++- .../swingset/src/stories/section.stories.tsx | 2 - .../swingset/src/stories/tabs.component.mdx | 58 --- .../src/stories/tabs.component.stories.tsx | 35 -- .../swingset/src/stories/text.stories.tsx | 2 - .../src/stories/user-button.stories.tsx | 1 - packages/swingset/src/types/global.d.ts | 2 - packages/swingset/src/types/react.d.ts | 8 + packages/ui/src/mosaic/MosaicProvider.tsx | 121 +---- .../__tests__/MosaicProvider.ssr.test.tsx | 31 +- .../mosaic/__tests__/MosaicProvider.test.tsx | 160 +------ .../src/mosaic/__tests__/conditions.test.ts | 68 --- .../src/mosaic/__tests__/resolveSlot.test.ts | 73 --- .../src/mosaic/__tests__/slot-recipe.test.ts | 288 ----------- .../ui/src/mosaic/__tests__/utils.test.ts | 22 - packages/ui/src/mosaic/appearance.ts | 114 ----- packages/ui/src/mosaic/block/destructive.tsx | 123 ----- packages/ui/src/mosaic/components/box.tsx | 30 -- .../src/mosaic/components/icon/icon.test.tsx | 26 +- .../ui/src/mosaic/components/icon/icon.tsx | 4 +- .../mosaic/components/section-skeleton.tsx | 54 --- .../ui/src/mosaic/components/skeleton.tsx | 72 --- packages/ui/src/mosaic/components/tabs.tsx | 177 ------- packages/ui/src/mosaic/conditions.ts | 63 --- packages/ui/src/mosaic/icons/overrides.ts | 26 + ...file-api-keys-panel-create.machine.test.ts | 143 ------ ...file-api-keys-panel-revoke.machine.test.ts | 91 ---- ...profile-api-keys-panel.controller.test.tsx | 224 --------- ...ation-profile-api-keys-panel.view.test.tsx | 239 ---------- ...profile-delete-section.controller.test.tsx | 163 ------- ...ion-profile-delete-section.machine.test.ts | 68 --- ...ation-profile-delete-section.view.test.tsx | 99 ---- ...domains-section-add-verify.machine.test.ts | 188 -------- ...e-domains-section-add-verify.view.test.tsx | 104 ---- ...domains-section-enrollment.machine.test.ts | 131 ----- ...e-domains-section-enrollment.view.test.tsx | 128 ----- ...ile-domains-section-remove.machine.test.ts | 66 --- ...rofile-domains-section.controller.test.tsx | 246 ---------- ...tion-profile-domains-section.view.test.tsx | 216 --------- ...-profile-leave-section.controller.test.tsx | 150 ------ ...tion-profile-leave-section.machine.test.ts | 68 --- ...zation-profile-leave-section.view.test.tsx | 99 ---- ...-profile-members-panel.controller.test.tsx | 213 --------- ...tion-profile-members-panel.machine.test.ts | 64 --- ...zation-profile-members-panel.view.test.tsx | 164 ------- ...le-profile-section-details.machine.test.ts | 103 ---- ...ofile-profile-section-logo.machine.test.ts | 53 --- ...rofile-profile-section.controller.test.tsx | 145 ------ .../organization-profile.controller.test.tsx | 53 --- .../__tests__/organization-profile.test.tsx | 20 - ...n-profile-api-keys-panel-create.machine.ts | 121 ----- ...n-profile-api-keys-panel-revoke.machine.ts | 86 ---- ...tion-profile-api-keys-panel.controller.tsx | 161 ------- .../organization-profile-api-keys-panel.tsx | 26 - ...ganization-profile-api-keys-panel.view.tsx | 326 ------------- ...tion-profile-delete-section.controller.tsx | 65 --- ...nization-profile-delete-section.machine.ts | 60 --- .../organization-profile-delete-section.tsx | 18 - ...ganization-profile-delete-section.view.tsx | 86 ---- ...file-domains-section-add-verify.machine.ts | 226 --------- ...rofile-domains-section-add-verify.view.tsx | 287 ----------- ...file-domains-section-enrollment.machine.ts | 134 ------ ...rofile-domains-section-enrollment.view.tsx | 190 -------- ...-profile-domains-section-remove.machine.ts | 64 --- ...on-profile-domains-section-remove.view.tsx | 86 ---- ...ion-profile-domains-section.controller.tsx | 205 -------- .../organization-profile-domains-section.tsx | 28 -- ...anization-profile-domains-section.view.tsx | 271 ----------- ...rganization-profile-general-panel-view.tsx | 49 -- .../organization-profile-general-panel.tsx | 18 - ...ation-profile-leave-section.controller.tsx | 70 --- ...anization-profile-leave-section.machine.ts | 61 --- .../organization-profile-leave-section.tsx | 18 - ...rganization-profile-leave-section.view.tsx | 85 ---- ...ation-profile-members-panel.controller.tsx | 123 ----- ...anization-profile-members-panel.machine.ts | 73 --- .../organization-profile-members-panel.tsx | 31 -- ...rganization-profile-members-panel.view.tsx | 321 ------------- ...profile-profile-section-details.machine.ts | 103 ---- ...on-profile-profile-section-logo.machine.ts | 50 -- ...ion-profile-profile-section.controller.tsx | 88 ---- .../organization-profile-profile-section.tsx | 18 - ...anization-profile-profile-section.view.tsx | 172 ------- .../organization-profile-view.tsx | 50 -- .../organization-profile.controller.tsx | 17 - .../organization/organization-profile.tsx | 44 -- packages/ui/src/mosaic/primitives/box.tsx | 17 - packages/ui/src/mosaic/primitives/tabs.tsx | 25 - .../primitives/withMosaicSlot.test-d.ts | 49 -- .../src/mosaic/primitives/withMosaicSlot.tsx | 36 -- packages/ui/src/mosaic/registry.ts | 28 -- packages/ui/src/mosaic/resolveSlot.ts | 36 -- packages/ui/src/mosaic/slot-recipe.ts | 335 ------------- packages/ui/src/mosaic/styles/index.ts | 1 + packages/ui/src/mosaic/useSlot.ts | 62 --- packages/ui/src/mosaic/variables.ts | 132 ------ pnpm-lock.yaml | 3 - references/mosaic-architecture.md | 446 +++++------------- 149 files changed, 355 insertions(+), 10891 deletions(-) create mode 100644 .changeset/tall-pans-tickle.md delete mode 100644 .claude/skills/mosaic/references/styling.md delete mode 100644 packages/swingset/src/components/VariablesPanel.tsx delete mode 100644 packages/swingset/src/stories/destructive.mdx delete mode 100644 packages/swingset/src/stories/destructive.stories.tsx delete mode 100644 packages/swingset/src/stories/organization-profile-api-keys-panel.mdx delete mode 100644 packages/swingset/src/stories/organization-profile-api-keys-panel.stories.tsx delete mode 100644 packages/swingset/src/stories/organization-profile-delete-section.mdx delete mode 100644 packages/swingset/src/stories/organization-profile-delete-section.stories.tsx delete mode 100644 packages/swingset/src/stories/organization-profile-domains-section.mdx delete mode 100644 packages/swingset/src/stories/organization-profile-domains-section.stories.tsx delete mode 100644 packages/swingset/src/stories/organization-profile-general-panel.mdx delete mode 100644 packages/swingset/src/stories/organization-profile-general-panel.stories.tsx delete mode 100644 packages/swingset/src/stories/organization-profile-leave-section.mdx delete mode 100644 packages/swingset/src/stories/organization-profile-leave-section.stories.tsx delete mode 100644 packages/swingset/src/stories/organization-profile-members-panel.mdx delete mode 100644 packages/swingset/src/stories/organization-profile-members-panel.stories.tsx delete mode 100644 packages/swingset/src/stories/organization-profile-profile-section.mdx delete mode 100644 packages/swingset/src/stories/organization-profile-profile-section.stories.tsx delete mode 100644 packages/swingset/src/stories/organization-profile.mdx delete mode 100644 packages/swingset/src/stories/organization-profile.stories.tsx delete mode 100644 packages/swingset/src/stories/tabs.component.mdx delete mode 100644 packages/swingset/src/stories/tabs.component.stories.tsx create mode 100644 packages/swingset/src/types/react.d.ts delete mode 100644 packages/ui/src/mosaic/__tests__/conditions.test.ts delete mode 100644 packages/ui/src/mosaic/__tests__/resolveSlot.test.ts delete mode 100644 packages/ui/src/mosaic/__tests__/slot-recipe.test.ts delete mode 100644 packages/ui/src/mosaic/__tests__/utils.test.ts delete mode 100644 packages/ui/src/mosaic/appearance.ts delete mode 100644 packages/ui/src/mosaic/block/destructive.tsx delete mode 100644 packages/ui/src/mosaic/components/box.tsx delete mode 100644 packages/ui/src/mosaic/components/section-skeleton.tsx delete mode 100644 packages/ui/src/mosaic/components/skeleton.tsx delete mode 100644 packages/ui/src/mosaic/components/tabs.tsx delete mode 100644 packages/ui/src/mosaic/conditions.ts create mode 100644 packages/ui/src/mosaic/icons/overrides.ts delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-api-keys-panel-create.machine.test.ts delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-api-keys-panel-revoke.machine.test.ts delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-api-keys-panel.controller.test.tsx delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-api-keys-panel.view.test.tsx delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-delete-section.controller.test.tsx delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-delete-section.machine.test.ts delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-delete-section.view.test.tsx delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-domains-section-add-verify.machine.test.ts delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-domains-section-add-verify.view.test.tsx delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-domains-section-enrollment.machine.test.ts delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-domains-section-enrollment.view.test.tsx delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-domains-section-remove.machine.test.ts delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-domains-section.controller.test.tsx delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-domains-section.view.test.tsx delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-leave-section.controller.test.tsx delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-leave-section.machine.test.ts delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-leave-section.view.test.tsx delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-members-panel.controller.test.tsx delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-members-panel.machine.test.ts delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-members-panel.view.test.tsx delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-profile-section-details.machine.test.ts delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-profile-section-logo.machine.test.ts delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile-profile-section.controller.test.tsx delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile.controller.test.tsx delete mode 100644 packages/ui/src/mosaic/organization/__tests__/organization-profile.test.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-api-keys-panel-create.machine.ts delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-api-keys-panel-revoke.machine.ts delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-api-keys-panel.controller.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-api-keys-panel.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-api-keys-panel.view.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-delete-section.controller.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-delete-section.machine.ts delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-delete-section.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-delete-section.view.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-domains-section-add-verify.machine.ts delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-domains-section-add-verify.view.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-domains-section-enrollment.machine.ts delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-domains-section-enrollment.view.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-domains-section-remove.machine.ts delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-domains-section-remove.view.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-domains-section.controller.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-domains-section.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-domains-section.view.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-general-panel-view.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-general-panel.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-leave-section.controller.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-leave-section.machine.ts delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-leave-section.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-leave-section.view.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-members-panel.controller.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-members-panel.machine.ts delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-members-panel.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-members-panel.view.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-profile-section-details.machine.ts delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-profile-section-logo.machine.ts delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-profile-section.controller.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-profile-section.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-profile-section.view.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile-view.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile.controller.tsx delete mode 100644 packages/ui/src/mosaic/organization/organization-profile.tsx delete mode 100644 packages/ui/src/mosaic/primitives/box.tsx delete mode 100644 packages/ui/src/mosaic/primitives/tabs.tsx delete mode 100644 packages/ui/src/mosaic/primitives/withMosaicSlot.test-d.ts delete mode 100644 packages/ui/src/mosaic/primitives/withMosaicSlot.tsx delete mode 100644 packages/ui/src/mosaic/registry.ts delete mode 100644 packages/ui/src/mosaic/resolveSlot.ts delete mode 100644 packages/ui/src/mosaic/slot-recipe.ts delete mode 100644 packages/ui/src/mosaic/useSlot.ts delete mode 100644 packages/ui/src/mosaic/variables.ts diff --git a/.changeset/tall-pans-tickle.md b/.changeset/tall-pans-tickle.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/tall-pans-tickle.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.claude/skills/mosaic/SKILL.md b/.claude/skills/mosaic/SKILL.md index 80165faf255..523a341bc07 100644 --- a/.claude/skills/mosaic/SKILL.md +++ b/.claude/skills/mosaic/SKILL.md @@ -1,8 +1,8 @@ --- name: mosaic description: >- - Work on Mosaic UI: styling a component with slot recipes (`defineSlotRecipe` / - `useRecipe` / slots / variants), or building a flow — authoring a state machine + Work on Mosaic UI: styling a component with StyleX (`stylex.create`, `--cl-*` + tokens, `themeProps`), or building a flow — authoring a state machine (`setup`, states/guards/`invoke`, wiring to React with `useMachine`/`useActor`/ `useSelector`), writing the controller (Clerk adapter) or view (rendering) layer, testing any of those layers, or migrating a legacy / pre-Mosaic component into the @@ -15,9 +15,10 @@ description: >- Two things live under Mosaic, and this skill covers the how-to for both: -- **Styled components** are authored with **slot recipes** — one recipe owns a - part's slot identity (`data-cl-slot`), variants, state, and appearance - cascade; `useRecipe` resolves it and hands back per-slot props to spread. +- **Styled components** are authored with **StyleX** — `stylex.create` declares + the styles, `themeProps` emits the part's public identity (the `.cl-` + class plus `data-` attrs), and `mergeStyleProps` fuses the two with the + consumer's `className`/`style`. - **Flows** follow a **machine → controller → view** split that keeps Clerk resource logic out of visual components and makes behavior testable without a running Clerk app: @@ -35,25 +36,24 @@ view Rendering only: receives a snapshot plus explicit props, renders UI, ``` `references/mosaic-architecture.md` (repo root, read by all agents) is the -canonical contract for the whole design system — tokens, theme delivery, the -`data-cl-*` styling API, slot recipes, appearance/cascade/scope, and the "Flow -and data architecture" section that defines the split. Read it for the _what_; -this skill is the _how-to_. +canonical contract for the whole design system — the `--cl-*` tokens, the +`.cl-` + `data-` styling API, the CSS build, and the "Flow and data +architecture" section that defines the split. Read it for the _what_; this skill +is the _how-to_. ## Which reference to read -| You are… | Read | -| -------------------------------------------------------------------- | ------------------------------------------------------ | -| Building on / authoring a headless primitive (`@clerk/headless`) | `references/headless.md` | -| Styling a component with StyleX (tokens, `stylex.create`, CSS build) | `references/stylex.md` | -| Building an enter/exit transition, or any motion that reads as wrong | `references/motion.md` | -| Styling a component the legacy way (slot recipes, `useRecipe`) | `references/styling.md` | -| Authoring or debugging a state machine, or wiring one to React | `references/machines.md` → in-tree `machine/README.md` | -| Writing the controller (Clerk adapter, permissions, revalidate) | `references/controllers.md` | -| Writing the view (rendering a snapshot, sending events) | `references/views.md` | -| Testing a machine, controller, or view | `references/testing.md` | -| Migrating a legacy component into Mosaic (the end-to-end workflow) | `references/migration.md` | -| Running the parity audit that guards a migration | `references/parity-audit.md` | +| You are… | Read | +| ---------------------------------------------------------------------- | ------------------------------------------------------ | +| Building on / authoring a headless primitive (`@clerk/headless`) | `references/headless.md` | +| Styling a component (tokens, `stylex.create`, `themeProps`, CSS build) | `references/stylex.md` | +| Building an enter/exit transition, or any motion that reads as wrong | `references/motion.md` | +| Authoring or debugging a state machine, or wiring one to React | `references/machines.md` → in-tree `machine/README.md` | +| Writing the controller (Clerk adapter, permissions, revalidate) | `references/controllers.md` | +| Writing the view (rendering a snapshot, sending events) | `references/views.md` | +| Testing a machine, controller, or view | `references/testing.md` | +| Migrating a legacy component into Mosaic (the end-to-end workflow) | `references/migration.md` | +| Running the parity audit that guards a migration | `references/parity-audit.md` | The migration workflow (`migration.md`) ties the flow references together: it treats the legacy component as the spec and drives you through the machine, diff --git a/.claude/skills/mosaic/references/stylex.md b/.claude/skills/mosaic/references/stylex.md index 38f86642269..995365d31dc 100644 --- a/.claude/skills/mosaic/references/stylex.md +++ b/.claude/skills/mosaic/references/stylex.md @@ -1,14 +1,13 @@ # Styling a component with StyleX -Mosaic styling is migrating off the Emotion slot-recipe engine (`styling.md`) -onto **StyleX** (`@stylexjs/stylex` 0.19). StyleX is compile-time atomic CSS: the -style objects become hashed atom classes plus one static stylesheet, with zero -runtime. This file is the authoring model for the StyleX layer; read it against -the pilot, `packages/ui/src/mosaic/components/button/`. - -The public contract is unchanged from the recipe era — consumers still target -`--cl-*` vars, the `.cl-` class, and `data-` attrs, never StyleX's -hashed `x…` atoms. What changes is how a component is authored internally. +Mosaic is styled with **StyleX** (`@stylexjs/stylex` 0.19). StyleX is +compile-time atomic CSS: the style objects become hashed atom classes plus one +static stylesheet, with zero runtime. This file is the authoring model; read it +against the reference component, `packages/ui/src/mosaic/components/button/`. + +Consumers target `--cl-*` vars, the `.cl-` class, and `data-` attrs, +never StyleX's hashed `x…` atoms. That public contract is fixed; everything +below is how a component is authored internally to produce it. The guidance below is opinionated and evidence-backed: it codifies patterns proven out across a full StyleX component library (tokens, ~100 components, diff --git a/.claude/skills/mosaic/references/styling.md b/.claude/skills/mosaic/references/styling.md deleted file mode 100644 index 8658a015689..00000000000 --- a/.claude/skills/mosaic/references/styling.md +++ /dev/null @@ -1,197 +0,0 @@ -# Styling a component with slot recipes - -> **Migration in progress:** Mosaic styling is moving off this Emotion slot-recipe -> engine onto **StyleX** (compile-time atomic CSS). New/migrated components are -> authored with StyleX — see **`references/stylex.md`**. This file documents the -> recipe system that StyleX is replacing; both coexist until the migration lands. -> The public `--cl-*` / `.cl-` / `data-` contract is identical across -> both, so it is preserved regardless of the internal engine. - -A styled Mosaic component is authored with one **slot recipe**. The recipe owns -everything about how the part looks and is targeted: its slot identity -(`data-cl-slot`), base styles, variants, and the appearance cascade. -`useRecipe(recipe, opts)` resolves it against the active theme + appearance and -hands back **per-slot props** — `css` already merged and every `data-cl-*` -attribute attached — that you spread onto the element. You never hand-thread -`css={[...]}`. - -`references/mosaic-architecture.md` (repo root) is the full contract: the -appearance cascade + scope, the token architecture (`theme.spacing`/`alpha`/ -`mix`/`text`), the condition vocabulary (`_hover`, `_disabled`, …), and the -`data-cl-*` public styling API. This file is the authoring how-to; read it -alongside two real components: - -- `packages/ui/src/mosaic/components/button.tsx` — single-slot, full variants. -- `packages/ui/src/mosaic/components/tabs.tsx` — multi-slot, bridged onto a - headless primitive. - ---- - -## Single-slot (the common case) - -`slot: 'button'` is shorthand for one implicit `root` slot. Define `base`, -`variants`, `compoundVariants`, and `defaultVariants`; register the slot id; -infer the props from the recipe; then destructure variants + state and spread the -resolved `root` props. - -```tsx -import React from 'react'; -import { defineSlotRecipe, useRecipe, type RecipeVariantProps } from '../slot-recipe'; - -export const buttonRecipe = defineSlotRecipe(theme => ({ - slot: 'button', - base: { - display: 'inline-flex', - borderRadius: theme.rounded.md, - ...theme.text('sm'), - _focusVisible: { outline: `2px solid ${theme.alpha('primary', 50)}` }, - _disabled: { opacity: 0.5, cursor: 'not-allowed', pointerEvents: 'none' }, - }, - variants: { - intent: { primary: {}, destructive: {} }, - variant: { filled: {}, outline: {}, ghost: {} }, - size: { sm: { ...theme.text('xs') }, md: { ...theme.text('sm') } }, - fullWidth: { true: { width: '100%' }, false: {} }, - }, - compoundVariants: [ - { - intent: 'primary', - variant: 'filled', - css: { - backgroundColor: theme.color.primary, - color: theme.color.primaryForeground, - _hover: { backgroundColor: theme.mix('primary', 'primaryForeground', 12) }, - }, - }, - ], - defaultVariants: { intent: 'primary', variant: 'filled', size: 'md', fullWidth: false }, -})); - -// Register the slot id — this is what makes `button` autocomplete in appearance.elements. -declare module '../registry' { - interface MosaicSlotRegistry { - button: true; - } -} - -// Infer variant props (+ sx) from the recipe — don't re-declare them by hand. -export type ButtonProps = React.ComponentPropsWithRef<'button'> & RecipeVariantProps; - -export const Button = React.forwardRef(function MosaicButton(props, ref) { - const { intent, variant, size, fullWidth, disabled, sx, children, ...rest } = props; - const { root } = useRecipe(buttonRecipe, { - variants: { intent, variant, size, fullWidth }, - state: { disabled: !!disabled }, - sx, - }); - return ( - - ); -}); -``` - -Key moves: - -- **`variants` vs `state`.** A visual axis chosen by the caller (`intent`, - `size`) is a **variant**. A runtime condition (`disabled`, `selected`, - `invalid`) is **state** — pass it under `state:` so it emits `data-cl-disabled` - and is styled via the `_disabled` condition, which keeps it overridable through - `appearance.elements`. Don't model `disabled` as a boolean variant. -- **`compoundVariants`** style a combination (`intent: 'primary'` **and** - `variant: 'filled'`) that no single axis owns. -- **`sx`** is the per-instance escape hatch; it merges last, before appearance. -- **`RecipeVariantProps`** derives `{ intent?, variant?, size?, -fullWidth?, sx? }` — the recipe is the single source of the prop types. - -## Multi-slot - -One recipe owns several parts under `slots`, each mapping a key to a public -`data-cl-slot` id (kebab-case). `base` is keyed by slot. Each rendered part calls -`useRecipe(recipe)` and spreads its own slot. `tabs.tsx` bridges the resolved -slots onto headless primitives, so slot identity lives in this styled layer: - -```tsx -export const tabsRecipe = defineSlotRecipe(theme => ({ - slots: { - list: { slot: 'tabs-list' }, - tab: { slot: 'tabs-tab' }, - panel: { slot: 'tabs-panel' }, - indicator: { slot: 'tabs-indicator' }, - }, - base: { - list: { display: 'flex', gap: theme.spacing(4), borderBottom: `1px solid ${theme.alpha('primary', 10)}` }, - tab: { - ...theme.text('sm'), - color: theme.color.mutedForeground, - '&[data-cl-selected]': { color: theme.color.primary }, - '&[data-cl-disabled]': { opacity: 0.5, cursor: 'not-allowed' }, - }, - panel: { '&[data-cl-hidden]': { display: 'none' } }, - indicator: { height: '2px', backgroundColor: theme.color.primary }, - }, -})); - -declare module '../registry' { - interface MosaicSlotRegistry { - 'tabs-list': true; - 'tabs-tab': true; - 'tabs-panel': true; - 'tabs-indicator': true; - } -} - -const List = React.forwardRef>( - function TabsList(props, ref) { - const { list } = useRecipe(tabsRecipe); - return ( - - ); - }, -); -// …Tab / Panel / Indicator each read their own slot from useRecipe(tabsRecipe)… -``` - -Note how `tabs` targets state with **raw attribute selectors** -(`'&[data-cl-selected]'`) because those states come from the headless primitive -rather than a `useRecipe({ state })` call. When your own component owns the -state, prefer `state: { … }` + the condition keys (`_disabled`, `_hover`) so it -stays overridable through `appearance.elements`; drop to raw `&[data-cl-*]` -selectors only for states applied by something else. - -## Lighter sugar: `useSlot` / `slot` - -Not every part needs variants. `slot-recipe.ts` is the heavy end of one spectrum -(`useSlot.ts`): - -- **`useSlot('avatarBox', { state })`** — themeable + targetable + appearance-aware, - but no recipe. `css` is just `sx` + appearance. -- **`slot('badgeText')`** — the barest: returns only `{ 'data-cl-slot': … }` to - make an element targetable. No hook, no css. - -Reach for a full recipe only when you have variants; otherwise use `useSlot` or -`slot`. See `references/mosaic-architecture.md` → "Sugar — useSlot / slot" for -the details. - -## Checklist - -- Recipe with `defineSlotRecipe(theme => …)`; `slot:` for one part, `slots:` for - many. -- `declare module '../registry'` block registering every slot id (co-located - with the recipe) so it autocompletes in `appearance.elements`. -- Caller-chosen axes → `variants`; runtime conditions → `state` + condition keys. -- `RecipeVariantProps` for the prop type; destructure variants + - state, spread the resolved slot(s). -- No `css={t => …}` function form in Mosaic (Emotion would type `t` as - `InternalTheme`); use the recipe's `theme => config` or `useMosaicTheme()`. diff --git a/.claude/skills/mosaic/references/testing.md b/.claude/skills/mosaic/references/testing.md index e7ef866fff2..f7143d66eac 100644 --- a/.claude/skills/mosaic/references/testing.md +++ b/.claude/skills/mosaic/references/testing.md @@ -132,8 +132,9 @@ gate from `checkAuthorization`, `loading` until Clerk is loaded, and that Build a plain `snapshot` object and pass a `vi.fn()` `send`. Assert what renders per `snapshot.value` and that interactions send the right event. **Wrap the view -in ``** — it's not a Clerk provider; it supplies the theme -context that `useRecipe`/`useSlot` read. No Clerk providers or fixtures. +in ``** — it's not a Clerk provider; it supplies the icon-override +context, and wrapping keeps the test tree matching production. No Clerk providers +or fixtures. ```tsx import { fireEvent, render, screen } from '@testing-library/react'; diff --git a/packages/headless/src/primitives/dialog/dialog.test.tsx b/packages/headless/src/primitives/dialog/dialog.test.tsx index d0513bbbe43..98441eae9c6 100644 --- a/packages/headless/src/primitives/dialog/dialog.test.tsx +++ b/packages/headless/src/primitives/dialog/dialog.test.tsx @@ -8,9 +8,9 @@ import { Dialog } from './index'; afterEach(() => cleanup()); -// Headless parts no longer emit `data-cl-slot` — slot identity is applied by the styled -// (mosaic) layer. Tests locate the surface-only parts (backdrop, viewport, trigger) via -// `data-testid` and everything else via its accessible role or text. +// Headless parts carry no slot identity — that is applied by the styled (mosaic) layer. +// Tests locate the surface-only parts (backdrop, viewport, trigger) via `data-testid` and +// everything else via its accessible role or text. function renderDialog(props: Partial> = {}) { return render( diff --git a/packages/swingset/CLAUDE.md b/packages/swingset/CLAUDE.md index 177d3f235ce..ddd0765758f 100644 --- a/packages/swingset/CLAUDE.md +++ b/packages/swingset/CLAUDE.md @@ -22,27 +22,25 @@ These require reading several files together; the `README.md` covers the step-by - **Consumes Mosaic from source, not build.** `@clerk/ui/mosaic` is aliased to `../ui/src/mosaic` in *two* places that must stay in sync: `next.config.mjs` (webpack `resolve.alias`) and `tsconfig.json` (`paths`). Editing Mosaic source in `packages/ui` reflects live in swingset's dev server — no rebuild of `@clerk/ui` needed. -- **Knobs are generated from CVA metadata, not hand-written.** A story's `meta.styles` is a Mosaic CVA style object exposing `_variants` / `_defaultVariants`. `lib/generateKnobs.ts` turns each variant into a control: variants whose keys are only `true`/`false` become boolean toggles, everything else becomes a select. Knob values are passed as props straight into the story component. This is why story functions take `Record` and cast to the real prop type. +- **Knobs are generated from a story's declared variant surface.** A story's `meta.styles` is a hand-written `{ _variants, _defaultVariants }` object describing the component's variant props — StyleX compiles its styles away, so there is no runtime recipe to derive this from. `lib/generateKnobs.ts` turns each variant into a control: variants whose keys are only `true`/`false` become boolean toggles, everything else becomes a select. Knob values are passed as props straight into the story component. This is why story functions take `Record` and cast to the real prop type. - **`lib/registry.ts` is the single source of truth for which components exist**, and they are imported *explicitly* (never `import *`) so sidebar order is deterministic. `getSidebarGroups`, `getModuleBySlug`, and slugging (`lib/slug.ts`, from `meta.title`) read from it. Adding a component touches up to three wiring points: `registry.ts` (sidebar entry + per-page playground lookup), `DocsViewer.tsx`'s `docModules` map (MDX docs), and the hardcoded redirect in `app/page.tsx`. - ⚠️ **Add each new import and its first usage in the same edit.** The on-save lint-fix (`unused-imports/no-unused-imports` is an `error`) deletes any import that isn't referenced yet, so importing a story export in `registry.ts` (or a component in a `*.stories.tsx`) *before* the code that uses it silently drops the import and you get `X is not defined` at runtime. After wiring, `grep` the new symbol to confirm both the import and its use survived. (Repo-wide footgun; see `clerk-monorepo` skill `references/setup-and-footguns.md`.) -- **Routing.** Each component is a single page: `/components/[component]` renders its MDX overview via `DocsViewer`. There are no per-story sub-pages — the interactive playground lives *inside* the overview. `app/page.tsx` is a static redirect (currently to `/components/button`) because `registry.ts` eagerly imports story modules (Emotion / `createContext`), so registry-derived data can't be computed in a Server Component. `DocsViewer` also renders a "View source" link (`ViewSource.tsx`) from `meta.source` — a repo-root-relative path turned into a GitHub URL by `lib/source.ts`. +- **Routing.** Each component is a single page: `/components/[component]` renders its MDX overview via `DocsViewer`. There are no per-story sub-pages — the interactive playground lives *inside* the overview. `app/page.tsx` is a static redirect (currently to `/components/button`) because `registry.ts` eagerly imports story modules (client components / `createContext`), so registry-derived data can't be computed in a Server Component. `DocsViewer` also renders a "View source" link (`ViewSource.tsx`) from `meta.source` — a repo-root-relative path turned into a GitHub URL by `lib/source.ts`. -- **Shared playground state.** `DocsViewer` wraps each overview in a `PlaygroundProvider` (`PlaygroundContext.tsx`), keyed by slug and seeded from the component's `meta` via `getModuleBySlug`. It owns the knob values (props) and live `MosaicVariables`. The `` and the interactive `` both read/write this single context, so editing a prop in the table updates the preview above it. +- **Shared playground state.** `DocsViewer` wraps each overview in a `PlaygroundProvider` (`PlaygroundContext.tsx`), keyed by slug and seeded from the component's `meta` via `getModuleBySlug`. It owns the knob values (props). The `` and the interactive `` both read/write this single context, so editing a prop in the table updates the preview above it. -- **Every story renders inside `MosaicProvider`.** `StoryPreview` (the MDX ``) renders a named story with the playground's knob values as props, applies the variable overrides, and exposes a Reset button plus a collapsible `VariablesPanel` attached to the preview. `StoryEmbed` (the MDX ``) renders a single static variation with default knob values and no controls. +- **Every story renders inside `MosaicProvider`.** `StoryPreview` (the MDX ``) renders a named story with the playground's knob values as props and exposes a Reset button. `StoryEmbed` (the MDX ``) renders a single static variation with default knob values and no controls. -- **The prop table is the knob surface.** `PropTable` (MDX ``) derives rows from `meta.styles._variants`/`_defaultVariants`, then appends the escape-hatch rows for the component's styling engine (`meta.styleEngine`): Emotion components get `sx`, StyleX components get `className` + `style`. Each variant row renders a `KnobControl` in its **Value** column, seeded with the prop's default and bound to the playground context. The engine rows and `extra` stay static. - -- **Variables live in the preview.** The `VariablesPanel` is a collapsible attached to `StoryPreview` (toggled from the preview's header), bound to the shared playground context so editing a Mosaic token override immediately re-themes the story rendered above it. +- **The prop table is the knob surface.** `PropTable` (MDX ``) derives rows from `meta.styles._variants`/`_defaultVariants`, then appends the `className` + `style` escape-hatch rows every Mosaic component accepts. Each variant row renders a `KnobControl` in its **Value** column, seeded with the prop's default and bound to the playground context. The escape-hatch rows and `extra` stay static. - **MDX.** `mdx-components.tsx` injects custom components into all MDX: `` (→ `StoryPreview`), `` (→ `StoryEmbed`, static), `` (→ interactive `PropTable`), `` (→ `UsageBlock`, a live code snippet that reflects the current knob values), and a `
` override routing fenced code through Shiki (`CodeBlock`). `next.config.mjs` configures `remark-gfm` and `rehype-raw` (with MDX node pass-through) so raw HTML in tables works.
 
 - **`` examples can show their source in a collapsible code footer.** When a story module exposes its own source as `__source` — via a `?raw` self-import (`export { default as __source } from './x.stories?raw'`) — `StoryEmbed` runs `extractStorySource` (`lib/extractStorySource.ts`) to pull the *previewed story function's* source out of that raw text, then `toUsageSnippet` (`lib/exampleSnippet.ts`) to reduce that knob harness to a clean usage snippet (unwraps `export function …() { return (…) }` down to the returned JSX and strips the `{...knobsAsProps(props)}` / `{...props}` knob plumbing), and renders a `CodeFooter` (`CodeFooter.tsx`): a "View code" toggle that's collapsed by default and reveals the snippet with a height animation (Base UI's `--collapsible-panel-height` + `data-starting/ending-style`). It's **opt-in per module** — only modules that export `__source` get a footer, and it's keyed to whichever story `name` the `` renders, so each example shows its own code. Shiki highlighting is shared with the `
`/`CodeBlock` path through the `useShikiHtml` hook. A `` can carry both a code footer and a `composition` footer; they stack under the preview.
   - The `?raw` query is wired in `next.config.mjs`: an `asset/source` rule handles `?raw` imports, and — crucially — a recursive `excludeRawQuery` pass adds `resourceQuery: { not: [/raw/] }` to every *other* loader so Next's SWC loader doesn't compile the file first (otherwise `__source` would contain `_jsxDEV(…)` output instead of the authored source).
 
-- **Two component layers.** `src/components/ui/*` are shadcn/ui primitives (`components.json`, `base-nova` style, neutral base) used for swingset's *own* chrome (sidebar, tabs, inputs). The components being *documented* come from `@clerk/ui/mosaic`. Don't confuse the two. Mosaic stories use Emotion (`/** @jsxImportSource @emotion/react */` pragma; `compiler.emotion` enabled in Next).
+- **Two component layers.** `src/components/ui/*` are shadcn/ui primitives (`components.json`, `base-nova` style, neutral base) used for swingset's *own* chrome (sidebar, tabs, inputs). The components being *documented* come from `@clerk/ui/mosaic`. Don't confuse the two.
 
 ## Documenting Mosaic components
 
@@ -61,25 +59,22 @@ Pick the archetype below by the component's **layer** (its `meta.group`), then f
 
 | Group        | What lives here                                                | Archetype |
 | ------------ | -------------------------------------------------------------- | --------- |
-| `AIO`        | All-in-one flows (e.g. `OrganizationProfile`)                  | C         |
-| `Panels`     | A pane within a flow (e.g. `OrganizationProfileGeneral`)       | C         |
-| `Sections`   | Self-contained feature sections (e.g. `DeleteOrganization`)    | C         |
-| `Blocks`     | Reusable composite UI (e.g. `Destructive`)                     | C         |
-| `Components` | Styled Mosaic components — simple CVA recipe (`Button`, `Input`) or compound/slot-based (`Dialog`, `Tabs`) | A         |
+| `User`       | Composed flow UI (e.g. `UserButton`)                           | C         |
+| `Components` | Styled Mosaic components — simple, with a flat variant surface (`Button`, `Input`), or compound (`Card`, `Field`, `Menu`, `Popover`) | A         |
 | `Primitives` | Headless `@clerk/headless` primitives (`Accordion`)            | B         |
 | `Styles`     | Atomic styles that ship as StyleX atoms, not components (`Scroll Area`) | B (adapted) |
 | `Hooks`      | Headless hooks (`useDataTable`)                                | B (adapted) |
 
-`AIO` → `Panels` → `Sections` → `Blocks` → `Components` → `Primitives` runs roughly high-level-composition → low-level-primitive. Composed layers (AIO/Panels/Sections/Blocks) are documented as compositions of lower layers (archetype C); leaf layers (Components, Primitives) get full prop/knob docs (archetypes A and B).
+`User` → `Components` → `Primitives` runs high-level-composition → low-level-primitive. Composed layers are documented as compositions of lower layers (archetype C); leaf layers (Components, Primitives) get full prop/knob docs (archetypes A and B).
 
 `Styles` and `Hooks` are the non-component layers: there is no element to knob, so they follow
 archetype B's shape (Example → Usage → Parts → Styling) with `Props` replaced by whatever the export
 actually surfaces — an argument table for a style function, a return-value table for a hook. A
-`Styles` entry documents the theme tokens its atoms read, since those tokens _are_ its API; the
+`Styles` entry documents the tokens its atoms read, since those tokens _are_ its API; the
 `Hooks` entry (`use-data-table.stories.tsx`) is `meta` alone, with no story exports at all, which is
 the minimum a section entry needs.
 
-Archetype A has two forms, chosen by whether the component exposes a single flat CVA recipe: **simple** components (`Button`, `Input`) are knob-driven; **compound** components built from slot recipes (`Dialog`, `Tabs`) have no flat variant props to knob, so they're documented like a primitive but themed. Both are detailed under Archetype A below.
+Archetype A has two forms, chosen by whether the component exposes a single flat set of variant props: **simple** components (`Button`, `Input`) are knob-driven; **compound** components (`Card`, `Field`, `Menu`, `Popover`) have no flat variant props to knob, so they're documented like a primitive but themed. Both are detailed under Archetype A below.
 
 ### `meta` conventions (all archetypes)
 
@@ -88,22 +83,24 @@ export const meta: StoryMeta = {
   group: 'Components', // exact group string from the table
   title: 'Button', // drives slug + the page 

label: 'Delete Org', // optional friendlier sidebar text - source: 'packages/ui/src/mosaic/components/button.tsx', // repo-root path → "View source" - styleEngine: 'stylex', // set on migrated components; defaults to 'emotion' - styles: buttonRecipe, // CVA recipe — archetype A · simple only + source: 'packages/ui/src/mosaic/components/button/button.tsx', // repo-root path → "View source" + styles: { + // Hand-written variant surface — archetype A · simple only + _variants: { variant: { primary: {}, outline: {} }, size: { sm: {}, md: {} } }, + _defaultVariants: { variant: 'primary', size: 'md' }, + }, }; ``` - `title` is the component's export name; it produces the slug and is what readers match against code. Set `label` only when the sidebar should read differently (the slug and page heading still come from `title`). - `source` is always a path **relative to the monorepo root**, pointing at the file that exports the documented component. Always set it — it powers the "View source" link. -- `styleEngine` names the styling engine behind the component. It only affects which escape-hatch row the `` appends (`sx` vs `className` + `style`), so it matters for archetype A. Set `'stylex'` on migrated components; leave it off for Emotion ones. -- `styles` is the component's CVA recipe/style object and is **required for archetype A's simple (knob-driven) form** (it generates the knobs and the ``). Omit it for compound A components, and for B and C. +- `styles` declares the component's variant props and is **required for archetype A's simple (knob-driven) form** (it generates the knobs and the ``). Keep it in sync with the component's real prop union by hand — StyleX compiles its styles away, so nothing derives it for you. Omit it for compound A components, and for B and C. -Story files that render styled Mosaic components must start with the Emotion pragma `/** @jsxImportSource @emotion/react */`. Headless-primitive demos render raw and don't need it. Always import the component and its recipe explicitly — never `import *`. +Always import the component explicitly — never `import *`. ### Archetype A — styled component (`Components`) -A styled Mosaic component. Which of the two forms below applies is decided by the component's shape, not by preference: if it exposes a single flat CVA recipe (`meta.styles`), use the **simple** form; if it's compound — built from slot recipes with no flat variant props (`Dialog`, `Tabs`) — use the **compound** form. +A styled Mosaic component. Which of the two forms below applies is decided by the component's shape, not by preference: if it exposes a single flat set of variant props (declared as `meta.styles`), use the **simple** form; if it's compound — a set of parts with no flat variant props (`Card`, `Field`, `Menu`, `Popover`) — use the **compound** form. **Every `Components`-layer story file exposes its source so each `` example renders a code footer.** Add the self-import once, right after the imports: @@ -115,9 +112,9 @@ export { default as __source } from './.stories?raw'; That's all the wiring needed — `StoryEmbed` picks `__source` up automatically and renders a collapsible "View code" footer keyed to each example's story function (see the `` code-footer architecture note above). No MDX change is required; keep authoring `` as before. This applies to both A forms (simple and compound) and to every example a Components page ships. -#### A · simple — single CVA recipe (`Button`, `Input`) +#### A · simple — flat variant props (`Button`, `Input`) -Has a CVA recipe, so the page is **knob-driven**: an interactive canvas plus an auto-generated prop table. Required MDX section order: +Has a declared variant surface, so the page is **knob-driven**: an interactive canvas plus a generated prop table. Required MDX section order: ```mdx import * as ButtonStories from './button.stories'; @@ -167,25 +164,25 @@ import * as ButtonStories from './button.stories'; - **Playground / Props / Usage are mandatory and always in this order.** The three share one playground state: editing a row in `` re-renders `` above it and regenerates the `` snippet below it. - The story file exports a primary demo (rendered by ``) plus one named export per variation under **Examples**. Each story takes `props: Record` and casts through a local `knobsAsProps` helper — knobs are dynamically typed, the component isn't. -- Use ``'s `extra` for documenting non-variant props; the styling escape hatch is appended for you (`sx`, or `className` + `style` when `meta.styleEngine` is `'stylex'`). +- Use ``'s `extra` for documenting non-variant props; the `className` + `style` escape-hatch rows are appended for you. - Use `` to pin static, non-knob props in the generated snippet. -- `` renders `Prop | Type | Default | Value`: the **Default** column is filled automatically from the recipe's `_defaultVariants`, and the **Value** column is the live knob seeded with that default. No manual default annotation is needed; see _Document the default value_ under Archetype B. +- `` renders `Prop | Type | Default | Value`: the **Default** column is filled from `meta.styles._defaultVariants`, and the **Value** column is the live knob seeded with that default. No per-row default annotation is needed; see _Document the default value_ under Archetype B. -#### A · compound — slot recipes, no flat CVA (`Dialog`, `Tabs`) +#### A · compound — parts, no flat variant props (`Card`, `Field`, `Menu`, `Popover`) -A compound styled component (`Dialog.Root`/`Dialog.Popup`/…) has no single flat prop interface to knob, so there's no `` or ``. Document it like a primitive (archetype B) but themed — the difference is the **Styling** section, which describes the Mosaic recipe and per-slot `appearance.elements` overrides rather than "bring your own CSS". Required MDX section order: +A compound styled component (`Popover.Root`/`Popover.Popup`/…) has no single flat prop interface to knob, so there's no `` or ``. Document it like a primitive (archetype B) but themed — the difference is the **Styling** section, which lists each part's `.cl-` class and `data-` attributes rather than saying "bring your own CSS". Required MDX section order: ```mdx -import * as DialogStories from './dialog.component.stories'; +import * as PopoverStories from './popover.component.stories'; -# Dialog +# Popover - + ## Example - + ## Usage @@ -193,19 +190,19 @@ import * as DialogStories from './dialog.component.stories'; ## Parts - + ## Styling - + ``` -The story is `meta` (no `styles`) plus a single `Default` export that renders the composed parts. The file pair is named `.component.stories.tsx` / `.component.mdx` so it doesn't collide with the headless `Primitives` entry of the same title (e.g. `Dialog`, `Tabs` exist in both layers); the `docModules` map disambiguates by group. +The story is `meta` (no `styles`) plus a single `Default` export that renders the composed parts. The file pair is named `.component.stories.tsx` / `.component.mdx` so it doesn't collide with the headless `Primitives` entry of the same title (e.g. `Menu`, `Popover` exist in both layers); the `docModules` map disambiguates by group. ### Archetype B — headless primitive (`Primitives`) -No styles, so there's no knob canvas. The single demo renders the primitive **raw (unstyled)** to show only behavior, state, and ARIA wiring. The prop/styling tables are **hand-written** (there's no CVA recipe to derive them). Required MDX section order: +No styles, so there's no knob canvas. The single demo renders the primitive **raw (unstyled)** to show only behavior, state, and ARIA wiring. The prop/styling tables are **hand-written** (there is no variant surface to derive them from). Required MDX section order: ```mdx # Accordion @@ -238,29 +235,29 @@ No styles, so there's no knob canvas. The single demo renders the primitive **ra custom properties (e.g. `--cl-accordion-panel-height`) with a CSS example. --> ``` -The story is `meta` (no `styles`) plus a single `Default` export that renders the primitive unstyled. Don't add a `` or `` — primitives have neither knobs nor a CVA recipe to drive them. +The story is `meta` (no `styles`) plus a single `Default` export that renders the primitive unstyled. Don't add a `` or `` — primitives have neither knobs nor a variant surface to drive them. -**Document the default value for every prop in a dedicated Default column.** Every props table — auto and hand-written — has a **Default** column; the `Type` stays a plain union/enum and the default is named in its own column (the convention every component-doc site and TypeDoc's `@default` tag follow), never inlined into the type. The auto `` renders `Prop | Type | Default | Value` and fills Default from the recipe's `_defaultVariants` (the **Value** column is the live knob seeded with that default); hand-written tables render `Prop | Type | Default | Description` and fill it by hand. Name the default member (`'base'`, `'multiple'`, `'bottom-start'`); use `—` when there is no default (a controlled-only or required prop) and append `(required)` for required props; when the default is behavioral rather than a literal, state it in words (`inherits Root`, `falls back to value`). +**Document the default value for every prop in a dedicated Default column.** Every props table — auto and hand-written — has a **Default** column; the `Type` stays a plain union/enum and the default is named in its own column (the convention every component-doc site and TypeDoc's `@default` tag follow), never inlined into the type. The auto `` renders `Prop | Type | Default | Value` and fills Default from `meta.styles._defaultVariants` (the **Value** column is the live knob seeded with that default); hand-written tables render `Prop | Type | Default | Description` and fill it by hand. Name the default member (`'base'`, `'multiple'`, `'bottom-start'`); use `—` when there is no default (a controlled-only or required prop) and append `(required)` for required props; when the default is behavioral rather than a literal, state it in words (`inherits Root`, `falls back to value`). -### Archetype C — composed layer (`AIO` / `Panels` / `Sections` / `Blocks`) +### Archetype C — composed layer (`User`) These compose lower layers, so the docs lead with the composition rather than knobs. Required MDX: ```mdx -import * as DeleteOrganizationStories from './delete-organization.stories'; +import * as UserButtonStories from './user-button.stories'; -# Delete Organization +# UserButton ``` @@ -277,9 +274,9 @@ import * as DeleteOrganizationStories from './delete-organization.stories'; ### Before you finish - [ ] `meta.source` is set to a repo-root-relative path. -- [ ] Story renders (Emotion pragma present for styled components). +- [ ] Story renders. - [ ] `Components`-layer story files export `__source` (the `?raw` self-import) so every `` example gets a "View code" footer. - [ ] MDX sections match the archetype's required order exactly. -- [ ] Every props-table row states its default in the **Default** column (auto `` fills it from `_defaultVariants`; `—` / `(required)` when none). +- [ ] Every props-table row states its default in the **Default** column (auto `` fills it from `meta.styles._defaultVariants`; `—` / `(required)` when none). - [ ] Wiring done per `README.md`: `registry.ts`, `DocsViewer.tsx`'s `docModules`, and the `app/page.tsx` redirect if this is now the first component. - [ ] `pnpm format --filter @clerk/swingset` is clean. diff --git a/packages/swingset/README.md b/packages/swingset/README.md index 620764a1b68..a527d3427ea 100644 --- a/packages/swingset/README.md +++ b/packages/swingset/README.md @@ -13,15 +13,18 @@ pnpm dev --filter @clerk/swingset **1. Create a story file** — `src/stories/my-component.stories.tsx` ```tsx -/** @jsxImportSource @emotion/react */ import type { StoryMeta } from '@/lib/types'; -import { MyComponent, myComponentStyles } from '@clerk/ui/mosaic/components/my-component'; +import { MyComponent, type MyComponentProps } from '@clerk/ui/mosaic/components/my-component'; export const meta: StoryMeta = { group: 'Components', title: 'My Component', - source: 'packages/ui/src/mosaic/components/my-component.tsx', // repo-root path → "View source" link - styles: myComponentStyles, // CVA style object — knobs auto-generated from _variants + source: 'packages/ui/src/mosaic/components/my-component/my-component.tsx', // repo-root path → "View source" link + // Variant surface, declared by hand — knobs are generated from `_variants` + styles: { + _variants: { variant: { primary: {}, outline: {} }, size: { sm: {}, md: {} } }, + _defaultVariants: { variant: 'primary', size: 'md' }, + }, }; export function Default(props: Record) { @@ -29,7 +32,7 @@ export function Default(props: Record) { } ``` -Knobs are generated automatically from the CVA `_variants` on the style object. Boolean variants (`true`/`false` keys) become toggles; all others become selects. Default values come from `defaultVariants`. +Knobs are generated automatically from the hand-written `_variants` on `meta.styles`. Boolean variants (`true`/`false` keys) become toggles; all others become selects. Default values come from `_defaultVariants`. `source` is the path to the component's exporting file relative to the monorepo root; `DocsViewer` renders it as a "View source" link to the file on GitHub (`lib/source.ts`). @@ -64,10 +67,9 @@ import * as Stories from './my-component.stories'; ``` `` renders the live component inline in the overview — there are no separate -per-story pages. Its props are edited through the controls in the `` below it, -and a collapsible Variables panel attached to the preview exposes Mosaic token overrides -that re-theme it. Both share the page's playground state. Use `` for -additional static demos of specific variations (no controls). +per-story pages. Its props are edited through the controls in the `` below it; +the two share the page's playground state. Use `` for additional +static demos of specific variations (no controls). Register in `src/components/DocsViewer.tsx`: @@ -81,7 +83,7 @@ Also update the root redirect in `src/app/page.tsx` if this is now the first com ## PropTable -In MDX, use `` to auto-generate the props table from the CVA style object: +In MDX, use `` to auto-generate the props table from the story's declared variant surface: ```mdx import * as Stories from './my-component.stories'; @@ -89,7 +91,7 @@ import * as Stories from './my-component.stories'; ``` -Variant props (type and default) are derived from `meta.styles._variants` and `meta.styles._defaultVariants`. The `sx` prop is always appended automatically. Pass `extra` for any other non-variant props. +Variant props (type and default) are derived from `meta.styles._variants` and `meta.styles._defaultVariants`. The `className` and `style` escape-hatch rows are always appended automatically. Pass `extra` for any other non-variant props. ## Architecture @@ -100,17 +102,16 @@ src/ app-sidebar.tsx Left nav (reads from registry) ClientRoot.tsx SidebarProvider + breadcrumb header DocsViewer.tsx Renders MDX docs for /components/[slug]; provides PlaygroundContext - PlaygroundContext.tsx Shared per-page knob values + Mosaic variables + PlaygroundContext.tsx Shared per-page knob values StoryPreview.tsx Live embed, props driven by the playground state StoryEmbed.tsx Static embed: a single variation, no controls PropTable.tsx Interactive props table — controls live in the Value column KnobControl.tsx A single auto-generated control (switch/select/input) - VariablesPanel.tsx Mosaic CSS variable overrides (collapsible, attached to the preview) CodeBlock.tsx Shiki syntax highlighter (css-variables theme) ViewSource.tsx "View source" link to the component's file on GitHub lib/ registry.ts Story registry — add new stories here - generateKnobs.ts CVA _variants → knob definitions + generateKnobs.ts meta.styles._variants → knob definitions types.ts StoryMeta, StoryModule, KnobDef etc. slug.ts URL slug utilities source.ts Builds GitHub URLs from meta.source paths diff --git a/packages/swingset/next.config.mjs b/packages/swingset/next.config.mjs index 6b36f780e26..ce720c0da34 100644 --- a/packages/swingset/next.config.mjs +++ b/packages/swingset/next.config.mjs @@ -26,9 +26,6 @@ const withMDX = createMDX({ /** @type {import('next').NextConfig} */ const nextConfig = { pageExtensions: ['tsx', 'ts', 'mdx'], - compiler: { - emotion: true, - }, webpack(config) { // `import src from './foo.stories.tsx?raw'` must return the file's *untransformed* text. // Next's SWC loader matches by extension and ignores the `?raw` query, so on its own it diff --git a/packages/swingset/package.json b/packages/swingset/package.json index a2510b959ec..beefb939fc5 100644 --- a/packages/swingset/package.json +++ b/packages/swingset/package.json @@ -13,7 +13,6 @@ "@base-ui/react": "^1.5.0", "@clerk/headless": "workspace:*", "@clerk/ui": "workspace:*", - "@emotion/react": "^11.11.1", "@stylexjs/stylex": "0.19.0", "@tailwindcss/typography": "^0.5.19", "class-variance-authority": "^0.7.1", diff --git a/packages/swingset/src/components/Composition.tsx b/packages/swingset/src/components/Composition.tsx index 3724b72d61b..60ee46ad440 100644 --- a/packages/swingset/src/components/Composition.tsx +++ b/packages/swingset/src/components/Composition.tsx @@ -5,15 +5,15 @@ import Link from 'next/link'; export interface CompositionPiece { /** Display name of the piece (e.g. `Destructive`). */ name: string; - /** Route to the piece's page in swingset (e.g. `/blocks/destructive`). */ + /** Route to the piece's page in swingset (e.g. `/components/button`). */ href: string; - /** Which Mosaic layer the piece lives in (e.g. `Blocks`, `Components`, `Primitives`). */ + /** Which Mosaic layer the piece lives in (e.g. `User`, `Components`, `Primitives`). */ layer: string; } // Mosaic layers, high → low. Drives the order the composition groups render in. // Matches the sidebar group names. -const LAYER_ORDER = ['Organization', 'Blocks', 'Components', 'Primitives']; +const LAYER_ORDER = ['User', 'Components', 'Styles', 'Primitives']; function layerRank(layer: string): number { const i = LAYER_ORDER.indexOf(layer); diff --git a/packages/swingset/src/components/DocsViewer.tsx b/packages/swingset/src/components/DocsViewer.tsx index 3895b47ccfe..8ac7bc81d51 100644 --- a/packages/swingset/src/components/DocsViewer.tsx +++ b/packages/swingset/src/components/DocsViewer.tsx @@ -13,23 +13,6 @@ const docModules: Record> = { user: { 'user-button': dynamic(() => import('../stories/user-button.mdx')), }, - organization: { - 'organization-profile': dynamic(() => import('../stories/organization-profile.mdx')), - 'organization-profile-general-panel': dynamic(() => import('../stories/organization-profile-general-panel.mdx')), - 'organization-profile-api-keys-panel': dynamic(() => import('../stories/organization-profile-api-keys-panel.mdx')), - 'organization-profile-members-panel': dynamic(() => import('../stories/organization-profile-members-panel.mdx')), - 'organization-profile-profile-section': dynamic( - () => import('../stories/organization-profile-profile-section.mdx'), - ), - 'organization-profile-domains-section': dynamic( - () => import('../stories/organization-profile-domains-section.mdx'), - ), - 'organization-profile-leave-section': dynamic(() => import('../stories/organization-profile-leave-section.mdx')), - 'organization-profile-delete-section': dynamic(() => import('../stories/organization-profile-delete-section.mdx')), - }, - blocks: { - destructive: dynamic(() => import('../stories/destructive.mdx')), - }, components: { avatar: dynamic(() => import('../stories/avatar.mdx')), badge: dynamic(() => import('../stories/badge.mdx')), @@ -43,7 +26,6 @@ const docModules: Record> = { menu: dynamic(() => import('../stories/menu.component.mdx')), popover: dynamic(() => import('../stories/popover.component.mdx')), section: dynamic(() => import('../stories/section.mdx')), - tabs: dynamic(() => import('../stories/tabs.component.mdx')), text: dynamic(() => import('../stories/text.mdx')), field: dynamic(() => import('../stories/field.component.mdx')), }, diff --git a/packages/swingset/src/components/PlaygroundContext.tsx b/packages/swingset/src/components/PlaygroundContext.tsx index c981d5794b6..de59bd4c5e7 100644 --- a/packages/swingset/src/components/PlaygroundContext.tsx +++ b/packages/swingset/src/components/PlaygroundContext.tsx @@ -1,6 +1,5 @@ 'use client'; -import type { MosaicVariables } from '@clerk/ui/mosaic/variables'; import type React from 'react'; import { createContext, useContext, useMemo, useState } from 'react'; @@ -8,15 +7,12 @@ import { generateKnobs, initKnobValues } from '@/lib/generateKnobs'; import type { KnobRecord, KnobValues, StoryMeta } from '@/lib/types'; interface PlaygroundContextValue { - /** Knob definitions derived from the component's CVA `_variants`. */ + /** Knob definitions derived from `meta.styles._variants`. */ knobs: KnobRecord; /** Current value for each knob (props passed into the live preview). */ values: KnobValues; setValue: (key: string, value: KnobValues[string]) => void; - /** Live Mosaic design-token overrides applied via `MosaicProvider`. */ - variables: MosaicVariables; - setVariables: (variables: MosaicVariables) => void; - /** Restore every knob to its default and clear variable overrides. */ + /** Restore every knob to its default. */ reset: () => void; } @@ -30,21 +26,15 @@ const PlaygroundContext = createContext(null); export function PlaygroundProvider({ meta, children }: { meta?: StoryMeta; children: React.ReactNode }) { const knobs = useMemo(() => (meta ? generateKnobs(meta) : {}), [meta]); const [values, setValues] = useState(() => initKnobValues(knobs)); - const [variables, setVariables] = useState({}); const value = useMemo( () => ({ knobs, values, setValue: (key, v) => setValues(prev => ({ ...prev, [key]: v })), - variables, - setVariables, - reset: () => { - setValues(initKnobValues(knobs)); - setVariables({}); - }, + reset: () => setValues(initKnobValues(knobs)), }), - [knobs, values, variables], + [knobs, values], ); return {children}; diff --git a/packages/swingset/src/components/PropTable.tsx b/packages/swingset/src/components/PropTable.tsx index b7084751802..616c811f6e0 100644 --- a/packages/swingset/src/components/PropTable.tsx +++ b/packages/swingset/src/components/PropTable.tsx @@ -17,7 +17,6 @@ interface PropTableProps { extra?: ExtraProp[]; } -const SX_ROW: ExtraProp = { name: 'sx', type: 'StyleRule | (theme) => StyleRule' }; const STYLEX_ROWS: ExtraProp[] = [ { name: 'className', type: 'string' }, { name: 'style', type: 'CSSProperties' }, @@ -39,7 +38,7 @@ export function PropTable({ meta, extra = [] }: PropTableProps) { return { name, type, default: defDisplay }; }), ...extra, - ...(meta.styleEngine === 'stylex' ? STYLEX_ROWS : [SX_ROW]), + ...STYLEX_ROWS, ]; return ( diff --git a/packages/swingset/src/components/StoryPreview.tsx b/packages/swingset/src/components/StoryPreview.tsx index ac202903489..25519efc451 100644 --- a/packages/swingset/src/components/StoryPreview.tsx +++ b/packages/swingset/src/components/StoryPreview.tsx @@ -1,16 +1,14 @@ 'use client'; import { MosaicProvider } from '@clerk/ui/mosaic/MosaicProvider'; -import { RotateCcwIcon, SlidersHorizontalIcon } from 'lucide-react'; +import { RotateCcwIcon } from 'lucide-react'; import type React from 'react'; import { useEffect, useState } from 'react'; -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { generateKnobs, initKnobValues } from '@/lib/generateKnobs'; import type { StoryModule } from '@/lib/types'; import { usePlayground } from './PlaygroundContext'; -import { VariablesPanel } from './VariablesPanel'; interface StoryPreviewProps { name: string; @@ -20,13 +18,11 @@ interface StoryPreviewProps { /** * Interactive preview embedded in a component's MDX overview. Renders the named story * inside `MosaicProvider`; its props are driven by the shared playground state, which is - * edited through the controls in the `` below it. A collapsible `VariablesPanel` - * attached to the preview overrides Mosaic design tokens to re-theme it live. + * edited through the controls in the `` below it. */ export function StoryPreview({ name, storyModule }: StoryPreviewProps) { const StoryComp = storyModule[name] as React.ComponentType>; const playground = usePlayground(); - const [variablesOpen, setVariablesOpen] = useState(false); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); @@ -38,14 +34,9 @@ export function StoryPreview({ name, storyModule }: StoryPreviewProps) { // Fall back to the story's own defaults if rendered outside a PlaygroundProvider. const values = playground?.values ?? initKnobValues(generateKnobs(storyModule.meta)); - const variables = playground?.variables ?? {}; return ( - +
- -
- - - Variables - - {variablesOpen && playground ? ( - - ) : null} -
- - {playground ? ( - - - - ) : null} - +
); } diff --git a/packages/swingset/src/components/VariablesPanel.tsx b/packages/swingset/src/components/VariablesPanel.tsx deleted file mode 100644 index d9a01cbbe99..00000000000 --- a/packages/swingset/src/components/VariablesPanel.tsx +++ /dev/null @@ -1,101 +0,0 @@ -'use client'; - -import type { MosaicVariables } from '@clerk/ui/mosaic/variables'; -import { defaultMosaicVariables } from '@clerk/ui/mosaic/variables'; - -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; - -interface VariablesPanelProps { - variables: MosaicVariables; - onChange: (variables: MosaicVariables) => void; -} - -export function VariablesPanel({ variables, onChange }: VariablesPanelProps) { - const colors = defaultMosaicVariables.color; - const radii = defaultMosaicVariables.rounded; - - function setColor(key: keyof typeof colors, value: string) { - onChange({ ...variables, color: { ...variables.color, [key]: value } }); - } - - function setRounded(key: keyof typeof radii, value: string) { - onChange({ ...variables, rounded: { ...variables.rounded, [key]: value } }); - } - - function setSpacing(value: string) { - onChange({ ...variables, spacing: value }); - } - - return ( -
-
-
-
Colors
- {(Object.keys(colors) as Array).map(key => ( -
- - )?.[key] ?? colors[key]} - onChange={e => setColor(key, e.target.value)} - className='h-7 font-mono text-xs' - /> -
- ))} -
- -
-
Radius
- {(Object.keys(radii) as Array) - .filter(k => k !== 'full') - .map(key => ( -
- - )?.[key] ?? radii[key]} - onChange={e => setRounded(key, e.target.value)} - className='h-7 font-mono text-xs' - /> -
- ))} -
- -
-
Spacing
-
- - setSpacing(e.target.value)} - className='h-7 font-mono text-xs' - /> -
-
-
-
- ); -} diff --git a/packages/swingset/src/lib/registry.ts b/packages/swingset/src/lib/registry.ts index 9baf653c30b..daa6b2e8f23 100644 --- a/packages/swingset/src/lib/registry.ts +++ b/packages/swingset/src/lib/registry.ts @@ -22,7 +22,6 @@ import { meta as cardComponentMeta, } from '../stories/card.component.stories'; import { meta as collapsibleMeta } from '../stories/collapsible.stories'; -import { Default as DestructiveDefault, meta as destructiveMeta } from '../stories/destructive.stories'; import { Default as DialogDefault, meta as dialogComponentMeta } from '../stories/dialog.component.stories'; import { meta as dialogMeta } from '../stories/dialog.stories'; import { meta as drawerMeta } from '../stories/drawer.stories'; @@ -57,38 +56,6 @@ import { } from '../stories/item.stories'; import { Default as MenuComponentDefault, meta as menuComponentMeta } from '../stories/menu.component.stories'; import { meta as menuMeta } from '../stories/menu.stories'; -import { - Default as OrganizationProfileDefault, - meta as organizationProfileMeta, -} from '../stories/organization-profile.stories'; -import { - Default as OrganizationProfileApiKeysPanelDefault, - meta as organizationProfileApiKeysPanelMeta, -} from '../stories/organization-profile-api-keys-panel.stories'; -import { - Default as OrganizationProfileDeleteSectionDefault, - meta as organizationProfileDeleteSectionMeta, -} from '../stories/organization-profile-delete-section.stories'; -import { - Default as OrganizationProfileDomainsSectionDefault, - meta as organizationProfileDomainsSectionMeta, -} from '../stories/organization-profile-domains-section.stories'; -import { - Default as OrganizationProfileGeneralPanelDefault, - meta as organizationProfileGeneralPanelMeta, -} from '../stories/organization-profile-general-panel.stories'; -import { - Default as OrganizationProfileLeaveSectionDefault, - meta as organizationProfileLeaveSectionMeta, -} from '../stories/organization-profile-leave-section.stories'; -import { - Default as OrganizationProfileMembersPanelDefault, - meta as organizationProfileMembersPanelMeta, -} from '../stories/organization-profile-members-panel.stories'; -import { - Default as OrganizationProfileProfileSectionDefault, - meta as organizationProfileProfileSectionMeta, -} from '../stories/organization-profile-profile-section.stories'; import { meta as otpMeta } from '../stories/otp.stories'; import { Alignment as PopoverComponentAlignment, @@ -114,7 +81,6 @@ import { MultipleEmailAndPhoneNumbers as SectionMultipleEmailAndPhoneNumbers, } from '../stories/section.stories'; import { meta as selectMeta } from '../stories/select.stories'; -import { Default as TabsComponentDefault, meta as tabsComponentMeta } from '../stories/tabs.component.stories'; import { meta as tabsMeta } from '../stories/tabs.stories'; import { Colors as TextColors, @@ -133,7 +99,6 @@ import { import { toSlug } from './slug'; import type { StoryModule } from './types'; -const destructiveModule: StoryModule = { meta: destructiveMeta, Default: DestructiveDefault }; const sectionModule: StoryModule = { meta: sectionMeta, Default: SectionDefault, @@ -141,35 +106,7 @@ const sectionModule: StoryModule = { ConnectedAccounts: SectionConnectedAccounts, Destructive: SectionDestructive, }; -const organizationProfileLeaveSectionModule: StoryModule = { - meta: organizationProfileLeaveSectionMeta, - Default: OrganizationProfileLeaveSectionDefault, -}; -const organizationProfileDeleteSectionModule: StoryModule = { - meta: organizationProfileDeleteSectionMeta, - Default: OrganizationProfileDeleteSectionDefault, -}; -const organizationProfileProfileSectionModule: StoryModule = { - meta: organizationProfileProfileSectionMeta, - Default: OrganizationProfileProfileSectionDefault, -}; -const organizationProfileDomainsSectionModule: StoryModule = { - meta: organizationProfileDomainsSectionMeta, - Default: OrganizationProfileDomainsSectionDefault, -}; -const organizationProfileModule: StoryModule = { meta: organizationProfileMeta, Default: OrganizationProfileDefault }; -const organizationProfileGeneralPanelModule: StoryModule = { - meta: organizationProfileGeneralPanelMeta, - Default: OrganizationProfileGeneralPanelDefault, -}; -const organizationProfileApiKeysPanelModule: StoryModule = { - meta: organizationProfileApiKeysPanelMeta, - Default: OrganizationProfileApiKeysPanelDefault, -}; -const organizationProfileMembersPanelModule: StoryModule = { - meta: organizationProfileMembersPanelMeta, - Default: OrganizationProfileMembersPanelDefault, -}; +const dialogComponentModule: StoryModule = { meta: dialogComponentMeta, Default: DialogDefault }; const cardComponentModule: StoryModule = { meta: cardComponentMeta, Default: CardDefault, Centered: CardCentered }; @@ -193,8 +130,6 @@ const buttonModule: StoryModule = { meta: buttonMeta, Primary, Sizes, Disabled } const inputModule: StoryModule = { meta: inputMeta, Default, Sizes: InputSizes, Disabled: InputDisabled, Invalid }; -const dialogComponentModule: StoryModule = { meta: dialogComponentMeta, Default: DialogDefault }; - const popoverComponentModule: StoryModule = { meta: popoverComponentMeta, Default: PopoverComponentDefault, @@ -226,8 +161,6 @@ const headingModule: StoryModule = { const menuComponentModule: StoryModule = { meta: menuComponentMeta, Default: MenuComponentDefault }; -const tabsComponentModule: StoryModule = { meta: tabsComponentMeta, Default: TabsComponentDefault }; - const textModule: StoryModule = { meta: textMeta, Default: TextDefault, Sizes: TextSizes, Colors: TextColors }; const fieldModule: StoryModule = { @@ -274,17 +207,6 @@ const useDataTableModule: StoryModule = { meta: useDataTableMeta }; export const registry: StoryModule[] = [ // User userButtonModule, - // Organization - organizationProfileModule, - organizationProfileGeneralPanelModule, - organizationProfileApiKeysPanelModule, - organizationProfileMembersPanelModule, - organizationProfileProfileSectionModule, - organizationProfileDomainsSectionModule, - organizationProfileLeaveSectionModule, - organizationProfileDeleteSectionModule, - // Blocks - destructiveModule, // Components avatarModule, badgeModule, @@ -298,7 +220,6 @@ export const registry: StoryModule[] = [ menuComponentModule, popoverComponentModule, sectionModule, - tabsComponentModule, textModule, fieldModule, // Primitives — alphabetical within the group. diff --git a/packages/swingset/src/lib/types.ts b/packages/swingset/src/lib/types.ts index de1faedc8ad..e031a80e6cd 100644 --- a/packages/swingset/src/lib/types.ts +++ b/packages/swingset/src/lib/types.ts @@ -50,11 +50,9 @@ export interface StoryMeta { */ source?: string; /** - * Which styling engine the documented component is built on. Drives the prop rows that - * are engine-specific: Emotion components take `sx`, StyleX components take `className` - * and `style`. Defaults to `'emotion'` — set `'stylex'` once a component is migrated. + * The component's variant surface, described by hand. Drives the playground knobs and + * the `` rows. StyleX has no runtime recipe to derive this from. */ - styleEngine?: 'emotion' | 'stylex'; styles?: { _variants: Record>; _defaultVariants?: Record; diff --git a/packages/swingset/src/stories/avatar.stories.tsx b/packages/swingset/src/stories/avatar.stories.tsx index bb5e9b39ff8..7bc6f2a542f 100644 --- a/packages/swingset/src/stories/avatar.stories.tsx +++ b/packages/swingset/src/stories/avatar.stories.tsx @@ -1,4 +1,3 @@ -/** @jsxImportSource @emotion/react */ import type { AvatarProps } from '@clerk/ui/mosaic/components/avatar'; import { Avatar } from '@clerk/ui/mosaic/components/avatar'; import { Icon } from '@clerk/ui/mosaic/components/icon'; diff --git a/packages/swingset/src/stories/badge.stories.tsx b/packages/swingset/src/stories/badge.stories.tsx index 6cf390eac50..57f4bce4cd4 100644 --- a/packages/swingset/src/stories/badge.stories.tsx +++ b/packages/swingset/src/stories/badge.stories.tsx @@ -13,7 +13,6 @@ export const meta: StoryMeta = { group: 'Components', title: 'Badge', source: 'packages/ui/src/mosaic/components/badge/badge.tsx', - styleEngine: 'stylex', styles: { _variants: { color: { primary: {}, neutral: {}, warning: {}, negative: {}, positive: {} }, diff --git a/packages/swingset/src/stories/button.stories.tsx b/packages/swingset/src/stories/button.stories.tsx index 3c74b4ac61d..28ce109c3ac 100644 --- a/packages/swingset/src/stories/button.stories.tsx +++ b/packages/swingset/src/stories/button.stories.tsx @@ -1,4 +1,3 @@ -/** @jsxImportSource @emotion/react */ import type { ButtonProps } from '@clerk/ui/mosaic/components/button'; import { Button, SubmitButton } from '@clerk/ui/mosaic/components/button'; import { Icon } from '@clerk/ui/mosaic/components/icon'; @@ -14,7 +13,6 @@ export const meta: StoryMeta = { group: 'Components', title: 'Button', source: 'packages/ui/src/mosaic/components/button/button.tsx', - styleEngine: 'stylex', styles: { _variants: { color: { primary: {}, neutral: {}, negative: {} }, diff --git a/packages/swingset/src/stories/card.component.stories.tsx b/packages/swingset/src/stories/card.component.stories.tsx index 1ddb1586f3c..600afdbcb52 100644 --- a/packages/swingset/src/stories/card.component.stories.tsx +++ b/packages/swingset/src/stories/card.component.stories.tsx @@ -1,4 +1,3 @@ -/** @jsxImportSource @emotion/react */ import { Button } from '@clerk/ui/mosaic/components/button'; import type { CardProps } from '@clerk/ui/mosaic/components/card'; import { Card } from '@clerk/ui/mosaic/components/card'; @@ -15,7 +14,6 @@ export const meta: StoryMeta = { group: 'Components', title: 'Card', source: 'packages/ui/src/mosaic/components/card/card.tsx', - styleEngine: 'stylex', styles: { _variants: { alignment: { start: {}, center: {} }, diff --git a/packages/swingset/src/stories/destructive.mdx b/packages/swingset/src/stories/destructive.mdx deleted file mode 100644 index a4990e31a23..00000000000 --- a/packages/swingset/src/stories/destructive.mdx +++ /dev/null @@ -1,15 +0,0 @@ -import * as DestructiveStories from './destructive.stories'; - -# Destructive - -A controlled block that composes a trigger button, a confirmation dialog, and a guarded input — the user must type the resource name exactly before the action is enabled. - - diff --git a/packages/swingset/src/stories/destructive.stories.tsx b/packages/swingset/src/stories/destructive.stories.tsx deleted file mode 100644 index e1addc2aeee..00000000000 --- a/packages/swingset/src/stories/destructive.stories.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import { Destructive } from '@clerk/ui/mosaic/block/destructive'; -import { Button } from '@clerk/ui/mosaic/components/button'; -import type { HTMLAttributes } from 'react'; -import { useState } from 'react'; - -import type { StoryMeta } from '@/lib/types'; - -export const meta: StoryMeta = { - group: 'Blocks', - title: 'Destructive', - source: 'packages/ui/src/mosaic/block/destructive.tsx', -}; - -function DestructiveTrigger(props: HTMLAttributes) { - return ( - - ); -} - -export function Default() { - const [open, setOpen] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); - const [confirmationValue, setConfirmationValue] = useState(''); - - const handleDelete = async () => { - setIsDeleting(true); - await new Promise(resolve => setTimeout(resolve, 2000)); - setIsDeleting(false); - setOpen(false); - }; - - return ( - - ); -} diff --git a/packages/swingset/src/stories/dialog.component.stories.tsx b/packages/swingset/src/stories/dialog.component.stories.tsx index 2d5e857a58d..4bf132b91fb 100644 --- a/packages/swingset/src/stories/dialog.component.stories.tsx +++ b/packages/swingset/src/stories/dialog.component.stories.tsx @@ -1,4 +1,3 @@ -/** @jsxImportSource @emotion/react */ import type { RenderProps } from '@clerk/headless/utils'; import { Button } from '@clerk/ui/mosaic/components/button'; import { Card } from '@clerk/ui/mosaic/components/card'; @@ -23,7 +22,6 @@ export const meta: StoryMeta = { group: 'Components', title: 'Dialog', source: 'packages/ui/src/mosaic/components/dialog/dialog.tsx', - styleEngine: 'stylex', styles: { _variants: { size: { prompt: {}, card: {}, panel: {} }, diff --git a/packages/swingset/src/stories/field.component.stories.tsx b/packages/swingset/src/stories/field.component.stories.tsx index 1a7ca15cb8a..79a220f720e 100644 --- a/packages/swingset/src/stories/field.component.stories.tsx +++ b/packages/swingset/src/stories/field.component.stories.tsx @@ -11,7 +11,6 @@ export const meta: StoryMeta = { group: 'Components', title: 'Field', source: 'packages/ui/src/mosaic/components/field/field.tsx', - styleEngine: 'stylex', }; const stackStyles = { diff --git a/packages/swingset/src/stories/heading.stories.tsx b/packages/swingset/src/stories/heading.stories.tsx index ad4a9de91f3..cf177cd8446 100644 --- a/packages/swingset/src/stories/heading.stories.tsx +++ b/packages/swingset/src/stories/heading.stories.tsx @@ -1,4 +1,3 @@ -/** @jsxImportSource @emotion/react */ import type { HeadingProps } from '@clerk/ui/mosaic/components/heading'; import { Heading } from '@clerk/ui/mosaic/components/heading'; @@ -12,7 +11,6 @@ export const meta: StoryMeta = { group: 'Components', title: 'Heading', source: 'packages/ui/src/mosaic/components/heading/heading.tsx', - styleEngine: 'stylex', styles: { _variants: { size: { xs: {}, sm: {}, base: {}, lg: {}, xl: {}, '2xl': {} }, diff --git a/packages/swingset/src/stories/icon.mdx b/packages/swingset/src/stories/icon.mdx index 96ac21d0b92..dd56a6b345b 100644 --- a/packages/swingset/src/stories/icon.mdx +++ b/packages/swingset/src/stories/icon.mdx @@ -2,7 +2,7 @@ import * as IconStories from './icon.stories'; # Icon -Icon renders a named glyph from Mosaic's icon set. It sizes through the `size` variant and inherits color from `currentColor`. Any glyph can be swapped per name through `appearance.icons` on `MosaicProvider`. Mosaic's styling applies to an override just like the built-in glyph — so swapped icons stay visually consistent — and the override also carries the `.cl-icon` class and `data-size` attribute for targeting. +Icon renders a named glyph from Mosaic's icon set. It sizes through the `size` variant and inherits color from `currentColor`. Any glyph can be swapped per name through the `icons` prop on `MosaicProvider`. Mosaic's styling applies to an override just like the built-in glyph — so swapped icons stay visually consistent — and the override also carries the `.cl-icon` class and `data-size` attribute for targeting. Set `placement` when the icon sits beside text inside a container. It adds no styling of its own; it reflects `data-icon="inline-start"` or `data-icon="inline-end"` so the container can react — a `Button`, for example, tightens its padding on the side the icon sits with `:has([data-icon='inline-end'])`. The values are the CSS logical directions, so they follow the writing mode rather than naming a physical edge. Leave it unset for a standalone icon. @@ -51,7 +51,7 @@ Set `placement` when the icon sits beside text inside a container. It adds no st ### Overriding a glyph -Pass `appearance.icons` to `MosaicProvider` to replace a glyph by name. The override is a plain element of any type, not just an `svg`; Mosaic clones it with the resolved sizing `className` and `data-size`, keeping any class the element already had. The replacement only supplies its own content. +Pass `icons` to `MosaicProvider` to replace a glyph by name. The override is a plain element of any type, not just an `svg`; Mosaic clones it with the resolved sizing `className` and `data-size`, keeping any class the element already had. The replacement only supplies its own content. - - - ), - }, + icons={{ + 'chevron-right': ( + + + + ), }} >
diff --git a/packages/swingset/src/stories/input.stories.tsx b/packages/swingset/src/stories/input.stories.tsx index be98937592e..c5f8600ad7b 100644 --- a/packages/swingset/src/stories/input.stories.tsx +++ b/packages/swingset/src/stories/input.stories.tsx @@ -11,7 +11,6 @@ export const meta: StoryMeta = { group: 'Components', title: 'Input', source: 'packages/ui/src/mosaic/components/input/input.tsx', - styleEngine: 'stylex', styles: { _variants: { size: { sm: {}, md: {}, lg: {} }, diff --git a/packages/swingset/src/stories/item.stories.tsx b/packages/swingset/src/stories/item.stories.tsx index eabc11f40f7..97594a694c4 100644 --- a/packages/swingset/src/stories/item.stories.tsx +++ b/packages/swingset/src/stories/item.stories.tsx @@ -1,4 +1,3 @@ -/** @jsxImportSource @emotion/react */ import { Avatar } from '@clerk/ui/mosaic/components/avatar'; import { Button } from '@clerk/ui/mosaic/components/button'; import { Icon } from '@clerk/ui/mosaic/components/icon'; diff --git a/packages/swingset/src/stories/menu.component.mdx b/packages/swingset/src/stories/menu.component.mdx index b1a749eaeea..aad2de21a64 100644 --- a/packages/swingset/src/stories/menu.component.mdx +++ b/packages/swingset/src/stories/menu.component.mdx @@ -117,7 +117,7 @@ const [open, setOpen] = useState(false); ## Styling -Unlike the slot-recipe components, the Mosaic menu is themed with **StyleX**. Each styled part +The Mosaic menu is themed with **StyleX**. Each styled part carries a stable `.cl-` class (the slots above) alongside the StyleX atoms. Consumers never target the hashed atomic classes — override by targeting the `.cl-*` slot from a CSS layer that wins over `@clerk/ui/styles.css`: diff --git a/packages/swingset/src/stories/menu.component.stories.tsx b/packages/swingset/src/stories/menu.component.stories.tsx index 82ee976fdac..276f205c8ca 100644 --- a/packages/swingset/src/stories/menu.component.stories.tsx +++ b/packages/swingset/src/stories/menu.component.stories.tsx @@ -1,4 +1,3 @@ -/** @jsxImportSource @emotion/react */ import { Icon } from '@clerk/ui/mosaic/components/icon'; import { Menu } from '@clerk/ui/mosaic/components/menu'; diff --git a/packages/swingset/src/stories/organization-profile-api-keys-panel.mdx b/packages/swingset/src/stories/organization-profile-api-keys-panel.mdx deleted file mode 100644 index 354a713c0bf..00000000000 --- a/packages/swingset/src/stories/organization-profile-api-keys-panel.mdx +++ /dev/null @@ -1,17 +0,0 @@ -import * as OrganizationProfileApiKeysPanelStories from './organization-profile-api-keys-panel.stories'; - -# Organization Profile API Keys Panel - -The API keys tab panel of the Organization Profile — lists an organization's API keys and owns the create and revoke flows. It gates on `org:sys_api_keys:read` to view and `org:sys_api_keys:manage` to create or revoke. - - diff --git a/packages/swingset/src/stories/organization-profile-api-keys-panel.stories.tsx b/packages/swingset/src/stories/organization-profile-api-keys-panel.stories.tsx deleted file mode 100644 index 573624f6832..00000000000 --- a/packages/swingset/src/stories/organization-profile-api-keys-panel.stories.tsx +++ /dev/null @@ -1,97 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import { useMachine } from '@clerk/ui/mosaic/machine/useMachine'; -import type { ApiKeyRow } from '@clerk/ui/mosaic/organization/organization-profile-api-keys-panel.view'; -import { OrganizationProfileApiKeysPanelView } from '@clerk/ui/mosaic/organization/organization-profile-api-keys-panel.view'; -import { organizationProfileApiKeysPanelCreateMachine } from '@clerk/ui/mosaic/organization/organization-profile-api-keys-panel-create.machine'; -import { organizationProfileApiKeysPanelRevokeMachine } from '@clerk/ui/mosaic/organization/organization-profile-api-keys-panel-revoke.machine'; -import { useMemo, useState } from 'react'; - -import type { StoryMeta } from '@/lib/types'; - -export const meta: StoryMeta = { - group: 'Organization', - title: 'OrganizationProfileApiKeysPanel', - source: 'packages/ui/src/mosaic/organization/organization-profile-api-keys-panel.tsx', -}; - -const SHOW_DESCRIPTION = false; - -// Fixed dates (not `Date.now()`) so the demo renders deterministically across reloads. -const DEMO_ROWS: ApiKeyRow[] = [ - { - id: 'key_prod', - name: 'Production', - createdAt: new Date('2026-01-12'), - expiration: null, - lastUsedAt: new Date('2026-06-28'), - }, - { - id: 'key_ci', - name: 'CI pipeline', - createdAt: new Date('2026-03-04'), - expiration: new Date('2026-09-04'), - lastUsedAt: null, - }, - { - id: 'key_staging', - name: 'Staging', - createdAt: new Date('2026-05-20'), - expiration: null, - lastUsedAt: new Date('2026-07-01'), - }, -]; - -const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - -export function Default() { - const [searchValue, setSearchValue] = useState(''); - - const rows = useMemo(() => { - const query = searchValue.trim().toLowerCase(); - return query ? DEMO_ROWS.filter(row => row.name.toLowerCase().includes(query)) : DEMO_ROWS; - }, [searchValue]); - - const [createSnapshot, sendCreate, createActor] = useMachine(organizationProfileApiKeysPanelCreateMachine, { - context: { - showDescription: SHOW_DESCRIPTION, - createAPIKey: async params => { - await delay(600); - return { name: params.name, secret: `sk_test_${params.name.replace(/\s+/g, '_').toLowerCase()}` }; - }, - }, - }); - - const [revokeSnapshot, sendRevoke, revokeActor] = useMachine(organizationProfileApiKeysPanelRevokeMachine, { - context: { - confirmationText: 'Revoke', - revokeAPIKey: () => delay(600), - }, - }); - - return ( - undefined, - searchValue, - onSearchChange: setSearchValue, - }} - canManage - create={{ - snapshot: createSnapshot, - send: sendCreate, - canSubmit: createActor.can({ type: 'SUBMIT' }), - showDescription: SHOW_DESCRIPTION, - }} - revoke={{ - snapshot: revokeSnapshot, - send: sendRevoke, - canConfirm: revokeActor.can({ type: 'CONFIRM' }), - }} - /> - ); -} diff --git a/packages/swingset/src/stories/organization-profile-delete-section.mdx b/packages/swingset/src/stories/organization-profile-delete-section.mdx deleted file mode 100644 index 0d885514caa..00000000000 --- a/packages/swingset/src/stories/organization-profile-delete-section.mdx +++ /dev/null @@ -1,16 +0,0 @@ -import * as OrganizationProfileDeleteSectionStories from './organization-profile-delete-section.stories'; - -# Organization Profile Delete Section - -A section that owns the open/deleting state and wires the `Destructive` block to the delete-organization flow. - - diff --git a/packages/swingset/src/stories/organization-profile-delete-section.stories.tsx b/packages/swingset/src/stories/organization-profile-delete-section.stories.tsx deleted file mode 100644 index 8e1282cbe99..00000000000 --- a/packages/swingset/src/stories/organization-profile-delete-section.stories.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import { useMachine } from '@clerk/ui/mosaic/machine/useMachine'; -import { organizationProfileDeleteSectionMachine } from '@clerk/ui/mosaic/organization/organization-profile-delete-section.machine'; -import { OrganizationProfileDeleteSectionView } from '@clerk/ui/mosaic/organization/organization-profile-delete-section.view'; - -import type { StoryMeta } from '@/lib/types'; - -export const meta: StoryMeta = { - group: 'Organization', - title: 'OrganizationProfileDeleteSection', - source: 'packages/ui/src/mosaic/organization/organization-profile-delete-section.tsx', -}; - -export function Default() { - const [snapshot, send, actor] = useMachine(organizationProfileDeleteSectionMachine, { - context: { - organizationName: 'Acme Inc', - destroyOrganization: () => new Promise(resolve => setTimeout(resolve, 800)), - }, - }); - - return ( - - ); -} diff --git a/packages/swingset/src/stories/organization-profile-domains-section.mdx b/packages/swingset/src/stories/organization-profile-domains-section.mdx deleted file mode 100644 index 232b4e3b4a5..00000000000 --- a/packages/swingset/src/stories/organization-profile-domains-section.mdx +++ /dev/null @@ -1,18 +0,0 @@ -import * as OrganizationProfileDomainsSectionStories from './organization-profile-domains-section.stories'; - -# Organization Profile Domains Section - -Manages an organization's domains — lists them, adds and verifies new ones, edits a verified -domain's enrollment mode, and removes them. It owns each flow's state (three machines) and wires -the list, the add/verify wizard, the enrollment editor, and the remove confirmation together. - - diff --git a/packages/swingset/src/stories/organization-profile-domains-section.stories.tsx b/packages/swingset/src/stories/organization-profile-domains-section.stories.tsx deleted file mode 100644 index ad288a294c4..00000000000 --- a/packages/swingset/src/stories/organization-profile-domains-section.stories.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import type { OrganizationDomainResource } from '@clerk/shared/types'; -import { useMachine } from '@clerk/ui/mosaic/machine/useMachine'; -import type { OrganizationProfileEnrollmentOption } from '@clerk/ui/mosaic/organization/organization-profile-domains-section.controller'; -import { OrganizationProfileDomainsSectionView } from '@clerk/ui/mosaic/organization/organization-profile-domains-section.view'; -import { organizationProfileDomainsSectionAddVerifyMachine } from '@clerk/ui/mosaic/organization/organization-profile-domains-section-add-verify.machine'; -import { organizationProfileDomainsSectionEnrollmentMachine } from '@clerk/ui/mosaic/organization/organization-profile-domains-section-enrollment.machine'; -import { organizationProfileDomainsSectionRemoveMachine } from '@clerk/ui/mosaic/organization/organization-profile-domains-section-remove.machine'; - -import type { StoryMeta } from '@/lib/types'; - -export const meta: StoryMeta = { - group: 'Organization', - title: 'OrganizationProfileDomainsSection', - source: 'packages/ui/src/mosaic/organization/organization-profile-domains-section.tsx', -}; - -// Demo async deps: resolve after a short delay so the loading/saving states are visible. -const delay = () => new Promise(resolve => setTimeout(resolve, 600)); - -const ENROLLMENT_OPTIONS: OrganizationProfileEnrollmentOption[] = [ - { - value: 'manual_invitation', - label: 'No automatic enrollment', - description: 'Users can only be invited manually to the organization.', - }, - { - value: 'automatic_invitation', - label: 'Automatic invitations', - description: 'Users are automatically invited to join the organization when they sign up and can join anytime.', - }, - { - value: 'automatic_suggestion', - label: 'Automatic suggestions', - description: 'Users receive a suggestion to request to join, but must be approved by an admin.', - }, -]; - -// The view reads only id/name/verification/enrollmentMode/pending counts, so a partial fixture -// is enough. The cast is confined to these swingset demo fixtures. -const demoDomains = [ - { - id: 'dmn_1', - name: 'acme.com', - enrollmentMode: 'automatic_invitation', - verification: { status: 'verified' }, - totalPendingInvitations: 3, - totalPendingSuggestions: 1, - }, - { - id: 'dmn_2', - name: 'acme.dev', - enrollmentMode: 'manual_invitation', - verification: { status: 'unverified' }, - totalPendingInvitations: 0, - totalPendingSuggestions: 0, - }, -] as unknown as OrganizationDomainResource[]; - -export function Default() { - const [addVerifySnapshot, sendAddVerify] = useMachine(organizationProfileDomainsSectionAddVerifyMachine, { - context: { - createDomain: async (name: string) => { - await delay(); - return { id: 'dmn_new', name, verified: false }; - }, - prepareVerification: async () => { - await delay(); - }, - attemptVerification: async () => { - await delay(); - return { verified: true }; - }, - updateEnrollmentMode: async () => { - await delay(); - }, - }, - }); - - const [enrollmentSnapshot, sendEnrollment, enrollmentActor] = useMachine( - organizationProfileDomainsSectionEnrollmentMachine, - { - context: { - updateEnrollmentMode: async () => { - await delay(); - }, - }, - }, - ); - - const [removeSnapshot, sendRemove] = useMachine(organizationProfileDomainsSectionRemoveMachine, { - context: { - deleteDomain: async () => { - await delay(); - }, - }, - }); - - return ( - {} }} - enrollmentOptions={ENROLLMENT_OPTIONS} - addVerify={{ snapshot: addVerifySnapshot, send: sendAddVerify }} - enrollment={{ - snapshot: enrollmentSnapshot, - send: sendEnrollment, - canSubmit: enrollmentActor.can({ type: 'SUBMIT' }), - }} - remove={{ snapshot: removeSnapshot, send: sendRemove }} - /> - ); -} diff --git a/packages/swingset/src/stories/organization-profile-general-panel.mdx b/packages/swingset/src/stories/organization-profile-general-panel.mdx deleted file mode 100644 index a5f5e040aa8..00000000000 --- a/packages/swingset/src/stories/organization-profile-general-panel.mdx +++ /dev/null @@ -1,27 +0,0 @@ -import * as OrganizationProfileGeneralPanelStories from './organization-profile-general-panel.stories'; - -# Organization Profile General Panel - -The General tab panel of the Organization Profile — composes the organization-level sections shown under "General". - - diff --git a/packages/swingset/src/stories/organization-profile-general-panel.stories.tsx b/packages/swingset/src/stories/organization-profile-general-panel.stories.tsx deleted file mode 100644 index 54ed789909a..00000000000 --- a/packages/swingset/src/stories/organization-profile-general-panel.stories.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import { OrganizationProfileGeneralPanelView } from '@clerk/ui/mosaic/organization/organization-profile-general-panel-view'; - -import type { StoryMeta } from '@/lib/types'; - -import { Default as OrganizationProfileDeleteSectionDemo } from './organization-profile-delete-section.stories'; -import { Default as OrganizationProfileDomainsSectionDemo } from './organization-profile-domains-section.stories'; -import { Default as OrganizationProfileLeaveSectionDemo } from './organization-profile-leave-section.stories'; -import { Default as OrganizationProfileProfileSectionDemo } from './organization-profile-profile-section.stories'; - -export const meta: StoryMeta = { - group: 'Organization', - title: 'OrganizationProfileGeneralPanel', - source: 'packages/ui/src/mosaic/organization/organization-profile-general-panel.tsx', -}; - -export function Default() { - return ( - } - domains={} - leaveOrganization={} - deleteOrganization={} - /> - ); -} diff --git a/packages/swingset/src/stories/organization-profile-leave-section.mdx b/packages/swingset/src/stories/organization-profile-leave-section.mdx deleted file mode 100644 index ce0be64a871..00000000000 --- a/packages/swingset/src/stories/organization-profile-leave-section.mdx +++ /dev/null @@ -1,16 +0,0 @@ -import * as OrganizationProfileLeaveSectionStories from './organization-profile-leave-section.stories'; - -# Organization Profile Leave Section - -A section that owns the open/leaving state and wires the `Destructive` block to the leave-organization flow. - - diff --git a/packages/swingset/src/stories/organization-profile-leave-section.stories.tsx b/packages/swingset/src/stories/organization-profile-leave-section.stories.tsx deleted file mode 100644 index e98a3111159..00000000000 --- a/packages/swingset/src/stories/organization-profile-leave-section.stories.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import { useMachine } from '@clerk/ui/mosaic/machine/useMachine'; -import { organizationProfileLeaveSectionMachine } from '@clerk/ui/mosaic/organization/organization-profile-leave-section.machine'; -import { OrganizationProfileLeaveSectionView } from '@clerk/ui/mosaic/organization/organization-profile-leave-section.view'; - -import type { StoryMeta } from '@/lib/types'; - -export const meta: StoryMeta = { - group: 'Organization', - title: 'OrganizationProfileLeaveSection', - source: 'packages/ui/src/mosaic/organization/organization-profile-leave-section.tsx', -}; - -export function Default() { - const [snapshot, send, actor] = useMachine(organizationProfileLeaveSectionMachine, { - context: { - organizationName: 'Acme Inc', - leaveOrganization: () => new Promise(resolve => setTimeout(resolve, 800)), - }, - }); - - return ( - - ); -} diff --git a/packages/swingset/src/stories/organization-profile-members-panel.mdx b/packages/swingset/src/stories/organization-profile-members-panel.mdx deleted file mode 100644 index 10bca05d65f..00000000000 --- a/packages/swingset/src/stories/organization-profile-members-panel.mdx +++ /dev/null @@ -1,17 +0,0 @@ -import * as OrganizationProfileMembersPanelStories from './organization-profile-members-panel.stories'; - -# Organization Profile Members Panel - -The Members tab panel of the Organization Profile — lists an organization's active members and owns the search and remove flows. It gates on `org:sys_memberships:read` to view and `org:sys_memberships:manage` to remove a member. - - diff --git a/packages/swingset/src/stories/organization-profile-members-panel.stories.tsx b/packages/swingset/src/stories/organization-profile-members-panel.stories.tsx deleted file mode 100644 index 8191d386541..00000000000 --- a/packages/swingset/src/stories/organization-profile-members-panel.stories.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import { useMachine } from '@clerk/ui/mosaic/machine/useMachine'; -import type { MemberRow } from '@clerk/ui/mosaic/organization/organization-profile-members-panel.controller'; -import { organizationProfileMembersPanelMachine } from '@clerk/ui/mosaic/organization/organization-profile-members-panel.machine'; -import { OrganizationProfileMembersPanelView } from '@clerk/ui/mosaic/organization/organization-profile-members-panel.view'; -import { useState } from 'react'; - -import type { StoryMeta } from '@/lib/types'; - -export const meta: StoryMeta = { - group: 'Organization', - title: 'OrganizationProfileMembersPanel', - source: 'packages/ui/src/mosaic/organization/organization-profile-members-panel.tsx', -}; - -const DEMO_MEMBERS: Omit[] = [ - { - id: 'mem_1', - name: 'Ada Lovelace', - identifier: 'ada@example.com', - roleLabel: 'Admin', - joinedAt: '1/12/2024', - isCurrentUser: true, - isBanned: false, - }, - { - id: 'mem_2', - name: 'Alan Turing', - identifier: 'alan@example.com', - roleLabel: 'Member', - joinedAt: '3/4/2024', - isCurrentUser: false, - isBanned: false, - }, - { - id: 'mem_3', - name: 'Grace Hopper', - identifier: 'grace@example.com', - roleLabel: 'Member', - joinedAt: '6/9/2024', - isCurrentUser: false, - isBanned: true, - }, -]; - -export function Default() { - const [snapshot, send] = useMachine(organizationProfileMembersPanelMachine); - const [page, setPage] = useState(1); - - const rows: MemberRow[] = DEMO_MEMBERS.map(member => ({ - ...member, - onRemove: () => - send({ - type: 'REMOVE_MEMBER', - membershipId: member.id, - // Simulate the network round-trip so the row shows its "Removing…" state. - run: () => new Promise(resolve => setTimeout(resolve, 700)), - }), - })); - - return ( - - ); -} diff --git a/packages/swingset/src/stories/organization-profile-profile-section.mdx b/packages/swingset/src/stories/organization-profile-profile-section.mdx deleted file mode 100644 index 5bba1e87ce1..00000000000 --- a/packages/swingset/src/stories/organization-profile-profile-section.mdx +++ /dev/null @@ -1,15 +0,0 @@ -import * as OrganizationProfileProfileSectionStories from './organization-profile-profile-section.stories'; - -# Organization Profile Profile Section - -A section that owns the open/editing/saving state for an organization's name and slug, wiring a `Dialog` around the edit form. Edits live as machine-owned drafts that fall through to the committed organization values, so the form seeds itself and closes on a successful save without a syncing effect. - - diff --git a/packages/swingset/src/stories/organization-profile-profile-section.stories.tsx b/packages/swingset/src/stories/organization-profile-profile-section.stories.tsx deleted file mode 100644 index 5ea047435f3..00000000000 --- a/packages/swingset/src/stories/organization-profile-profile-section.stories.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import { useMachine } from '@clerk/ui/mosaic/machine/useMachine'; -import { OrganizationProfileProfileSectionView } from '@clerk/ui/mosaic/organization/organization-profile-profile-section.view'; -import { organizationProfileProfileSectionDetailsMachine } from '@clerk/ui/mosaic/organization/organization-profile-profile-section-details.machine'; - -import type { StoryMeta } from '@/lib/types'; - -export const meta: StoryMeta = { - group: 'Organization', - title: 'OrganizationProfileProfileSection', - source: 'packages/ui/src/mosaic/organization/organization-profile-profile-section.tsx', -}; - -export function Default() { - const [snapshot, send, actor] = useMachine(organizationProfileProfileSectionDetailsMachine, { - context: { - committedName: 'Acme Inc', - committedSlug: 'acme', - slugEnabled: true, - updateOrganization: () => new Promise(resolve => setTimeout(resolve, 800)), - }, - }); - - return ( - - ); -} diff --git a/packages/swingset/src/stories/organization-profile.mdx b/packages/swingset/src/stories/organization-profile.mdx deleted file mode 100644 index b8ef6cd48be..00000000000 --- a/packages/swingset/src/stories/organization-profile.mdx +++ /dev/null @@ -1,23 +0,0 @@ -import * as OrganizationProfileStories from './organization-profile.stories'; - -# Organization Profile - -The full Organization Profile AIO — lays out the organization panels under a tabbed shell as a single view. - - diff --git a/packages/swingset/src/stories/organization-profile.stories.tsx b/packages/swingset/src/stories/organization-profile.stories.tsx deleted file mode 100644 index 0d389ec1ab2..00000000000 --- a/packages/swingset/src/stories/organization-profile.stories.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import { OrganizationProfileView } from '@clerk/ui/mosaic/organization/organization-profile-view'; - -import type { StoryMeta } from '@/lib/types'; - -import { Default as OrganizationProfileApiKeysPanelDemo } from './organization-profile-api-keys-panel.stories'; -import { Default as OrganizationProfileGeneralPanelDemo } from './organization-profile-general-panel.stories'; -import { Default as OrganizationProfileMembersPanelDemo } from './organization-profile-members-panel.stories'; - -export const meta: StoryMeta = { - group: 'Organization', - title: 'OrganizationProfile', - source: 'packages/ui/src/mosaic/organization/organization-profile.tsx', -}; - -export function Default() { - return ( - } - members={} - apiKeys={} - /> - ); -} diff --git a/packages/swingset/src/stories/popover.component.mdx b/packages/swingset/src/stories/popover.component.mdx index e1b8e2aecad..52350546230 100644 --- a/packages/swingset/src/stories/popover.component.mdx +++ b/packages/swingset/src/stories/popover.component.mdx @@ -186,7 +186,7 @@ them through your own typography (`Heading`, `Text`) inside the surface. ## Styling -Unlike the slot-recipe components, the Mosaic popover is themed with **StyleX**. Each styled part +The Mosaic popover is themed with **StyleX**. Each styled part carries a stable `.cl-` class (the slots in the table above) alongside the StyleX atoms. Consumers never target the hashed atomic classes — override by targeting the `.cl-*` slot from a CSS layer that wins over `@clerk/ui/styles.css`: diff --git a/packages/swingset/src/stories/popover.component.stories.tsx b/packages/swingset/src/stories/popover.component.stories.tsx index b775dbc7f64..60f8dc5af1e 100644 --- a/packages/swingset/src/stories/popover.component.stories.tsx +++ b/packages/swingset/src/stories/popover.component.stories.tsx @@ -1,4 +1,3 @@ -/** @jsxImportSource @emotion/react */ import { Button } from '@clerk/ui/mosaic/components/button'; import { Card } from '@clerk/ui/mosaic/components/card'; import { Heading } from '@clerk/ui/mosaic/components/heading'; @@ -15,7 +14,6 @@ export const meta: StoryMeta = { group: 'Components', title: 'Popover', source: 'packages/ui/src/mosaic/components/popover/popover.tsx', - styleEngine: 'stylex', }; export function Default() { diff --git a/packages/swingset/src/stories/scroll-area.stories.tsx b/packages/swingset/src/stories/scroll-area.stories.tsx index 3e64cd2b3ab..17994801316 100644 --- a/packages/swingset/src/stories/scroll-area.stories.tsx +++ b/packages/swingset/src/stories/scroll-area.stories.tsx @@ -1,4 +1,3 @@ -/** @jsxImportSource @emotion/react */ import { Avatar } from '@clerk/ui/mosaic/components/avatar'; import { Button } from '@clerk/ui/mosaic/components/button'; import { Item } from '@clerk/ui/mosaic/components/item'; @@ -212,8 +211,11 @@ export function HoverReveal() {
{manyRows.map(name => ( @@ -236,29 +238,36 @@ export function ThemedScrollbar() { const root = stylex.props(scrollAreaRoot); return ( -
- - {manyRows.map(name => ( - - ))} - -
+ <> + {/* Both halves of the amber → teal pair live here: an inline base value would outrank the + hover rule, since inline styles beat any stylesheet selector. */} + +
+ + {manyRows.map(name => ( + + ))} + +
+ ); } diff --git a/packages/swingset/src/stories/section.stories.tsx b/packages/swingset/src/stories/section.stories.tsx index 387b5173464..44dfec684c8 100644 --- a/packages/swingset/src/stories/section.stories.tsx +++ b/packages/swingset/src/stories/section.stories.tsx @@ -1,4 +1,3 @@ -/** @jsxImportSource @emotion/react */ import { Avatar } from '@clerk/ui/mosaic/components/avatar'; import { Badge } from '@clerk/ui/mosaic/components/badge'; import { Button } from '@clerk/ui/mosaic/components/button'; @@ -25,7 +24,6 @@ export const meta: StoryMeta = { group: 'Components', title: 'Section', source: 'packages/ui/src/mosaic/components/section/section.tsx', - styleEngine: 'stylex', }; export function Default() { diff --git a/packages/swingset/src/stories/tabs.component.mdx b/packages/swingset/src/stories/tabs.component.mdx deleted file mode 100644 index d22e625cfd8..00000000000 --- a/packages/swingset/src/stories/tabs.component.mdx +++ /dev/null @@ -1,58 +0,0 @@ -import * as TabsStories from './tabs.component.stories'; - -# Tabs - -The styled Mosaic `Tabs` — the headless `@clerk/headless` tabs primitives composed with Mosaic -slot recipes. It inherits selection state, roving-tabindex keyboard navigation, and ARIA wiring -(`role="tablist"` / `tab` / `tabpanel`) from the primitive, and adds Mosaic's themed styling for -each part. Slot identity (`data-cl-slot`) is applied by this styled layer, not by the headless -parts. - -## Example - -Click a tab or focus the list and use the arrow keys. The active tab is tracked by a sliding -`Indicator`; the third tab is `disabled`. - - - -## Usage - -```tsx -import { Tabs } from '@clerk/ui/mosaic/components/tabs'; - - - - Account - Password - - - Manage your account settings here. - Change your password here. -; -``` - -`Tab` participates in roving-tabindex keyboard navigation (arrow keys move focus). `Trigger` is a -click-only alternative for cases that don't want keyboard roving. Any part accepts a `render` prop -for polymorphic rendering — pass a function that receives the part's computed props and spreads -them onto your element. - -## Parts - -| Part | Slot | Description | -| ---------------- | ---------------- | ------------------------------------------------------------ | -| `Tabs.Root` | none (context) | Owns the selected value, orientation, and activation mode | -| `Tabs.List` | `tabs-list` | `role="tablist"` container; anchors the indicator | -| `Tabs.Tab` | `tabs-tab` | Selectable tab with roving-tabindex keyboard navigation | -| `Tabs.Trigger` | `tabs-trigger` | Click-only tab (no keyboard roving) | -| `Tabs.Panel` | `tabs-panel` | `role="tabpanel"`; hidden via `data-hidden` when inactive | -| `Tabs.Indicator` | `tabs-indicator` | Sliding underline tracking the active tab (positions inline) | - -## Styling - -Each styled part is themed by the Mosaic tabs recipe and stays targetable through its -`data-cl-slot` plus the state attributes the primitive emits (`data-selected`, -`data-disabled` on tabs; `data-hidden` on panels). Override per slot through -`appearance.elements` — e.g. `{ 'tabs-tab': { fontWeight: 600 } }`. diff --git a/packages/swingset/src/stories/tabs.component.stories.tsx b/packages/swingset/src/stories/tabs.component.stories.tsx deleted file mode 100644 index b037ef09938..00000000000 --- a/packages/swingset/src/stories/tabs.component.stories.tsx +++ /dev/null @@ -1,35 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import { Tabs } from '@clerk/ui/mosaic/components/tabs'; - -import type { StoryMeta } from '@/lib/types'; - -// Exposes this file's own source (via the `?raw` webpack rule) so each `` example -// renders a code footer with its function's source. See `StoryModule.__source`. -export { default as __source } from './tabs.component.stories?raw'; - -export const meta: StoryMeta = { - group: 'Components', - title: 'Tabs', - source: 'packages/ui/src/mosaic/components/tabs.tsx', -}; - -export function Default() { - return ( - - - Account - Password - - Disabled - - - - Manage your account settings here. - Change your password here. - This panel is unreachable. - - ); -} diff --git a/packages/swingset/src/stories/text.stories.tsx b/packages/swingset/src/stories/text.stories.tsx index 0b5a4860917..2fba0639dcf 100644 --- a/packages/swingset/src/stories/text.stories.tsx +++ b/packages/swingset/src/stories/text.stories.tsx @@ -1,4 +1,3 @@ -/** @jsxImportSource @emotion/react */ import type { TextProps } from '@clerk/ui/mosaic/components/text'; import { Text } from '@clerk/ui/mosaic/components/text'; @@ -12,7 +11,6 @@ export const meta: StoryMeta = { group: 'Components', title: 'Text', source: 'packages/ui/src/mosaic/components/text/text.tsx', - styleEngine: 'stylex', styles: { _variants: { size: { xs: {}, sm: {}, base: {}, lg: {}, xl: {}, '2xl': {} }, diff --git a/packages/swingset/src/stories/user-button.stories.tsx b/packages/swingset/src/stories/user-button.stories.tsx index 88cbe8836f3..06e1b30a79b 100644 --- a/packages/swingset/src/stories/user-button.stories.tsx +++ b/packages/swingset/src/stories/user-button.stories.tsx @@ -1,4 +1,3 @@ -/** @jsxImportSource @emotion/react */ import { Icon } from '@clerk/ui/mosaic/components/icon'; import { userButtonBusyKeys, diff --git a/packages/swingset/src/types/global.d.ts b/packages/swingset/src/types/global.d.ts index 5478227e618..4fbe7772b46 100644 --- a/packages/swingset/src/types/global.d.ts +++ b/packages/swingset/src/types/global.d.ts @@ -1,5 +1,3 @@ -/// - declare module '*.css'; declare module '*.mdx' { diff --git a/packages/swingset/src/types/react.d.ts b/packages/swingset/src/types/react.d.ts new file mode 100644 index 00000000000..cec85bdcf3a --- /dev/null +++ b/packages/swingset/src/types/react.d.ts @@ -0,0 +1,8 @@ +import type {} from 'react'; + +declare module 'react' { + // Mosaic is themed through `--cl-*` custom properties, which stories set inline. + interface CSSProperties { + [key: `--${string}`]: string | number | undefined; + } +} diff --git a/packages/ui/src/mosaic/MosaicProvider.tsx b/packages/ui/src/mosaic/MosaicProvider.tsx index 1a817750aed..60e2b464d01 100644 --- a/packages/ui/src/mosaic/MosaicProvider.tsx +++ b/packages/ui/src/mosaic/MosaicProvider.tsx @@ -1,126 +1,19 @@ -// eslint-disable-next-line no-restricted-imports -import createCache from '@emotion/cache'; -// eslint-disable-next-line no-restricted-imports -import { CacheProvider } from '@emotion/react'; import React from 'react'; -import type { MosaicAppearance } from './appearance'; -import { MosaicAppearanceProvider, MosaicIconsProvider, parseMosaicAppearance } from './appearance'; -import type { MosaicTheme } from './variables'; -import { defaultMosaicVariables, resolveVariables } from './variables'; - -const INSERTION_POINT_ID = 'cl-mosaic-style-insertion-point'; - -// Anchor Emotion's