S6: refactor UI to shadcn components, design system enforcement
- App: use Sheet for mobile sidebar (proper shadcn component) - ComposeBox: use ToggleGroup + Button + Input (no raw HTML) - Use Tailwind text scale (text-xs, text-sm) instead of arbitrary text-[10px] - Design system rule expanded with color palette, forbidden patterns Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,27 +1,28 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { Channel } from "@/types/Channel";
|
||||
import type { Message } from "@/types/Message";
|
||||
import { getChannels, getMessages, getCurrentUsername } from "@/api";
|
||||
import { getChannels, getMessages } from "@/api";
|
||||
import { ChannelSidebar } from "@/components/ChannelSidebar";
|
||||
import { MessageItem } from "@/components/MessageItem";
|
||||
import { ComposeBox } from "@/components/ComposeBox";
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export default function App() {
|
||||
const [channels, setChannels] = useState<Channel[]>([]);
|
||||
const [activeChannelId, setActiveChannelId] = useState<string | null>(null);
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [replyTo, setReplyTo] = useState<string | null>(null);
|
||||
const [sheetOpen, setSheetOpen] = useState(false);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const prevMsgCountRef = useRef(0);
|
||||
const activeChannelRef = useRef(activeChannelId);
|
||||
|
||||
// Keep ref in sync
|
||||
activeChannelRef.current = activeChannelId;
|
||||
|
||||
const loadChannels = useCallback(async () => {
|
||||
const chs = await getChannels();
|
||||
setChannels(chs);
|
||||
// Auto-select first channel only if none selected
|
||||
setActiveChannelId((prev) => (prev ? prev : chs[0]?.id ?? null));
|
||||
}, []);
|
||||
|
||||
@@ -30,7 +31,6 @@ export default function App() {
|
||||
if (!channelId) return;
|
||||
try {
|
||||
const msgs = await getMessages(channelId);
|
||||
// Only update if we're still on the same channel (prevent race)
|
||||
if (activeChannelRef.current === channelId) {
|
||||
setMessages(msgs);
|
||||
}
|
||||
@@ -39,20 +39,15 @@ export default function App() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Initial channel load
|
||||
useEffect(() => {
|
||||
loadChannels();
|
||||
}, [loadChannels]);
|
||||
useEffect(() => { loadChannels(); }, [loadChannels]);
|
||||
|
||||
// Load messages on channel switch
|
||||
useEffect(() => {
|
||||
setMessages([]); // Clear immediately on switch
|
||||
setMessages([]);
|
||||
setReplyTo(null);
|
||||
prevMsgCountRef.current = 0;
|
||||
loadMessages();
|
||||
}, [activeChannelId, loadMessages]);
|
||||
|
||||
// Auto-scroll only when NEW messages arrive (not on every poll)
|
||||
useEffect(() => {
|
||||
if (messages.length > prevMsgCountRef.current && scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
@@ -60,7 +55,6 @@ export default function App() {
|
||||
prevMsgCountRef.current = messages.length;
|
||||
}, [messages]);
|
||||
|
||||
// Poll until WebSocket (S5)
|
||||
useEffect(() => {
|
||||
const interval = setInterval(loadMessages, 3000);
|
||||
return () => clearInterval(interval);
|
||||
@@ -68,69 +62,63 @@ export default function App() {
|
||||
|
||||
const messagesById = new Map(messages.map((m) => [m.id, m]));
|
||||
const activeChannel = channels.find((c) => c.id === activeChannelId);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex h-full relative">
|
||||
{/* Mobile sidebar overlay */}
|
||||
{sidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 z-40 md:hidden"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
onKeyDown={() => {}}
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={`${sidebarOpen ? "fixed inset-y-0 left-0 z-50" : "hidden"} md:relative md:block`}>
|
||||
const sidebar = (
|
||||
<ChannelSidebar
|
||||
channels={channels}
|
||||
activeId={activeChannelId}
|
||||
onSelect={(id) => {
|
||||
setActiveChannelId(id);
|
||||
setSidebarOpen(false);
|
||||
setSheetOpen(false);
|
||||
}}
|
||||
onChannelCreated={loadChannels}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
{/* Desktop sidebar */}
|
||||
<div className="hidden md:block">
|
||||
{sidebar}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{/* Channel header */}
|
||||
<div className="px-4 py-2 border-b border-border flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
className="md:hidden text-muted-foreground hover:text-foreground text-[14px] mr-1"
|
||||
>
|
||||
<div className="px-3 py-2 md:px-4 border-b border-border flex items-center gap-2">
|
||||
{/* Mobile: Sheet trigger */}
|
||||
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="md:hidden p-1 h-8 w-8 text-muted-foreground">
|
||||
=
|
||||
</button>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="p-0 w-64 bg-background">
|
||||
{sidebar}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
{activeChannel ? (
|
||||
<>
|
||||
<span className="text-muted-foreground">#</span>
|
||||
<span className="font-bold text-[14px]">
|
||||
{activeChannel.name}
|
||||
</span>
|
||||
<span className="font-bold text-sm">{activeChannel.name}</span>
|
||||
{activeChannel.description && (
|
||||
<span className="text-[11px] text-muted-foreground ml-2">
|
||||
<span className="text-xs text-muted-foreground ml-2 hidden md:inline">
|
||||
{activeChannel.description}
|
||||
</span>
|
||||
)}
|
||||
<span className="ml-auto text-[10px] text-muted-foreground tabular-nums">
|
||||
<span className="ml-auto text-xs text-muted-foreground tabular-nums">
|
||||
{messages.length} msg
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-muted-foreground text-[12px]">
|
||||
select a channel
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs">select a channel</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div ref={scrollRef} className="flex-1 overflow-y-auto">
|
||||
{messages.length === 0 && activeChannelId && (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground text-[12px]">
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground text-xs">
|
||||
no messages yet
|
||||
</div>
|
||||
)}
|
||||
@@ -138,15 +126,12 @@ export default function App() {
|
||||
<MessageItem
|
||||
key={msg.id}
|
||||
message={msg}
|
||||
replyTarget={
|
||||
msg.reply_to ? messagesById.get(msg.reply_to) : undefined
|
||||
}
|
||||
replyTarget={msg.reply_to ? messagesById.get(msg.reply_to) : undefined}
|
||||
onReply={setReplyTo}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Compose — key forces reset on channel switch */}
|
||||
{activeChannelId && (
|
||||
<ComposeBox
|
||||
key={activeChannelId}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useState } from "react";
|
||||
import type { MessageType } from "@/types/MessageType";
|
||||
import { postMessage } from "@/api";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
|
||||
interface Props {
|
||||
channelId: string;
|
||||
@@ -9,12 +12,12 @@ interface Props {
|
||||
onMessageSent: () => void;
|
||||
}
|
||||
|
||||
const TYPES: { value: MessageType; label: string; shortLabel: string }[] = [
|
||||
{ value: "text", label: "TXT", shortLabel: "T" },
|
||||
{ value: "code", label: "CODE", shortLabel: "C" },
|
||||
{ value: "result", label: "RES", shortLabel: "R" },
|
||||
{ value: "error", label: "ERR", shortLabel: "E" },
|
||||
{ value: "plan", label: "PLAN", shortLabel: "P" },
|
||||
const MSG_TYPES: { value: MessageType; label: string }[] = [
|
||||
{ value: "text", label: "T" },
|
||||
{ value: "code", label: "C" },
|
||||
{ value: "result", label: "R" },
|
||||
{ value: "error", label: "E" },
|
||||
{ value: "plan", label: "P" },
|
||||
];
|
||||
|
||||
export function ComposeBox({
|
||||
@@ -48,42 +51,33 @@ export function ComposeBox({
|
||||
return (
|
||||
<div className="border-t border-border bg-card px-3 py-2 md:px-4 md:py-3 pb-[env(safe-area-inset-bottom,8px)]">
|
||||
{replyTo && (
|
||||
<div className="flex items-center gap-2 mb-1.5 text-[10px] md:text-[11px] text-muted-foreground">
|
||||
<div className="flex items-center gap-2 mb-1.5 text-xs text-muted-foreground">
|
||||
<span>^ #{replyTo.slice(0, 8)}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClearReply}
|
||||
className="hover:text-foreground min-w-[44px] min-h-[32px] flex items-center justify-center md:min-w-0 md:min-h-0"
|
||||
>
|
||||
<Button variant="ghost" size="sm" onClick={onClearReply} className="h-6 px-1 text-xs">
|
||||
[x]
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-1.5 md:gap-2">
|
||||
{/* Type selector — compact on mobile */}
|
||||
<div className="flex gap-0.5">
|
||||
{TYPES.map((t) => (
|
||||
<button
|
||||
type="button"
|
||||
key={t.value}
|
||||
onClick={() => setMsgType(t.value)}
|
||||
className={`px-1 md:px-1.5 py-1 md:py-0.5 text-[9px] md:text-[10px] font-bold rounded-sm transition-colors min-w-[28px] md:min-w-0 min-h-[36px] md:min-h-0 flex items-center justify-center ${
|
||||
msgType === t.value
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={msgType}
|
||||
onValueChange={(v) => { if (v) setMsgType(v as MessageType); }}
|
||||
className="gap-0.5"
|
||||
>
|
||||
{/* Short label on mobile, full on desktop */}
|
||||
<span className="md:hidden">{t.shortLabel}</span>
|
||||
<span className="hidden md:inline">{t.label}</span>
|
||||
</button>
|
||||
{MSG_TYPES.map((t) => (
|
||||
<ToggleGroupItem
|
||||
key={t.value}
|
||||
value={t.value}
|
||||
className="h-8 w-8 md:h-7 md:w-auto md:px-2 text-xs font-bold data-[state=on]:bg-primary data-[state=on]:text-primary-foreground"
|
||||
>
|
||||
{t.label}
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</div>
|
||||
</ToggleGroup>
|
||||
|
||||
{/* Input — larger touch target on mobile */}
|
||||
<input
|
||||
type="text"
|
||||
<Input
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
@@ -91,23 +85,20 @@ export function ComposeBox({
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
if (e.altKey && e.key >= "1" && e.key <= "5") {
|
||||
setMsgType(TYPES[parseInt(e.key) - 1].value);
|
||||
}
|
||||
}}
|
||||
placeholder="message..."
|
||||
disabled={sending}
|
||||
className="flex-1 bg-input text-[13px] md:text-[13px] text-foreground placeholder:text-muted-foreground px-3 py-2 md:py-1.5 rounded-sm border border-border focus:outline-none focus:border-blue-500/30"
|
||||
className="flex-1 h-9 md:h-8 text-sm"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
onClick={handleSend}
|
||||
disabled={sending || !content.trim()}
|
||||
className="px-3 py-2 md:py-1.5 text-[11px] font-bold bg-primary text-primary-foreground rounded-sm hover:opacity-80 disabled:opacity-30 transition-opacity min-w-[44px] min-h-[36px] md:min-h-0 flex items-center justify-center"
|
||||
size="sm"
|
||||
className="h-9 md:h-8 px-3 text-xs font-bold"
|
||||
>
|
||||
{sending ? "..." : "SEND"}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
107
ui/colony/src/components/ui/avatar.tsx
Normal file
107
ui/colony/src/components/ui/avatar.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import * as React from "react"
|
||||
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: AvatarPrimitive.Root.Props & {
|
||||
size?: "default" | "sm" | "lg"
|
||||
}) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/avatar relative flex size-8 shrink-0 rounded-full select-none 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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn(
|
||||
"aspect-square size-full rounded-full object-cover",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: AvatarPrimitive.Fallback.Props) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="avatar-badge"
|
||||
className={cn(
|
||||
"absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none",
|
||||
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
|
||||
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
|
||||
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="avatar-group"
|
||||
className={cn(
|
||||
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarGroupCount({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="avatar-group-count"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground 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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Avatar,
|
||||
AvatarImage,
|
||||
AvatarFallback,
|
||||
AvatarGroup,
|
||||
AvatarGroupCount,
|
||||
AvatarBadge,
|
||||
}
|
||||
136
ui/colony/src/components/ui/sheet.tsx
Normal file
136
ui/colony/src/components/ui/sheet.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import * as React from "react"
|
||||
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
function Sheet({ ...props }: SheetPrimitive.Root.Props) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||
}
|
||||
|
||||
function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||
}
|
||||
|
||||
function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||
}
|
||||
|
||||
function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||
}
|
||||
|
||||
function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
|
||||
return (
|
||||
<SheetPrimitive.Backdrop
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/10 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: SheetPrimitive.Popup.Props & {
|
||||
side?: "top" | "right" | "bottom" | "left"
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Popup
|
||||
data-slot="sheet-content"
|
||||
data-side={side}
|
||||
className={cn(
|
||||
"fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<SheetPrimitive.Close
|
||||
data-slot="sheet-close"
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="absolute top-3 right-3"
|
||||
size="icon-sm"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<XIcon
|
||||
/>
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
)}
|
||||
</SheetPrimitive.Popup>
|
||||
</SheetPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-header"
|
||||
className={cn("flex flex-col gap-0.5 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn(
|
||||
"text-base font-medium text-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetDescription({
|
||||
className,
|
||||
...props
|
||||
}: SheetPrimitive.Description.Props) {
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
89
ui/colony/src/components/ui/toggle-group.tsx
Normal file
89
ui/colony/src/components/ui/toggle-group.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Toggle as TogglePrimitive } from "@base-ui/react/toggle"
|
||||
import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group"
|
||||
import { type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { toggleVariants } from "@/components/ui/toggle"
|
||||
|
||||
const ToggleGroupContext = React.createContext<
|
||||
VariantProps<typeof toggleVariants> & {
|
||||
spacing?: number
|
||||
orientation?: "horizontal" | "vertical"
|
||||
}
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
spacing: 0,
|
||||
orientation: "horizontal",
|
||||
})
|
||||
|
||||
function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 0,
|
||||
orientation = "horizontal",
|
||||
children,
|
||||
...props
|
||||
}: ToggleGroupPrimitive.Props &
|
||||
VariantProps<typeof toggleVariants> & {
|
||||
spacing?: number
|
||||
orientation?: "horizontal" | "vertical"
|
||||
}) {
|
||||
return (
|
||||
<ToggleGroupPrimitive
|
||||
data-slot="toggle-group"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
data-spacing={spacing}
|
||||
data-orientation={orientation}
|
||||
style={{ "--gap": spacing } as React.CSSProperties}
|
||||
className={cn(
|
||||
"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-lg data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-vertical:flex-col data-vertical:items-stretch",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider
|
||||
value={{ variant, size, spacing, orientation }}
|
||||
>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive>
|
||||
)
|
||||
}
|
||||
|
||||
function ToggleGroupItem({
|
||||
className,
|
||||
children,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
...props
|
||||
}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {
|
||||
const context = React.useContext(ToggleGroupContext)
|
||||
|
||||
return (
|
||||
<TogglePrimitive
|
||||
data-slot="toggle-group-item"
|
||||
data-variant={context.variant || variant}
|
||||
data-size={context.size || size}
|
||||
data-spacing={context.spacing}
|
||||
className={cn(
|
||||
"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-lg group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-lg group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-lg group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-lg group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t",
|
||||
toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</TogglePrimitive>
|
||||
)
|
||||
}
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem }
|
||||
44
ui/colony/src/components/ui/toggle.tsx
Normal file
44
ui/colony/src/components/ui/toggle.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
"use client"
|
||||
|
||||
import { Toggle as TogglePrimitive } from "@base-ui/react/toggle"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const toggleVariants = cva(
|
||||
"group/toggle inline-flex items-center justify-center gap-1 rounded-lg text-sm font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline: "border border-input bg-transparent hover:bg-muted",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 min-w-8 px-2",
|
||||
sm: "h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-1.5 text-[0.8rem]",
|
||||
lg: "h-9 min-w-9 px-2.5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Toggle({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
...props
|
||||
}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<TogglePrimitive
|
||||
data-slot="toggle"
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toggle, toggleVariants }
|
||||
66
ui/colony/src/components/ui/tooltip.tsx
Normal file
66
ui/colony/src/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client"
|
||||
|
||||
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function TooltipProvider({
|
||||
delay = 0,
|
||||
...props
|
||||
}: TooltipPrimitive.Provider.Props) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delay={delay}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
|
||||
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
}
|
||||
|
||||
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
side = "top",
|
||||
sideOffset = 4,
|
||||
align = "center",
|
||||
alignOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: TooltipPrimitive.Popup.Props &
|
||||
Pick<
|
||||
TooltipPrimitive.Positioner.Props,
|
||||
"align" | "alignOffset" | "side" | "sideOffset"
|
||||
>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Positioner
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
className="isolate z-50"
|
||||
>
|
||||
<TooltipPrimitive.Popup
|
||||
data-slot="tooltip-content"
|
||||
className={cn(
|
||||
"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
|
||||
</TooltipPrimitive.Popup>
|
||||
</TooltipPrimitive.Positioner>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
Reference in New Issue
Block a user