Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,149 changes: 0 additions & 1,149 deletions .cursorrules

This file was deleted.

3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ Thumbs.db
shadcn-ui

nitro-main
router-main
router-main
base-ui
33 changes: 20 additions & 13 deletions packages/core/src/__registry__/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export default Cards;
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 } 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";
Expand All @@ -173,7 +173,10 @@ export function AccountMenu({ className, ...props }: React.ComponentProps<"div">
return (
<Card className={cn("min-w-56 gap-0 py-0", className)} {...props}>
<CardHeader className="flex w-full items-center border-b px-4 py-3">
<Avatar src="https://github.com/mehdibha.png" size="sm" />
<Avatar size="sm">
<AvatarImage src="https://github.com/mehdibha.png" alt="mehdibha" />
<AvatarFallback>M</AvatarFallback>
</Avatar>
<div className="w-full text-sm">
<p className="font-semibold">mehdibha</p>
<p className="text-fg-muted">
Expand Down Expand Up @@ -716,7 +719,7 @@ export function Filters({ className, ...props }: React.ComponentProps<"div">) {
import { PlusCircleIcon } from "lucide-react";

import { ExternalLinkIcon } from "@dotui/registry/icons";
import { Avatar } from "@dotui/registry/ui/avatar";
import { Avatar, AvatarFallback, AvatarImage } from "@dotui/registry/ui/avatar";
import { Button } from "@dotui/registry/ui/button";
import {
Card,
Expand Down Expand Up @@ -795,7 +798,10 @@ export function InviteMembers(props: React.ComponentProps<"div">) {
{teamMembers.map((member) => (
<div key={member.name} className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<Avatar src={member.avatar} size="sm" />
<Avatar size="sm">
<AvatarImage src={member.avatar} alt={member.name} />
<AvatarFallback>{member.name.charAt(0)}</AvatarFallback>
</Avatar>
<div className="text-sm">
<p>{member.name}</p>
<p className="text-fg-muted">{member.role}</p>
Expand Down Expand Up @@ -905,7 +911,7 @@ export function LoginForm(props: React.ComponentProps<"div">) {
content: `import React from "react";

import { cn } from "@dotui/registry/lib/utils";
import { Avatar } from "@dotui/registry/ui/avatar";
import { Avatar, AvatarFallback, AvatarImage } 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";
Expand Down Expand Up @@ -952,14 +958,15 @@ export function Notifications({ className, ...props }: React.ComponentProps<"div
<Separator />
<ListBoxItem textValue={notification.text}>
<div className="flex items-start gap-3 py-2">
<Avatar
src={notification.user.avatar}
fallback={notification.user.name
.split(" ")
.map((n) => n[0])
.join("")}
size="md"
/>
<Avatar size="md">
<AvatarImage src={notification.user.avatar} alt={notification.user.name} />
<AvatarFallback>
{notification.user.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<p className="text-sm">
<span className="font-medium">{notification.user.name}</span>{" "}
Expand Down
165 changes: 88 additions & 77 deletions packages/core/src/__registry__/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,9 @@ import type { VariantProps } from "tailwind-variants";

const alertVariants = tv({
slots: {
root: "relative grid w-full items-start gap-y-0.5 rounded-lg border bg-card has-[>svg]:has-data-alert-action:grid-cols-[calc(var(--spacing)*4)_1fr_auto] has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-data-alert-action:grid-cols-[1fr_auto] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
root: "relative grid w-full items-start gap-y-0.5 rounded-lg border bg-card px-4 py-3 text-sm has-[>svg]:has-data-alert-action:grid-cols-[calc(var(--spacing)*4)_1fr_auto] has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-data-alert-action:grid-cols-[1fr_auto] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
title: "line-clamp-1 min-h-4 font-medium tracking-tight [svg~&]:col-start-2",
description:
"grid justify-items-start gap-1 text-fg-muted [&_p]:leading-relaxed [svg~&]:col-start-2",
description: "grid justify-items-start gap-1 text-fg-muted [&_p]:leading-relaxed [svg~&]:col-start-2",
action:
"flex gap-1 sm:row-start-1 sm:row-end-3 sm:self-center sm:[[data-alert-title]~&]:col-start-2 sm:[svg~&]:col-start-2 sm:[svg~[data-alert-description]~&]:col-start-3 sm:[svg~[data-alert-title]~&]:col-start-3",
},
Expand Down Expand Up @@ -138,13 +137,13 @@ export type { AlertProps, AlertTitleProps, AlertDescriptionProps, AlertActionPro
{
name: "avatar",
type: "registry:ui",
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",
content: `"use client";

Expand All @@ -158,131 +157,136 @@ import type { ImageLoadingStatus } from "@dotui/registry/hooks/use-image-loading

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",
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",
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-[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" },
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 } = avatarStyles();

/* -----------------------------------------------------------------------------------------------*/

interface AvatarGroupProps extends React.ComponentProps<"div">, VariantProps<typeof avatarStyles> {}

const AvatarGroup = ({ className, size, ...props }: AvatarGroupProps) => {
return <div className={group({ className, size })} {...props} />;
};

/* -----------------------------------------------------------------------------------------------*/
const { group, root, image, fallback, badge, groupCount } = avatarStyles();

interface AvatarProps extends AvatarImageProps, VariantProps<typeof avatarStyles> {
fallback?: React.ReactNode;
}
const Avatar = ({ className, style, fallback, size, ...props }: AvatarProps) => {
return (
<AvatarRoot className={className} style={style} size={size}>
<AvatarImage {...props} />
<AvatarFallback>{fallback}</AvatarFallback>
<AvatarPlaceholder />
</AvatarRoot>
);
};

/* -----------------------------------------------------------------------------------------------*/

const [AvatarInternalContext, useAvatarInternalContext] = createContext<{
const [AvatarContext, useAvatarContext] = createContext<{
status: ImageLoadingStatus;
setStatus: (status: ImageLoadingStatus) => void;
}>({
name: "AvatarRoot",
name: "Avatar",
strict: true,
});

interface AvatarRootProps extends React.ComponentProps<"span">, VariantProps<typeof avatarStyles> {}
function AvatarRoot({ className, size, ...props }: AvatarRootProps) {
/* -------------------------------------------------------------------------------------------------
* Avatar
* -----------------------------------------------------------------------------------------------*/

interface AvatarProps extends React.ComponentProps<"span">, VariantProps<typeof avatarStyles> {}

function Avatar({ className, size = "md", ...props }: AvatarProps) {
const [status, setStatus] = React.useState<ImageLoadingStatus>("idle");

return (
<AvatarInternalContext value={{ status, setStatus }}>
<span data-slot="avatar" className={root({ className, size })} {...props} />
</AvatarInternalContext>
<AvatarContext value={{ status, setStatus }}>
<span data-avatar="" data-size={size} className={root({ className, size })} {...props} />
</AvatarContext>
);
}

/* -----------------------------------------------------------------------------------------------*/
/* -------------------------------------------------------------------------------------------------
* Avatar Image
* -----------------------------------------------------------------------------------------------*/

interface AvatarImageProps extends Omit<React.ComponentProps<"img">, "src"> {
src?: string;
}

function AvatarImage({ src, alt, className, referrerPolicy, crossOrigin, ...props }: AvatarImageProps) {
const status = useImageLoadingStatus(src, { referrerPolicy, crossOrigin });
const { setStatus } = useAvatarInternalContext("AvatarImage");
const { setStatus } = useAvatarContext("AvatarImage");

React.useLayoutEffect(() => {
if (status !== "idle") {
setStatus(status);
}
setStatus(status);
}, [status, setStatus]);

if (status === "loaded")
return <img slot="avatar-image" className={image({ className })} src={src} alt={alt} {...props} />;
return <img data-avatar-image="" className={image({ className })} src={src} alt={alt} {...props} />;

return null;
}

/* -----------------------------------------------------------------------------------------------*/
/* -------------------------------------------------------------------------------------------------
* Avatar Fallback
* -----------------------------------------------------------------------------------------------*/

type AvatarFallbackProps = React.HTMLAttributes<HTMLSpanElement>;
interface AvatarFallbackProps extends React.ComponentProps<"span"> {}

const AvatarFallback = ({ className, ...props }: AvatarFallbackProps) => {
const { status } = useAvatarInternalContext("AvatarFallback");
if (status === "error") return <span slot="avatar-fallback" className={fallback({ className })} {...props} />;
const { status } = useAvatarContext("AvatarFallback");
if (status !== "loaded") return <span data-avatar-fallback="" className={fallback({ className })} {...props} />;
return null;
};

/* -----------------------------------------------------------------------------------------------*/
/* -------------------------------------------------------------------------------------------------
* Avatar Badge
* -----------------------------------------------------------------------------------------------*/

interface AvatarPlaceholderProps extends React.ComponentProps<"span"> {}
interface AvatarBadgeProps extends React.ComponentProps<"span"> {}

const AvatarPlaceholder = ({ className, ...props }: AvatarPlaceholderProps) => {
const { status } = useAvatarInternalContext("AvatarPlaceholder");
if (["idle", "loading"].includes(status)) return <span className={placeholder({ className })} {...props} />;
return null;
const AvatarBadge = ({ className, ...props }: AvatarBadgeProps) => {
return <span data-avatar-badge="" className={badge({ className })} {...props} />;
};

/* -----------------------------------------------------------------------------------------------*/
/* -------------------------------------------------------------------------------------------------
* Avatar Group
* -----------------------------------------------------------------------------------------------*/

const CompoundAvatar = Object.assign(Avatar, {
Group: AvatarGroup,
Root: AvatarRoot,
Image: AvatarImage,
Fallback: AvatarFallback,
Placeholder: AvatarPlaceholder,
});
interface AvatarGroupProps extends React.ComponentProps<"div">, VariantProps<typeof avatarStyles> {}

export { CompoundAvatar as Avatar, AvatarGroup, AvatarRoot, AvatarImage, AvatarFallback, AvatarPlaceholder };
const AvatarGroup = ({ className, size, ...props }: AvatarGroupProps) => {
return <div data-avatar-group="" className={group({ className, size })} {...props} />;
};

/* -------------------------------------------------------------------------------------------------
* Avatar Group Count
* -----------------------------------------------------------------------------------------------*/

interface AvatarGroupCountProps extends React.ComponentProps<"span"> {}

const AvatarGroupCount = ({ className, ...props }: AvatarGroupCountProps) => {
return <span data-avatar-group-count="" className={groupCount({ className })} {...props} />;
};

/* -----------------------------------------------------------------------------------------------*/

export { AvatarGroup, Avatar, AvatarImage, AvatarFallback, AvatarBadge, AvatarGroupCount };

export type {
AvatarGroupProps,
AvatarProps,
AvatarRootProps,
AvatarImageProps,
AvatarFallbackProps,
AvatarPlaceholderProps,
AvatarBadgeProps,
AvatarGroupCountProps,
};
`,
},
Expand All @@ -306,24 +310,31 @@ import type * as React from "react";
import type { VariantProps } from "tailwind-variants";

const badgeStyles = tv({
base: "inline-flex w-fit shrink-0 items-center justify-center gap-1 whitespace-nowrap rounded-md px-2 py-0.5 font-medium text-xs [&>svg]:pointer-events-none [&>svg]:size-3",
base: "inline-flex w-fit shrink-0 items-center justify-center gap-1 whitespace-nowrap rounded-md px-2 py-0.5 font-medium text-xs [&>svg]:pointer-events-none",
variants: {
variant: {
default: "bg-neutral text-fg-on-neutral",
primary: "bg-primary text-fg-on-primary",
danger: "bg-danger text-fg-on-danger",
success: "bg-success text-fg-on-success",
warning: "bg-warning text-fg-on-warning",
info: "bg-info text-fg-on-info",
},
size: {
sm: "px-1.5 py-0.25 [&>svg]:size-3",
md: "px-2 py-0.5 [&>svg]:size-3",
lg: "px-2.5 py-0.75 [&>svg]:size-3",
},
},
defaultVariants: {
variant: "default",
size: "md",
},
});

interface BadgeProps extends React.ComponentProps<"span">, VariantProps<typeof badgeStyles> {}
const Badge = ({ className, variant, ...props }: BadgeProps) => {
return <span role="presentation" className={badgeStyles({ variant, className })} {...props} />;
const Badge = ({ className, variant, size, ...props }: BadgeProps) => {
return <span role="presentation" data-badge="" className={badgeStyles({ variant, size, className })} {...props} />;
};

export type { BadgeProps };
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/__registry__/variants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export const VARIANTS = {
group: "feedback",
},
avatar: {
options: ["basic"] as const,
default: "basic",
options: ["base"] as const,
default: "base",
group: "data-display",
},
badge: {
Expand Down
9 changes: 3 additions & 6 deletions packages/core/src/react/dynamic-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ import { StyleContext } from "./context";

type VariantKey = keyof typeof VARIANTS;

export type VariantsMap<Props, _V extends string = string> = Record<
string,
React.LazyExoticComponent<React.ComponentType<Props>>
>;
export type VariantsMap<Props> = Record<string, React.LazyExoticComponent<React.ComponentType<Props>>>;

export interface CreateDynamicComponentOptions {
/** Disable skeleton fallback during loading */
Expand Down Expand Up @@ -149,11 +146,11 @@ class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundarySta
* @param variants - Map of variant names to lazy-loaded components
* @param options - Additional options
*/
export function createDynamicComponent<Props extends object, V extends string = string>(
export function createDynamicComponent<Props extends object>(
componentName: VariantKey,
displayName: string,
DefaultComponent: React.ComponentType<Props>,
variants: VariantsMap<Props, V>,
variants: VariantsMap<Props>,
options?: CreateDynamicComponentOptions,
): React.FC<Props> {
const { disableSkeleton = false } = options ?? {};
Expand Down
Loading
Loading