From 0f23752536fe63685bfcca8932332104ea7a82bd Mon Sep 17 00:00:00 2001 From: mehdibha Date: Wed, 11 Mar 2026 13:53:32 +0100 Subject: [PATCH 01/10] refactor: rewrite avatar component with hasImage tracking and fallback-only support - add hasImage/setHasImage context to track AvatarImage mount state - fix AvatarFallback to show when no AvatarImage is present or on error - fix AvatarPlaceholder to only show when AvatarImage is mounted and loading - update avatar/index.tsx to use createDynamicComponent pattern - add missing AvatarBadgeProps and AvatarGroupCountProps to types.ts - add missing AvatarFallback to account-menu, invite-members, and homepage - add API references for all avatar sub-components in docs - remove misleading react-aria link from avatar.mdx Co-Authored-By: Claude Opus 4.6 --- .../cards/components/account-menu.tsx | 8 +- .../cards/components/invite-members.tsx | 8 +- .../cards/components/notifications.tsx | 20 +- packages/registry/src/ui/avatar/base.tsx | 169 ++++ packages/registry/src/ui/avatar/basic.tsx | 138 --- .../ui/avatar/demos/avatar-group-count.tsx | 31 + .../ui/avatar/demos/badge-notification.tsx | 14 + .../src/ui/avatar/demos/badge-top-right.tsx | 12 + .../src/ui/avatar/demos/badge-with-icon.tsx | 15 + .../registry/src/ui/avatar/demos/badge.tsx | 11 + .../registry/src/ui/avatar/demos/basic.tsx | 11 + .../src/ui/avatar/demos/composition.tsx | 14 - .../registry/src/ui/avatar/demos/default.tsx | 15 +- .../src/ui/avatar/demos/fallback-only.tsx | 9 + .../registry/src/ui/avatar/demos/group.tsx | 20 + .../src/ui/avatar/demos/icon-fallback.tsx | 13 + .../src/ui/avatar/demos/playground.tsx | 10 +- .../registry/src/ui/avatar/demos/radii.tsx | 20 + .../registry/src/ui/avatar/demos/shape.tsx | 10 - .../registry/src/ui/avatar/demos/sizes.tsx | 19 +- packages/registry/src/ui/avatar/index.tsx | 25 +- packages/registry/src/ui/avatar/meta.ts | 6 +- packages/registry/src/ui/avatar/types.ts | 38 +- www/content/docs/components/avatar.mdx | 55 +- www/src/modules/auth/user-profile-menu.tsx | 13 +- .../docs/components-list/demos/avatar.tsx | 31 +- .../references/generated/avatar-badge.json | 6 + .../references/generated/avatar-fallback.json | 8 +- .../generated/avatar-group-count.json | 6 + .../references/generated/avatar-group.json | 56 +- .../references/generated/avatar-image.json | 26 +- .../generated/avatar-placeholder.json | 8 +- .../modules/references/generated/avatar.json | 940 +----------------- www/src/routes/index.tsx | 8 +- 34 files changed, 577 insertions(+), 1216 deletions(-) create mode 100644 packages/registry/src/ui/avatar/base.tsx delete mode 100644 packages/registry/src/ui/avatar/basic.tsx create mode 100644 packages/registry/src/ui/avatar/demos/avatar-group-count.tsx create mode 100644 packages/registry/src/ui/avatar/demos/badge-notification.tsx create mode 100644 packages/registry/src/ui/avatar/demos/badge-top-right.tsx create mode 100644 packages/registry/src/ui/avatar/demos/badge-with-icon.tsx create mode 100644 packages/registry/src/ui/avatar/demos/badge.tsx create mode 100644 packages/registry/src/ui/avatar/demos/basic.tsx delete mode 100644 packages/registry/src/ui/avatar/demos/composition.tsx create mode 100644 packages/registry/src/ui/avatar/demos/fallback-only.tsx create mode 100644 packages/registry/src/ui/avatar/demos/group.tsx create mode 100644 packages/registry/src/ui/avatar/demos/icon-fallback.tsx create mode 100644 packages/registry/src/ui/avatar/demos/radii.tsx delete mode 100644 packages/registry/src/ui/avatar/demos/shape.tsx create mode 100644 www/src/modules/references/generated/avatar-badge.json create mode 100644 www/src/modules/references/generated/avatar-group-count.json diff --git a/packages/registry/src/blocks/showcase/cards/components/account-menu.tsx b/packages/registry/src/blocks/showcase/cards/components/account-menu.tsx index 59943829a..55dc1aa87 100644 --- a/packages/registry/src/blocks/showcase/cards/components/account-menu.tsx +++ b/packages/registry/src/blocks/showcase/cards/components/account-menu.tsx @@ -3,7 +3,7 @@ import { BookIcon, ContrastIcon, LanguagesIcon, LogOutIcon, SettingsIcon, User2Icon, Users2Icon } from "lucide-react"; import { cn } from "@dotui/registry/lib/utils"; -import { Avatar } from "@dotui/registry/ui/avatar"; +import { Avatar, AvatarFallback, AvatarImage, AvatarPlaceholder } from "@dotui/registry/ui/avatar"; import { Card, CardContent, CardHeader } from "@dotui/registry/ui/card"; import { ListBox, ListBoxItem, ListBoxSection, ListBoxSectionHeader } from "@dotui/registry/ui/list-box"; import { Separator } from "@dotui/registry/ui/separator"; @@ -12,7 +12,11 @@ export function AccountMenu({ className, ...props }: React.ComponentProps<"div"> return ( - + + + M + +

mehdibha

diff --git a/packages/registry/src/blocks/showcase/cards/components/invite-members.tsx b/packages/registry/src/blocks/showcase/cards/components/invite-members.tsx index f9120deb5..ec0c1ecce 100644 --- a/packages/registry/src/blocks/showcase/cards/components/invite-members.tsx +++ b/packages/registry/src/blocks/showcase/cards/components/invite-members.tsx @@ -3,7 +3,7 @@ import { PlusCircleIcon } from "lucide-react"; import { ExternalLinkIcon } from "@dotui/registry/icons"; -import { Avatar } from "@dotui/registry/ui/avatar"; +import { Avatar, AvatarFallback, AvatarImage, AvatarPlaceholder } from "@dotui/registry/ui/avatar"; import { Button } from "@dotui/registry/ui/button"; import { Card, @@ -82,7 +82,11 @@ export function InviteMembers(props: React.ComponentProps<"div">) { {teamMembers.map((member) => (

- + + + {member.name.charAt(0)} + +

{member.name}

{member.role}

diff --git a/packages/registry/src/blocks/showcase/cards/components/notifications.tsx b/packages/registry/src/blocks/showcase/cards/components/notifications.tsx index c28f74296..a42c8b056 100644 --- a/packages/registry/src/blocks/showcase/cards/components/notifications.tsx +++ b/packages/registry/src/blocks/showcase/cards/components/notifications.tsx @@ -1,7 +1,7 @@ import React from "react"; import { cn } from "@dotui/registry/lib/utils"; -import { Avatar } from "@dotui/registry/ui/avatar"; +import { Avatar, AvatarFallback, AvatarImage, AvatarPlaceholder } from "@dotui/registry/ui/avatar"; import { Badge } from "@dotui/registry/ui/badge"; import { Button } from "@dotui/registry/ui/button"; import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@dotui/registry/ui/card"; @@ -48,14 +48,16 @@ export function Notifications({ className, ...props }: React.ComponentProps<"div
- n[0]) - .join("")} - size="md" - /> + + + + {notification.user.name + .split(" ") + .map((n) => n[0]) + .join("")} + + +

{notification.user.name}{" "} diff --git a/packages/registry/src/ui/avatar/base.tsx b/packages/registry/src/ui/avatar/base.tsx new file mode 100644 index 000000000..d31519ef0 --- /dev/null +++ b/packages/registry/src/ui/avatar/base.tsx @@ -0,0 +1,169 @@ +"use client"; + +import * as React from "react"; +import { tv } from "tailwind-variants"; +import type { VariantProps } from "tailwind-variants"; + +import { useImageLoadingStatus } from "@dotui/registry/hooks/use-image-loading-status"; +import { createContext } from "@dotui/registry/lib/context"; +import type { ImageLoadingStatus } from "@dotui/registry/hooks/use-image-loading-status"; + +const avatarStyles = tv({ + slots: { + root: [ + "group/avatar relative inline-flex shrink-0 rounded-full bg-muted align-middle", + "*:data-badge:absolute *:data-badge:not-with-[right]:not-with-[left]:right-0 *:data-badge:not-with-[bottom]:not-with-[top]:bottom-0", + ], + image: "aspect-square size-full rounded-[inherit] object-cover", + fallback: + "flex size-full select-none items-center justify-center rounded-[inherit] bg-muted text-sm group-data-[size=sm]/avatar:text-xs", + placeholder: "flex size-full h-full animate-pulse items-center justify-center rounded-[inherit] bg-muted", + badge: [ + "absolute right-0 with-[left]:right-auto bottom-0 with-[top]:bottom-auto z-10 inline-flex select-none items-center justify-center rounded-full bg-primary text-fg-on-primary bg-blend-color ring-2 ring-bg", + "not-with-[size]:group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden", + "not-with-[size]:group-data-[size=md]/avatar:size-2.5 group-data-[size=md]/avatar:[&>svg]:size-2", + "not-with-[size]:group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", + ], + group: "group/avatar-group flex -space-x-2 *:data-avatar:ring-2 *:data-avatar:ring-bg", + groupCount: + "relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-fg-muted text-sm ring-2 ring-bg group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3", + }, + variants: { + size: { + sm: { group: "*:data-avatar:size-6", root: "size-6" }, + md: { group: "*:data-avatar:size-8", root: "size-8" }, + lg: { group: "*:data-avatar:size-10", root: "size-10" }, + }, + }, + defaultVariants: { + size: "md", + }, +}); + +const { group, root, image, fallback, placeholder, badge, groupCount } = avatarStyles(); + +const [AvatarContext, useAvatarContext] = createContext<{ + status: ImageLoadingStatus; + setStatus: (status: ImageLoadingStatus) => void; + hasImage: boolean; + setHasImage: (hasImage: boolean) => void; +}>({ + name: "Avatar", + strict: true, +}); + +/* ------------------------------------------------------------------------------------------------- + * Avatar + * -----------------------------------------------------------------------------------------------*/ + +interface AvatarProps extends React.ComponentProps<"span">, VariantProps {} + +function Avatar({ className, size = "md", ...props }: AvatarProps) { + const [status, setStatus] = React.useState("idle"); + const [hasImage, setHasImage] = React.useState(false); + + return ( + + + + ); +} + +/* ------------------------------------------------------------------------------------------------- + * Avatar Image + * -----------------------------------------------------------------------------------------------*/ + +interface AvatarImageProps extends Omit, "src"> { + src?: string; +} + +function AvatarImage({ src, alt, className, referrerPolicy, crossOrigin, ...props }: AvatarImageProps) { + const status = useImageLoadingStatus(src, { referrerPolicy, crossOrigin }); + const { setStatus, setHasImage } = useAvatarContext("AvatarImage"); + + React.useLayoutEffect(() => { + setHasImage(true); + return () => setHasImage(false); + }, [setHasImage]); + + React.useLayoutEffect(() => { + if (status !== "idle") { + setStatus(status); + } + }, [status, setStatus]); + + if (status === "loaded") + return {alt}; + + return null; +} + +/* ------------------------------------------------------------------------------------------------- + * Avatar Fallback + * -----------------------------------------------------------------------------------------------*/ + +interface AvatarFallbackProps extends React.ComponentProps<"span"> {} + +const AvatarFallback = ({ className, ...props }: AvatarFallbackProps) => { + const { status, hasImage } = useAvatarContext("AvatarFallback"); + if (!hasImage || status === "error") + return ; + return null; +}; + +/* ------------------------------------------------------------------------------------------------- + * Avatar Placeholder + * -----------------------------------------------------------------------------------------------*/ + +interface AvatarPlaceholderProps extends React.ComponentProps<"span"> {} + +const AvatarPlaceholder = ({ className, ...props }: AvatarPlaceholderProps) => { + const { status, hasImage } = useAvatarContext("AvatarPlaceholder"); + if (hasImage && ["idle", "loading"].includes(status)) + return ; + return null; +}; + +/* ------------------------------------------------------------------------------------------------- + * Avatar Badge + * -----------------------------------------------------------------------------------------------*/ + +interface AvatarBadgeProps extends React.ComponentProps<"span"> {} + +const AvatarBadge = ({ className, ...props }: AvatarBadgeProps) => { + return ; +}; + +/* ------------------------------------------------------------------------------------------------- + * Avatar Group + * -----------------------------------------------------------------------------------------------*/ + +interface AvatarGroupProps extends React.ComponentProps<"div">, VariantProps {} + +const AvatarGroup = ({ className, size, ...props }: AvatarGroupProps) => { + return

; +}; + +/* ------------------------------------------------------------------------------------------------- + * Avatar Group Count + * -----------------------------------------------------------------------------------------------*/ + +interface AvatarGroupCountProps extends React.ComponentProps<"span"> {} + +const AvatarGroupCount = ({ className, ...props }: AvatarGroupCountProps) => { + return ; +}; + +/* -----------------------------------------------------------------------------------------------*/ + +export { AvatarGroup, Avatar, AvatarImage, AvatarFallback, AvatarPlaceholder, AvatarBadge, AvatarGroupCount }; + +export type { + AvatarGroupProps, + AvatarProps, + AvatarImageProps, + AvatarFallbackProps, + AvatarPlaceholderProps, + AvatarBadgeProps, + AvatarGroupCountProps, +}; diff --git a/packages/registry/src/ui/avatar/basic.tsx b/packages/registry/src/ui/avatar/basic.tsx deleted file mode 100644 index 9a686cdf9..000000000 --- a/packages/registry/src/ui/avatar/basic.tsx +++ /dev/null @@ -1,138 +0,0 @@ -"use client"; - -import * as React from "react"; -import { tv } from "tailwind-variants"; -import type { VariantProps } from "tailwind-variants"; - -import { useImageLoadingStatus } from "@dotui/registry/hooks/use-image-loading-status"; -import { createContext } from "@dotui/registry/lib/context"; -import type { ImageLoadingStatus } from "@dotui/registry/hooks/use-image-loading-status"; - -const avatarStyles = tv({ - slots: { - group: "flex flex-wrap -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-bg", - root: "relative inline-flex shrink-0 overflow-hidden rounded-full bg-bg align-middle", - image: "aspect-square size-full", - fallback: "flex size-full select-none items-center justify-center bg-muted", - placeholder: "flex size-full h-full animate-pulse items-center justify-center bg-muted", - }, - variants: { - size: { - sm: { group: "*:data-[slot=avatar]:size-8", root: "size-8" }, - md: { group: "*:data-[slot=avatar]:size-10", root: "size-10" }, - lg: { group: "*:data-[slot=avatar]:size-12", root: "size-12" }, - }, - }, - defaultVariants: { - size: "md", - }, -}); - -const { group, root, image, fallback, placeholder } = avatarStyles(); - -/* -----------------------------------------------------------------------------------------------*/ - -interface AvatarGroupProps extends React.ComponentProps<"div">, VariantProps {} - -const AvatarGroup = ({ className, size, ...props }: AvatarGroupProps) => { - return
; -}; - -/* -----------------------------------------------------------------------------------------------*/ - -interface AvatarProps extends AvatarImageProps, VariantProps { - fallback?: React.ReactNode; -} -const Avatar = ({ className, style, fallback, size, ...props }: AvatarProps) => { - return ( - - - {fallback} - - - ); -}; - -/* -----------------------------------------------------------------------------------------------*/ - -const [AvatarInternalContext, useAvatarInternalContext] = createContext<{ - status: ImageLoadingStatus; - setStatus: (status: ImageLoadingStatus) => void; -}>({ - name: "AvatarRoot", - strict: true, -}); - -interface AvatarRootProps extends React.ComponentProps<"span">, VariantProps {} -function AvatarRoot({ className, size, ...props }: AvatarRootProps) { - const [status, setStatus] = React.useState("idle"); - - return ( - - - - ); -} - -/* -----------------------------------------------------------------------------------------------*/ - -interface AvatarImageProps extends Omit, "src"> { - src?: string; -} - -function AvatarImage({ src, alt, className, referrerPolicy, crossOrigin, ...props }: AvatarImageProps) { - const status = useImageLoadingStatus(src, { referrerPolicy, crossOrigin }); - const { setStatus } = useAvatarInternalContext("AvatarImage"); - - React.useLayoutEffect(() => { - if (status !== "idle") { - setStatus(status); - } - }, [status, setStatus]); - - if (status === "loaded") - return {alt}; - - return null; -} - -/* -----------------------------------------------------------------------------------------------*/ - -type AvatarFallbackProps = React.HTMLAttributes; - -const AvatarFallback = ({ className, ...props }: AvatarFallbackProps) => { - const { status } = useAvatarInternalContext("AvatarFallback"); - if (status === "error") return ; - return null; -}; - -/* -----------------------------------------------------------------------------------------------*/ - -interface AvatarPlaceholderProps extends React.ComponentProps<"span"> {} - -const AvatarPlaceholder = ({ className, ...props }: AvatarPlaceholderProps) => { - const { status } = useAvatarInternalContext("AvatarPlaceholder"); - if (["idle", "loading"].includes(status)) return ; - return null; -}; - -/* -----------------------------------------------------------------------------------------------*/ - -const CompoundAvatar = Object.assign(Avatar, { - Group: AvatarGroup, - Root: AvatarRoot, - Image: AvatarImage, - Fallback: AvatarFallback, - Placeholder: AvatarPlaceholder, -}); - -export { CompoundAvatar as Avatar, AvatarGroup, AvatarRoot, AvatarImage, AvatarFallback, AvatarPlaceholder }; - -export type { - AvatarGroupProps, - AvatarProps, - AvatarRootProps, - AvatarImageProps, - AvatarFallbackProps, - AvatarPlaceholderProps, -}; diff --git a/packages/registry/src/ui/avatar/demos/avatar-group-count.tsx b/packages/registry/src/ui/avatar/demos/avatar-group-count.tsx new file mode 100644 index 000000000..01eb25d90 --- /dev/null +++ b/packages/registry/src/ui/avatar/demos/avatar-group-count.tsx @@ -0,0 +1,31 @@ +import { + Avatar, + AvatarFallback, + AvatarGroup, + AvatarGroupCount, + AvatarImage, + AvatarPlaceholder, +} from "@dotui/registry/ui/avatar"; + +export default function Demo() { + return ( + + + + M + + + + + T + + + + + D + + + +3 + + ); +} diff --git a/packages/registry/src/ui/avatar/demos/badge-notification.tsx b/packages/registry/src/ui/avatar/demos/badge-notification.tsx new file mode 100644 index 000000000..0daf41667 --- /dev/null +++ b/packages/registry/src/ui/avatar/demos/badge-notification.tsx @@ -0,0 +1,14 @@ +import { Avatar, AvatarFallback, AvatarImage } from "@dotui/registry/ui/avatar"; +import { Badge } from "@dotui/registry/ui/badge"; + +export default function Demo() { + return ( + + + M + + 6 + + + ); +} diff --git a/packages/registry/src/ui/avatar/demos/badge-top-right.tsx b/packages/registry/src/ui/avatar/demos/badge-top-right.tsx new file mode 100644 index 000000000..93f3b0ae2 --- /dev/null +++ b/packages/registry/src/ui/avatar/demos/badge-top-right.tsx @@ -0,0 +1,12 @@ +import { Avatar, AvatarBadge, AvatarFallback, AvatarImage, AvatarPlaceholder } from "@dotui/registry/ui/avatar"; + +export default function Demo() { + return ( + + + M + + + + ); +} diff --git a/packages/registry/src/ui/avatar/demos/badge-with-icon.tsx b/packages/registry/src/ui/avatar/demos/badge-with-icon.tsx new file mode 100644 index 000000000..c9a2b65bd --- /dev/null +++ b/packages/registry/src/ui/avatar/demos/badge-with-icon.tsx @@ -0,0 +1,15 @@ +import { PlusIcon } from "lucide-react"; + +import { Avatar, AvatarBadge, AvatarFallback, AvatarImage } from "@dotui/registry/ui/avatar"; + +export default function Demo() { + return ( + + + M + + + + + ); +} diff --git a/packages/registry/src/ui/avatar/demos/badge.tsx b/packages/registry/src/ui/avatar/demos/badge.tsx new file mode 100644 index 000000000..8811e0e62 --- /dev/null +++ b/packages/registry/src/ui/avatar/demos/badge.tsx @@ -0,0 +1,11 @@ +import { Avatar, AvatarBadge, AvatarFallback, AvatarImage } from "@dotui/registry/ui/avatar"; + +export default function Demo() { + return ( + + + M + + + ); +} diff --git a/packages/registry/src/ui/avatar/demos/basic.tsx b/packages/registry/src/ui/avatar/demos/basic.tsx new file mode 100644 index 000000000..637590fe1 --- /dev/null +++ b/packages/registry/src/ui/avatar/demos/basic.tsx @@ -0,0 +1,11 @@ +import { Avatar, AvatarFallback, AvatarImage, AvatarPlaceholder } from "@dotui/registry/ui/avatar"; + +export default function Demo() { + return ( + + + M + + + ); +} diff --git a/packages/registry/src/ui/avatar/demos/composition.tsx b/packages/registry/src/ui/avatar/demos/composition.tsx deleted file mode 100644 index 20a45d92d..000000000 --- a/packages/registry/src/ui/avatar/demos/composition.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { User2Icon } from "@dotui/registry/icons"; -import { AvatarFallback, AvatarImage, AvatarPlaceholder, AvatarRoot } from "@dotui/registry/ui/avatar"; - -export default function Demo() { - return ( - - - M - - - - - ); -} diff --git a/packages/registry/src/ui/avatar/demos/default.tsx b/packages/registry/src/ui/avatar/demos/default.tsx index d05aafd24..637590fe1 100644 --- a/packages/registry/src/ui/avatar/demos/default.tsx +++ b/packages/registry/src/ui/avatar/demos/default.tsx @@ -1,14 +1,11 @@ -import { Avatar } from "@dotui/registry/ui/avatar"; +import { Avatar, AvatarFallback, AvatarImage, AvatarPlaceholder } from "@dotui/registry/ui/avatar"; export default function Demo() { return ( -
- - -
+ + + M + + ); } diff --git a/packages/registry/src/ui/avatar/demos/fallback-only.tsx b/packages/registry/src/ui/avatar/demos/fallback-only.tsx new file mode 100644 index 000000000..f2ad12d54 --- /dev/null +++ b/packages/registry/src/ui/avatar/demos/fallback-only.tsx @@ -0,0 +1,9 @@ +import { Avatar, AvatarFallback } from "@dotui/registry/ui/avatar"; + +export default function Demo() { + return ( + + MB + + ); +} diff --git a/packages/registry/src/ui/avatar/demos/group.tsx b/packages/registry/src/ui/avatar/demos/group.tsx new file mode 100644 index 000000000..95f9e75cd --- /dev/null +++ b/packages/registry/src/ui/avatar/demos/group.tsx @@ -0,0 +1,20 @@ +import { Avatar, AvatarFallback, AvatarGroup, AvatarImage } from "@dotui/registry/ui/avatar"; + +export default function Demo() { + return ( + + + + M + + + + T + + + + D + + + ); +} diff --git a/packages/registry/src/ui/avatar/demos/icon-fallback.tsx b/packages/registry/src/ui/avatar/demos/icon-fallback.tsx new file mode 100644 index 000000000..990d04f2d --- /dev/null +++ b/packages/registry/src/ui/avatar/demos/icon-fallback.tsx @@ -0,0 +1,13 @@ +import { UserIcon } from "lucide-react"; + +import { Avatar, AvatarFallback } from "@dotui/registry/ui/avatar"; + +export default function Demo() { + return ( + + + + + + ); +} diff --git a/packages/registry/src/ui/avatar/demos/playground.tsx b/packages/registry/src/ui/avatar/demos/playground.tsx index 82abfae18..3ed5dd204 100644 --- a/packages/registry/src/ui/avatar/demos/playground.tsx +++ b/packages/registry/src/ui/avatar/demos/playground.tsx @@ -1,6 +1,6 @@ "use client"; -import { Avatar } from "@dotui/registry/ui/avatar"; +import { Avatar, AvatarFallback, AvatarImage, AvatarPlaceholder } from "@dotui/registry/ui/avatar"; interface AvatarPlaygroundProps { src?: string; @@ -15,5 +15,11 @@ export function AvatarPlayground({ fallback = "MB", size = "md", }: AvatarPlaygroundProps) { - return ; + return ( + + + {fallback} + + + ); } diff --git a/packages/registry/src/ui/avatar/demos/radii.tsx b/packages/registry/src/ui/avatar/demos/radii.tsx new file mode 100644 index 000000000..25ab1ea18 --- /dev/null +++ b/packages/registry/src/ui/avatar/demos/radii.tsx @@ -0,0 +1,20 @@ +import { Avatar, AvatarFallback, AvatarImage } from "@dotui/registry/ui/avatar"; + +export default function Demo() { + return ( +
+ + + M + + + + M + + + + M + +
+ ); +} diff --git a/packages/registry/src/ui/avatar/demos/shape.tsx b/packages/registry/src/ui/avatar/demos/shape.tsx deleted file mode 100644 index fbd832270..000000000 --- a/packages/registry/src/ui/avatar/demos/shape.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Avatar } from "@dotui/registry/ui/avatar"; - -export default function Demo() { - return ( -
- - -
- ); -} diff --git a/packages/registry/src/ui/avatar/demos/sizes.tsx b/packages/registry/src/ui/avatar/demos/sizes.tsx index a23401cd1..806b450e4 100644 --- a/packages/registry/src/ui/avatar/demos/sizes.tsx +++ b/packages/registry/src/ui/avatar/demos/sizes.tsx @@ -1,11 +1,20 @@ -import { Avatar } from "@dotui/registry/ui/avatar"; +import { Avatar, AvatarFallback, AvatarImage } from "@dotui/registry/ui/avatar"; export default function Demo() { return ( -
- {(["sm", "md", "lg"] as const).map((size) => ( - - ))} +
+ + + M + + + + M + + + + M +
); } diff --git a/packages/registry/src/ui/avatar/index.tsx b/packages/registry/src/ui/avatar/index.tsx index 85bfbd54e..22a119a5a 100644 --- a/packages/registry/src/ui/avatar/index.tsx +++ b/packages/registry/src/ui/avatar/index.tsx @@ -2,22 +2,19 @@ import { createDynamicComponent } from "@dotui/core/react/dynamic-component"; -import * as Default from "./basic"; +import * as Default from "./base"; import type { + AvatarBadgeProps, AvatarFallbackProps, + AvatarGroupCountProps, AvatarGroupProps, AvatarImageProps, AvatarPlaceholderProps, AvatarProps, - AvatarRootProps, } from "./types"; -export const AvatarGroup = createDynamicComponent("avatar", "AvatarGroup", Default.AvatarGroup, {}); - export const Avatar = createDynamicComponent("avatar", "Avatar", Default.Avatar, {}); -export const AvatarRoot = createDynamicComponent("avatar", "AvatarRoot", Default.AvatarRoot, {}); - export const AvatarImage = createDynamicComponent("avatar", "AvatarImage", Default.AvatarImage, {}); export const AvatarFallback = createDynamicComponent( @@ -34,11 +31,23 @@ export const AvatarPlaceholder = createDynamicComponent( {}, ); +export const AvatarBadge = createDynamicComponent("avatar", "AvatarBadge", Default.AvatarBadge, {}); + +export const AvatarGroup = createDynamicComponent("avatar", "AvatarGroup", Default.AvatarGroup, {}); + +export const AvatarGroupCount = createDynamicComponent( + "avatar", + "AvatarGroupCount", + Default.AvatarGroupCount, + {}, +); + export type { AvatarProps, - AvatarRootProps, AvatarImageProps, AvatarFallbackProps, - AvatarGroupProps, AvatarPlaceholderProps, + AvatarBadgeProps, + AvatarGroupProps, + AvatarGroupCountProps, }; diff --git a/packages/registry/src/ui/avatar/meta.ts b/packages/registry/src/ui/avatar/meta.ts index e03ce38b3..3dbc0a5a5 100644 --- a/packages/registry/src/ui/avatar/meta.ts +++ b/packages/registry/src/ui/avatar/meta.ts @@ -4,13 +4,13 @@ const avatarMeta = { name: "avatar", type: "registry:ui", group: "data-display", - defaultVariant: "basic", + defaultVariant: "base", variants: { - basic: { + base: { files: [ { type: "registry:ui", - path: "ui/avatar/basic.tsx", + path: "ui/avatar/base.tsx", target: "ui/avatar.tsx", }, ], diff --git a/packages/registry/src/ui/avatar/types.ts b/packages/registry/src/ui/avatar/types.ts index 3d2126303..a71c2b9ac 100644 --- a/packages/registry/src/ui/avatar/types.ts +++ b/packages/registry/src/ui/avatar/types.ts @@ -1,5 +1,5 @@ /** - * Missing description. + * A group container that displays multiple avatars together with overlapping layout. */ export interface AvatarGroupProps extends React.ComponentProps<"div"> { /** @@ -10,25 +10,9 @@ export interface AvatarGroupProps extends React.ComponentProps<"div"> { } /** - * Missing description. + * A visual representation of a user or entity, displayed as an image, initials, or placeholder. */ -export interface AvatarProps extends AvatarImageProps { - /** - * The size of the avatar. - * @default 'md' - */ - size?: "sm" | "md" | "lg"; - - /** - * Content to display when the image fails to load. - */ - fallback?: React.ReactNode; -} - -/** - * Missing description. - */ -export interface AvatarRootProps extends React.ComponentProps<"span"> { +export interface AvatarProps extends React.ComponentProps<"span"> { /** * The size of the avatar. * @default 'md' @@ -37,7 +21,7 @@ export interface AvatarRootProps extends React.ComponentProps<"span"> { } /** - * Missing description. + * The image element displayed within the avatar. Automatically hidden when the image fails to load. */ export interface AvatarImageProps extends Omit, "src"> { /** @@ -47,11 +31,21 @@ export interface AvatarImageProps extends Omit, "src } /** - * Missing description. + * Content displayed when the avatar image fails to load. Typically shows initials or an icon. */ export interface AvatarFallbackProps extends React.ComponentProps<"span"> {} /** - * Missing description. + * A loading placeholder displayed while the avatar image is being loaded. */ export interface AvatarPlaceholderProps extends React.ComponentProps<"span"> {} + +/** + * A small indicator displayed on the avatar, typically used to show online status. + */ +export interface AvatarBadgeProps extends React.ComponentProps<"span"> {} + +/** + * A count indicator displayed at the end of an avatar group to show remaining items. + */ +export interface AvatarGroupCountProps extends React.ComponentProps<"span"> {} diff --git a/www/content/docs/components/avatar.mdx b/www/content/docs/components/avatar.mdx index 725663f2c..749020392 100644 --- a/www/content/docs/components/avatar.mdx +++ b/www/content/docs/components/avatar.mdx @@ -1,13 +1,9 @@ --- title: Avatar description: An avatar is a visual representation of a user or entity. -links: - - label: Docs - href: https://react-spectrum.adobe.com/react-aria/ wip: true --- - @@ -24,23 +20,23 @@ wip: true Use avatars to represent users or entities with an image or initials. ```tsx - import { Avatar } from "@/components/ui/avatar"; + import { Avatar, AvatarImage, AvatarFallback, AvatarPlaceholder } from "@/components/ui/avatar"; ``` ```tsx - + + + MB + + ``` ## Playground Use the controls below to experiment with different avatar props and see the live preview and code update. - + ### AvatarImage + + + + ### AvatarFallback + + + + ### AvatarPlaceholder + + + + ### AvatarBadge + + + ### AvatarGroup + ### AvatarGroupCount + + + - - - - + + + + + + + + + + + diff --git a/www/src/modules/auth/user-profile-menu.tsx b/www/src/modules/auth/user-profile-menu.tsx index ca51e2b4f..b398e6d9f 100644 --- a/www/src/modules/auth/user-profile-menu.tsx +++ b/www/src/modules/auth/user-profile-menu.tsx @@ -3,7 +3,7 @@ import { Button } from "react-aria-components"; import { focusRing } from "@dotui/registry/lib/focus-styles"; import { cn } from "@dotui/registry/lib/utils"; -import { Avatar } from "@dotui/registry/ui/avatar"; +import { Avatar, AvatarFallback, AvatarImage, AvatarPlaceholder } from "@dotui/registry/ui/avatar"; import { Menu, MenuContent, MenuItem } from "@dotui/registry/ui/menu"; import { Overlay } from "@dotui/registry/ui/overlay"; import type { ButtonProps } from "@dotui/registry/ui/button"; @@ -30,12 +30,11 @@ export function UserProfileMenu({ props.className, )} > - + + + {session?.user?.name?.charAt(0)} + + - - + + M + + + + + M + + - - - + + + U1 + + + + + U2 + + + + + U3 + +
); diff --git a/www/src/modules/references/generated/avatar-badge.json b/www/src/modules/references/generated/avatar-badge.json new file mode 100644 index 000000000..5cf213455 --- /dev/null +++ b/www/src/modules/references/generated/avatar-badge.json @@ -0,0 +1,6 @@ +{ + "name": "AvatarBadgeProps", + "description": "A small indicator displayed on the avatar, typically used to show online status.", + "extendsElement": "span", + "props": {} +} diff --git a/www/src/modules/references/generated/avatar-fallback.json b/www/src/modules/references/generated/avatar-fallback.json index ce5d7149d..0e1fa0ba4 100644 --- a/www/src/modules/references/generated/avatar-fallback.json +++ b/www/src/modules/references/generated/avatar-fallback.json @@ -1,6 +1,6 @@ { - "name": "AvatarFallbackProps", - "description": "Missing description.", - "extendsElement": "span", - "props": {} + "name": "AvatarFallbackProps", + "description": "Content displayed when the avatar image fails to load. Typically shows initials or an icon.", + "extendsElement": "span", + "props": {} } diff --git a/www/src/modules/references/generated/avatar-group-count.json b/www/src/modules/references/generated/avatar-group-count.json new file mode 100644 index 000000000..cc8aa1fee --- /dev/null +++ b/www/src/modules/references/generated/avatar-group-count.json @@ -0,0 +1,6 @@ +{ + "name": "AvatarGroupCountProps", + "description": "A count indicator displayed at the end of an avatar group to show remaining items.", + "extendsElement": "span", + "props": {} +} diff --git a/www/src/modules/references/generated/avatar-group.json b/www/src/modules/references/generated/avatar-group.json index c33698383..5ccbb7a79 100644 --- a/www/src/modules/references/generated/avatar-group.json +++ b/www/src/modules/references/generated/avatar-group.json @@ -1,30 +1,30 @@ { - "name": "AvatarGroupProps", - "description": "Missing description.", - "extendsElement": "div", - "props": { - "size": { - "type": "\"sm\" | \"md\" | \"lg\"", - "detailedType": "'sm' | 'md' | 'lg' | undefined", - "typeAst": { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "sm" - }, - { - "type": "stringLiteral", - "value": "md" - }, - { - "type": "stringLiteral", - "value": "lg" - } - ] - }, - "description": "The size of all avatars in the group.", - "default": "'md'" - } - } + "name": "AvatarGroupProps", + "description": "A group container that displays multiple avatars together with overlapping layout.", + "extendsElement": "div", + "props": { + "size": { + "type": "\"sm\" | \"md\" | \"lg\"", + "detailedType": "'sm' | 'md' | 'lg' | undefined", + "typeAst": { + "type": "union", + "elements": [ + { + "type": "stringLiteral", + "value": "sm" + }, + { + "type": "stringLiteral", + "value": "md" + }, + { + "type": "stringLiteral", + "value": "lg" + } + ] + }, + "description": "The size of all avatars in the group.", + "default": "'md'" + } + } } diff --git a/www/src/modules/references/generated/avatar-image.json b/www/src/modules/references/generated/avatar-image.json index 89715a4a4..777814993 100644 --- a/www/src/modules/references/generated/avatar-image.json +++ b/www/src/modules/references/generated/avatar-image.json @@ -1,15 +1,15 @@ { - "name": "AvatarImageProps", - "description": "Missing description.", - "extendsElement": "img", - "props": { - "src": { - "type": "string", - "detailedType": "string | undefined", - "typeAst": { - "type": "string" - }, - "description": "The URL of the avatar image." - } - } + "name": "AvatarImageProps", + "description": "The image element displayed within the avatar. Automatically hidden when the image fails to load.", + "extendsElement": "img", + "props": { + "src": { + "type": "string", + "detailedType": "string | undefined", + "typeAst": { + "type": "string" + }, + "description": "The URL of the avatar image." + } + } } diff --git a/www/src/modules/references/generated/avatar-placeholder.json b/www/src/modules/references/generated/avatar-placeholder.json index e22be2717..77a8177db 100644 --- a/www/src/modules/references/generated/avatar-placeholder.json +++ b/www/src/modules/references/generated/avatar-placeholder.json @@ -1,6 +1,6 @@ { - "name": "AvatarPlaceholderProps", - "description": "Missing description.", - "extendsElement": "span", - "props": {} + "name": "AvatarPlaceholderProps", + "description": "A loading placeholder displayed while the avatar image is being loaded.", + "extendsElement": "span", + "props": {} } diff --git a/www/src/modules/references/generated/avatar.json b/www/src/modules/references/generated/avatar.json index eb69d036a..389580a84 100644 --- a/www/src/modules/references/generated/avatar.json +++ b/www/src/modules/references/generated/avatar.json @@ -1,914 +1,30 @@ { - "name": "AvatarProps", - "description": "Missing description.", - "props": { - "size": { - "type": "\"sm\" | \"md\" | \"lg\"", - "detailedType": "'sm' | 'md' | 'lg' | undefined", - "typeAst": { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "sm" - }, - { - "type": "stringLiteral", - "value": "md" - }, - { - "type": "stringLiteral", - "value": "lg" - } - ] - }, - "description": "The size of the avatar.", - "default": "'md'" - }, - "fallback": { - "type": "ReactNode", - "typeAst": { - "type": "identifier", - "name": "ReactNode" - }, - "description": "Content to display when the image fails to load." - }, - "src": { - "type": "string", - "detailedType": "string | undefined", - "typeAst": { - "type": "string" - }, - "description": "The URL of the avatar image." - }, - "inputMode": { - "type": "\"search\" | \"text\" | \"none\" | \"tel\" | \"url\" | \"email\" | \"numeric\" | \"decimal\"", - "detailedType": "| 'search'\n| 'text'\n| 'none'\n| 'tel'\n| 'url'\n| 'email'\n| 'numeric'\n| 'decimal'\n| undefined", - "typeAst": { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "search" - }, - { - "type": "stringLiteral", - "value": "text" - }, - { - "type": "stringLiteral", - "value": "none" - }, - { - "type": "stringLiteral", - "value": "tel" - }, - { - "type": "stringLiteral", - "value": "url" - }, - { - "type": "stringLiteral", - "value": "email" - }, - { - "type": "stringLiteral", - "value": "numeric" - }, - { - "type": "stringLiteral", - "value": "decimal" - } - ] - }, - "description": "Hints at the type of data that might be entered by the user while editing the element or its contents" - }, - "is": { - "type": "string", - "detailedType": "string | undefined", - "typeAst": { - "type": "string" - }, - "description": "Specify that a standard HTML element should behave like a defined custom built-in element" - }, - "tw": { - "type": "string", - "detailedType": "string | undefined", - "typeAst": { - "type": "string" - }, - "description": "Specify styles using Tailwind CSS classes. This feature is currently experimental.\nIf `style` prop is also specified, styles generated with `tw` prop will be overridden.\n\nExample:\n- `tw='w-full h-full bg-blue-200'`\n- `tw='text-9xl'`\n- `tw='text-[80px]'`" - }, - "aria-activedescendant": { - "type": "string", - "detailedType": "string | undefined", - "typeAst": { - "type": "string" - }, - "description": "Identifies the currently active element when DOM focus is on a composite widget, textbox, group, or application." - }, - "aria-atomic": { - "type": "Booleanish", - "detailedType": "Booleanish | undefined", - "typeAst": { - "type": "identifier", - "name": "Booleanish" - }, - "description": "Indicates whether assistive technologies will present all, or only parts of, the changed region based on the change notifications defined by the aria-relevant attribute." - }, - "aria-autocomplete": { - "type": "\"list\" | \"none\" | \"inline\" | \"both\"", - "detailedType": "'list' | 'none' | 'inline' | 'both' | undefined", - "typeAst": { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "list" - }, - { - "type": "stringLiteral", - "value": "none" - }, - { - "type": "stringLiteral", - "value": "inline" - }, - { - "type": "stringLiteral", - "value": "both" - } - ] - }, - "description": "Indicates whether inputting text could trigger display of one or more predictions of the user's intended value for an input and specifies how predictions would be\npresented if they are made." - }, - "aria-braillelabel": { - "type": "string", - "detailedType": "string | undefined", - "typeAst": { - "type": "string" - }, - "description": "Defines a string value that labels the current element, which is intended to be converted into Braille." - }, - "aria-brailleroledescription": { - "type": "string", - "detailedType": "string | undefined", - "typeAst": { - "type": "string" - }, - "description": "Defines a human-readable, author-localized abbreviated description for the role of an element, which is intended to be converted into Braille." - }, - "aria-checked": { - "type": "boolean | \"true\" | \"false\" | \"mixed\"", - "detailedType": "boolean | 'true' | 'false' | 'mixed' | undefined", - "typeAst": { - "type": "union", - "elements": [ - { - "type": "boolean" - }, - { - "type": "undefined" - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "true" - } - ] - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "false" - } - ] - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "mixed" - } - ] - } - ] - }, - "description": "Indicates the current \"checked\" state of checkboxes, radio buttons, and other widgets." - }, - "aria-colcount": { - "type": "number", - "detailedType": "number | undefined", - "typeAst": { - "type": "number" - }, - "description": "Defines the total number of columns in a table, grid, or treegrid." - }, - "aria-colindex": { - "type": "number", - "detailedType": "number | undefined", - "typeAst": { - "type": "number" - }, - "description": "Defines an element's column index or position with respect to the total number of columns within a table, grid, or treegrid." - }, - "aria-colindextext": { - "type": "string", - "detailedType": "string | undefined", - "typeAst": { - "type": "string" - }, - "description": "Defines a human readable text alternative of aria-colindex." - }, - "aria-colspan": { - "type": "number", - "detailedType": "number | undefined", - "typeAst": { - "type": "number" - }, - "description": "Defines the number of columns spanned by a cell or gridcell within a table, grid, or treegrid." - }, - "aria-controls": { - "type": "string", - "detailedType": "string | undefined", - "typeAst": { - "type": "string" - }, - "description": "Identifies the element (or elements) whose contents or presence are controlled by the current element." - }, - "aria-current": { - "type": "boolean | \"time\" | \"step\" | \"true\" | \"false\" | \"page\" | \"location\" | \"date\"", - "detailedType": "| boolean\n| 'time'\n| 'step'\n| 'true'\n| 'false'\n| 'page'\n| 'location'\n| 'date'\n| undefined", - "typeAst": { - "type": "union", - "elements": [ - { - "type": "boolean" - }, - { - "type": "undefined" - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "time" - } - ] - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "step" - } - ] - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "true" - } - ] - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "false" - } - ] - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "page" - } - ] - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "location" - } - ] - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "date" - } - ] - } - ] - }, - "description": "Indicates the element that represents the current item within a container or set of related elements." - }, - "aria-describedby": { - "type": "string", - "detailedType": "string | undefined", - "typeAst": { - "type": "string" - }, - "description": "Identifies the element (or elements) that describes the object." - }, - "aria-description": { - "type": "string", - "detailedType": "string | undefined", - "typeAst": { - "type": "string" - }, - "description": "Defines a string value that describes or annotates the current element." - }, - "aria-details": { - "type": "string", - "detailedType": "string | undefined", - "typeAst": { - "type": "string" - }, - "description": "Identifies the element that provides a detailed, extended description for the object." - }, - "aria-disabled": { - "type": "Booleanish", - "detailedType": "Booleanish | undefined", - "typeAst": { - "type": "identifier", - "name": "Booleanish" - }, - "description": "Indicates that the element is perceivable but disabled, so it is not editable or otherwise operable." - }, - "aria-dropeffect": { - "type": "\"link\" | \"none\" | \"copy\" | \"execute\" | \"move\" | \"popup\"", - "detailedType": "| 'link'\n| 'none'\n| 'copy'\n| 'execute'\n| 'move'\n| 'popup'\n| undefined", - "typeAst": { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "link" - }, - { - "type": "stringLiteral", - "value": "none" - }, - { - "type": "stringLiteral", - "value": "copy" - }, - { - "type": "stringLiteral", - "value": "execute" - }, - { - "type": "stringLiteral", - "value": "move" - }, - { - "type": "stringLiteral", - "value": "popup" - } - ] - }, - "description": "Indicates what functions can be performed when a dragged object is released on the drop target." - }, - "aria-errormessage": { - "type": "string", - "detailedType": "string | undefined", - "typeAst": { - "type": "string" - }, - "description": "Identifies the element that provides an error message for the object." - }, - "aria-expanded": { - "type": "Booleanish", - "detailedType": "Booleanish | undefined", - "typeAst": { - "type": "identifier", - "name": "Booleanish" - }, - "description": "Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed." - }, - "aria-flowto": { - "type": "string", - "detailedType": "string | undefined", - "typeAst": { - "type": "string" - }, - "description": "Identifies the next element (or elements) in an alternate reading order of content which, at the user's discretion,\nallows assistive technology to override the general default of reading in document source order." - }, - "aria-grabbed": { - "type": "Booleanish", - "detailedType": "Booleanish | undefined", - "typeAst": { - "type": "identifier", - "name": "Booleanish" - }, - "description": "Indicates an element's \"grabbed\" state in a drag-and-drop operation." - }, - "aria-haspopup": { - "type": "boolean | \"dialog\" | \"menu\" | \"true\" | \"false\" | \"listbox\" | \"tree\" | \"grid\"", - "detailedType": "| boolean\n| 'dialog'\n| 'menu'\n| 'true'\n| 'false'\n| 'listbox'\n| 'tree'\n| 'grid'\n| undefined", - "typeAst": { - "type": "union", - "elements": [ - { - "type": "boolean" - }, - { - "type": "undefined" - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "dialog" - } - ] - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "menu" - } - ] - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "true" - } - ] - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "false" - } - ] - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "listbox" - } - ] - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "tree" - } - ] - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "grid" - } - ] - } - ] - }, - "description": "Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element." - }, - "aria-hidden": { - "type": "Booleanish", - "detailedType": "Booleanish | undefined", - "typeAst": { - "type": "identifier", - "name": "Booleanish" - }, - "description": "Indicates whether the element is exposed to an accessibility API." - }, - "aria-invalid": { - "type": "boolean | \"true\" | \"false\" | \"grammar\" | \"spelling\"", - "detailedType": "| boolean\n| 'true'\n| 'false'\n| 'grammar'\n| 'spelling'\n| undefined", - "typeAst": { - "type": "union", - "elements": [ - { - "type": "boolean" - }, - { - "type": "undefined" - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "true" - } - ] - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "false" - } - ] - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "grammar" - } - ] - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "spelling" - } - ] - } - ] - }, - "description": "Indicates the entered value does not conform to the format expected by the application." - }, - "aria-keyshortcuts": { - "type": "string", - "detailedType": "string | undefined", - "typeAst": { - "type": "string" - }, - "description": "Indicates keyboard shortcuts that an author has implemented to activate or give focus to an element." - }, - "aria-label": { - "type": "string", - "detailedType": "string | undefined", - "typeAst": { - "type": "string" - }, - "description": "Defines a string value that labels the current element." - }, - "aria-labelledby": { - "type": "string", - "detailedType": "string | undefined", - "typeAst": { - "type": "string" - }, - "description": "Identifies the element (or elements) that labels the current element." - }, - "aria-level": { - "type": "number", - "detailedType": "number | undefined", - "typeAst": { - "type": "number" - }, - "description": "Defines the hierarchical level of an element within a structure." - }, - "aria-live": { - "type": "\"off\" | \"assertive\" | \"polite\"", - "detailedType": "'off' | 'assertive' | 'polite' | undefined", - "typeAst": { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "off" - }, - { - "type": "stringLiteral", - "value": "assertive" - }, - { - "type": "stringLiteral", - "value": "polite" - } - ] - }, - "description": "Indicates that an element will be updated, and describes the types of updates the user agents, assistive technologies, and user can expect from the live region." - }, - "aria-modal": { - "type": "Booleanish", - "detailedType": "Booleanish | undefined", - "typeAst": { - "type": "identifier", - "name": "Booleanish" - }, - "description": "Indicates whether an element is modal when displayed." - }, - "aria-multiline": { - "type": "Booleanish", - "detailedType": "Booleanish | undefined", - "typeAst": { - "type": "identifier", - "name": "Booleanish" - }, - "description": "Indicates whether a text box accepts multiple lines of input or only a single line." - }, - "aria-multiselectable": { - "type": "Booleanish", - "detailedType": "Booleanish | undefined", - "typeAst": { - "type": "identifier", - "name": "Booleanish" - }, - "description": "Indicates that the user may select more than one item from the current selectable descendants." - }, - "aria-orientation": { - "type": "\"horizontal\" | \"vertical\"", - "detailedType": "'horizontal' | 'vertical' | undefined", - "typeAst": { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "horizontal" - }, - { - "type": "stringLiteral", - "value": "vertical" - } - ] - }, - "description": "Indicates whether the element's orientation is horizontal, vertical, or unknown/ambiguous." - }, - "aria-owns": { - "type": "string", - "detailedType": "string | undefined", - "typeAst": { - "type": "string" - }, - "description": "Identifies an element (or elements) in order to define a visual, functional, or contextual parent/child relationship\nbetween DOM elements where the DOM hierarchy cannot be used to represent the relationship." - }, - "aria-placeholder": { - "type": "string", - "detailedType": "string | undefined", - "typeAst": { - "type": "string" - }, - "description": "Defines a short hint (a word or short phrase) intended to aid the user with data entry when the control has no value.\nA hint could be a sample value or a brief description of the expected format." - }, - "aria-posinset": { - "type": "number", - "detailedType": "number | undefined", - "typeAst": { - "type": "number" - }, - "description": "Defines an element's number or position in the current set of listitems or treeitems. Not required if all elements in the set are present in the DOM." - }, - "aria-pressed": { - "type": "boolean | \"true\" | \"false\" | \"mixed\"", - "detailedType": "boolean | 'true' | 'false' | 'mixed' | undefined", - "typeAst": { - "type": "union", - "elements": [ - { - "type": "boolean" - }, - { - "type": "undefined" - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "true" - } - ] - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "false" - } - ] - }, - { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "mixed" - } - ] - } - ] - }, - "description": "Indicates the current \"pressed\" state of toggle buttons." - }, - "aria-readonly": { - "type": "Booleanish", - "detailedType": "Booleanish | undefined", - "typeAst": { - "type": "identifier", - "name": "Booleanish" - }, - "description": "Indicates that the element is not editable, but is otherwise operable." - }, - "aria-relevant": { - "type": "\"text\" | \"additions\" | \"additions removals\" | \"additions text\" | \"all\" | \"removals\" | \"removals additions\" | \"removals text\" | \"text additions\" | \"text removals\"", - "detailedType": "| 'text'\n| 'additions'\n| 'additions removals'\n| 'additions text'\n| 'all'\n| 'removals'\n| 'removals additions'\n| 'removals text'\n| 'text additions'\n| 'text removals'\n| undefined", - "typeAst": { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "text" - }, - { - "type": "stringLiteral", - "value": "additions" - }, - { - "type": "stringLiteral", - "value": "additions removals" - }, - { - "type": "stringLiteral", - "value": "additions text" - }, - { - "type": "stringLiteral", - "value": "all" - }, - { - "type": "stringLiteral", - "value": "removals" - }, - { - "type": "stringLiteral", - "value": "removals additions" - }, - { - "type": "stringLiteral", - "value": "removals text" - }, - { - "type": "stringLiteral", - "value": "text additions" - }, - { - "type": "stringLiteral", - "value": "text removals" - } - ] - }, - "description": "Indicates what notifications the user agent will trigger when the accessibility tree within a live region is modified." - }, - "aria-required": { - "type": "Booleanish", - "detailedType": "Booleanish | undefined", - "typeAst": { - "type": "identifier", - "name": "Booleanish" - }, - "description": "Indicates that user input is required on the element before a form may be submitted." - }, - "aria-roledescription": { - "type": "string", - "detailedType": "string | undefined", - "typeAst": { - "type": "string" - }, - "description": "Defines a human-readable, author-localized description for the role of an element." - }, - "aria-rowcount": { - "type": "number", - "detailedType": "number | undefined", - "typeAst": { - "type": "number" - }, - "description": "Defines the total number of rows in a table, grid, or treegrid." - }, - "aria-rowindex": { - "type": "number", - "detailedType": "number | undefined", - "typeAst": { - "type": "number" - }, - "description": "Defines an element's row index or position with respect to the total number of rows within a table, grid, or treegrid." - }, - "aria-rowindextext": { - "type": "string", - "detailedType": "string | undefined", - "typeAst": { - "type": "string" - }, - "description": "Defines a human readable text alternative of aria-rowindex." - }, - "aria-rowspan": { - "type": "number", - "detailedType": "number | undefined", - "typeAst": { - "type": "number" - }, - "description": "Defines the number of rows spanned by a cell or gridcell within a table, grid, or treegrid." - }, - "aria-selected": { - "type": "Booleanish", - "detailedType": "Booleanish | undefined", - "typeAst": { - "type": "identifier", - "name": "Booleanish" - }, - "description": "Indicates the current \"selected\" state of various widgets." - }, - "aria-setsize": { - "type": "number", - "detailedType": "number | undefined", - "typeAst": { - "type": "number" - }, - "description": "Defines the number of items in the current set of listitems or treeitems. Not required if all elements in the set are present in the DOM." - }, - "aria-sort": { - "type": "\"none\" | \"ascending\" | \"descending\" | \"other\"", - "detailedType": "| 'none'\n| 'ascending'\n| 'descending'\n| 'other'\n| undefined", - "typeAst": { - "type": "union", - "elements": [ - { - "type": "stringLiteral", - "value": "none" - }, - { - "type": "stringLiteral", - "value": "ascending" - }, - { - "type": "stringLiteral", - "value": "descending" - }, - { - "type": "stringLiteral", - "value": "other" - } - ] - }, - "description": "Indicates if items in a table or grid are sorted in ascending or descending order." - }, - "aria-valuemax": { - "type": "number", - "detailedType": "number | undefined", - "typeAst": { - "type": "number" - }, - "description": "Defines the maximum allowed value for a range widget." - }, - "aria-valuemin": { - "type": "number", - "detailedType": "number | undefined", - "typeAst": { - "type": "number" - }, - "description": "Defines the minimum allowed value for a range widget." - }, - "aria-valuenow": { - "type": "number", - "detailedType": "number | undefined", - "typeAst": { - "type": "number" - }, - "description": "Defines the current value for a range widget." - }, - "aria-valuetext": { - "type": "string", - "detailedType": "string | undefined", - "typeAst": { - "type": "string" - }, - "description": "Defines the human readable text alternative of aria-valuenow for a range widget." - } - } + "name": "AvatarProps", + "description": "A visual representation of a user or entity, displayed as an image, initials, or placeholder.", + "extendsElement": "span", + "props": { + "size": { + "type": "\"sm\" | \"md\" | \"lg\"", + "detailedType": "'sm' | 'md' | 'lg' | undefined", + "typeAst": { + "type": "union", + "elements": [ + { + "type": "stringLiteral", + "value": "sm" + }, + { + "type": "stringLiteral", + "value": "md" + }, + { + "type": "stringLiteral", + "value": "lg" + } + ] + }, + "description": "The size of the avatar.", + "default": "'md'" + } + } } diff --git a/www/src/routes/index.tsx b/www/src/routes/index.tsx index b1c0b6f82..2e31a5da1 100644 --- a/www/src/routes/index.tsx +++ b/www/src/routes/index.tsx @@ -7,7 +7,7 @@ import { ShadcnIcon } from "@dotui/registry/components/icons/shadcn"; import { TailwindWordmark } from "@dotui/registry/components/icons/tailwind-wordmark"; import { TypeScriptIcon } from "@dotui/registry/components/icons/typescript"; import { Alert, AlertTitle } from "@dotui/registry/ui/alert"; -import { Avatar } from "@dotui/registry/ui/avatar"; +import { Avatar, AvatarFallback, AvatarImage, AvatarPlaceholder } from "@dotui/registry/ui/avatar"; import { LinkButton } from "@dotui/registry/ui/button"; import { Tooltip, TooltipContent } from "@dotui/registry/ui/tooltip"; @@ -152,7 +152,11 @@ function HomePage() { {contributors.map((contributor) => ( - + + + {contributor.login?.charAt(0).toUpperCase()} + + {contributor.login} From 10ca7302a426d7fe1ad27af8f8a3fa43c2013aa5 Mon Sep 17 00:00:00 2001 From: mehdibha Date: Wed, 11 Mar 2026 13:53:43 +0100 Subject: [PATCH 02/10] refactor: remove unused variant type param from createDynamicComponent The V generic on VariantsMap and createDynamicComponent was unused (_V was prefixed with underscore and the record key was always string). Co-Authored-By: Claude Opus 4.6 --- packages/core/src/react/dynamic-component.tsx | 6 +++--- packages/registry/src/ui/accordion/index.tsx | 3 +-- packages/registry/src/ui/alert/index.tsx | 9 ++++----- packages/registry/src/ui/disclosure/index.tsx | 7 +++---- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/packages/core/src/react/dynamic-component.tsx b/packages/core/src/react/dynamic-component.tsx index d5f3add58..687239380 100644 --- a/packages/core/src/react/dynamic-component.tsx +++ b/packages/core/src/react/dynamic-component.tsx @@ -13,7 +13,7 @@ import { StyleContext } from "./context"; type VariantKey = keyof typeof VARIANTS; -export type VariantsMap = Record< +export type VariantsMap = Record< string, React.LazyExoticComponent> >; @@ -149,11 +149,11 @@ class ErrorBoundary extends React.Component( +export function createDynamicComponent( componentName: VariantKey, displayName: string, DefaultComponent: React.ComponentType, - variants: VariantsMap, + variants: VariantsMap, options?: CreateDynamicComponentOptions, ): React.FC { const { disableSkeleton = false } = options ?? {}; diff --git a/packages/registry/src/ui/accordion/index.tsx b/packages/registry/src/ui/accordion/index.tsx index 218ea7370..769d2723c 100644 --- a/packages/registry/src/ui/accordion/index.tsx +++ b/packages/registry/src/ui/accordion/index.tsx @@ -3,10 +3,9 @@ import { createDynamicComponent } from "@dotui/core/react/dynamic-component"; import * as Default from "./basic"; -import type { AccordionVariant } from "./meta"; import type { AccordionProps } from "./types"; -export const Accordion = createDynamicComponent( +export const Accordion = createDynamicComponent( "accordion", "Accordion", Default.Accordion, diff --git a/packages/registry/src/ui/alert/index.tsx b/packages/registry/src/ui/alert/index.tsx index be53c56fe..7923a8bef 100644 --- a/packages/registry/src/ui/alert/index.tsx +++ b/packages/registry/src/ui/alert/index.tsx @@ -3,26 +3,25 @@ import { createDynamicComponent } from "@dotui/core/react/dynamic-component"; import * as Default from "./basic"; -import type { AlertVariant } from "./meta"; import type { AlertActionProps, AlertDescriptionProps, AlertProps, AlertTitleProps } from "./types"; -export const Alert = createDynamicComponent("alert", "Alert", Default.Alert, {}); +export const Alert = createDynamicComponent("alert", "Alert", Default.Alert, {}); -export const AlertTitle = createDynamicComponent( +export const AlertTitle = createDynamicComponent( "alert", "AlertTitle", Default.AlertTitle, {}, ); -export const AlertDescription = createDynamicComponent( +export const AlertDescription = createDynamicComponent( "alert", "AlertDescription", Default.AlertDescription, {}, ); -export const AlertAction = createDynamicComponent( +export const AlertAction = createDynamicComponent( "alert", "AlertAction", Default.AlertAction, diff --git a/packages/registry/src/ui/disclosure/index.tsx b/packages/registry/src/ui/disclosure/index.tsx index 7241ed24d..542d28d05 100644 --- a/packages/registry/src/ui/disclosure/index.tsx +++ b/packages/registry/src/ui/disclosure/index.tsx @@ -3,24 +3,23 @@ import { createDynamicComponent } from "@dotui/core/react/dynamic-component"; import * as Default from "./basic"; -import type { DisclosureVariant } from "./meta"; import type { DisclosurePanelProps, DisclosureProps, DisclosureTriggerProps } from "./types"; -export const Disclosure = createDynamicComponent( +export const Disclosure = createDynamicComponent( "disclosure", "Disclosure", Default.Disclosure, {}, ); -export const DisclosurePanel = createDynamicComponent( +export const DisclosurePanel = createDynamicComponent( "disclosure", "DisclosurePanel", Default.DisclosurePanel, {}, ); -export const DisclosureTrigger = createDynamicComponent( +export const DisclosureTrigger = createDynamicComponent( "disclosure", "DisclosureTrigger", Default.DisclosureTrigger, From df23ab2a4560cee1ee0727ffd52ac4cad48144d2 Mon Sep 17 00:00:00 2001 From: mehdibha Date: Wed, 11 Mar 2026 13:57:07 +0100 Subject: [PATCH 03/10] chore: regenerate references, update registry, and misc cleanup Co-Authored-By: Claude Opus 4.6 --- .cursorrules | 1149 ----------------- .gitignore | 3 +- packages/core/src/__registry__/blocks.ts | 34 +- packages/core/src/__registry__/ui.ts | 147 ++- packages/core/src/__registry__/variants.ts | 4 +- packages/core/src/react/dynamic-component.tsx | 5 +- packages/registry/src/__generated__/demos.tsx | 44 +- .../cards/components/invite-members.tsx | 8 +- packages/registry/src/ui/accordion/index.tsx | 7 +- packages/registry/src/ui/alert/index.tsx | 14 +- packages/registry/src/ui/badge/basic.tsx | 13 +- packages/registry/src/ui/badge/types.ts | 7 +- packages/registry/src/ui/disclosure/index.tsx | 7 +- packages/tailwindcss-with/src/index.js | 8 +- www/src/modules/docs/mdx-components.tsx | 5 +- .../references/generated/accordion.json | 145 ++- .../references/generated/avatar-badge.json | 8 +- .../references/generated/avatar-fallback.json | 8 +- .../generated/avatar-group-count.json | 8 +- .../references/generated/avatar-group.json | 56 +- .../references/generated/avatar-image.json | 26 +- .../generated/avatar-placeholder.json | 8 +- .../modules/references/generated/avatar.json | 56 +- .../modules/references/generated/badge.json | 31 +- .../references/generated/breadcrumb-item.json | 143 +- .../references/generated/breadcrumb-link.json | 324 +++-- .../references/generated/breadcrumbs.json | 59 +- .../modules/references/generated/button.json | 889 ++++++++++--- .../references/generated/calendar-cell.json | 243 +++- .../generated/calendar-grid-body.json | 53 +- .../generated/calendar-grid-header.json | 51 +- .../references/generated/calendar-grid.json | 53 +- .../generated/calendar-header-cell.json | 51 +- .../references/generated/checkbox-group.json | 275 ++-- .../references/generated/checkbox.json | 304 +++-- .../references/generated/color-field.json | 526 +++++--- .../generated/color-picker-content.json | 65 +- .../generated/color-picker-trigger.json | 821 ++++++++++-- .../references/generated/color-picker.json | 123 +- .../generated/color-slider-control.json | 128 +- .../generated/color-slider-output.json | 106 +- .../references/generated/color-slider.json | 280 ++-- .../generated/color-swatch-picker-item.json | 306 ++++- .../generated/color-swatch-picker.json | 231 +++- .../references/generated/color-swatch.json | 141 +- .../references/generated/color-thumb.json | 108 ++ .../generated/combobox-content.json | 213 ++- .../references/generated/combobox-input.json | 669 +++++----- .../references/generated/combobox.json | 238 ++-- .../references/generated/command-content.json | 213 ++- .../references/generated/command-input.json | 799 +++++++----- .../references/generated/date-field.json | 297 +++-- .../references/generated/date-input.json | 99 +- .../generated/date-picker-content.json | 159 ++- .../generated/date-picker-input.json | 669 +++++----- .../references/generated/date-segment.json | 257 +++- .../references/generated/description.json | 526 ++++---- .../references/generated/dialog-content.json | 65 +- .../generated/dialog-description.json | 526 ++++---- .../references/generated/dialog-heading.json | 58 +- .../generated/disclosure-panel.json | 165 ++- .../generated/disclosure-trigger.json | 823 ++++++++++-- .../references/generated/disclosure.json | 147 ++- .../modules/references/generated/drawer.json | 137 +- .../references/generated/drop-zone-label.json | 526 ++++---- .../references/generated/drop-zone.json | 181 ++- .../references/generated/field-error.json | 114 +- .../modules/references/generated/group.json | 669 +++++----- .../references/generated/input-group.json | 669 +++++----- .../modules/references/generated/input.json | 687 +++++----- www/src/modules/references/generated/kbd.json | 24 +- .../modules/references/generated/label.json | 58 +- .../references/generated/link-button.json | 332 +++-- .../modules/references/generated/link.json | 332 +++-- .../references/generated/list-box-item.json | 403 +++++- .../generated/list-box-section-header.json | 58 +- .../generated/list-box-section.json | 63 +- .../references/generated/list-box.json | 325 +++-- .../modules/references/generated/loader.json | 171 ++- .../references/generated/menu-content.json | 155 ++- .../references/generated/menu-item.json | 419 +++++- .../generated/menu-section-header.json | 58 +- .../references/generated/menu-section.json | 71 +- .../references/generated/modal-content.json | 137 +- .../references/generated/modal-overlay.json | 137 +- .../modules/references/generated/modal.json | 137 +- .../references/generated/number-field.json | 277 ++-- .../modules/references/generated/overlay.json | 280 ++-- .../modules/references/generated/popover.json | 203 ++- .../references/generated/progress-bar.json | 177 ++- .../references/generated/radio-group.json | 300 +++-- .../modules/references/generated/radio.json | 254 +++- .../references/generated/search-field.json | 797 +++++++----- .../references/generated/select-content.json | 213 ++- .../references/generated/select-value.json | 1002 +++++++------- .../modules/references/generated/select.json | 355 +++-- .../references/generated/separator.json | 55 +- .../references/generated/slider-control.json | 128 +- .../references/generated/slider-output.json | 106 +- .../references/generated/slider-thumb.json | 217 +++- .../modules/references/generated/slider.json | 254 ++-- .../modules/references/generated/switch.json | 273 ++-- .../references/generated/tab-indicator.json | 665 +++++----- .../references/generated/tab-list.json | 88 +- .../references/generated/tab-panel.json | 203 ++- www/src/modules/references/generated/tab.json | 301 ++++- .../references/generated/table-body.json | 137 +- .../references/generated/table-cell.json | 222 +++- .../references/generated/table-column.json | 257 +++- .../references/generated/table-container.json | 59 +- .../references/generated/table-header.json | 131 +- .../references/generated/table-load-more.json | 55 +- .../references/generated/table-row.json | 321 ++++- .../modules/references/generated/table.json | 173 ++- .../modules/references/generated/tabs.json | 198 ++- .../references/generated/tag-group.json | 131 +- .../references/generated/tag-list.json | 163 ++- www/src/modules/references/generated/tag.json | 329 ++++- .../references/generated/text-area.json | 687 +++++----- .../references/generated/text-field.json | 827 ++++++------ .../modules/references/generated/text.json | 52 +- .../references/generated/time-field.json | 297 +++-- .../modules/references/generated/toast.json | 321 +++-- .../generated/toggle-button-group.json | 171 ++- .../references/generated/toggle-button.json | 290 ++++- .../references/generated/tooltip-content.json | 197 ++- .../modules/references/generated/tooltip.json | 9 + www/src/modules/styles/server.ts | 18 +- 128 files changed, 20339 insertions(+), 9721 deletions(-) delete mode 100644 .cursorrules diff --git a/.cursorrules b/.cursorrules deleted file mode 100644 index 90d9edc7b..000000000 --- a/.cursorrules +++ /dev/null @@ -1,1149 +0,0 @@ -# DotUI Component Usage Guide - -This document provides comprehensive usage examples for all DotUI components. Components are built on React Aria Components and use Tailwind Variants for styling. - -## Import Pattern - -All components are imported from `@dotui/registry/ui/{component-name}`: - -```tsx -import { ComponentName } from "@dotui/registry/ui/component-name"; -``` - ---- - -## Form Components - -### TextField - -A wrapper component for text input fields that provides proper form structure. - -```tsx -import { Label } from "@dotui/registry/ui/field"; -import { Input } from "@dotui/registry/ui/input"; -import { TextField } from "@dotui/registry/ui/text-field"; - - - - -; -``` - -### Input - -Basic text input component. Can be used standalone or within InputGroup. - -```tsx -import { Input, InputGroup, InputAddon } from "@dotui/registry/ui/input"; - -// Standalone - - -// With InputGroup (for addons/icons) - - @ - - - -// With TextArea - -