this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add avatars and character icons

Introduces avatars to chat messages and displays character icons in the
character selection dropdown. Also includes styling updates for the chat
form and character select components.

+183 -29
+8 -1
src/components/character-select.tsx
··· 1 + import { DynamicIcon } from "lucide-react/dynamic"; 1 2 import type * as React from "react"; 2 3 import { useCharacters } from "#/hooks/use-characters"; 3 4 import { ··· 15 16 16 17 return ( 17 18 <Select {...props}> 18 - <SelectTrigger className="min-w-40"> 19 + <SelectTrigger className="min-w-40 px-4 data-[size=default]:h-10"> 19 20 <SelectValue placeholder="Characters" /> 20 21 </SelectTrigger> 21 22 <SelectContent alignItemWithTrigger={false}> ··· 24 25 {characters.map((character) => ( 25 26 <SelectItem key={character.id} value={character.name}> 26 27 {character.name} 28 + <DynamicIcon 29 + name={character.icon} 30 + className="size-4 self-center" 31 + strokeWidth={0} 32 + fill="currentColor" 33 + /> 27 34 </SelectItem> 28 35 ))} 29 36 </SelectGroup>
+3 -1
src/components/chat-form.tsx
··· 1 + import { SendIcon } from "lucide-react"; 1 2 import { CharacterSelect } from "./character-select"; 2 3 import { Row, Stack } from "./layout"; 3 4 import { Button } from "./ui/button"; ··· 11 12 placeholder="Write your message here..." 12 13 className="wrap-anywhere min-h-25 focus-visible:border-transparent focus-visible:ring-0" 13 14 /> 14 - <Row justify="between"> 15 + <Row justify="between" items="end"> 15 16 <CharacterSelect /> 16 17 <Button type="submit" size="lg"> 17 18 Send 19 + <SendIcon /> 18 20 </Button> 19 21 </Row> 20 22 </Stack>
+29 -16
src/components/chat-message.tsx
··· 1 1 import { cva, type VariantProps } from "class-variance-authority"; 2 2 import { cn } from "#/lib/utils"; 3 + import { Row } from "./layout"; 4 + import { Avatar, AvatarFallback } from "./ui/avatar"; 3 5 4 - const chatBubble = cva( 5 - "max-w-[80%] rounded-2xl px-4 py-2 text-sm leading-relaxed", 6 - { 7 - variants: { 8 - author: { 9 - assistant: "self-start bg-primary text-primary-foreground", 10 - user: "self-end bg-secondary text-secondary-foreground", 11 - }, 6 + const chatMessage = cva("rounded-2xl px-4 py-2 text-sm leading-relaxed", { 7 + variants: { 8 + author: { 9 + assistant: "bg-primary text-primary-foreground", 10 + user: "bg-secondary text-secondary-foreground", 12 11 }, 13 - defaultVariants: { 14 - author: "assistant", 15 - }, 12 + }, 13 + defaultVariants: { 14 + author: "assistant", 16 15 }, 17 - ); 16 + }); 18 17 19 - type ChatBubbleProps = React.HTMLAttributes<HTMLDivElement> & 20 - VariantProps<typeof chatBubble>; 18 + type ChatMessageProps = React.HTMLAttributes<HTMLDivElement> & 19 + VariantProps<typeof chatMessage>; 21 20 22 - export function ChatBubble({ className, author, ...props }: ChatBubbleProps) { 23 - return <div className={cn(chatBubble({ author }), className)} {...props} />; 21 + export function ChatMessage({ className, author, ...props }: ChatMessageProps) { 22 + return ( 23 + <Row 24 + className={cn( 25 + "max-w-[80%]", 26 + author === "assistant" ? "self-start" : "flex-row-reverse self-end", 27 + )} 28 + items="start" 29 + gap="sm" 30 + > 31 + <Avatar className="size-[38.75px]"> 32 + <AvatarFallback>{author === "assistant" ? "A" : "U"}</AvatarFallback> 33 + </Avatar> 34 + <div className={cn(chatMessage({ author }), className)} {...props} /> 35 + </Row> 36 + ); 24 37 }
+4 -4
src/components/chat-messages.tsx
··· 1 1 import type { ChatMessage as ChatMessageType } from "#/lib/types"; 2 - import { ChatBubble } from "./chat-message"; 2 + import { ChatMessage } from "./chat-message"; 3 3 import { Stack } from "./layout"; 4 4 5 5 interface ChatMessageProps { ··· 10 10 return ( 11 11 <Stack> 12 12 {messages.map((message) => ( 13 - <ChatBubble key={message.id} author={message.author}> 14 - {message.text} 15 - </ChatBubble> 13 + <ChatMessage key={message.id} author={message.author}> 14 + <p>{message.text}</p> 15 + </ChatMessage> 16 16 ))} 17 17 </Stack> 18 18 );
-5
src/components/section.tsx
··· 1 - import type { ReactNode } from "react"; 2 - 3 - export function Section({ children }: { children: ReactNode }) { 4 - return <section className="py-12">{children}</section>; 5 - }
+107
src/components/ui/avatar.tsx
··· 1 + import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"; 2 + import type * as React from "react"; 3 + 4 + import { cn } from "#/lib/utils"; 5 + 6 + function Avatar({ 7 + className, 8 + size = "default", 9 + ...props 10 + }: AvatarPrimitive.Root.Props & { 11 + size?: "default" | "sm" | "lg"; 12 + }) { 13 + return ( 14 + <AvatarPrimitive.Root 15 + data-slot="avatar" 16 + data-size={size} 17 + className={cn( 18 + "group/avatar relative flex size-8 shrink-0 select-none rounded-full after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten", 19 + className, 20 + )} 21 + {...props} 22 + /> 23 + ); 24 + } 25 + 26 + function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) { 27 + return ( 28 + <AvatarPrimitive.Image 29 + data-slot="avatar-image" 30 + className={cn( 31 + "aspect-square size-full rounded-full object-cover", 32 + className, 33 + )} 34 + {...props} 35 + /> 36 + ); 37 + } 38 + 39 + function AvatarFallback({ 40 + className, 41 + ...props 42 + }: AvatarPrimitive.Fallback.Props) { 43 + return ( 44 + <AvatarPrimitive.Fallback 45 + data-slot="avatar-fallback" 46 + className={cn( 47 + "flex size-full items-center justify-center rounded-full bg-muted text-muted-foreground text-sm group-data-[size=sm]/avatar:text-xs", 48 + className, 49 + )} 50 + {...props} 51 + /> 52 + ); 53 + } 54 + 55 + function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { 56 + return ( 57 + <span 58 + data-slot="avatar-badge" 59 + className={cn( 60 + "absolute right-0 bottom-0 z-10 inline-flex select-none items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background", 61 + "group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden", 62 + "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", 63 + "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", 64 + className, 65 + )} 66 + {...props} 67 + /> 68 + ); 69 + } 70 + 71 + function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { 72 + return ( 73 + <div 74 + data-slot="avatar-group" 75 + className={cn( 76 + "group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background", 77 + className, 78 + )} 79 + {...props} 80 + /> 81 + ); 82 + } 83 + 84 + function AvatarGroupCount({ 85 + className, 86 + ...props 87 + }: React.ComponentProps<"div">) { 88 + return ( 89 + <div 90 + data-slot="avatar-group-count" 91 + className={cn( 92 + "relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground text-sm ring-2 ring-background 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", 93 + className, 94 + )} 95 + {...props} 96 + /> 97 + ); 98 + } 99 + 100 + export { 101 + Avatar, 102 + AvatarBadge, 103 + AvatarFallback, 104 + AvatarGroup, 105 + AvatarGroupCount, 106 + AvatarImage, 107 + };
+2
src/lib/mock.ts
··· 4 4 { 5 5 id: "1", 6 6 name: "Katsune", 7 + icon: "heart", 7 8 }, 8 9 { 9 10 id: "2", 10 11 name: "Neko", 12 + icon: "cat", 11 13 }, 12 14 ]; 13 15
+3
src/lib/types.ts
··· 1 + import type { IconName } from "lucide-react/dynamic"; 2 + 1 3 export interface Character { 2 4 id: string; 3 5 name: string; 6 + icon: IconName; 4 7 } 5 8 6 9 export interface Chat {
+24 -1
src/routes/chats.index.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 + import { Attribution } from "#/components/attribution"; 2 3 import { ChatForm } from "#/components/chat-form"; 4 + import { Heading } from "#/components/heading"; 3 5 import { Stack } from "#/components/layout"; 4 6 5 7 export const Route = createFileRoute("/chats/")({ ··· 8 10 9 11 function RouteComponent() { 10 12 return ( 11 - <Stack justify="end" className="flex-1 py-4 md:py-8"> 13 + <Stack justify="between" className="flex-1 py-4 md:py-8"> 14 + <Stack justify="center" className="flex-1"> 15 + <Heading>What's on your mind?</Heading> 16 + 17 + <figure className="relative"> 18 + <img 19 + src="/kitsune.gif" 20 + alt="Fox girl" 21 + width="318" 22 + height="292" 23 + className="h-36 w-auto" 24 + /> 25 + <figcaption className="absolute top-full pt-2"> 26 + <Attribution 27 + title="RoopyRoo!" 28 + artist="Doosio" 29 + license="CC BY-NC-ND 3.0" 30 + href="https://www.deviantart.com/doosio/art/RoopyRoo-CM-887196542" 31 + /> 32 + </figcaption> 33 + </figure> 34 + </Stack> 12 35 <ChatForm /> 13 36 </Stack> 14 37 );
+3 -1
src/routes/index.tsx
··· 1 1 import { createFileRoute, Link } from "@tanstack/react-router"; 2 + import { MessageCircleIcon } from "lucide-react"; 2 3 import { Attribution } from "#/components/attribution"; 3 4 import { Container } from "#/components/container"; 4 5 import { Heading } from "#/components/heading"; ··· 25 26 <Row gap="sm"> 26 27 <Button size="lg" render={<Link to="/chats" />}> 27 28 Chat 29 + <MessageCircleIcon /> 28 30 </Button> 29 31 <Button size="lg" variant="ghost" render={<Link to="/" />}> 30 32 Learn more ··· 41 43 /> 42 44 <figcaption className="absolute top-full pt-2"> 43 45 <Attribution 44 - title="RoopyRoo! - CM" 46 + title="RoopyRoo!" 45 47 artist="Doosio" 46 48 license="CC BY-NC-ND 3.0" 47 49 href="https://www.deviantart.com/doosio/art/RoopyRoo-CM-887196542"