Skip to content

Commit 9a52bc7

Browse files
feat(ui): add Mosaic Avatar component (#9230)
1 parent f67a641 commit 9a52bc7

12 files changed

Lines changed: 527 additions & 13 deletions

File tree

.changeset/mosaic-avatar.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/ui': minor
3+
---
4+
5+
Add a new Mosaic `Avatar` compound component (StyleX). `Avatar.Root` owns `shape` (`circle` | `square`) and `size` (`lg` | `md` | `sm` | `xs`); compose `Avatar.Image` (renders once the image loads) and `Avatar.Fallback` (shown while the image is pending or has failed, with an optional `delayMs`) inside it.

.claude/skills/clerk-monorepo/references/setup-and-footguns.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,20 @@ reports here.
3535

3636
## Footguns
3737

38-
| Trap | Symptom | Fix |
39-
| ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
40-
| System Node instead of 24.15 | Build "passes" but `Cannot find module @clerk/...` or missing types appear later | `node --version`; `nvm use` (`.nvmrc` pins 24.15.0) |
41-
| `pnpm install` before `corepack enable` (or using npm/yarn) | `preinstall` aborts with an "only pnpm allowed" error | `corepack enable`, then `pnpm install` |
42-
| Installing from a package subdirectory | Workspace links incomplete; runtime `Cannot resolve @clerk/shared` | Always `pnpm install` from the repo root |
43-
| `pnpm dev` before `pnpm build` | Watch mode emits broken output; phantom type errors | Run `pnpm build` once first, then `dev` |
44-
| Stale turbo cache after a Node/pnpm change | Old code runs, types do not update, unrelated tests fail | `pnpm nuke` (removes `.turbo`, `node_modules`, `dist`, coverage), then `pnpm install && pnpm build` |
45-
| `pnpm --filter <pkg> build/test` expecting deps to build | Filtered pnpm scripts skip turbo's `^build`, so deps may be stale | Use `pnpm turbo build/test --filter=@clerk/<pkg>` to include dependencies |
46-
| Stale `@clerk/shared` / types after editing shared | Type errors that "should not" exist in consumers | `pnpm turbo build --filter=@clerk/shared` |
47-
| Editing the hosted UI but seeing no change | `ui` ships as its own `ui.browser.js` loaded alongside `clerk-js`; you watched the wrong target | Use `pnpm dev:fe-libs` so `ui` + `clerk-js` rebuild together |
48-
| Committing integration secrets | `integration/.env.local`, `.keys.json`, `.keys.staging.json`, or `certs/sessions*.pem` leaked | Generated/fetched locally and gitignored; never add them. Under `certs/` only `sessions.pem` / `sessions-key.pem` are ignored, so keep other cert names out of git |
49-
| Running integration tests without 1Password set up | `pnpm integration:secrets` fails to read from 1Password | Install the `op` CLI and enable desktop-app integration (below) |
50-
| First `pnpm install` "hangs" | Large monorepo, large lockfile | Expected for the first run; give it a few minutes before assuming failure |
38+
| Trap | Symptom | Fix |
39+
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
40+
| System Node instead of 24.15 | Build "passes" but `Cannot find module @clerk/...` or missing types appear later | `node --version`; `nvm use` (`.nvmrc` pins 24.15.0) |
41+
| `pnpm install` before `corepack enable` (or using npm/yarn) | `preinstall` aborts with an "only pnpm allowed" error | `corepack enable`, then `pnpm install` |
42+
| Installing from a package subdirectory | Workspace links incomplete; runtime `Cannot resolve @clerk/shared` | Always `pnpm install` from the repo root |
43+
| `pnpm dev` before `pnpm build` | Watch mode emits broken output; phantom type errors | Run `pnpm build` once first, then `dev` |
44+
| Stale turbo cache after a Node/pnpm change | Old code runs, types do not update, unrelated tests fail | `pnpm nuke` (removes `.turbo`, `node_modules`, `dist`, coverage), then `pnpm install && pnpm build` |
45+
| `pnpm --filter <pkg> build/test` expecting deps to build | Filtered pnpm scripts skip turbo's `^build`, so deps may be stale | Use `pnpm turbo build/test --filter=@clerk/<pkg>` to include dependencies |
46+
| Stale `@clerk/shared` / types after editing shared | Type errors that "should not" exist in consumers | `pnpm turbo build --filter=@clerk/shared` |
47+
| Editing the hosted UI but seeing no change | `ui` ships as its own `ui.browser.js` loaded alongside `clerk-js`; you watched the wrong target | Use `pnpm dev:fe-libs` so `ui` + `clerk-js` rebuild together |
48+
| Committing integration secrets | `integration/.env.local`, `.keys.json`, `.keys.staging.json`, or `certs/sessions*.pem` leaked | Generated/fetched locally and gitignored; never add them. Under `certs/` only `sessions.pem` / `sessions-key.pem` are ignored, so keep other cert names out of git |
49+
| Running integration tests without 1Password set up | `pnpm integration:secrets` fails to read from 1Password | Install the `op` CLI and enable desktop-app integration (below) |
50+
| First `pnpm install` "hangs" | Large monorepo, large lockfile | Expected for the first run; give it a few minutes before assuming failure |
51+
| Adding an `import` in a separate edit before its first use | On-save lint-fix (`unused-imports/no-unused-imports`, an `error` in `eslint.config.mjs`) deletes the not-yet-referenced import; the next edit that adds the usage then throws `X is not defined` at runtime. Common when wiring a new export across files (e.g. swingset `registry.ts` + a `*.stories.tsx`). | Add the import and its first usage in the **same** edit, or add the usage first. After a multi-file wiring change, `grep` the new symbol to confirm both its `import` and its use survived before committing. |
5152

5253
## Unit tests vs integration tests
5354

packages/swingset/CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ These require reading several files together; the `README.md` covers the step-by
2525
- **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<string, unknown>` and cast to the real prop type.
2626

2727
- **`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`.
28+
- ⚠️ **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`.)
2829

2930
- **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`.
3031

packages/swingset/src/components/DocsViewer.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const docModules: Record<string, Record<string, React.ComponentType>> = {
2828
destructive: dynamic(() => import('../stories/destructive.mdx')),
2929
},
3030
components: {
31+
avatar: dynamic(() => import('../stories/avatar.mdx')),
3132
badge: dynamic(() => import('../stories/badge.mdx')),
3233
button: dynamic(() => import('../stories/button.mdx')),
3334
card: dynamic(() => import('../stories/card.component.mdx')),

packages/swingset/src/lib/registry.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
// Import stories explicitly to control order and avoid type casting through unknown.
22
import { meta as accordionMeta } from '../stories/accordion.stories';
33
import { meta as autocompleteMeta } from '../stories/autocomplete.stories';
4+
import {
5+
Fallback as AvatarFallbackStory,
6+
meta as avatarMeta,
7+
Primary as AvatarPrimary,
8+
Shapes as AvatarShapes,
9+
Sizes as AvatarSizes,
10+
} from '../stories/avatar.stories';
411
import {
512
Colors as BadgeColors,
613
meta as badgeMeta,
@@ -121,6 +128,14 @@ const organizationProfileMembersPanelModule: StoryModule = {
121128

122129
const cardComponentModule: StoryModule = { meta: cardComponentMeta, Default: CardDefault, Centered: CardCentered };
123130

131+
const avatarModule: StoryModule = {
132+
meta: avatarMeta,
133+
Primary: AvatarPrimary,
134+
Fallback: AvatarFallbackStory,
135+
Sizes: AvatarSizes,
136+
Shapes: AvatarShapes,
137+
};
138+
124139
const badgeModule: StoryModule = {
125140
meta: badgeMeta,
126141
Primary: BadgePrimary,
@@ -184,6 +199,7 @@ export const registry: StoryModule[] = [
184199
// Blocks
185200
destructiveModule,
186201
// Components
202+
avatarModule,
187203
badgeModule,
188204
buttonModule,
189205
cardComponentModule,
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import * as AvatarStories from './avatar.stories';
2+
3+
# Avatar
4+
5+
Avatar represents a user or entity as an image, falling back to initials or an icon when the image is missing or fails to load. It is a compound component: `Avatar.Root` clips and sizes the box, `Avatar.Image` renders the picture once it loads, and `Avatar.Fallback` shows until then.
6+
7+
## Playground
8+
9+
<Preview
10+
name='Primary'
11+
storyModule={AvatarStories}
12+
/>
13+
14+
## Props
15+
16+
`shape` and `size` live on `Avatar.Root`:
17+
18+
<PropTable meta={AvatarStories.meta} />
19+
20+
## Usage
21+
22+
```tsx
23+
import { Avatar } from '@clerk/ui/mosaic/components/avatar';
24+
25+
<Avatar.Root>
26+
<Avatar.Image
27+
src='https://github.com/clerk.png'
28+
alt='@clerk'
29+
/>
30+
<Avatar.Fallback>CL</Avatar.Fallback>
31+
</Avatar.Root>;
32+
```
33+
34+
## Parts
35+
36+
| Part | Slot (`.cl-*`) | Description |
37+
| ----------------- | -------------------- | ---------------------------------------------------------------------- |
38+
| `Avatar.Root` | `cl-avatar` | Owns `shape` / `size`, clips its children to the shape. |
39+
| `Avatar.Image` | `cl-avatar-image` | Renders an `<img>` once the source loads; renders nothing until then. |
40+
| `Avatar.Fallback` | `cl-avatar-fallback` | Rendered while the image is pending or has failed. Optional `delayMs`. |
41+
42+
---
43+
44+
## Examples
45+
46+
### Fallback
47+
48+
When the image fails to load, the fallback stays visible.
49+
50+
<Story
51+
name='Fallback'
52+
storyModule={AvatarStories}
53+
/>
54+
55+
### Sizes
56+
57+
<Story
58+
name='Sizes'
59+
storyModule={AvatarStories}
60+
/>
61+
62+
### Shapes
63+
64+
<Story
65+
name='Shapes'
66+
storyModule={AvatarStories}
67+
/>

0 commit comments

Comments
 (0)