From e509c8a01c4797576fb54a4a4a03725f7fbf5f24 Mon Sep 17 00:00:00 2001 From: pasqualevitiello Date: Sun, 18 Oct 2026 19:26:34 +0200 Subject: [PATCH 1/7] feat(avatar): add AvatarGroup with clipped separation Overlapping avatars are separated by masking each one with its neighbour's circle, so the gap is transparent rather than a coloured ring. The stack keeps its shape on gradients, images and dark surfaces, where a ring tinted with the page background does not. - AvatarGroup, with `topmost` choosing which end stays a whole circle - AvatarGroupCount for the "+N" chip, labelled in muted foreground - Size, overlap, gap and initials nudge read inheritable --avatar-size, --avatar-overlap, --avatar-gap and --avatar-nudge, settable on the avatar, the group or any ancestor - Fallback initials shift out of the clipped edge so they read as centred in what is left of the circle - The cut is an SVG circle composited with mask-composite: subtract, which stays crisp at any avatar size and under browser zoom - An @supports guard degrades to a plain overlap on Chrome 111-119, where mask-composite would otherwise turn the bite into a union Avatar now sizes from --avatar-size and scales its type with it, and the image and fallback follow the root radius through rounded-[inherit], so a rounded avatar no longer repeats its radius. Co-Authored-By: Claude Opus 5 --- apps/ui/content/docs/components/avatar.mdx | 72 ++++++++++++++++- apps/ui/public/r/avatar.json | 2 +- apps/ui/public/r/p-avatar-13.json | 2 +- apps/ui/public/r/p-avatar-14.json | 2 +- apps/ui/public/r/p-avatar-15.json | 19 +++++ apps/ui/public/r/p-avatar-16.json | 19 +++++ apps/ui/public/r/p-avatar-3.json | 2 +- apps/ui/public/r/p-avatar-5.json | 2 +- apps/ui/public/r/registry.json | 34 ++++++++ apps/ui/registry.json | 34 ++++++++ apps/ui/registry/__index__.tsx | 36 +++++++++ .../default/particles/p-avatar-11.tsx | 2 +- .../default/particles/p-avatar-13.tsx | 23 +++--- .../default/particles/p-avatar-14.tsx | 23 +++--- .../default/particles/p-avatar-15.tsx | 34 ++++++++ .../default/particles/p-avatar-16.tsx | 36 +++++++++ .../registry/default/particles/p-avatar-3.tsx | 77 +++++++++++++------ .../registry/default/particles/p-avatar-5.tsx | 25 +++--- .../registry/default/particles/p-avatar-9.tsx | 2 +- apps/ui/registry/default/ui/avatar.tsx | 69 ++++++++++++++++- apps/ui/registry/registry-particles.ts | 16 ++++ packages/ui/src/components/avatar.tsx | 69 ++++++++++++++++- 22 files changed, 530 insertions(+), 70 deletions(-) create mode 100644 apps/ui/public/r/p-avatar-15.json create mode 100644 apps/ui/public/r/p-avatar-16.json create mode 100644 apps/ui/registry/default/particles/p-avatar-15.tsx create mode 100644 apps/ui/registry/default/particles/p-avatar-16.tsx diff --git a/apps/ui/content/docs/components/avatar.mdx b/apps/ui/content/docs/components/avatar.mdx index 4c584671e..baa3b58e8 100644 --- a/apps/ui/content/docs/components/avatar.mdx +++ b/apps/ui/content/docs/components/avatar.mdx @@ -50,7 +50,13 @@ npm install @base-ui/react ## Usage ```tsx -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { + Avatar, + AvatarFallback, + AvatarGroup, + AvatarGroupCount, + AvatarImage, +} from "@/components/ui/avatar" ``` ```tsx @@ -78,7 +84,15 @@ Use `keepMounted` when the image relies on native lazy loading or an optimized i ### Avatar -Root component. Styled wrapper for `Avatar.Root` from Base UI with default size of 32px (8 units) and rounded-full styling. +Root component. Styled wrapper for `Avatar.Root` from Base UI with rounded-full styling. + +Sizing is driven by `--avatar-size`, which defaults to 32px and scales the font size with it. The property is inherited, so set it on the avatar itself or on any ancestor — an `AvatarGroup`, a card, a whole page: + +```tsx +… +``` + +A `size-*` utility still resizes a single avatar, but it leaves `--avatar-size` alone, so the font size and any group geometry stay at the inherited value. ### AvatarImage @@ -88,6 +102,45 @@ Image element for the avatar. Styled wrapper for `Avatar.Image` from Base UI wit Fallback content displayed when the image fails to load. Styled wrapper for `Avatar.Fallback` from Base UI with muted background. +### AvatarGroupCount + +The "+N" chip at the end of a group. It is an `Avatar` with a fallback, so it is sized, overlapped and clipped like its neighbours, and its label uses `text-muted-foreground`: + +```tsx + + … + +2 + +``` + +### AvatarGroup + +Lays out overlapping avatars. Each avatar is clipped by a circle matching its neighbor, so the avatars are separated by a transparent gap instead of a ring and the stack holds up on gradients, images and other non-solid surfaces. + +| Prop | Type | Default | Description | +| --------- | ------------------- | --------- | -------------------------------------------- | +| `topmost` | `"first" \| "last"` | `"first"` | Which end of the stack stays a whole circle. | + +The group derives its geometry from the `--avatar-size` it inherits, so sizing a stack is the same gesture as sizing one avatar. The clip math assumes every avatar in a group is the same size, so set the property on the group rather than on individual avatars. + +Fallback initials are nudged out of the clipped edge so they read as centered in what is left of the circle. The rest of the layout is driven by custom properties you can override: + +| Property | Default | Description | +| ------------------ | -------------------------------- | ---------------------------------------- | +| `--avatar-overlap` | `calc(var(--avatar-size) * 0.3)` | How far each avatar sits over the previous one. | +| `--avatar-gap` | `calc(var(--avatar-size) / 16)` | Thickness of the transparent gap. | +| `--avatar-nudge` | `calc((var(--avatar-overlap) + var(--avatar-gap)) * 0.3)` | How far initials shift out of the bite. Set to `0` to center them geometrically. | + +Like `--avatar-size`, all three are inherited, so you can set them on the group or on any ancestor to restyle every stack below it: + +```tsx + + … + +``` + +Masking replaces the whole avatar, so a status dot or badge placed on a clipped avatar is clipped with it. Put those on the avatar that stays whole. + ## Examples ### Fallback Only @@ -96,6 +149,8 @@ Fallback content displayed when the image fails to load. Styled wrapper for `Ava ### Different Sizes +Sizes are set with `--avatar-size` — 16, 24, 32, 40 and 48px below. Initials scale with the circle, so a fallback reads the same at every size. + ### Different Radius @@ -105,3 +160,16 @@ Fallback content displayed when the image fails to load. Styled wrapper for `Ava ### Group Avatars + +### Group Sizes + + + + + +### Last Avatar On Top + +By default the first avatar stays a whole circle and every avatar after it is clipped. Set `topmost="last"` for the conventional order, where the last avatar is the one left intact. + + + diff --git a/apps/ui/public/r/avatar.json b/apps/ui/public/r/avatar.json index 677171f31..ab48a3479 100644 --- a/apps/ui/public/r/avatar.json +++ b/apps/ui/public/r/avatar.json @@ -7,7 +7,7 @@ "files": [ { "path": "registry/default/ui/avatar.tsx", - "content": "\"use client\";\n\nimport { Avatar as AvatarPrimitive } from \"@base-ui/react/avatar\";\nimport type React from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nexport function Avatar({\n className,\n ...props\n}: AvatarPrimitive.Root.Props): React.ReactElement {\n return (\n \n );\n}\n\nexport function AvatarImage({\n className,\n ...props\n}: AvatarPrimitive.Image.Props): React.ReactElement {\n return (\n \n );\n}\n\nexport function AvatarFallback({\n className,\n ...props\n}: AvatarPrimitive.Fallback.Props): React.ReactElement {\n return (\n \n );\n}\n\nexport { AvatarPrimitive };\n", + "content": "\"use client\";\n\nimport { Avatar as AvatarPrimitive } from \"@base-ui/react/avatar\";\nimport type React from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nconst AVATAR_GROUP_CUT =\n \"url(\\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2 2'%3E%3Ccircle cx='1' cy='1' r='1'/%3E%3C/svg%3E\\\")\";\n\nexport function Avatar({\n className,\n ...props\n}: AvatarPrimitive.Root.Props): React.ReactElement {\n return (\n \n );\n}\n\nexport function AvatarImage({\n className,\n ...props\n}: AvatarPrimitive.Image.Props): React.ReactElement {\n return (\n \n );\n}\n\nexport function AvatarFallback({\n className,\n ...props\n}: AvatarPrimitive.Fallback.Props): React.ReactElement {\n return (\n \n );\n}\n\nexport interface AvatarGroupProps extends React.ComponentProps<\"div\"> {\n topmost?: \"first\" | \"last\";\n}\n\nexport function AvatarGroup({\n className,\n style,\n topmost = \"first\",\n ...props\n}: AvatarGroupProps): React.ReactElement {\n return (\n \n );\n}\n\nexport function AvatarGroupCount({\n children,\n className,\n ...props\n}: AvatarPrimitive.Root.Props): React.ReactElement {\n return (\n \n {children}\n \n );\n}\n\nexport { AvatarPrimitive };\n", "type": "registry:ui" } ], diff --git a/apps/ui/public/r/p-avatar-13.json b/apps/ui/public/r/p-avatar-13.json index 5929e2606..6693ebbc9 100644 --- a/apps/ui/public/r/p-avatar-13.json +++ b/apps/ui/public/r/p-avatar-13.json @@ -8,7 +8,7 @@ "files": [ { "path": "registry/default/particles/p-avatar-13.tsx", - "content": "import {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\n\nexport default function Particle() {\n return (\n
\n \n \n U1\n \n \n \n U2\n \n \n \n U3\n \n
\n );\n}\n", + "content": "import {\n Avatar,\n AvatarFallback,\n AvatarGroup,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\n\nexport default function Particle() {\n return (\n \n \n \n FK\n \n \n \n NE\n \n \n \n EW\n \n \n );\n}\n", "type": "registry:block" } ], diff --git a/apps/ui/public/r/p-avatar-14.json b/apps/ui/public/r/p-avatar-14.json index 656e83c07..9f03bfe04 100644 --- a/apps/ui/public/r/p-avatar-14.json +++ b/apps/ui/public/r/p-avatar-14.json @@ -8,7 +8,7 @@ "files": [ { "path": "registry/default/particles/p-avatar-14.tsx", - "content": "import {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\n\nexport default function Particle() {\n return (\n
\n \n \n U1\n \n \n \n U2\n \n \n \n U3\n \n
\n );\n}\n", + "content": "import {\n Avatar,\n AvatarFallback,\n AvatarGroup,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\n\nexport default function Particle() {\n return (\n \n \n \n FK\n \n \n \n NE\n \n \n \n EW\n \n \n );\n}\n", "type": "registry:block" } ], diff --git a/apps/ui/public/r/p-avatar-15.json b/apps/ui/public/r/p-avatar-15.json new file mode 100644 index 000000000..3a4037b58 --- /dev/null +++ b/apps/ui/public/r/p-avatar-15.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "p-avatar-15", + "description": "Avatar group with the last avatar on top", + "registryDependencies": [ + "@coss/avatar" + ], + "files": [ + { + "path": "registry/default/particles/p-avatar-15.tsx", + "content": "import {\n Avatar,\n AvatarFallback,\n AvatarGroup,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\n\nexport default function Particle() {\n return (\n \n \n \n FK\n \n \n \n NE\n \n \n \n EW\n \n \n );\n}\n", + "type": "registry:block" + } + ], + "categories": [ + "avatar" + ], + "type": "registry:block" +} \ No newline at end of file diff --git a/apps/ui/public/r/p-avatar-16.json b/apps/ui/public/r/p-avatar-16.json new file mode 100644 index 000000000..1292f88e0 --- /dev/null +++ b/apps/ui/public/r/p-avatar-16.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "p-avatar-16", + "description": "Avatar group with a count and the last avatar on top", + "registryDependencies": [ + "@coss/avatar" + ], + "files": [ + { + "path": "registry/default/particles/p-avatar-16.tsx", + "content": "import {\n Avatar,\n AvatarFallback,\n AvatarGroup,\n AvatarGroupCount,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\n\nexport default function Particle() {\n return (\n \n \n \n FK\n \n \n \n NE\n \n \n \n EW\n \n +2\n \n );\n}\n", + "type": "registry:block" + } + ], + "categories": [ + "avatar" + ], + "type": "registry:block" +} \ No newline at end of file diff --git a/apps/ui/public/r/p-avatar-3.json b/apps/ui/public/r/p-avatar-3.json index bdb073b40..14714628b 100644 --- a/apps/ui/public/r/p-avatar-3.json +++ b/apps/ui/public/r/p-avatar-3.json @@ -8,7 +8,7 @@ "files": [ { "path": "registry/default/particles/p-avatar-3.tsx", - "content": "import {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\n\nexport default function Particle() {\n return (\n
\n \n \n AV\n \n \n \n AV\n \n \n \n AV\n \n
\n );\n}\n", + "content": "import {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\n\nexport default function Particle() {\n return (\n
\n
\n \n \n AV\n \n \n \n AV\n \n \n \n AV\n \n \n \n AV\n \n \n \n AV\n \n
\n
\n \n AV\n \n \n AV\n \n \n AV\n \n \n AV\n \n \n AV\n \n
\n
\n );\n}\n", "type": "registry:block" } ], diff --git a/apps/ui/public/r/p-avatar-5.json b/apps/ui/public/r/p-avatar-5.json index a2dbb8f0e..e882b9a97 100644 --- a/apps/ui/public/r/p-avatar-5.json +++ b/apps/ui/public/r/p-avatar-5.json @@ -8,7 +8,7 @@ "files": [ { "path": "registry/default/particles/p-avatar-5.tsx", - "content": "import {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\n\nexport default function Particle() {\n return (\n
\n \n \n U1\n \n \n \n U2\n \n \n \n U3\n \n
\n );\n}\n", + "content": "import {\n Avatar,\n AvatarFallback,\n AvatarGroup,\n AvatarGroupCount,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\n\nexport default function Particle() {\n return (\n \n \n \n FK\n \n \n \n NE\n \n \n \n EW\n \n +2\n \n );\n}\n", "type": "registry:block" } ], diff --git a/apps/ui/public/r/registry.json b/apps/ui/public/r/registry.json index 796bc1326..2c7eeb4c2 100644 --- a/apps/ui/public/r/registry.json +++ b/apps/ui/public/r/registry.json @@ -1911,6 +1911,23 @@ ], "type": "registry:block" }, + { + "categories": [ + "avatar" + ], + "description": "Avatar group with a count and the last avatar on top", + "files": [ + { + "path": "registry/default/particles/p-avatar-16.tsx", + "type": "registry:block" + } + ], + "name": "p-avatar-16", + "registryDependencies": [ + "@coss/avatar" + ], + "type": "registry:block" + }, { "categories": [ "avatar" @@ -2069,6 +2086,23 @@ ], "type": "registry:block" }, + { + "categories": [ + "avatar" + ], + "description": "Avatar group with the last avatar on top", + "files": [ + { + "path": "registry/default/particles/p-avatar-15.tsx", + "type": "registry:block" + } + ], + "name": "p-avatar-15", + "registryDependencies": [ + "@coss/avatar" + ], + "type": "registry:block" + }, { "categories": [ "badge" diff --git a/apps/ui/registry.json b/apps/ui/registry.json index 796bc1326..2c7eeb4c2 100644 --- a/apps/ui/registry.json +++ b/apps/ui/registry.json @@ -1911,6 +1911,23 @@ ], "type": "registry:block" }, + { + "categories": [ + "avatar" + ], + "description": "Avatar group with a count and the last avatar on top", + "files": [ + { + "path": "registry/default/particles/p-avatar-16.tsx", + "type": "registry:block" + } + ], + "name": "p-avatar-16", + "registryDependencies": [ + "@coss/avatar" + ], + "type": "registry:block" + }, { "categories": [ "avatar" @@ -2069,6 +2086,23 @@ ], "type": "registry:block" }, + { + "categories": [ + "avatar" + ], + "description": "Avatar group with the last avatar on top", + "files": [ + { + "path": "registry/default/particles/p-avatar-15.tsx", + "type": "registry:block" + } + ], + "name": "p-avatar-15", + "registryDependencies": [ + "@coss/avatar" + ], + "type": "registry:block" + }, { "categories": [ "badge" diff --git a/apps/ui/registry/__index__.tsx b/apps/ui/registry/__index__.tsx index 3e7665705..dccc5207f 100644 --- a/apps/ui/registry/__index__.tsx +++ b/apps/ui/registry/__index__.tsx @@ -1761,6 +1761,24 @@ export const Index: Record = { categories: ["avatar"], meta: undefined, }, + "p-avatar-16": { + name: "p-avatar-16", + description: "Avatar group with a count and the last avatar on top", + type: "registry:block", + registryDependencies: ["@coss/avatar"], + files: [{ + path: "registry/default/particles/p-avatar-16.tsx", + type: "registry:block", + target: "" + }], + component: React.lazy(async () => { + const mod = await import("@/registry/default/particles/p-avatar-16.tsx") + const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name + return { default: mod.default || mod[exportName] } + }), + categories: ["avatar"], + meta: undefined, + }, "p-avatar-6": { name: "p-avatar-6", description: "Avatar with user icon fallback", @@ -1923,6 +1941,24 @@ export const Index: Record = { categories: ["avatar"], meta: undefined, }, + "p-avatar-15": { + name: "p-avatar-15", + description: "Avatar group with the last avatar on top", + type: "registry:block", + registryDependencies: ["@coss/avatar"], + files: [{ + path: "registry/default/particles/p-avatar-15.tsx", + type: "registry:block", + target: "" + }], + component: React.lazy(async () => { + const mod = await import("@/registry/default/particles/p-avatar-15.tsx") + const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name + return { default: mod.default || mod[exportName] } + }), + categories: ["avatar"], + meta: undefined, + }, "p-badge-1": { name: "p-badge-1", description: "Basic badge", diff --git a/apps/ui/registry/default/particles/p-avatar-11.tsx b/apps/ui/registry/default/particles/p-avatar-11.tsx index 44a73f3c1..58c637083 100644 --- a/apps/ui/registry/default/particles/p-avatar-11.tsx +++ b/apps/ui/registry/default/particles/p-avatar-11.tsx @@ -13,7 +13,7 @@ export default function Particle() { alt="User" src="https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=128&h=128&dpr=2&q=80" /> - LT + LT - + + - U1 + FK - + - U2 + NE - + - U3 + EW - + ); } diff --git a/apps/ui/registry/default/particles/p-avatar-14.tsx b/apps/ui/registry/default/particles/p-avatar-14.tsx index a189c73f3..e89fdd0c0 100644 --- a/apps/ui/registry/default/particles/p-avatar-14.tsx +++ b/apps/ui/registry/default/particles/p-avatar-14.tsx @@ -1,33 +1,34 @@ import { Avatar, AvatarFallback, + AvatarGroup, AvatarImage, } from "@/registry/default/ui/avatar"; export default function Particle() { return ( -
- + + - U1 + FK - + - U2 + NE - + - U3 + EW -
+ ); } diff --git a/apps/ui/registry/default/particles/p-avatar-15.tsx b/apps/ui/registry/default/particles/p-avatar-15.tsx new file mode 100644 index 000000000..b1f3fa1c5 --- /dev/null +++ b/apps/ui/registry/default/particles/p-avatar-15.tsx @@ -0,0 +1,34 @@ +import { + Avatar, + AvatarFallback, + AvatarGroup, + AvatarImage, +} from "@/registry/default/ui/avatar"; + +export default function Particle() { + return ( + + + + FK + + + + NE + + + + EW + + + ); +} diff --git a/apps/ui/registry/default/particles/p-avatar-16.tsx b/apps/ui/registry/default/particles/p-avatar-16.tsx new file mode 100644 index 000000000..dae6de617 --- /dev/null +++ b/apps/ui/registry/default/particles/p-avatar-16.tsx @@ -0,0 +1,36 @@ +import { + Avatar, + AvatarFallback, + AvatarGroup, + AvatarGroupCount, + AvatarImage, +} from "@/registry/default/ui/avatar"; + +export default function Particle() { + return ( + + + + FK + + + + NE + + + + EW + + +2 + + ); +} diff --git a/apps/ui/registry/default/particles/p-avatar-3.tsx b/apps/ui/registry/default/particles/p-avatar-3.tsx index 26c6cdaa8..a0a9e22af 100644 --- a/apps/ui/registry/default/particles/p-avatar-3.tsx +++ b/apps/ui/registry/default/particles/p-avatar-3.tsx @@ -6,28 +6,61 @@ import { export default function Particle() { return ( -
- - - AV - - - - AV - - - - AV - +
+
+ + + AV + + + + AV + + + + AV + + + + AV + + + + AV + +
+
+ + AV + + + AV + + + AV + + + AV + + + AV + +
); } diff --git a/apps/ui/registry/default/particles/p-avatar-5.tsx b/apps/ui/registry/default/particles/p-avatar-5.tsx index 13ca6396b..bfb4e6348 100644 --- a/apps/ui/registry/default/particles/p-avatar-5.tsx +++ b/apps/ui/registry/default/particles/p-avatar-5.tsx @@ -1,33 +1,36 @@ import { Avatar, AvatarFallback, + AvatarGroup, + AvatarGroupCount, AvatarImage, } from "@/registry/default/ui/avatar"; export default function Particle() { return ( -
- + + - U1 + FK - + - U2 + NE - + - U3 + EW -
+ +2 + ); } diff --git a/apps/ui/registry/default/particles/p-avatar-9.tsx b/apps/ui/registry/default/particles/p-avatar-9.tsx index 89955462c..316574702 100644 --- a/apps/ui/registry/default/particles/p-avatar-9.tsx +++ b/apps/ui/registry/default/particles/p-avatar-9.tsx @@ -12,7 +12,7 @@ export default function Particle() { alt="User" src="https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=128&h=128&dpr=2&q=80" /> - LT + LT Online { + topmost?: "first" | "last"; +} + +export function AvatarGroup({ + className, + style, + topmost = "first", + ...props +}: AvatarGroupProps): React.ReactElement { + return ( +
+ ); +} + +export function AvatarGroupCount({ + children, + className, + ...props +}: AvatarPrimitive.Root.Props): React.ReactElement { + return ( + + {children} + + ); +} + export { AvatarPrimitive }; diff --git a/apps/ui/registry/registry-particles.ts b/apps/ui/registry/registry-particles.ts index 6cd9b954a..db44a83b7 100644 --- a/apps/ui/registry/registry-particles.ts +++ b/apps/ui/registry/registry-particles.ts @@ -411,6 +411,14 @@ export const particles: ParticleItem[] = [ registryDependencies: ["@coss/avatar"], type: "registry:block", }, + { + categories: categories("avatar"), + description: "Avatar group with a count and the last avatar on top", + files: [{ path: "particles/p-avatar-16.tsx", type: "registry:block" }], + name: "p-avatar-16", + registryDependencies: ["@coss/avatar"], + type: "registry:block", + }, { categories: categories("avatar"), dependencies: ["lucide-react"], @@ -484,6 +492,14 @@ export const particles: ParticleItem[] = [ registryDependencies: ["@coss/avatar"], type: "registry:block", }, + { + categories: categories("avatar"), + description: "Avatar group with the last avatar on top", + files: [{ path: "particles/p-avatar-15.tsx", type: "registry:block" }], + name: "p-avatar-15", + registryDependencies: ["@coss/avatar"], + type: "registry:block", + }, { categories: categories("badge"), description: "Basic badge", diff --git a/packages/ui/src/components/avatar.tsx b/packages/ui/src/components/avatar.tsx index 6f658c559..ee3057bee 100644 --- a/packages/ui/src/components/avatar.tsx +++ b/packages/ui/src/components/avatar.tsx @@ -4,6 +4,9 @@ import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"; import { cn } from "@coss/ui/lib/utils"; import type React from "react"; +const AVATAR_GROUP_CUT = + "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2 2'%3E%3Ccircle cx='1' cy='1' r='1'/%3E%3C/svg%3E\")"; + export function Avatar({ className, ...props @@ -11,7 +14,7 @@ export function Avatar({ return ( { + topmost?: "first" | "last"; +} + +export function AvatarGroup({ + className, + style, + topmost = "first", + ...props +}: AvatarGroupProps): React.ReactElement { + return ( +
+ ); +} + +export function AvatarGroupCount({ + children, + className, + ...props +}: AvatarPrimitive.Root.Props): React.ReactElement { + return ( + + {children} + + ); +} + export { AvatarPrimitive }; From f68f88d6dd37acc81a6c20b5df5464525da95119 Mon Sep 17 00:00:00 2001 From: pasqualevitiello Date: Sun, 18 Oct 2026 19:58:47 +0200 Subject: [PATCH 2/7] refactor(avatar): use canonical mask utilities and size particles by var Swap the arbitrary mask properties for Tailwind's own utilities, which compile to identical CSS: mask-subtract, mask-no-repeat and the mask-size / mask-position functional forms. The supports-[] prefixes stay in bracket form, since an @supports condition takes a raw declaration rather than a utility name. Particles now size avatars through --avatar-size instead of a size-* class, so the fallback type scales with the circle rather than being hand-corrected per call site: - p-select-20 drops size-8 entirely, since it restated the default - p-button-28 and p-skeleton-1 move to --avatar-size, keeping their 24px and 40px boxes while the type follows - p-combobox-19/20, p-select-19 and p-group-23 move to --avatar-size and drop their text-[...] overrides on AvatarFallback No Avatar in the registry now carries a size-* class, and no AvatarFallback carries a text-* class; both dimensions come from the one custom property. Co-Authored-By: Claude Opus 5 --- apps/ui/public/r/avatar.json | 2 +- apps/ui/public/r/p-button-28.json | 2 +- apps/ui/public/r/p-combobox-19.json | 2 +- apps/ui/public/r/p-combobox-20.json | 2 +- apps/ui/public/r/p-group-23.json | 2 +- apps/ui/public/r/p-select-19.json | 2 +- apps/ui/public/r/p-select-20.json | 2 +- apps/ui/public/r/p-skeleton-1.json | 2 +- apps/ui/registry/default/particles/p-button-28.tsx | 2 +- apps/ui/registry/default/particles/p-combobox-19.tsx | 6 ++---- apps/ui/registry/default/particles/p-combobox-20.tsx | 6 ++---- apps/ui/registry/default/particles/p-group-23.tsx | 6 ++---- apps/ui/registry/default/particles/p-select-19.tsx | 12 ++++-------- apps/ui/registry/default/particles/p-select-20.tsx | 4 ++-- apps/ui/registry/default/particles/p-skeleton-1.tsx | 2 +- apps/ui/registry/default/ui/avatar.tsx | 4 ++-- packages/ui/src/components/avatar.tsx | 4 ++-- 17 files changed, 26 insertions(+), 36 deletions(-) diff --git a/apps/ui/public/r/avatar.json b/apps/ui/public/r/avatar.json index ab48a3479..61982d84c 100644 --- a/apps/ui/public/r/avatar.json +++ b/apps/ui/public/r/avatar.json @@ -7,7 +7,7 @@ "files": [ { "path": "registry/default/ui/avatar.tsx", - "content": "\"use client\";\n\nimport { Avatar as AvatarPrimitive } from \"@base-ui/react/avatar\";\nimport type React from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nconst AVATAR_GROUP_CUT =\n \"url(\\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2 2'%3E%3Ccircle cx='1' cy='1' r='1'/%3E%3C/svg%3E\\\")\";\n\nexport function Avatar({\n className,\n ...props\n}: AvatarPrimitive.Root.Props): React.ReactElement {\n return (\n \n );\n}\n\nexport function AvatarImage({\n className,\n ...props\n}: AvatarPrimitive.Image.Props): React.ReactElement {\n return (\n \n );\n}\n\nexport function AvatarFallback({\n className,\n ...props\n}: AvatarPrimitive.Fallback.Props): React.ReactElement {\n return (\n \n );\n}\n\nexport interface AvatarGroupProps extends React.ComponentProps<\"div\"> {\n topmost?: \"first\" | \"last\";\n}\n\nexport function AvatarGroup({\n className,\n style,\n topmost = \"first\",\n ...props\n}: AvatarGroupProps): React.ReactElement {\n return (\n \n );\n}\n\nexport function AvatarGroupCount({\n children,\n className,\n ...props\n}: AvatarPrimitive.Root.Props): React.ReactElement {\n return (\n \n {children}\n \n );\n}\n\nexport { AvatarPrimitive };\n", + "content": "\"use client\";\n\nimport { Avatar as AvatarPrimitive } from \"@base-ui/react/avatar\";\nimport type React from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nconst AVATAR_GROUP_CUT =\n \"url(\\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2 2'%3E%3Ccircle cx='1' cy='1' r='1'/%3E%3C/svg%3E\\\")\";\n\nexport function Avatar({\n className,\n ...props\n}: AvatarPrimitive.Root.Props): React.ReactElement {\n return (\n \n );\n}\n\nexport function AvatarImage({\n className,\n ...props\n}: AvatarPrimitive.Image.Props): React.ReactElement {\n return (\n \n );\n}\n\nexport function AvatarFallback({\n className,\n ...props\n}: AvatarPrimitive.Fallback.Props): React.ReactElement {\n return (\n \n );\n}\n\nexport interface AvatarGroupProps extends React.ComponentProps<\"div\"> {\n topmost?: \"first\" | \"last\";\n}\n\nexport function AvatarGroup({\n className,\n style,\n topmost = \"first\",\n ...props\n}: AvatarGroupProps): React.ReactElement {\n return (\n \n );\n}\n\nexport function AvatarGroupCount({\n children,\n className,\n ...props\n}: AvatarPrimitive.Root.Props): React.ReactElement {\n return (\n \n {children}\n \n );\n}\n\nexport { AvatarPrimitive };\n", "type": "registry:ui" } ], diff --git a/apps/ui/public/r/p-button-28.json b/apps/ui/public/r/p-button-28.json index da5cc68a0..a27e4eac0 100644 --- a/apps/ui/public/r/p-button-28.json +++ b/apps/ui/public/r/p-button-28.json @@ -9,7 +9,7 @@ "files": [ { "path": "registry/default/particles/p-button-28.tsx", - "content": "import {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\nimport { Button } from \"@/registry/default/ui/button\";\n\nexport default function Particle() {\n return (\n \n );\n}\n", + "content": "import {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\nimport { Button } from \"@/registry/default/ui/button\";\n\nexport default function Particle() {\n return (\n \n );\n}\n", "type": "registry:block" } ], diff --git a/apps/ui/public/r/p-combobox-19.json b/apps/ui/public/r/p-combobox-19.json index 21a141aa1..c1e9b04e1 100644 --- a/apps/ui/public/r/p-combobox-19.json +++ b/apps/ui/public/r/p-combobox-19.json @@ -14,7 +14,7 @@ "files": [ { "path": "registry/default/particles/p-combobox-19.tsx", - "content": "\"use client\";\n\nimport { SearchIcon, XIcon } from \"lucide-react\";\nimport { useState } from \"react\";\nimport {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\nimport { Badge } from \"@/registry/default/ui/badge\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport {\n Combobox,\n ComboboxEmpty,\n ComboboxInput,\n ComboboxItem,\n ComboboxList,\n ComboboxPopup,\n} from \"@/registry/default/ui/combobox\";\n\ntype TeamMember = {\n avatar: string;\n initials: string;\n label: string;\n priority: \"Lowest\" | \"Low\" | \"Medium\" | \"High\" | \"Highest\";\n value: string;\n weight: number;\n};\n\nconst teamMembers: TeamMember[] = [\n {\n avatar:\n \"https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=72&h=72&dpr=2&q=80\",\n initials: \"JH\",\n label: \"Jenny Hamilton\",\n priority: \"Highest\",\n value: \"jenny\",\n weight: 200,\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1628157588553-5eeea00af15c?w=72&h=72&dpr=2&q=80\",\n initials: \"PS\",\n label: \"Paul Smith\",\n priority: \"Medium\",\n value: \"paul\",\n weight: 100,\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1655874819398-c6dfbec68ac7?w=72&h=72&dpr=2&q=80\",\n initials: \"LW\",\n label: \"Luna Wyen\",\n priority: \"High\",\n value: \"luna\",\n weight: 150,\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=72&h=72&dpr=2&q=80\",\n initials: \"AC\",\n label: \"Alex Chen\",\n priority: \"Low\",\n value: \"alex\",\n weight: 100,\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=72&h=72&dpr=2&q=80\",\n initials: \"SJ\",\n label: \"Sarah Johnson\",\n priority: \"Medium\",\n value: \"sarah\",\n weight: 50,\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=72&h=72&dpr=2&q=80\",\n initials: \"ED\",\n label: \"Emma Davis\",\n priority: \"Lowest\",\n value: \"emma\",\n weight: 100,\n },\n];\n\nexport default function Particle() {\n const [open, setOpen] = useState(false);\n const [selected, setSelected] = useState(\n teamMembers.slice(0, 2),\n );\n\n return (\n
\n {\n setSelected(value);\n setOpen(false);\n }}\n open={open}\n value={selected}\n >\n }\n />\n \n No team members found.\n \n {(item: TeamMember) => (\n \n {item.label}\n \n )}\n \n \n \n {selected.length > 0 && (\n
    \n {selected.map((member) => (\n \n \n \n \n {member.initials}\n \n \n {member.label}\n \n {member.priority}\n \n \n {member.weight}%\n \n \n setSelected((current) =>\n current.filter((item) => item.value !== member.value),\n )\n }\n size=\"icon-xs\"\n variant=\"ghost\"\n >\n \n \n \n ))}\n
\n )}\n
\n );\n}\n", + "content": "\"use client\";\n\nimport { SearchIcon, XIcon } from \"lucide-react\";\nimport { useState } from \"react\";\nimport {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\nimport { Badge } from \"@/registry/default/ui/badge\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport {\n Combobox,\n ComboboxEmpty,\n ComboboxInput,\n ComboboxItem,\n ComboboxList,\n ComboboxPopup,\n} from \"@/registry/default/ui/combobox\";\n\ntype TeamMember = {\n avatar: string;\n initials: string;\n label: string;\n priority: \"Lowest\" | \"Low\" | \"Medium\" | \"High\" | \"Highest\";\n value: string;\n weight: number;\n};\n\nconst teamMembers: TeamMember[] = [\n {\n avatar:\n \"https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=72&h=72&dpr=2&q=80\",\n initials: \"JH\",\n label: \"Jenny Hamilton\",\n priority: \"Highest\",\n value: \"jenny\",\n weight: 200,\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1628157588553-5eeea00af15c?w=72&h=72&dpr=2&q=80\",\n initials: \"PS\",\n label: \"Paul Smith\",\n priority: \"Medium\",\n value: \"paul\",\n weight: 100,\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1655874819398-c6dfbec68ac7?w=72&h=72&dpr=2&q=80\",\n initials: \"LW\",\n label: \"Luna Wyen\",\n priority: \"High\",\n value: \"luna\",\n weight: 150,\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=72&h=72&dpr=2&q=80\",\n initials: \"AC\",\n label: \"Alex Chen\",\n priority: \"Low\",\n value: \"alex\",\n weight: 100,\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=72&h=72&dpr=2&q=80\",\n initials: \"SJ\",\n label: \"Sarah Johnson\",\n priority: \"Medium\",\n value: \"sarah\",\n weight: 50,\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=72&h=72&dpr=2&q=80\",\n initials: \"ED\",\n label: \"Emma Davis\",\n priority: \"Lowest\",\n value: \"emma\",\n weight: 100,\n },\n];\n\nexport default function Particle() {\n const [open, setOpen] = useState(false);\n const [selected, setSelected] = useState(\n teamMembers.slice(0, 2),\n );\n\n return (\n
\n {\n setSelected(value);\n setOpen(false);\n }}\n open={open}\n value={selected}\n >\n }\n />\n \n No team members found.\n \n {(item: TeamMember) => (\n \n {item.label}\n \n )}\n \n \n \n {selected.length > 0 && (\n
    \n {selected.map((member) => (\n \n \n \n {member.initials}\n \n {member.label}\n \n {member.priority}\n \n \n {member.weight}%\n \n \n setSelected((current) =>\n current.filter((item) => item.value !== member.value),\n )\n }\n size=\"icon-xs\"\n variant=\"ghost\"\n >\n \n \n \n ))}\n
\n )}\n
\n );\n}\n", "type": "registry:block" } ], diff --git a/apps/ui/public/r/p-combobox-20.json b/apps/ui/public/r/p-combobox-20.json index 0d1a22d6c..4d889628c 100644 --- a/apps/ui/public/r/p-combobox-20.json +++ b/apps/ui/public/r/p-combobox-20.json @@ -14,7 +14,7 @@ "files": [ { "path": "registry/default/particles/p-combobox-20.tsx", - "content": "\"use client\";\n\nimport { SearchIcon, XIcon } from \"lucide-react\";\nimport { useState } from \"react\";\nimport {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\nimport { Badge } from \"@/registry/default/ui/badge\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport {\n Combobox,\n ComboboxEmpty,\n ComboboxInput,\n ComboboxItem,\n ComboboxList,\n ComboboxPopup,\n} from \"@/registry/default/ui/combobox\";\n\ntype TeamMember = {\n avatar: string;\n initials: string;\n label: string;\n priority: \"Lowest\" | \"Low\" | \"Medium\" | \"High\" | \"Highest\";\n value: string;\n weight: number;\n};\n\nconst teamMembers: TeamMember[] = [\n {\n avatar:\n \"https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=72&h=72&dpr=2&q=80\",\n initials: \"JH\",\n label: \"Jenny Hamilton\",\n priority: \"Highest\",\n value: \"jenny\",\n weight: 200,\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1628157588553-5eeea00af15c?w=72&h=72&dpr=2&q=80\",\n initials: \"PS\",\n label: \"Paul Smith\",\n priority: \"Medium\",\n value: \"paul\",\n weight: 100,\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1655874819398-c6dfbec68ac7?w=72&h=72&dpr=2&q=80\",\n initials: \"LW\",\n label: \"Luna Wyen\",\n priority: \"High\",\n value: \"luna\",\n weight: 150,\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=72&h=72&dpr=2&q=80\",\n initials: \"AC\",\n label: \"Alex Chen\",\n priority: \"Low\",\n value: \"alex\",\n weight: 100,\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=72&h=72&dpr=2&q=80\",\n initials: \"SJ\",\n label: \"Sarah Johnson\",\n priority: \"Medium\",\n value: \"sarah\",\n weight: 50,\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=72&h=72&dpr=2&q=80\",\n initials: \"ED\",\n label: \"Emma Davis\",\n priority: \"Lowest\",\n value: \"emma\",\n weight: 100,\n },\n];\n\nexport default function Particle() {\n const [open, setOpen] = useState(false);\n const [selected, setSelected] = useState(\n teamMembers.slice(0, 2),\n );\n\n return (\n
\n {\n setSelected(value);\n setOpen(false);\n }}\n open={open}\n value={selected}\n >\n }\n />\n \n No team members found.\n \n {(item: TeamMember) => (\n \n {item.label}\n \n )}\n \n \n \n {selected.length > 0 && (\n
    \n {selected.map((member) => (\n \n \n \n \n {member.initials}\n \n \n {member.label}\n \n {member.priority}\n \n \n {member.weight}%\n \n \n setSelected((current) =>\n current.filter((item) => item.value !== member.value),\n )\n }\n size=\"icon-xs\"\n variant=\"ghost\"\n >\n \n \n \n ))}\n
\n )}\n
\n );\n}\n", + "content": "\"use client\";\n\nimport { SearchIcon, XIcon } from \"lucide-react\";\nimport { useState } from \"react\";\nimport {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\nimport { Badge } from \"@/registry/default/ui/badge\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport {\n Combobox,\n ComboboxEmpty,\n ComboboxInput,\n ComboboxItem,\n ComboboxList,\n ComboboxPopup,\n} from \"@/registry/default/ui/combobox\";\n\ntype TeamMember = {\n avatar: string;\n initials: string;\n label: string;\n priority: \"Lowest\" | \"Low\" | \"Medium\" | \"High\" | \"Highest\";\n value: string;\n weight: number;\n};\n\nconst teamMembers: TeamMember[] = [\n {\n avatar:\n \"https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=72&h=72&dpr=2&q=80\",\n initials: \"JH\",\n label: \"Jenny Hamilton\",\n priority: \"Highest\",\n value: \"jenny\",\n weight: 200,\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1628157588553-5eeea00af15c?w=72&h=72&dpr=2&q=80\",\n initials: \"PS\",\n label: \"Paul Smith\",\n priority: \"Medium\",\n value: \"paul\",\n weight: 100,\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1655874819398-c6dfbec68ac7?w=72&h=72&dpr=2&q=80\",\n initials: \"LW\",\n label: \"Luna Wyen\",\n priority: \"High\",\n value: \"luna\",\n weight: 150,\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=72&h=72&dpr=2&q=80\",\n initials: \"AC\",\n label: \"Alex Chen\",\n priority: \"Low\",\n value: \"alex\",\n weight: 100,\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=72&h=72&dpr=2&q=80\",\n initials: \"SJ\",\n label: \"Sarah Johnson\",\n priority: \"Medium\",\n value: \"sarah\",\n weight: 50,\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=72&h=72&dpr=2&q=80\",\n initials: \"ED\",\n label: \"Emma Davis\",\n priority: \"Lowest\",\n value: \"emma\",\n weight: 100,\n },\n];\n\nexport default function Particle() {\n const [open, setOpen] = useState(false);\n const [selected, setSelected] = useState(\n teamMembers.slice(0, 2),\n );\n\n return (\n
\n {\n setSelected(value);\n setOpen(false);\n }}\n open={open}\n value={selected}\n >\n }\n />\n \n No team members found.\n \n {(item: TeamMember) => (\n \n {item.label}\n \n )}\n \n \n \n {selected.length > 0 && (\n
    \n {selected.map((member) => (\n \n \n \n {member.initials}\n \n {member.label}\n \n {member.priority}\n \n \n {member.weight}%\n \n \n setSelected((current) =>\n current.filter((item) => item.value !== member.value),\n )\n }\n size=\"icon-xs\"\n variant=\"ghost\"\n >\n \n \n \n ))}\n
\n )}\n
\n );\n}\n", "type": "registry:block" } ], diff --git a/apps/ui/public/r/p-group-23.json b/apps/ui/public/r/p-group-23.json index ef090276a..b4094a953 100644 --- a/apps/ui/public/r/p-group-23.json +++ b/apps/ui/public/r/p-group-23.json @@ -12,7 +12,7 @@ "files": [ { "path": "registry/default/particles/p-group-23.tsx", - "content": "\"use client\";\n\nimport {\n ChevronsUpDownIcon,\n FunnelIcon,\n SearchIcon,\n XIcon,\n} from \"lucide-react\";\nimport { useState } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\nimport {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\nimport { Badge } from \"@/registry/default/ui/badge\";\nimport { Button, buttonVariants } from \"@/registry/default/ui/button\";\nimport {\n Combobox,\n ComboboxEmpty,\n ComboboxInput,\n ComboboxItem,\n ComboboxList,\n ComboboxPopup,\n ComboboxTrigger,\n} from \"@/registry/default/ui/combobox\";\nimport { Group, GroupSeparator, GroupText } from \"@/registry/default/ui/group\";\n\ntype FilterOption = {\n id: string;\n label: string;\n avatar?: string;\n};\n\nconst members: FilterOption[] = [\n {\n avatar:\n \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=64&h=64&fit=crop&crop=faces\",\n id: \"alex-chen\",\n label: \"Alex Chen\",\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=64&h=64&fit=crop&crop=faces\",\n id: \"sarah-johnson\",\n label: \"Sarah Johnson\",\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=64&h=64&fit=crop&crop=faces\",\n id: \"marcus-williams\",\n label: \"Marcus Williams\",\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=64&h=64&fit=crop&crop=faces\",\n id: \"emma-davis\",\n label: \"Emma Davis\",\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=64&h=64&fit=crop&crop=faces\",\n id: \"james-miller\",\n label: \"James Miller\",\n },\n];\n\nfunction getInitials(name: string): string {\n const parts = name.trim().split(/\\s+/);\n if (parts.length === 1) {\n return parts[0]?.charAt(0).toUpperCase() ?? \"\";\n }\n const first = parts[0]?.charAt(0) ?? \"\";\n const last = parts[parts.length - 1]?.charAt(0) ?? \"\";\n return (first + last).toUpperCase();\n}\n\nfunction MemberAvatar({\n name,\n avatarUrl,\n className,\n}: {\n name: string;\n avatarUrl?: string;\n className?: string;\n}) {\n return (\n \n {avatarUrl ? : null}\n \n {getInitials(name)}\n \n \n );\n}\n\nexport default function Particle() {\n const [selectedMembers, setSelectedMembers] = useState(\n members.slice(0, 2),\n );\n\n const renderTriggerContent = () => {\n if (selectedMembers.length === 0) return \"Select\";\n const firstMember = selectedMembers[0];\n const remainingCount = selectedMembers.length - 1;\n\n return (\n
\n \n {firstMember?.label}\n {remainingCount > 0 && (\n \n +{remainingCount}\n \n )}\n
\n );\n };\n\n return (\n \n \n \n Member\n \n \n {\n if (Array.isArray(value)) {\n setSelectedMembers(value);\n }\n }}\n value={selectedMembers}\n >\n \n }\n >\n {renderTriggerContent()}\n {selectedMembers.length === 0 && (\n \n )}\n \n \n
\n }\n />\n
\n No members found.\n \n {(option: FilterOption) => (\n \n
\n \n {option.label}\n
\n
\n )}\n
\n
\n \n \n setSelectedMembers([])}\n size=\"icon-sm\"\n variant=\"outline\"\n >\n \n \n
\n );\n}\n", + "content": "\"use client\";\n\nimport {\n ChevronsUpDownIcon,\n FunnelIcon,\n SearchIcon,\n XIcon,\n} from \"lucide-react\";\nimport { useState } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\nimport {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\nimport { Badge } from \"@/registry/default/ui/badge\";\nimport { Button, buttonVariants } from \"@/registry/default/ui/button\";\nimport {\n Combobox,\n ComboboxEmpty,\n ComboboxInput,\n ComboboxItem,\n ComboboxList,\n ComboboxPopup,\n ComboboxTrigger,\n} from \"@/registry/default/ui/combobox\";\nimport { Group, GroupSeparator, GroupText } from \"@/registry/default/ui/group\";\n\ntype FilterOption = {\n id: string;\n label: string;\n avatar?: string;\n};\n\nconst members: FilterOption[] = [\n {\n avatar:\n \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=64&h=64&fit=crop&crop=faces\",\n id: \"alex-chen\",\n label: \"Alex Chen\",\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=64&h=64&fit=crop&crop=faces\",\n id: \"sarah-johnson\",\n label: \"Sarah Johnson\",\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=64&h=64&fit=crop&crop=faces\",\n id: \"marcus-williams\",\n label: \"Marcus Williams\",\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=64&h=64&fit=crop&crop=faces\",\n id: \"emma-davis\",\n label: \"Emma Davis\",\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=64&h=64&fit=crop&crop=faces\",\n id: \"james-miller\",\n label: \"James Miller\",\n },\n];\n\nfunction getInitials(name: string): string {\n const parts = name.trim().split(/\\s+/);\n if (parts.length === 1) {\n return parts[0]?.charAt(0).toUpperCase() ?? \"\";\n }\n const first = parts[0]?.charAt(0) ?? \"\";\n const last = parts[parts.length - 1]?.charAt(0) ?? \"\";\n return (first + last).toUpperCase();\n}\n\nfunction MemberAvatar({\n name,\n avatarUrl,\n className,\n}: {\n name: string;\n avatarUrl?: string;\n className?: string;\n}) {\n return (\n \n {avatarUrl ? : null}\n {getInitials(name)}\n \n );\n}\n\nexport default function Particle() {\n const [selectedMembers, setSelectedMembers] = useState(\n members.slice(0, 2),\n );\n\n const renderTriggerContent = () => {\n if (selectedMembers.length === 0) return \"Select\";\n const firstMember = selectedMembers[0];\n const remainingCount = selectedMembers.length - 1;\n\n return (\n
\n \n {firstMember?.label}\n {remainingCount > 0 && (\n \n +{remainingCount}\n \n )}\n
\n );\n };\n\n return (\n \n \n \n Member\n \n \n {\n if (Array.isArray(value)) {\n setSelectedMembers(value);\n }\n }}\n value={selectedMembers}\n >\n \n }\n >\n {renderTriggerContent()}\n {selectedMembers.length === 0 && (\n \n )}\n \n \n
\n }\n />\n
\n No members found.\n \n {(option: FilterOption) => (\n \n
\n \n {option.label}\n
\n
\n )}\n
\n
\n \n \n setSelectedMembers([])}\n size=\"icon-sm\"\n variant=\"outline\"\n >\n \n \n
\n );\n}\n", "type": "registry:block" } ], diff --git a/apps/ui/public/r/p-select-19.json b/apps/ui/public/r/p-select-19.json index 3a93969d1..4579fab58 100644 --- a/apps/ui/public/r/p-select-19.json +++ b/apps/ui/public/r/p-select-19.json @@ -9,7 +9,7 @@ "files": [ { "path": "registry/default/particles/p-select-19.tsx", - "content": "\"use client\";\n\nimport {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\nimport {\n Select,\n SelectGroup,\n SelectGroupLabel,\n SelectItem,\n SelectPopup,\n SelectTrigger,\n SelectValue,\n} from \"@/registry/default/ui/select\";\n\nconst users = [\n {\n avatar:\n \"https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=72&h=72&dpr=2&q=80\",\n initials: \"JH\",\n label: \"Jenny Hamilton\",\n value: \"jenny\",\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1628157588553-5eeea00af15c?w=72&h=72&dpr=2&q=80\",\n initials: \"PS\",\n label: \"Paul Smith\",\n value: \"paul\",\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1655874819398-c6dfbec68ac7?w=72&h=72&dpr=2&q=80\",\n initials: \"LW\",\n label: \"Luna Wyen\",\n value: \"luna\",\n },\n];\n\nexport default function Particle() {\n return (\n \n );\n}\n", + "content": "\"use client\";\n\nimport {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\nimport {\n Select,\n SelectGroup,\n SelectGroupLabel,\n SelectItem,\n SelectPopup,\n SelectTrigger,\n SelectValue,\n} from \"@/registry/default/ui/select\";\n\nconst users = [\n {\n avatar:\n \"https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=72&h=72&dpr=2&q=80\",\n initials: \"JH\",\n label: \"Jenny Hamilton\",\n value: \"jenny\",\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1628157588553-5eeea00af15c?w=72&h=72&dpr=2&q=80\",\n initials: \"PS\",\n label: \"Paul Smith\",\n value: \"paul\",\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1655874819398-c6dfbec68ac7?w=72&h=72&dpr=2&q=80\",\n initials: \"LW\",\n label: \"Luna Wyen\",\n value: \"luna\",\n },\n];\n\nexport default function Particle() {\n return (\n \n );\n}\n", "type": "registry:block" } ], diff --git a/apps/ui/public/r/p-select-20.json b/apps/ui/public/r/p-select-20.json index 84d5952aa..6f7b7ec50 100644 --- a/apps/ui/public/r/p-select-20.json +++ b/apps/ui/public/r/p-select-20.json @@ -9,7 +9,7 @@ "files": [ { "path": "registry/default/particles/p-select-20.tsx", - "content": "\"use client\";\n\nimport {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\nimport {\n Select,\n SelectItem,\n SelectPopup,\n SelectTrigger,\n SelectValue,\n} from \"@/registry/default/ui/select\";\n\nconst users = [\n {\n avatar:\n \"https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=72&h=72&dpr=2&q=80\",\n initials: \"JH\",\n label: \"Jenny Hamilton\",\n username: \"@jennycodes\",\n value: \"jenny\",\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1628157588553-5eeea00af15c?w=72&h=72&dpr=2&q=80\",\n initials: \"PS\",\n label: \"Paul Smith\",\n username: \"@paulsmith\",\n value: \"paul\",\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1655874819398-c6dfbec68ac7?w=72&h=72&dpr=2&q=80\",\n initials: \"LW\",\n label: \"Luna Wyen\",\n username: \"@wyen.luna\",\n value: \"luna\",\n },\n];\n\nexport default function Particle() {\n return (\n \n );\n}\n", + "content": "\"use client\";\n\nimport {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\nimport {\n Select,\n SelectItem,\n SelectPopup,\n SelectTrigger,\n SelectValue,\n} from \"@/registry/default/ui/select\";\n\nconst users = [\n {\n avatar:\n \"https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=72&h=72&dpr=2&q=80\",\n initials: \"JH\",\n label: \"Jenny Hamilton\",\n username: \"@jennycodes\",\n value: \"jenny\",\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1628157588553-5eeea00af15c?w=72&h=72&dpr=2&q=80\",\n initials: \"PS\",\n label: \"Paul Smith\",\n username: \"@paulsmith\",\n value: \"paul\",\n },\n {\n avatar:\n \"https://images.unsplash.com/photo-1655874819398-c6dfbec68ac7?w=72&h=72&dpr=2&q=80\",\n initials: \"LW\",\n label: \"Luna Wyen\",\n username: \"@wyen.luna\",\n value: \"luna\",\n },\n];\n\nexport default function Particle() {\n return (\n \n );\n}\n", "type": "registry:block" } ], diff --git a/apps/ui/public/r/p-skeleton-1.json b/apps/ui/public/r/p-skeleton-1.json index 24cbbe520..cbb770a41 100644 --- a/apps/ui/public/r/p-skeleton-1.json +++ b/apps/ui/public/r/p-skeleton-1.json @@ -10,7 +10,7 @@ "files": [ { "path": "registry/default/particles/p-skeleton-1.tsx", - "content": "\"use client\";\n\nimport { UserRoundPlusIcon, UsersRoundIcon } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport { Skeleton } from \"@/registry/default/ui/skeleton\";\n\nconst users = [\n {\n delay: 3000,\n fallback: \"SJ\",\n followers: \"15k\",\n image:\n \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=80&h=80&dpr=2&q=80\",\n name: \"Sarah Johnson\",\n role: \"Design Engineer\",\n },\n {\n delay: 4000,\n fallback: \"MA\",\n followers: \"8k\",\n image:\n \"https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=80&h=80&dpr=2&q=80\",\n name: \"Mark Bennett Andersson\",\n role: \"Product Designer\",\n },\n {\n delay: 3400,\n fallback: \"AR\",\n followers: \"12k\",\n image:\n \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=80&h=80&dpr=2&q=80\",\n name: \"Alex Rivera\",\n role: \"UI/UX Designer\",\n },\n];\n\nfunction UserCard({ delay, user }: { delay: number; user: (typeof users)[0] }) {\n const [isLoaded, setIsLoaded] = useState(false);\n\n useEffect(() => {\n const timer = setTimeout(() => {\n setIsLoaded(true);\n }, delay);\n\n return () => clearTimeout(timer);\n }, [delay]);\n\n if (!isLoaded) {\n return ;\n }\n\n return (\n <>\n \n \n {user.fallback}\n \n
\n

{user.name}

\n
\n {user.role}\n
\n \n \n {user.followers}\n followers\n \n
\n
\n
\n \n \n );\n}\n\nfunction UserCardSkeleton() {\n return (\n <>\n \n
\n \n
\n \n \n
\n
\n \n \n );\n}\n\nexport default function Particle() {\n return (\n
\n {users.map((user) => (\n
\n \n
\n ))}\n
\n );\n}\n", + "content": "\"use client\";\n\nimport { UserRoundPlusIcon, UsersRoundIcon } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport { Skeleton } from \"@/registry/default/ui/skeleton\";\n\nconst users = [\n {\n delay: 3000,\n fallback: \"SJ\",\n followers: \"15k\",\n image:\n \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=80&h=80&dpr=2&q=80\",\n name: \"Sarah Johnson\",\n role: \"Design Engineer\",\n },\n {\n delay: 4000,\n fallback: \"MA\",\n followers: \"8k\",\n image:\n \"https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=80&h=80&dpr=2&q=80\",\n name: \"Mark Bennett Andersson\",\n role: \"Product Designer\",\n },\n {\n delay: 3400,\n fallback: \"AR\",\n followers: \"12k\",\n image:\n \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=80&h=80&dpr=2&q=80\",\n name: \"Alex Rivera\",\n role: \"UI/UX Designer\",\n },\n];\n\nfunction UserCard({ delay, user }: { delay: number; user: (typeof users)[0] }) {\n const [isLoaded, setIsLoaded] = useState(false);\n\n useEffect(() => {\n const timer = setTimeout(() => {\n setIsLoaded(true);\n }, delay);\n\n return () => clearTimeout(timer);\n }, [delay]);\n\n if (!isLoaded) {\n return ;\n }\n\n return (\n <>\n \n \n {user.fallback}\n \n
\n

{user.name}

\n
\n {user.role}\n
\n \n \n {user.followers}\n followers\n \n
\n
\n
\n \n \n );\n}\n\nfunction UserCardSkeleton() {\n return (\n <>\n \n
\n \n
\n \n \n
\n
\n \n \n );\n}\n\nexport default function Particle() {\n return (\n
\n {users.map((user) => (\n
\n \n
\n ))}\n
\n );\n}\n", "type": "registry:block" } ], diff --git a/apps/ui/registry/default/particles/p-button-28.tsx b/apps/ui/registry/default/particles/p-button-28.tsx index 3dc003591..eb87d9c15 100644 --- a/apps/ui/registry/default/particles/p-button-28.tsx +++ b/apps/ui/registry/default/particles/p-button-28.tsx @@ -8,7 +8,7 @@ import { Button } from "@/registry/default/ui/button"; export default function Particle() { return (