merge projects, signals, and mobile UI
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"components": "@code/ui/components",
|
||||
"utils": "@code/ui/lib/utils",
|
||||
"ui": "@code/ui/components",
|
||||
"lib": "@/lib",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"dev": "react-router dev",
|
||||
"dev:tailscale": "react-router dev --host 0.0.0.0",
|
||||
"start": "react-router-serve ./build/server/index.js",
|
||||
"typecheck": "react-router typegen && tsc"
|
||||
"check-types": "react-router typegen && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@code/auth": "workspace:*",
|
||||
@@ -40,7 +40,6 @@
|
||||
"tailwindcss": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vitest": "catalog:",
|
||||
"vite-tsconfig-paths": "^6.1.1"
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { MessageHeader } from "@code/ui/components/message";
|
||||
import { Sparkles } from "lucide-react";
|
||||
import { MobileChatAssistantLabel } from "@code/ui/components/mobile-chat";
|
||||
|
||||
import type { AssistantResponseState } from "@/lib/chat/types";
|
||||
|
||||
@@ -8,20 +7,18 @@ interface AssistantIdentityProps {
|
||||
}
|
||||
|
||||
export const AssistantIdentity = ({ state }: AssistantIdentityProps) => (
|
||||
<MessageHeader className="mb-1.5 gap-2.5 px-0 text-xs font-medium text-foreground/70">
|
||||
<span className="zopu-mark flex size-6 items-center justify-center rounded-lg">
|
||||
<Sparkles className="size-3.5" />
|
||||
</span>
|
||||
Zopu
|
||||
{state && (
|
||||
<span className="response-state" data-state={state}>
|
||||
<span aria-hidden="true" className="response-state-dots">
|
||||
<i />
|
||||
<i />
|
||||
<i />
|
||||
<MobileChatAssistantLabel
|
||||
state={
|
||||
state ? (
|
||||
<span className="response-state" data-state={state}>
|
||||
<span aria-hidden="true" className="response-state-dots">
|
||||
<i />
|
||||
<i />
|
||||
<i />
|
||||
</span>
|
||||
{state === "thinking" ? "Thinking" : "Writing"}
|
||||
</span>
|
||||
{state === "thinking" ? "Thinking" : "Writing"}
|
||||
</span>
|
||||
)}
|
||||
</MessageHeader>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupTextarea,
|
||||
} from "@code/ui/components/input-group";
|
||||
import { CircleAlert, LoaderCircle, Send } from "lucide-react";
|
||||
import { MobileChatComposer } from "@code/ui/components/mobile-chat";
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { useChatComposer } from "@/hooks/chat/use-chat-composer";
|
||||
@@ -15,90 +10,31 @@ export const ChatComposer = ({ error, onSend, status }: ChatComposerProps) => {
|
||||
status === "connecting" || status === "submitted" || status === "streaming";
|
||||
const composer = useChatComposer({ busy, onSend });
|
||||
|
||||
let statusContent: ReactNode = (
|
||||
<span className="hidden sm:inline">
|
||||
Enter to send · Shift + Enter for a new line
|
||||
</span>
|
||||
);
|
||||
let statusMessage: ReactNode;
|
||||
if (busy) {
|
||||
statusContent = (
|
||||
<>
|
||||
<LoaderCircle className="size-3.5 animate-spin" />
|
||||
statusMessage = (
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<LoaderCircle className="size-3 animate-spin" />
|
||||
{status === "connecting" ? "Preparing Zopu" : "Zopu is responding"}
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (error) {
|
||||
statusContent = (
|
||||
<span
|
||||
id="composer-error"
|
||||
className="flex items-center gap-1.5 text-destructive"
|
||||
>
|
||||
<CircleAlert className="size-3.5" />
|
||||
{error.message || "Message failed to send"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="composer-dock shrink-0 pb-[env(safe-area-inset-bottom)]">
|
||||
<form
|
||||
aria-label="Send a message"
|
||||
className="mx-auto w-full max-w-3xl px-3 pb-3 sm:px-5 sm:pb-5"
|
||||
onSubmit={composer.handleSubmit}
|
||||
>
|
||||
<InputGroup
|
||||
className={`composer-shell overflow-hidden rounded-[1.5rem] border-border/70 bg-card/95 shadow-[0_16px_50px_-18px_rgb(0_0_0/0.38)] backdrop-blur-xl transition-[border-color,box-shadow] focus-within:border-foreground/20 focus-within:shadow-[0_18px_60px_-20px_rgb(0_0_0/0.48)] dark:bg-card/90 ${
|
||||
composer.isFocused
|
||||
? "composer-shell-expanded"
|
||||
: "composer-shell-compact"
|
||||
}`}
|
||||
data-disabled={busy || undefined}
|
||||
>
|
||||
<InputGroupTextarea
|
||||
{...composer.inputProps}
|
||||
aria-describedby={error ? "composer-error" : undefined}
|
||||
aria-invalid={error ? true : undefined}
|
||||
aria-label="Message Zopu"
|
||||
className={`max-h-40 px-4 text-base leading-6 transition-[min-height,padding] duration-200 placeholder:text-muted-foreground/70 sm:px-5 ${
|
||||
composer.isFocused
|
||||
? "min-h-24 pb-2 pt-4"
|
||||
: "min-h-14 py-[0.95rem] pr-2"
|
||||
}`}
|
||||
disabled={busy}
|
||||
placeholder="Ask anything…"
|
||||
rows={1}
|
||||
/>
|
||||
<InputGroupAddon
|
||||
align="inline-end"
|
||||
className={`shrink-0 self-end pr-2 transition-[padding] duration-200 ${
|
||||
composer.isFocused ? "pb-2.5" : "pb-1.5"
|
||||
}`}
|
||||
>
|
||||
<InputGroupButton
|
||||
aria-label="Send message"
|
||||
className="size-11 shrink-0 rounded-2xl bg-foreground text-background transition-all hover:scale-[1.03] hover:bg-foreground/85 active:scale-95 disabled:bg-muted disabled:text-muted-foreground sm:size-10 sm:rounded-xl"
|
||||
disabled={!composer.canSend}
|
||||
size="icon-sm"
|
||||
type="submit"
|
||||
variant="default"
|
||||
>
|
||||
{busy ? (
|
||||
<LoaderCircle className="size-5 animate-spin" />
|
||||
) : (
|
||||
<Send className="size-5" />
|
||||
)}
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<div className="mt-2 flex min-h-4 items-center justify-center px-2 text-center text-[10px] text-muted-foreground/80 sm:text-[11px]">
|
||||
{composer.isFocused || busy || error ? (
|
||||
<span className="flex items-center gap-2">{statusContent}</span>
|
||||
) : (
|
||||
<span>Zopu can make mistakes. Check important information.</span>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<MobileChatComposer
|
||||
busy={busy}
|
||||
canSend={composer.canSend}
|
||||
errorMessage={
|
||||
error ? error.message || "Message failed to send" : undefined
|
||||
}
|
||||
onSubmit={composer.handleSubmit}
|
||||
statusMessage={statusMessage}
|
||||
textareaProps={{
|
||||
...composer.inputProps,
|
||||
"aria-describedby": error ? "composer-error" : undefined,
|
||||
"aria-invalid": error ? true : undefined,
|
||||
disabled: busy,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
import { ConversationEmptyState } from "@code/ui/components/ai-elements/conversation";
|
||||
import { Button } from "@code/ui/components/button";
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@code/ui/components/empty";
|
||||
import { MessageGroup } from "@code/ui/components/message";
|
||||
import { MessageScrollerItem } from "@code/ui/components/message-scroller";
|
||||
import { Sparkles } from "lucide-react";
|
||||
import { MobileChatMark } from "@code/ui/components/mobile-chat";
|
||||
|
||||
import { SUGGESTIONS } from "@/lib/chat/constants";
|
||||
import type { ChatConversationProps } from "@/lib/chat/types";
|
||||
@@ -25,23 +16,21 @@ export const ChatConversation = ({
|
||||
}: ChatConversationProps) => {
|
||||
if (historyReady && messages.length === 0) {
|
||||
return (
|
||||
<Empty className="min-h-full items-start justify-end px-1 pb-7 pt-20 sm:items-center sm:justify-center sm:px-6 sm:py-20">
|
||||
<EmptyMedia className="zopu-mark size-11 rounded-2xl" variant="icon">
|
||||
<Sparkles className="size-5" />
|
||||
</EmptyMedia>
|
||||
<EmptyHeader className="items-start text-left sm:items-center sm:text-center">
|
||||
<EmptyTitle className="text-2xl font-semibold tracking-[-0.035em] sm:text-3xl">
|
||||
<ConversationEmptyState className="min-h-full items-start justify-end px-0 pt-14 pb-5 text-left">
|
||||
<MobileChatMark className="size-10 rounded-[12px] [&_svg]:size-5" />
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-[22px] font-semibold tracking-[-0.035em]">
|
||||
What can I help with?
|
||||
</EmptyTitle>
|
||||
<EmptyDescription className="max-w-md text-[15px] leading-6">
|
||||
</h2>
|
||||
<p className="max-w-[320px] text-[14px] leading-5 text-muted-foreground">
|
||||
Think, write, plan, or explore an idea with Zopu.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent className="mt-3 grid w-full max-w-xl gap-2 sm:grid-cols-3">
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-3 grid w-full gap-2">
|
||||
{SUGGESTIONS.map(([title, prompt]) => (
|
||||
<Button
|
||||
key={title}
|
||||
className="group h-auto min-h-16 w-full flex-col items-start justify-center gap-0.5 rounded-2xl border-border/60 bg-card/60 px-4 py-3 text-left shadow-sm transition-all hover:-translate-y-0.5 hover:border-foreground/20 hover:bg-card hover:shadow-md sm:min-h-24"
|
||||
className="group h-auto min-h-14 w-full flex-col items-start justify-center gap-0.5 rounded-[16px] border-[#e7e7e4] bg-[#f7f7f5] px-4 py-3 text-left shadow-none transition-colors active:bg-[#eeeeeb]"
|
||||
onClick={() => onSuggestion(prompt)}
|
||||
variant="outline"
|
||||
>
|
||||
@@ -51,27 +40,17 @@ export const ChatConversation = ({
|
||||
</span>
|
||||
</Button>
|
||||
))}
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</div>
|
||||
</ConversationEmptyState>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageGroup className="gap-8 pb-5 md:gap-10 md:pb-8">
|
||||
{messages.map((message, index) => (
|
||||
<MessageScrollerItem
|
||||
key={message.id}
|
||||
messageId={message.id}
|
||||
scrollAnchor={index === messages.length - 1}
|
||||
>
|
||||
<ChatMessage message={message} />
|
||||
</MessageScrollerItem>
|
||||
<div className="flex min-w-0 flex-col gap-5 pb-5">
|
||||
{messages.map((message) => (
|
||||
<ChatMessage key={message.id} message={message} />
|
||||
))}
|
||||
{status === "submitted" && (
|
||||
<MessageScrollerItem scrollAnchor>
|
||||
<ChatThinkingResponse />
|
||||
</MessageScrollerItem>
|
||||
)}
|
||||
</MessageGroup>
|
||||
{status === "submitted" && <ChatThinkingResponse />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,32 +1,11 @@
|
||||
import { Sparkles } from "lucide-react";
|
||||
import { MobileChatHeader } from "@code/ui/components/mobile-chat";
|
||||
|
||||
import { MODEL_LABEL, STATUS_COPY } from "@/lib/chat/constants";
|
||||
import { getStatusDotClass } from "@/lib/chat/transforms";
|
||||
import { STATUS_COPY } from "@/lib/chat/constants";
|
||||
import type { ChatHeaderProps } from "@/lib/chat/types";
|
||||
|
||||
export const ChatHeader = ({ status }: ChatHeaderProps) => (
|
||||
<header className="chat-header flex h-14 shrink-0 items-center border-b border-border/60 px-4 sm:h-16 sm:px-6">
|
||||
<div className="mx-auto flex w-full max-w-5xl items-center justify-between">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="zopu-mark flex size-9 shrink-0 items-center justify-center rounded-xl">
|
||||
<Sparkles className="size-4" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h1 className="truncate text-sm font-semibold tracking-tight sm:text-[15px]">
|
||||
Zopu
|
||||
</h1>
|
||||
<p className="flex items-center gap-1.5 text-[11px] text-muted-foreground">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`size-1.5 rounded-full ${getStatusDotClass(status)}`}
|
||||
/>
|
||||
<span aria-live="polite">{STATUS_COPY[status]}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="rounded-full border border-border/60 bg-card/50 px-2.5 py-1 text-[10px] font-medium tracking-wide text-muted-foreground sm:text-[11px]">
|
||||
{MODEL_LABEL}
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
<MobileChatHeader
|
||||
active={status !== "connecting" && status !== "error"}
|
||||
statusLabel={STATUS_COPY[status]}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { Bubble, BubbleContent } from "@code/ui/components/bubble";
|
||||
import { Message, MessageContent } from "@code/ui/components/message";
|
||||
import {
|
||||
Message,
|
||||
MessageContent,
|
||||
MessageResponse,
|
||||
} from "@code/ui/components/ai-elements/message";
|
||||
import {
|
||||
MobileChatBubble,
|
||||
MobileChatMessage,
|
||||
} from "@code/ui/components/mobile-chat";
|
||||
import type { ReactNode } from "react";
|
||||
import { Streamdown } from "streamdown";
|
||||
|
||||
import { getMessageText, isMessageStreaming } from "@/lib/chat/transforms";
|
||||
import type { ChatMessageProps } from "@/lib/chat/types";
|
||||
@@ -21,13 +27,13 @@ export const ChatMessage = ({ message }: ChatMessageProps) => {
|
||||
let content: ReactNode = text;
|
||||
if (!isUser) {
|
||||
content = text ? (
|
||||
<Streamdown
|
||||
<MessageResponse
|
||||
caret={isStreaming ? "block" : undefined}
|
||||
className="chat-markdown min-w-0"
|
||||
isAnimating={isStreaming}
|
||||
>
|
||||
{text}
|
||||
</Streamdown>
|
||||
</MessageResponse>
|
||||
) : (
|
||||
<div className="thinking-line" aria-label="Zopu is preparing a response">
|
||||
<span />
|
||||
@@ -37,41 +43,29 @@ export const ChatMessage = ({ message }: ChatMessageProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
const sender = isUser ? "user" : "assistant";
|
||||
|
||||
return (
|
||||
<Message
|
||||
align={isUser ? "end" : "start"}
|
||||
className="chat-message text-[15px] md:text-base"
|
||||
>
|
||||
<MessageContent
|
||||
className={
|
||||
isUser
|
||||
? "max-w-[88%] sm:max-w-[76%] md:max-w-[68%]"
|
||||
: "max-w-full md:max-w-[92%]"
|
||||
}
|
||||
>
|
||||
{!isUser && (
|
||||
<AssistantIdentity state={isStreaming ? "writing" : undefined} />
|
||||
)}
|
||||
<Bubble
|
||||
align={isUser ? "end" : "start"}
|
||||
variant={isUser ? "secondary" : "ghost"}
|
||||
className="max-w-full"
|
||||
>
|
||||
<BubbleContent
|
||||
className={
|
||||
isUser
|
||||
? "rounded-[1.35rem] rounded-br-md border border-border/60 bg-secondary/80 px-4 py-2.5 leading-6 text-foreground shadow-sm whitespace-pre-wrap dark:bg-secondary/70"
|
||||
: "w-full max-w-none overflow-visible leading-7 text-foreground/95"
|
||||
}
|
||||
>
|
||||
{message.parts.map((part) =>
|
||||
part.type === "dynamic-tool" ? (
|
||||
<ChatToolCall key={part.toolCallId} part={part} />
|
||||
) : null
|
||||
<Message className="max-w-full gap-0" from={message.role}>
|
||||
<MessageContent className="w-full max-w-none gap-0 overflow-visible rounded-none bg-transparent p-0 group-[.is-user]:rounded-none group-[.is-user]:bg-transparent group-[.is-user]:p-0">
|
||||
<MobileChatMessage className="chat-message" sender={sender}>
|
||||
<div className={isUser ? "max-w-full" : "w-full min-w-0"}>
|
||||
{!isUser && (
|
||||
<AssistantIdentity state={isStreaming ? "writing" : undefined} />
|
||||
)}
|
||||
{content}
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
<MobileChatBubble
|
||||
className={isUser ? "whitespace-pre-wrap" : undefined}
|
||||
sender={sender}
|
||||
>
|
||||
{message.parts.map((part) =>
|
||||
part.type === "dynamic-tool" ? (
|
||||
<ChatToolCall key={part.toolCallId} part={part} />
|
||||
) : null
|
||||
)}
|
||||
{content}
|
||||
</MobileChatBubble>
|
||||
</div>
|
||||
</MobileChatMessage>
|
||||
</MessageContent>
|
||||
</Message>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import {
|
||||
MessageScroller,
|
||||
MessageScrollerButton,
|
||||
MessageScrollerContent,
|
||||
MessageScrollerProvider,
|
||||
MessageScrollerViewport,
|
||||
} from "@code/ui/components/message-scroller";
|
||||
Conversation,
|
||||
ConversationContent,
|
||||
ConversationScrollButton,
|
||||
} from "@code/ui/components/ai-elements/conversation";
|
||||
|
||||
import { useChatAgent } from "@/hooks/chat/use-chat-agent";
|
||||
|
||||
@@ -17,36 +15,24 @@ export const ChatPage = () => {
|
||||
const handleSendMessage = (message: string) => agent.sendMessage(message);
|
||||
|
||||
return (
|
||||
<section className="chat-surface flex h-full min-h-0 flex-col bg-background">
|
||||
<section className="chat-surface flex h-full min-h-0 flex-col bg-[#fefefe]">
|
||||
<ChatHeader status={agent.status} />
|
||||
|
||||
<main aria-label="Conversation" className="min-h-0 flex-1">
|
||||
<MessageScrollerProvider
|
||||
autoScroll
|
||||
defaultScrollPosition="end"
|
||||
scrollEdgeThreshold={96}
|
||||
scrollMargin={24}
|
||||
>
|
||||
<MessageScroller>
|
||||
<MessageScrollerViewport aria-label="Chat messages">
|
||||
<MessageScrollerContent className="mx-auto w-full max-w-3xl px-4 pb-8 pt-7 sm:px-6 sm:pb-10 sm:pt-10">
|
||||
<ChatConversation
|
||||
historyReady={agent.historyReady}
|
||||
messages={agent.messages}
|
||||
onSuggestion={(suggestion) =>
|
||||
void handleSendMessage(suggestion)
|
||||
}
|
||||
status={agent.status}
|
||||
/>
|
||||
</MessageScrollerContent>
|
||||
</MessageScrollerViewport>
|
||||
<MessageScrollerButton
|
||||
aria-label="Scroll to latest message"
|
||||
className="bottom-4 size-10 rounded-full border border-border/70 bg-card/95 shadow-lg backdrop-blur"
|
||||
direction="end"
|
||||
<Conversation className="ai-conversation-mobile h-full">
|
||||
<ConversationContent className="min-h-full w-full gap-0 px-4 pt-4 pb-6">
|
||||
<ChatConversation
|
||||
historyReady={agent.historyReady}
|
||||
messages={agent.messages}
|
||||
onSuggestion={(suggestion) => void handleSendMessage(suggestion)}
|
||||
status={agent.status}
|
||||
/>
|
||||
</MessageScroller>
|
||||
</MessageScrollerProvider>
|
||||
</ConversationContent>
|
||||
<ConversationScrollButton
|
||||
aria-label="Scroll to latest message"
|
||||
className="bottom-3 size-9 rounded-full border border-[#dededb] bg-[#fefefe]/95 shadow-sm backdrop-blur"
|
||||
/>
|
||||
</Conversation>
|
||||
</main>
|
||||
|
||||
<ChatComposer
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
import { Message, MessageContent } from "@code/ui/components/message";
|
||||
import {
|
||||
Message,
|
||||
MessageContent,
|
||||
} from "@code/ui/components/ai-elements/message";
|
||||
import {
|
||||
MobileChatBubble,
|
||||
MobileChatMessage,
|
||||
} from "@code/ui/components/mobile-chat";
|
||||
|
||||
import { AssistantIdentity } from "./assistant-identity";
|
||||
|
||||
export const ChatThinkingResponse = () => (
|
||||
<Message align="start" className="chat-message">
|
||||
<MessageContent className="max-w-full md:max-w-[92%]">
|
||||
<AssistantIdentity state="thinking" />
|
||||
<div className="thinking-line" aria-label="Zopu is thinking">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</div>
|
||||
<Message className="max-w-full gap-0" from="assistant">
|
||||
<MessageContent className="w-full max-w-none gap-0 overflow-visible p-0">
|
||||
<MobileChatMessage className="chat-message" sender="assistant">
|
||||
<div className="w-full min-w-0">
|
||||
<AssistantIdentity state="thinking" />
|
||||
<MobileChatBubble sender="assistant">
|
||||
<div className="thinking-line" aria-label="Zopu is thinking">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</div>
|
||||
</MobileChatBubble>
|
||||
</div>
|
||||
</MobileChatMessage>
|
||||
</MessageContent>
|
||||
</Message>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { Check, LoaderCircle, Terminal, TriangleAlert } from "lucide-react";
|
||||
import {
|
||||
Tool,
|
||||
ToolContent,
|
||||
ToolInput,
|
||||
ToolOutput,
|
||||
} from "@code/ui/components/ai-elements/tool";
|
||||
import { MobileChatToolCall } from "@code/ui/components/mobile-chat";
|
||||
import { Search, SquareTerminal } from "lucide-react";
|
||||
|
||||
import type { ChatToolCallProps } from "@/lib/chat/types";
|
||||
|
||||
@@ -7,35 +14,47 @@ export const ChatToolCall = ({ part }: ChatToolCallProps) => {
|
||||
typeof part.input === "string"
|
||||
? part.input
|
||||
: JSON.stringify(part.input, null, 2);
|
||||
let Icon = LoaderCircle;
|
||||
let status = "Running";
|
||||
let status = "running";
|
||||
let tone: "error" | "neutral" | "success" = "neutral";
|
||||
|
||||
if (part.state === "output-available") {
|
||||
detail =
|
||||
typeof part.output === "string"
|
||||
? part.output
|
||||
: JSON.stringify(part.output, null, 2);
|
||||
Icon = Check;
|
||||
status = "Completed";
|
||||
status = "done";
|
||||
tone = "success";
|
||||
} else if (part.state === "output-error") {
|
||||
detail = part.errorText;
|
||||
Icon = TriangleAlert;
|
||||
status = "Failed";
|
||||
status = "failed";
|
||||
tone = "error";
|
||||
}
|
||||
const isRunning = part.state === "input-available";
|
||||
|
||||
const isSearch = part.toolName.toLowerCase().includes("search");
|
||||
const output = part.state === "output-available" ? part.output : undefined;
|
||||
const errorText = part.state === "output-error" ? part.errorText : undefined;
|
||||
|
||||
return (
|
||||
<details className="group/tool overflow-hidden rounded-xl border border-border/60 bg-card/45 text-xs">
|
||||
<summary className="flex cursor-pointer list-none items-center gap-2 px-3 py-2 text-muted-foreground marker:hidden">
|
||||
<Terminal className="size-3.5 shrink-0" />
|
||||
<span className="min-w-0 flex-1 truncate font-medium text-foreground/85">
|
||||
{part.toolName}
|
||||
</span>
|
||||
<span>{status}</span>
|
||||
<Icon className={`size-3.5 ${isRunning ? "animate-spin" : ""}`} />
|
||||
</summary>
|
||||
<pre className="max-h-72 overflow-auto border-t border-border/50 bg-background/60 px-3 py-2 font-mono text-[11px] leading-5 whitespace-pre-wrap text-muted-foreground">
|
||||
{detail}
|
||||
</pre>
|
||||
</details>
|
||||
<Tool className="mb-0 w-full border-0" defaultOpen>
|
||||
<ToolContent className="space-y-0 p-0">
|
||||
<MobileChatToolCall
|
||||
detail={detail}
|
||||
icon={
|
||||
isSearch ? (
|
||||
<Search className="size-3" strokeWidth={2.2} />
|
||||
) : (
|
||||
<SquareTerminal className="size-3" strokeWidth={2.2} />
|
||||
)
|
||||
}
|
||||
status={status}
|
||||
tone={tone}
|
||||
toolName={part.toolName}
|
||||
/>
|
||||
<div className="sr-only">
|
||||
<ToolInput input={part.input} />
|
||||
<ToolOutput errorText={errorText} output={output} />
|
||||
</div>
|
||||
</ToolContent>
|
||||
</Tool>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
MobileAssistantChatScreen,
|
||||
MobileExpandedWorkScreen,
|
||||
MobileHomeScreen,
|
||||
MobileWorkListScreen,
|
||||
MobileWorkUnitDetailScreen,
|
||||
} from "@code/ui/components/mobile-product";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
import { useMobileWorkspace } from "@/hooks/use-mobile-workspace";
|
||||
|
||||
export type MobileFlowScreen =
|
||||
| "assistant-chat"
|
||||
| "home"
|
||||
| "work-list"
|
||||
| "work-unit-detail";
|
||||
|
||||
interface MobileFlowPageProps {
|
||||
screen: MobileFlowScreen;
|
||||
}
|
||||
|
||||
export const MobileFlowPage = ({ screen }: MobileFlowPageProps) => {
|
||||
const navigate = useNavigate();
|
||||
const workspace = useMobileWorkspace();
|
||||
const homePath = "/";
|
||||
const workPath = "/work";
|
||||
const chatPath = "/chat";
|
||||
const detailPath = "/work/flow-08";
|
||||
|
||||
const handleOpenUnit = () => {
|
||||
if (screen === "work-list" && !workspace.expanded) {
|
||||
workspace.setExpanded(true);
|
||||
return;
|
||||
}
|
||||
navigate(detailPath);
|
||||
};
|
||||
|
||||
const screenProps = {
|
||||
composerValue: workspace.composerValue,
|
||||
onBack: () => navigate(homePath),
|
||||
onComposerChange: workspace.handleComposerChange,
|
||||
onComposerSubmit: workspace.handleComposerSubmit,
|
||||
onOpenAssistant: () => navigate(chatPath),
|
||||
onOpenUnit: handleOpenUnit,
|
||||
onViewWork: () => navigate(workPath),
|
||||
statusMessage: workspace.statusMessage,
|
||||
};
|
||||
|
||||
if (screen === "assistant-chat") {
|
||||
return <MobileAssistantChatScreen {...screenProps} />;
|
||||
}
|
||||
if (screen === "home") {
|
||||
return <MobileHomeScreen {...screenProps} />;
|
||||
}
|
||||
if (screen === "work-unit-detail") {
|
||||
return <MobileWorkUnitDetailScreen {...screenProps} />;
|
||||
}
|
||||
if (workspace.expanded) {
|
||||
return <MobileExpandedWorkScreen {...screenProps} />;
|
||||
}
|
||||
return <MobileWorkListScreen {...screenProps} />;
|
||||
};
|
||||
35
apps/web/src/hooks/use-mobile-workspace.ts
Normal file
35
apps/web/src/hooks/use-mobile-workspace.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { FormEvent } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
export const useMobileWorkspace = (initialExpanded = false) => {
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [composerValue, setComposerValue] = useState("");
|
||||
const [expanded, setExpanded] = useState(initialExpanded);
|
||||
const [statusMessage, setStatusMessage] = useState<string>();
|
||||
|
||||
const handleComposerChange = (value: string) => {
|
||||
setComposerValue(value);
|
||||
setStatusMessage(undefined);
|
||||
};
|
||||
|
||||
const handleComposerSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const message = composerValue.trim();
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
setComposerValue("");
|
||||
setStatusMessage("Added to Zopu’s queue");
|
||||
};
|
||||
|
||||
return {
|
||||
activeIndex,
|
||||
composerValue,
|
||||
expanded,
|
||||
handleActiveIndexChange: setActiveIndex,
|
||||
handleComposerChange,
|
||||
handleComposerSubmit,
|
||||
setExpanded,
|
||||
statusMessage,
|
||||
};
|
||||
};
|
||||
@@ -4,49 +4,10 @@
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-surface {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 50% -12%,
|
||||
oklch(0.72 0.045 255 / 9%),
|
||||
transparent 32rem
|
||||
),
|
||||
var(--background);
|
||||
}
|
||||
|
||||
.chat-surface::before {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
inset: 0;
|
||||
background-image: radial-gradient(
|
||||
oklch(0.5 0 0 / 9%) 0.55px,
|
||||
transparent 0.55px
|
||||
);
|
||||
background-size: 18px 18px;
|
||||
mask-image: linear-gradient(to bottom, black, transparent 42%);
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
background: color-mix(in oklch, var(--background) 88%, transparent);
|
||||
backdrop-filter: blur(18px) saturate(1.25);
|
||||
}
|
||||
|
||||
.zopu-mark {
|
||||
border: 1px solid color-mix(in oklch, var(--foreground) 10%, transparent);
|
||||
background:
|
||||
linear-gradient(145deg, oklch(1 0 0 / 65%), transparent 55%),
|
||||
color-mix(in oklch, var(--muted) 88%, var(--background));
|
||||
color: var(--foreground);
|
||||
box-shadow:
|
||||
inset 0 1px 0 oklch(1 0 0 / 48%),
|
||||
0 6px 18px -12px oklch(0 0 0 / 55%);
|
||||
.ai-conversation-mobile > div {
|
||||
scrollbar-gutter: auto !important;
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
@@ -120,15 +81,6 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.composer-dock {
|
||||
position: relative;
|
||||
background: linear-gradient(to top, var(--background) 68%, transparent);
|
||||
}
|
||||
|
||||
.composer-shell {
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
@keyframes chat-message-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
||||
@@ -68,7 +68,7 @@ export const Layout = ({ children }: { children: React.ReactNode }) => (
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
<body className="bg-[#11110f]">
|
||||
{children}
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
@@ -81,13 +81,12 @@ const App = () => (
|
||||
<AuthenticatedFlueProvider>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
defaultTheme="light"
|
||||
forcedTheme="light"
|
||||
disableTransitionOnChange
|
||||
storageKey="vite-ui-theme"
|
||||
>
|
||||
<div className="h-svh min-h-0 overflow-hidden">
|
||||
<Outlet />
|
||||
</div>
|
||||
<Outlet />
|
||||
<Toaster richColors />
|
||||
</ThemeProvider>
|
||||
</AuthenticatedFlueProvider>
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
import { index, layout, route } from "@react-router/dev/routes";
|
||||
import type { RouteConfig } from "@react-router/dev/routes";
|
||||
import { flatRoutes } from "@react-router/fs-routes";
|
||||
|
||||
export default flatRoutes() satisfies RouteConfig;
|
||||
export default [
|
||||
layout("./routes/auth/layout.tsx", [
|
||||
route("login", "./routes/auth/login/page.tsx"),
|
||||
route("signup", "./routes/auth/signup/page.tsx"),
|
||||
]),
|
||||
layout("./routes/app/layout.tsx", [
|
||||
layout("./routes/app/mobile/layout.tsx", [
|
||||
index("./routes/app/mobile/page.tsx"),
|
||||
route("chat", "./routes/app/mobile/chat/page.tsx"),
|
||||
route("work", "./routes/app/mobile/work-list/page.tsx"),
|
||||
route("work/:workUnitId", "./routes/app/mobile/work/page.tsx"),
|
||||
]),
|
||||
route("dashboard", "./routes/app/dashboard/page.tsx"),
|
||||
route("todos", "./routes/app/todos/page.tsx"),
|
||||
]),
|
||||
] satisfies RouteConfig;
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { ChatPage } from "@/components/chat/chat-page";
|
||||
|
||||
import type { Route } from "./+types/_app._index";
|
||||
|
||||
export const meta = (_args: Route.MetaArgs) => [
|
||||
{ title: "Zopu" },
|
||||
{ content: "Chat with Zopu", name: "description" },
|
||||
];
|
||||
|
||||
const Home = () => <ChatPage />;
|
||||
|
||||
export default Home;
|
||||
@@ -1,102 +0,0 @@
|
||||
import { api } from "@code/backend/convex/_generated/api";
|
||||
import type { Id } from "@code/backend/convex/_generated/dataModel";
|
||||
import { Button } from "@code/ui/components/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@code/ui/components/card";
|
||||
import { Checkbox } from "@code/ui/components/checkbox";
|
||||
import { Input } from "@code/ui/components/input";
|
||||
import { useMutation, useQuery } from "convex/react";
|
||||
import { Loader2, Trash2 } from "lucide-react";
|
||||
import { useState, type FormEvent } from "react";
|
||||
|
||||
export default function Todos() {
|
||||
const [newTodoText, setNewTodoText] = useState("");
|
||||
|
||||
const todos = useQuery(api.todos.getAll);
|
||||
const createTodo = useMutation(api.todos.create);
|
||||
const toggleTodo = useMutation(api.todos.toggle);
|
||||
const deleteTodo = useMutation(api.todos.deleteTodo);
|
||||
|
||||
const handleAddTodo = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const text = newTodoText.trim();
|
||||
if (!text) return;
|
||||
await createTodo({ text });
|
||||
setNewTodoText("");
|
||||
};
|
||||
|
||||
const handleToggleTodo = (id: Id<"todos">, currentCompleted: boolean) => {
|
||||
toggleTodo({ id, completed: !currentCompleted });
|
||||
};
|
||||
|
||||
const handleDeleteTodo = (id: Id<"todos">) => {
|
||||
deleteTodo({ id });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full mx-auto max-w-md py-10">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Todo List</CardTitle>
|
||||
<CardDescription>Manage your tasks efficiently</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleAddTodo} className="mb-6 flex items-center space-x-2">
|
||||
<Input
|
||||
value={newTodoText}
|
||||
onChange={(e) => setNewTodoText(e.target.value)}
|
||||
placeholder="Add a new task..."
|
||||
/>
|
||||
<Button type="submit" disabled={!newTodoText.trim()}>
|
||||
Add
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{todos === undefined ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
) : todos.length === 0 ? (
|
||||
<p className="py-4 text-center">No todos yet. Add one above!</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{todos.map((todo) => (
|
||||
<li
|
||||
key={todo._id}
|
||||
className="flex items-center justify-between rounded-md border p-2"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={todo.completed}
|
||||
onCheckedChange={() => handleToggleTodo(todo._id, todo.completed)}
|
||||
id={`todo-${todo._id}`}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`todo-${todo._id}`}
|
||||
className={`${todo.completed ? "line-through text-muted-foreground" : ""}`}
|
||||
>
|
||||
{todo.text}
|
||||
</label>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDeleteTodo(todo._id)}
|
||||
aria-label="Delete todo"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
apps/web/src/routes/app/mobile/chat/page.tsx
Normal file
12
apps/web/src/routes/app/mobile/chat/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { MobileFlowPage } from "@/components/mobile-workspace/mobile-flow-page";
|
||||
|
||||
import type { Route } from "./+types/page";
|
||||
|
||||
export const meta = (_args: Route.MetaArgs) => [
|
||||
{ title: "Zopu" },
|
||||
{ content: "Chat with Zopu", name: "description" },
|
||||
];
|
||||
|
||||
export default function MobileChatPage() {
|
||||
return <MobileFlowPage screen="assistant-chat" />;
|
||||
}
|
||||
9
apps/web/src/routes/app/mobile/layout.tsx
Normal file
9
apps/web/src/routes/app/mobile/layout.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Outlet } from "react-router";
|
||||
|
||||
export default function MobileProductLayout() {
|
||||
return (
|
||||
<div className="min-h-svh bg-[#11110f]">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
apps/web/src/routes/app/mobile/page.tsx
Normal file
15
apps/web/src/routes/app/mobile/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { MobileFlowPage } from "@/components/mobile-workspace/mobile-flow-page";
|
||||
|
||||
import type { Route } from "./+types/page";
|
||||
|
||||
export const meta = (_args: Route.MetaArgs) => [
|
||||
{ title: "Daily focus | Zopu" },
|
||||
{
|
||||
content: "Supervise active work and open the next action",
|
||||
name: "description",
|
||||
},
|
||||
];
|
||||
|
||||
export default function MobileHomePage() {
|
||||
return <MobileFlowPage screen="home" />;
|
||||
}
|
||||
15
apps/web/src/routes/app/mobile/work-list/page.tsx
Normal file
15
apps/web/src/routes/app/mobile/work-list/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { MobileFlowPage } from "@/components/mobile-workspace/mobile-flow-page";
|
||||
|
||||
import type { Route } from "./+types/page";
|
||||
|
||||
export const meta = (_args: Route.MetaArgs) => [
|
||||
{ title: "Active work | Zopu" },
|
||||
{
|
||||
content: "Browse active work and expand a work unit in place",
|
||||
name: "description",
|
||||
},
|
||||
];
|
||||
|
||||
export default function MobileWorkListPage() {
|
||||
return <MobileFlowPage screen="work-list" />;
|
||||
}
|
||||
15
apps/web/src/routes/app/mobile/work/page.tsx
Normal file
15
apps/web/src/routes/app/mobile/work/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { MobileFlowPage } from "@/components/mobile-workspace/mobile-flow-page";
|
||||
|
||||
import type { Route } from "./+types/page";
|
||||
|
||||
export const meta = (_args: Route.MetaArgs) => [
|
||||
{ title: "Work unit | Zopu" },
|
||||
{
|
||||
content: "Review the state, next milestone, and activity for a work unit",
|
||||
name: "description",
|
||||
},
|
||||
];
|
||||
|
||||
export default function MobileWorkUnitPage() {
|
||||
return <MobileFlowPage screen="work-unit-detail" />;
|
||||
}
|
||||
123
apps/web/src/routes/app/todos/page.tsx
Normal file
123
apps/web/src/routes/app/todos/page.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { api } from "@code/backend/convex/_generated/api";
|
||||
import type { Id } from "@code/backend/convex/_generated/dataModel";
|
||||
import { Button } from "@code/ui/components/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@code/ui/components/card";
|
||||
import { Checkbox } from "@code/ui/components/checkbox";
|
||||
import { Input } from "@code/ui/components/input";
|
||||
import { useMutation, useQuery } from "convex/react";
|
||||
import { Loader2, Trash2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { FormEvent, ReactNode } from "react";
|
||||
|
||||
export default function Todos() {
|
||||
const [newTodoText, setNewTodoText] = useState("");
|
||||
|
||||
const todos = useQuery(api.todos.getAll);
|
||||
const createTodo = useMutation(api.todos.create);
|
||||
const toggleTodo = useMutation(api.todos.toggle);
|
||||
const deleteTodo = useMutation(api.todos.deleteTodo);
|
||||
|
||||
const handleAddTodo = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const text = newTodoText.trim();
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
await createTodo({ text });
|
||||
setNewTodoText("");
|
||||
};
|
||||
|
||||
const handleToggleTodo = (id: Id<"todos">, currentCompleted: boolean) => {
|
||||
void toggleTodo({ completed: !currentCompleted, id });
|
||||
};
|
||||
|
||||
const handleDeleteTodo = (id: Id<"todos">) => {
|
||||
void deleteTodo({ id });
|
||||
};
|
||||
|
||||
let todoContent: ReactNode;
|
||||
if (todos === undefined) {
|
||||
todoContent = (
|
||||
<div className="flex justify-center py-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
} else if (todos.length === 0) {
|
||||
todoContent = (
|
||||
<p className="py-4 text-center">No todos yet. Add one above!</p>
|
||||
);
|
||||
} else {
|
||||
todoContent = (
|
||||
<ul className="space-y-2">
|
||||
{todos.map((todo) => (
|
||||
<li
|
||||
className="flex items-center justify-between rounded-md border p-2"
|
||||
key={todo._id}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={todo.completed}
|
||||
id={`todo-${todo._id}`}
|
||||
onCheckedChange={() =>
|
||||
handleToggleTodo(todo._id, todo.completed)
|
||||
}
|
||||
/>
|
||||
<label
|
||||
className={
|
||||
todo.completed
|
||||
? "text-muted-foreground line-through"
|
||||
: undefined
|
||||
}
|
||||
htmlFor={`todo-${todo._id}`}
|
||||
>
|
||||
{todo.text}
|
||||
</label>
|
||||
</div>
|
||||
<Button
|
||||
aria-label="Delete todo"
|
||||
onClick={() => handleDeleteTodo(todo._id)}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md py-10">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Todo List</CardTitle>
|
||||
<CardDescription>Manage your tasks efficiently</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form
|
||||
className="mb-6 flex items-center space-x-2"
|
||||
onSubmit={handleAddTodo}
|
||||
>
|
||||
<Input
|
||||
onChange={(event) => setNewTodoText(event.target.value)}
|
||||
placeholder="Add a new task..."
|
||||
value={newTodoText}
|
||||
/>
|
||||
<Button disabled={!newTodoText.trim()} type="submit">
|
||||
Add
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{todoContent}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LoginForm } from "@code/auth/web";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
import type { Route } from "./+types/_auth.login";
|
||||
import type { Route } from "./+types/page";
|
||||
|
||||
export const meta = (_args: Route.MetaArgs) => [
|
||||
{ title: "Sign in | Zopu" },
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SignupForm } from "@code/auth/web";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
import type { Route } from "./+types/_auth.signup";
|
||||
import type { Route } from "./+types/page";
|
||||
|
||||
export const meta = (_args: Route.MetaArgs) => [
|
||||
{ title: "Create account | Zopu" },
|
||||
@@ -3,12 +3,12 @@ import path from "node:path";
|
||||
import { reactRouter } from "@react-router/dev/vite";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { defineConfig } from "vite-plus";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
export default defineConfig({
|
||||
envDir: path.resolve(import.meta.dirname, "../.."),
|
||||
plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
|
||||
plugins: [tailwindcss(), reactRouter()],
|
||||
resolve: {
|
||||
dedupe: ["convex", "react", "react-dom"],
|
||||
tsconfigPaths: true,
|
||||
},
|
||||
});
|
||||
|
||||
141
bun.lock
141
bun.lock
@@ -139,7 +139,6 @@
|
||||
"tailwindcss": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vite-tsconfig-paths": "^6.1.1",
|
||||
"vitest": "catalog:",
|
||||
},
|
||||
},
|
||||
@@ -248,6 +247,11 @@
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.6.0",
|
||||
"@shadcn/react": "^0.2.0",
|
||||
"@streamdown/cjk": "^1.0.3",
|
||||
"@streamdown/code": "^1.1.1",
|
||||
"@streamdown/math": "^1.0.2",
|
||||
"@streamdown/mermaid": "^1.0.2",
|
||||
"ai": "^7.0.35",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "catalog:",
|
||||
@@ -255,9 +259,12 @@
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"shadcn": "^4.12.0",
|
||||
"shiki": "^4.3.1",
|
||||
"sonner": "catalog:",
|
||||
"streamdown": "^2.5.0",
|
||||
"tailwind-merge": "catalog:",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"use-stick-to-bottom": "^1.1.6",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@code/config": "workspace:*",
|
||||
@@ -330,6 +337,12 @@
|
||||
|
||||
"@agentos-software/tar": ["@agentos-software/tar@0.3.4", "", {}, "sha512-/SSqfgS5xOufvulsz5kVDTCFxpDy+5s7Og1iJe9krjU7Jvtgnp9TpPaJnSr721h6uY6tsWHcs3u8E457EwSkNw=="],
|
||||
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "4.0.3", "@ai-sdk/provider-utils": "5.0.12", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gqTMvV0N8/JirIZ3OzwjSZRYxzwZu/PeOFCKb8NB9fstWH39tI+L6CkeMNVou5/HCKEYAw6RCOHW59Vhquv8vA=="],
|
||||
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@4.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-e0CpNWJUY7OxAFAnCZkw+ri9QOHWwTs1tXP42782KFGCU07qt8NiXCrCVowyCB5dP2r5/Uls+g2oPd8kOJn9dw=="],
|
||||
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.12", "", { "dependencies": { "@ai-sdk/provider": "4.0.3", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bbhlOgHeYwrIGheLkM6fhS8hVger8uFPmcOLg+kxc9EFh7y30XYorWhthlYAgpadO3SJhFZrIcEknN7qEqEVvA=="],
|
||||
|
||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||
|
||||
"@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="],
|
||||
@@ -1346,6 +1359,22 @@
|
||||
|
||||
"@shadcn/react": ["@shadcn/react@0.2.1", "", { "peerDependencies": { "@types/react": ">=19", "react": ">=19" }, "optionalPeers": ["@types/react", "react"] }, "sha512-5krgi3dRMKb5jH6a+qPzVJUy/54s0kKE4Rw4LjDfLqOdVQTWKUgxWf1kW8r912I0jX/Lzxqc+pgjkjWxUIK5BQ=="],
|
||||
|
||||
"@shikijs/core": ["@shikijs/core@4.3.1", "", { "dependencies": { "@shikijs/primitive": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA=="],
|
||||
|
||||
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ=="],
|
||||
|
||||
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg=="],
|
||||
|
||||
"@shikijs/langs": ["@shikijs/langs@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ=="],
|
||||
|
||||
"@shikijs/primitive": ["@shikijs/primitive@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A=="],
|
||||
|
||||
"@shikijs/themes": ["@shikijs/themes@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA=="],
|
||||
|
||||
"@shikijs/types": ["@shikijs/types@4.3.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g=="],
|
||||
|
||||
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||
|
||||
"@silvia-odwyer/photon-node": ["@silvia-odwyer/photon-node@0.3.4", "", {}, "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA=="],
|
||||
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.34.52", "", {}, "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw=="],
|
||||
@@ -1392,6 +1421,14 @@
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@streamdown/cjk": ["@streamdown/cjk@1.0.3", "", { "dependencies": { "remark-cjk-friendly": "^2.0.1", "remark-cjk-friendly-gfm-strikethrough": "^2.0.1", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-WRg8HR/gHbBoTgsMd91OKFUClIoDcEFVofJvluvEAyjx3KpU0aGgD9tGDqHkHj14ShoMSkX0IYetWGegTcwIJw=="],
|
||||
|
||||
"@streamdown/code": ["@streamdown/code@1.1.1", "", { "dependencies": { "shiki": "^3.19.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-i7HTNuDgZWb+VdrNVOam9gQhIc5MSSDXKWXgbUrn/4vSRaSMM+Rtl10MQj4wLWPNpF7p80waJsAqFP8HZfb0Jg=="],
|
||||
|
||||
"@streamdown/math": ["@streamdown/math@1.0.2", "", { "dependencies": { "katex": "^0.16.27", "rehype-katex": "^7.0.1", "remark-math": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-r8Ur9/lBuFnzZAFdEWrLUF2s/gRwRRRwruqltdZibyjbCBnuW7SJbFm26nXqvpJPW/gzpBUMrBVBzd88z05D5g=="],
|
||||
|
||||
"@streamdown/mermaid": ["@streamdown/mermaid@1.0.2", "", { "dependencies": { "mermaid": "^11.12.2" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-Fr/4sBWnAeSnxM3PcrV/+DiZe5oPMq9gOkUIAH7ZauJeuwrZ/DVzD4g0zlav6AH0axh2m/sOfrfLtY5aLT7niw=="],
|
||||
|
||||
"@t3-oss/env-core": ["@t3-oss/env-core@0.13.11", "", { "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["arktype", "typescript", "valibot", "zod"] }, "sha512-sM7GYY+KL7H/Hl0BE0inWfk3nRHZOLhmVn7sHGxaZt9FAR6KqREXAE+6TqKfiavfXmpRxO/OZ2QgKRd+oiBYRQ=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
|
||||
@@ -1560,6 +1597,8 @@
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="],
|
||||
|
||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||
|
||||
"@types/mime-types": ["@types/mime-types@2.1.4", "", {}, "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w=="],
|
||||
@@ -1618,6 +1657,8 @@
|
||||
|
||||
"@vercel/detect-agent": ["@vercel/detect-agent@1.2.3", "", {}, "sha512-VYNCgUc0nOmC4WJmWw9GkrKdfr8Zl4/rxhC5SvgacBgxiW9W/9NRttUoHHXV8xdII3MaRgkZZVX8Ikzc/Jmjag=="],
|
||||
|
||||
"@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="],
|
||||
|
||||
"@vitest/browser": ["@vitest/browser@4.1.9", "", { "dependencies": { "@blazediff/core": "1.9.1", "@vitest/mocker": "4.1.9", "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.1.0", "ws": "^8.19.0" }, "peerDependencies": { "vitest": "4.1.9" } }, "sha512-j1BKtWmPcqpMhmx/L9EPLgAJpCb0zKfwoWLmqBbxaogCXHjOwHFSEoHCBfnGtx93xKQwilZ26m+UOsHqHMkRNg=="],
|
||||
|
||||
"@vitest/browser-preview": ["@vitest/browser-preview@4.1.9", "", { "dependencies": { "@testing-library/dom": "^10.4.1", "@testing-library/user-event": "^14.6.1", "@vitest/browser": "4.1.9" }, "peerDependencies": { "vitest": "4.1.9" } }, "sha512-a4/OrkMDb/WUnE4OOB/4FJbK3rYVO7YykqtUgcTKG4p2a0R3XcjPVu7SLRHFBs2+NIYhv5yxp1Lz3dbdGBjIow=="],
|
||||
@@ -1654,6 +1695,8 @@
|
||||
|
||||
"@voidzero-dev/vite-plus-win32-x64-msvc": ["@voidzero-dev/vite-plus-win32-x64-msvc@0.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-+VUui1OIaFX0tqdUAXjmoKVlujEtWVdcsFDw2Jff+D6b4LUTQaOMAaaic8nNdfZL6wEjweRREQLZi/icZAXtNQ=="],
|
||||
|
||||
"@workflow/serde": ["@workflow/serde@4.1.0", "", {}, "sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ=="],
|
||||
|
||||
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="],
|
||||
|
||||
"@xterm/headless": ["@xterm/headless@6.0.0", "", {}, "sha512-5Yj1QINYCyzrZtf8OFIHi47iQtI+0qYFPHmouEfG8dHNxbZ9Tb9YGSuLcsEwj9Z+OL75GJqPyJbyoFer80a2Hw=="],
|
||||
@@ -1670,6 +1713,8 @@
|
||||
|
||||
"agent-cli-detector": ["agent-cli-detector@0.1.4", "", { "bin": { "agent-cli-detector": "dist/cli.js" } }, "sha512-qPgevFvpaQoBaRJVKzr8R7h1WPvV3DtbgRIQlne4le66KBzXx5hNBwo/+NTw67LgkKBlhCzksrdautpUdlls0Q=="],
|
||||
|
||||
"ai": ["ai@7.0.36", "", { "dependencies": { "@ai-sdk/gateway": "4.0.27", "@ai-sdk/provider": "4.0.3", "@ai-sdk/provider-utils": "5.0.12" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-1XJjua58GVQ0CyO2Xbioyladt85x71Joup2U8qKrjHUl8tHYwrDw8iFRtav6e94AxSVCn9FVgTS17oX1OCquKA=="],
|
||||
|
||||
"ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
@@ -2016,7 +2061,7 @@
|
||||
|
||||
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
|
||||
|
||||
"d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
|
||||
"d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
|
||||
|
||||
"d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="],
|
||||
|
||||
@@ -2156,7 +2201,7 @@
|
||||
|
||||
"enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
|
||||
|
||||
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
|
||||
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
||||
|
||||
@@ -2402,8 +2447,6 @@
|
||||
|
||||
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="],
|
||||
|
||||
"goober": ["goober@2.1.19", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-U7veizMqxyKlM58+Z5j2ngJBH/r9siDmxpvNxSw0PylF6WQvrASJEZrxh1hidRBJc2jqoBVSyOban5u8m+6Rxg=="],
|
||||
|
||||
"google-auth-library": ["google-auth-library@9.15.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="],
|
||||
@@ -2436,18 +2479,30 @@
|
||||
|
||||
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
|
||||
|
||||
"hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="],
|
||||
|
||||
"hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="],
|
||||
|
||||
"hast-util-from-html-isomorphic": ["hast-util-from-html-isomorphic@2.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-dom": "^5.0.0", "hast-util-from-html": "^2.0.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw=="],
|
||||
|
||||
"hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
|
||||
|
||||
"hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="],
|
||||
|
||||
"hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
|
||||
|
||||
"hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="],
|
||||
|
||||
"hast-util-sanitize": ["hast-util-sanitize@5.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="],
|
||||
|
||||
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
|
||||
|
||||
"hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
|
||||
|
||||
"hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="],
|
||||
|
||||
"hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="],
|
||||
|
||||
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
|
||||
|
||||
"hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
|
||||
@@ -2616,6 +2671,8 @@
|
||||
|
||||
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
|
||||
|
||||
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||
|
||||
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
@@ -2722,7 +2779,7 @@
|
||||
|
||||
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
||||
|
||||
"marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
|
||||
"marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="],
|
||||
|
||||
"marky": ["marky@1.3.0", "", {}, "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="],
|
||||
|
||||
@@ -2746,6 +2803,8 @@
|
||||
|
||||
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
|
||||
|
||||
"mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="],
|
||||
|
||||
"mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
|
||||
|
||||
"mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
|
||||
@@ -2758,6 +2817,10 @@
|
||||
|
||||
"mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
|
||||
|
||||
"mdast-util-to-markdown-cjk-friendly": ["mdast-util-to-markdown-cjk-friendly@1.0.0", "", { "dependencies": { "mdast-util-to-markdown": "^2.1.2", "micromark-extension-cjk-friendly-util": "3.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "@types/mdast": "*" }, "optionalPeers": ["@types/mdast"] }, "sha512-BoaAm8mlJ+LAYz0Qs532Y3ciTuQYgBUPZcSFbvC/ZKmEMAKgulw84YvQK1gI34t/vL2euSfuaWlqczkTBgamkw=="],
|
||||
|
||||
"mdast-util-to-markdown-cjk-friendly-gfm-strikethrough": ["mdast-util-to-markdown-cjk-friendly-gfm-strikethrough@1.0.0", "", { "dependencies": { "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-to-markdown": "^2.1.2", "micromark-extension-cjk-friendly-util": "3.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "@types/mdast": "*" }, "optionalPeers": ["@types/mdast"] }, "sha512-1ePVfB4P/vz3xSsm6H3D32r6VYGErxclnuLLFK02/2ReF+UdEKm7caulK6Vm0LBIp5gPRtB2Z1OYDznCkX3k2w=="],
|
||||
|
||||
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
|
||||
|
||||
"mdn-data": ["mdn-data@2.0.14", "", {}, "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="],
|
||||
@@ -2808,6 +2871,12 @@
|
||||
|
||||
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
||||
|
||||
"micromark-extension-cjk-friendly": ["micromark-extension-cjk-friendly@2.0.1", "", { "dependencies": { "devlop": "^1.1.0", "micromark-extension-cjk-friendly-util": "3.0.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-OkzoYVTL1ChbvQ8Cc1ayTIz7paFQz8iS9oIYmewncweUSwmWR+hkJF9spJ1lxB90XldJl26A1F4IkPOKS3bDXw=="],
|
||||
|
||||
"micromark-extension-cjk-friendly-gfm-strikethrough": ["micromark-extension-cjk-friendly-gfm-strikethrough@2.0.1", "", { "dependencies": { "devlop": "^1.1.0", "get-east-asian-width": "^1.4.0", "micromark-extension-cjk-friendly-util": "3.0.1", "micromark-util-character": "^2.1.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-wVC0zwjJNqQeX+bb07YTPu/CvSAyCTafyYb7sMhX1r62/Lw5M/df3JyYaANyp8g15c1ypJRFSsookTqA1IDsUg=="],
|
||||
|
||||
"micromark-extension-cjk-friendly-util": ["micromark-extension-cjk-friendly-util@3.0.1", "", { "dependencies": { "get-east-asian-width": "^1.4.0", "micromark-util-character": "^2.1.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark-util-types": "*" }, "optionalPeers": ["micromark-util-types"] }, "sha512-GcbXqTTHOsiZHyF753oIddP/J2eH8j9zpyQPhkof6B2JNxfEJabnQqxbCgzJNuNes0Y2jTNJ3LiYPSXr6eJA8w=="],
|
||||
|
||||
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
|
||||
|
||||
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
|
||||
@@ -2822,6 +2891,8 @@
|
||||
|
||||
"micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
|
||||
|
||||
"micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="],
|
||||
|
||||
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
|
||||
|
||||
"micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
|
||||
@@ -2992,6 +3063,10 @@
|
||||
|
||||
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||
|
||||
"oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],
|
||||
|
||||
"oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="],
|
||||
|
||||
"open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
|
||||
|
||||
"openai": ["openai@6.26.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA=="],
|
||||
@@ -3266,6 +3341,12 @@
|
||||
|
||||
"regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="],
|
||||
|
||||
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
|
||||
|
||||
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
|
||||
|
||||
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
|
||||
|
||||
"regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="],
|
||||
|
||||
"regjsgen": ["regjsgen@0.8.0", "", {}, "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q=="],
|
||||
@@ -3274,12 +3355,20 @@
|
||||
|
||||
"rehype-harden": ["rehype-harden@1.1.8", "", { "dependencies": { "unist-util-visit": "^5.0.0" } }, "sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw=="],
|
||||
|
||||
"rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="],
|
||||
|
||||
"rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="],
|
||||
|
||||
"rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="],
|
||||
|
||||
"remark-cjk-friendly": ["remark-cjk-friendly@2.3.1", "", { "dependencies": { "mdast-util-to-markdown-cjk-friendly": "1.0.0", "micromark-extension-cjk-friendly": "2.0.1" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-f+pKZRxCRwNEGFBKNRAZAqU91GIK1SAo3ZyFHWRUgC9zcxRR0BXKd6YwqgSsxtW0rNpUDtONj7H5nje2WL3fcA=="],
|
||||
|
||||
"remark-cjk-friendly-gfm-strikethrough": ["remark-cjk-friendly-gfm-strikethrough@2.3.1", "", { "dependencies": { "mdast-util-to-markdown-cjk-friendly-gfm-strikethrough": "1.0.0", "micromark-extension-cjk-friendly-gfm-strikethrough": "2.0.1" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-JE3TGgouk/sy92SemNMEUhO5mNP4on04cmzOV3s3R5Dbk160ewmpM4tgPiinKKvoJ5UW2fTu7FOYsjVbusSA9w=="],
|
||||
|
||||
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
|
||||
|
||||
"remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="],
|
||||
|
||||
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
||||
|
||||
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
|
||||
@@ -3384,6 +3473,8 @@
|
||||
|
||||
"shell-quote": ["shell-quote@1.8.4", "", {}, "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ=="],
|
||||
|
||||
"shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
|
||||
@@ -3578,8 +3669,6 @@
|
||||
|
||||
"ts-morph": ["ts-morph@26.0.0", "", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="],
|
||||
|
||||
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
|
||||
|
||||
"tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
@@ -3632,10 +3721,14 @@
|
||||
|
||||
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
|
||||
|
||||
"unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="],
|
||||
|
||||
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
|
||||
|
||||
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
|
||||
|
||||
"unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="],
|
||||
|
||||
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
||||
|
||||
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
|
||||
@@ -3662,6 +3755,8 @@
|
||||
|
||||
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
|
||||
|
||||
"use-stick-to-bottom": ["use-stick-to-bottom@1.1.6", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-z3Up8jYQGTkUCsGBnwg6/wj70KgXoW5Kz1AAc1j8MtQuYMBo6ZsdhrIXoegxa7gaMMilgQYyTohTrt3p94jHog=="],
|
||||
|
||||
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||
|
||||
"util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="],
|
||||
@@ -3692,8 +3787,6 @@
|
||||
|
||||
"vite-plus": ["vite-plus@0.2.2", "", { "dependencies": { "@oxc-project/types": "=0.138.0", "@oxlint/plugins": "=1.68.0", "@vitest/browser": "4.1.9", "@vitest/browser-preview": "4.1.9", "@vitest/expect": "4.1.9", "@vitest/mocker": "4.1.9", "@vitest/pretty-format": "4.1.9", "@vitest/runner": "4.1.9", "@vitest/snapshot": "4.1.9", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "@voidzero-dev/vite-plus-core": "0.2.2", "oxfmt": "=0.57.0", "oxlint": "=1.72.0", "oxlint-tsgolint": "=0.24.0", "vitest": "4.1.9" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "0.2.2", "@voidzero-dev/vite-plus-darwin-x64": "0.2.2", "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.2.2", "@voidzero-dev/vite-plus-linux-arm64-musl": "0.2.2", "@voidzero-dev/vite-plus-linux-x64-gnu": "0.2.2", "@voidzero-dev/vite-plus-linux-x64-musl": "0.2.2", "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.2.2", "@voidzero-dev/vite-plus-win32-x64-msvc": "0.2.2" }, "peerDependencies": { "@vitest/browser-playwright": "4.1.9", "@vitest/browser-webdriverio": "4.1.9" }, "optionalPeers": ["@vitest/browser-playwright", "@vitest/browser-webdriverio"], "bin": { "vp": "bin/vp", "vpr": "bin/vpr", "oxfmt": "bin/oxfmt", "oxlint": "bin/oxlint" } }, "sha512-bXO3O0F2/uxtvX9Ck0o67stTErH/Zh0GEcCMd9pAh22tTHABCNTDPrRMWVo733e7Ux3h0Y7HanJ7neOV/nid4g=="],
|
||||
|
||||
"vite-tsconfig-paths": ["vite-tsconfig-paths@6.1.1", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg=="],
|
||||
|
||||
"vitest": ["vitest@4.1.10", "", { "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@vitest/pretty-format": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.10", "@vitest/browser-preview": "4.1.10", "@vitest/browser-webdriverio": "4.1.10", "@vitest/coverage-istanbul": "4.1.10", "@vitest/coverage-v8": "4.1.10", "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw=="],
|
||||
|
||||
"vlq": ["vlq@1.0.1", "", {}, "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w=="],
|
||||
@@ -3892,6 +3985,8 @@
|
||||
|
||||
"@opentui/core/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
|
||||
|
||||
"@opentui/core/marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
|
||||
|
||||
"@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
|
||||
|
||||
"@react-native/dev-middleware/serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
|
||||
@@ -3914,6 +4009,8 @@
|
||||
|
||||
"@secure-exec/nodejs/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="],
|
||||
|
||||
"@streamdown/code/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
|
||||
@@ -4000,24 +4097,24 @@
|
||||
|
||||
"d3/d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="],
|
||||
|
||||
"d3/d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
|
||||
|
||||
"d3/d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
|
||||
|
||||
"d3-chord/d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
|
||||
|
||||
"d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
||||
|
||||
"d3-dsv/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="],
|
||||
|
||||
"d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
|
||||
|
||||
"defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],
|
||||
|
||||
"degenerator/ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="],
|
||||
|
||||
"diffie-hellman/bn.js": ["bn.js@4.12.5", "", {}, "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ=="],
|
||||
|
||||
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="],
|
||||
|
||||
"elliptic/bn.js": ["bn.js@4.12.5", "", {}, "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ=="],
|
||||
@@ -4132,8 +4229,6 @@
|
||||
|
||||
"parse-png/pngjs": ["pngjs@3.4.0", "", {}, "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="],
|
||||
|
||||
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
|
||||
"parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="],
|
||||
|
||||
"path-scurry/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="],
|
||||
@@ -4208,8 +4303,6 @@
|
||||
|
||||
"stacktrace-parser/type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="],
|
||||
|
||||
"streamdown/marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="],
|
||||
|
||||
"string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||
|
||||
"strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
@@ -4470,6 +4563,18 @@
|
||||
|
||||
"@secure-exec/nodejs/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="],
|
||||
|
||||
"@streamdown/code/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="],
|
||||
|
||||
"@streamdown/code/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="],
|
||||
|
||||
"@streamdown/code/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="],
|
||||
|
||||
"@streamdown/code/shiki/@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="],
|
||||
|
||||
"@streamdown/code/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="],
|
||||
|
||||
"@streamdown/code/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.6.0",
|
||||
"@shadcn/react": "^0.2.0",
|
||||
"@streamdown/cjk": "^1.0.3",
|
||||
"@streamdown/code": "^1.1.1",
|
||||
"@streamdown/math": "^1.0.2",
|
||||
"@streamdown/mermaid": "^1.0.2",
|
||||
"ai": "^7.0.35",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "catalog:",
|
||||
@@ -23,9 +28,12 @@
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"shadcn": "^4.12.0",
|
||||
"shiki": "^4.3.1",
|
||||
"sonner": "catalog:",
|
||||
"streamdown": "^2.5.0",
|
||||
"tailwind-merge": "catalog:",
|
||||
"tw-animate-css": "^1.4.0"
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"use-stick-to-bottom": "^1.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@code/config": "workspace:*",
|
||||
|
||||
525
packages/ui/src/components/ai-elements/code-block.tsx
Normal file
525
packages/ui/src/components/ai-elements/code-block.tsx
Normal file
@@ -0,0 +1,525 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@code/ui/components/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@code/ui/components/select";
|
||||
import { cn } from "@code/ui/lib/utils";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import type { ComponentProps, CSSProperties, HTMLAttributes } from "react";
|
||||
import {
|
||||
createContext,
|
||||
memo,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import type {
|
||||
BundledLanguage,
|
||||
BundledTheme,
|
||||
HighlighterGeneric,
|
||||
ThemedToken,
|
||||
} from "shiki";
|
||||
import { createHighlighter } from "shiki";
|
||||
|
||||
// Shiki uses bitflags for font styles: 1=italic, 2=bold, 4=underline.
|
||||
const hasFontStyle = (fontStyle: number | undefined, flag: number) =>
|
||||
Math.floor((fontStyle ?? 0) / flag) % 2 === 1;
|
||||
const isItalic = (fontStyle: number | undefined) => hasFontStyle(fontStyle, 1);
|
||||
const isBold = (fontStyle: number | undefined) => hasFontStyle(fontStyle, 2);
|
||||
const isUnderline = (fontStyle: number | undefined) =>
|
||||
hasFontStyle(fontStyle, 4);
|
||||
|
||||
// Transform tokens to include pre-computed keys to avoid noArrayIndexKey lint
|
||||
interface KeyedToken {
|
||||
token: ThemedToken;
|
||||
key: string;
|
||||
}
|
||||
interface KeyedLine {
|
||||
tokens: KeyedToken[];
|
||||
key: string;
|
||||
}
|
||||
|
||||
const addKeysToTokens = (lines: ThemedToken[][]): KeyedLine[] =>
|
||||
lines.map((line, lineIdx) => ({
|
||||
key: `line-${lineIdx}`,
|
||||
tokens: line.map((token, tokenIdx) => ({
|
||||
key: `line-${lineIdx}-${tokenIdx}`,
|
||||
token,
|
||||
})),
|
||||
}));
|
||||
|
||||
// Token rendering component
|
||||
const TokenSpan = ({ token }: { token: ThemedToken }) => (
|
||||
<span
|
||||
className="dark:!bg-[var(--shiki-dark-bg)] dark:!text-[var(--shiki-dark)]"
|
||||
style={
|
||||
{
|
||||
backgroundColor: token.bgColor,
|
||||
color: token.color,
|
||||
fontStyle: isItalic(token.fontStyle) ? "italic" : undefined,
|
||||
fontWeight: isBold(token.fontStyle) ? "bold" : undefined,
|
||||
textDecoration: isUnderline(token.fontStyle) ? "underline" : undefined,
|
||||
...token.htmlStyle,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
{token.content}
|
||||
</span>
|
||||
);
|
||||
|
||||
// Line number styles using CSS counters
|
||||
const LINE_NUMBER_CLASSES = cn(
|
||||
"block",
|
||||
"before:content-[counter(line)]",
|
||||
"before:inline-block",
|
||||
"before:[counter-increment:line]",
|
||||
"before:w-8",
|
||||
"before:mr-4",
|
||||
"before:text-right",
|
||||
"before:text-muted-foreground/50",
|
||||
"before:font-mono",
|
||||
"before:select-none"
|
||||
);
|
||||
|
||||
// Line rendering component
|
||||
const LineSpan = ({
|
||||
keyedLine,
|
||||
showLineNumbers,
|
||||
}: {
|
||||
keyedLine: KeyedLine;
|
||||
showLineNumbers: boolean;
|
||||
}) => (
|
||||
<span className={showLineNumbers ? LINE_NUMBER_CLASSES : "block"}>
|
||||
{keyedLine.tokens.length === 0
|
||||
? "\n"
|
||||
: keyedLine.tokens.map(({ token, key }) => (
|
||||
<TokenSpan key={key} token={token} />
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
|
||||
// Types
|
||||
type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
|
||||
code: string;
|
||||
language: BundledLanguage;
|
||||
showLineNumbers?: boolean;
|
||||
};
|
||||
|
||||
interface TokenizedCode {
|
||||
tokens: ThemedToken[][];
|
||||
fg: string;
|
||||
bg: string;
|
||||
}
|
||||
|
||||
interface CodeBlockContextType {
|
||||
code: string;
|
||||
}
|
||||
|
||||
// Context
|
||||
const CodeBlockContext = createContext<CodeBlockContextType>({
|
||||
code: "",
|
||||
});
|
||||
|
||||
// Highlighter cache (singleton per language)
|
||||
const highlighterCache = new Map<
|
||||
string,
|
||||
Promise<HighlighterGeneric<BundledLanguage, BundledTheme>>
|
||||
>();
|
||||
|
||||
// Token cache
|
||||
const tokensCache = new Map<string, TokenizedCode>();
|
||||
|
||||
const getTokensCacheKey = (code: string, language: BundledLanguage) => {
|
||||
const start = code.slice(0, 100);
|
||||
const end = code.length > 100 ? code.slice(-100) : "";
|
||||
return `${language}:${code.length}:${start}:${end}`;
|
||||
};
|
||||
|
||||
const getHighlighter = (
|
||||
language: BundledLanguage
|
||||
): Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> => {
|
||||
const cached = highlighterCache.get(language);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const highlighterPromise = createHighlighter({
|
||||
langs: [language],
|
||||
themes: ["github-light", "github-dark"],
|
||||
});
|
||||
|
||||
highlighterCache.set(language, highlighterPromise);
|
||||
return highlighterPromise;
|
||||
};
|
||||
|
||||
// Create raw tokens for immediate display while highlighting loads
|
||||
const createRawTokens = (code: string): TokenizedCode => ({
|
||||
bg: "transparent",
|
||||
fg: "inherit",
|
||||
tokens: code.split("\n").map((line) =>
|
||||
line === ""
|
||||
? []
|
||||
: [
|
||||
{
|
||||
color: "inherit",
|
||||
content: line,
|
||||
} as ThemedToken,
|
||||
]
|
||||
),
|
||||
});
|
||||
|
||||
export const highlightCode = async (
|
||||
code: string,
|
||||
language: BundledLanguage
|
||||
): Promise<TokenizedCode> => {
|
||||
const tokensCacheKey = getTokensCacheKey(code, language);
|
||||
|
||||
const cached = tokensCache.get(tokensCacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const highlighter = await getHighlighter(language);
|
||||
const availableLangs = highlighter.getLoadedLanguages();
|
||||
const langToUse = availableLangs.includes(language) ? language : "text";
|
||||
const result = highlighter.codeToTokens(code, {
|
||||
lang: langToUse,
|
||||
themes: {
|
||||
dark: "github-dark",
|
||||
light: "github-light",
|
||||
},
|
||||
});
|
||||
const tokenized: TokenizedCode = {
|
||||
bg: result.bg ?? "transparent",
|
||||
fg: result.fg ?? "inherit",
|
||||
tokens: result.tokens,
|
||||
};
|
||||
|
||||
tokensCache.set(tokensCacheKey, tokenized);
|
||||
return tokenized;
|
||||
};
|
||||
|
||||
const CodeBlockBody = memo(
|
||||
({
|
||||
tokenized,
|
||||
showLineNumbers,
|
||||
className,
|
||||
}: {
|
||||
tokenized: TokenizedCode;
|
||||
showLineNumbers: boolean;
|
||||
className?: string;
|
||||
}) => {
|
||||
const preStyle = useMemo(
|
||||
() => ({
|
||||
backgroundColor: tokenized.bg,
|
||||
color: tokenized.fg,
|
||||
}),
|
||||
[tokenized.bg, tokenized.fg]
|
||||
);
|
||||
|
||||
const keyedLines = useMemo(
|
||||
() => addKeysToTokens(tokenized.tokens),
|
||||
[tokenized.tokens]
|
||||
);
|
||||
|
||||
return (
|
||||
<pre
|
||||
className={cn(
|
||||
"dark:!bg-[var(--shiki-dark-bg)] dark:!text-[var(--shiki-dark)] m-0 p-4 text-sm",
|
||||
className
|
||||
)}
|
||||
style={preStyle}
|
||||
>
|
||||
<code
|
||||
className={cn(
|
||||
"font-mono text-sm",
|
||||
showLineNumbers && "[counter-increment:line_0] [counter-reset:line]"
|
||||
)}
|
||||
>
|
||||
{keyedLines.map((keyedLine) => (
|
||||
<LineSpan
|
||||
key={keyedLine.key}
|
||||
keyedLine={keyedLine}
|
||||
showLineNumbers={showLineNumbers}
|
||||
/>
|
||||
))}
|
||||
</code>
|
||||
</pre>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.tokenized === nextProps.tokenized &&
|
||||
prevProps.showLineNumbers === nextProps.showLineNumbers &&
|
||||
prevProps.className === nextProps.className
|
||||
);
|
||||
|
||||
CodeBlockBody.displayName = "CodeBlockBody";
|
||||
|
||||
export const CodeBlockContainer = ({
|
||||
className,
|
||||
language,
|
||||
style,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement> & { language: string }) => (
|
||||
<div
|
||||
className={cn(
|
||||
"group relative w-full overflow-hidden rounded-md border bg-background text-foreground",
|
||||
className
|
||||
)}
|
||||
data-language={language}
|
||||
style={{
|
||||
containIntrinsicSize: "auto 200px",
|
||||
contentVisibility: "auto",
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const CodeBlockHeader = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-between border-b bg-muted/80 px-3 py-2 text-muted-foreground text-xs",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const CodeBlockTitle = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex items-center gap-2", className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const CodeBlockFilename = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLSpanElement>) => (
|
||||
<span className={cn("font-mono", className)} {...props}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
export const CodeBlockActions = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("-my-1 -mr-1 flex items-center gap-2", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const CodeBlockContent = ({
|
||||
code,
|
||||
language,
|
||||
showLineNumbers = false,
|
||||
}: {
|
||||
code: string;
|
||||
language: BundledLanguage;
|
||||
showLineNumbers?: boolean;
|
||||
}) => {
|
||||
// Memoized raw tokens for immediate display
|
||||
const rawTokens = useMemo(() => createRawTokens(code), [code]);
|
||||
|
||||
const cacheKey = getTokensCacheKey(code, language);
|
||||
|
||||
// Synchronous cache lookup avoids a loading flash after the first highlight.
|
||||
const syncTokens = useMemo(
|
||||
() => tokensCache.get(cacheKey) ?? rawTokens,
|
||||
[cacheKey, rawTokens]
|
||||
);
|
||||
|
||||
const [asyncResult, setAsyncResult] = useState<{
|
||||
cacheKey: string;
|
||||
tokens: TokenizedCode;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const loadHighlightedTokens = async () => {
|
||||
try {
|
||||
const result = await highlightCode(code, language);
|
||||
if (!cancelled) {
|
||||
setAsyncResult({ cacheKey, tokens: result });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to highlight code:", error);
|
||||
}
|
||||
};
|
||||
void loadHighlightedTokens();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [cacheKey, code, language]);
|
||||
|
||||
const tokenized =
|
||||
asyncResult?.cacheKey === cacheKey ? asyncResult.tokens : syncTokens;
|
||||
|
||||
return (
|
||||
<div className="relative overflow-auto">
|
||||
<CodeBlockBody showLineNumbers={showLineNumbers} tokenized={tokenized} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CodeBlock = ({
|
||||
code,
|
||||
language,
|
||||
showLineNumbers = false,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: CodeBlockProps) => {
|
||||
const contextValue = useMemo(() => ({ code }), [code]);
|
||||
|
||||
return (
|
||||
<CodeBlockContext.Provider value={contextValue}>
|
||||
<CodeBlockContainer className={className} language={language} {...props}>
|
||||
{children}
|
||||
<CodeBlockContent
|
||||
code={code}
|
||||
language={language}
|
||||
showLineNumbers={showLineNumbers}
|
||||
/>
|
||||
</CodeBlockContainer>
|
||||
</CodeBlockContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
|
||||
onCopy?: () => void;
|
||||
onError?: (error: Error) => void;
|
||||
timeout?: number;
|
||||
};
|
||||
|
||||
export const CodeBlockCopyButton = ({
|
||||
onCopy,
|
||||
onError,
|
||||
timeout = 2000,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: CodeBlockCopyButtonProps) => {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const timeoutRef = useRef<number>(0);
|
||||
const { code } = useContext(CodeBlockContext);
|
||||
|
||||
const copyToClipboard = useCallback(async () => {
|
||||
if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
|
||||
onError?.(new Error("Clipboard API not available"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!isCopied) {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setIsCopied(true);
|
||||
onCopy?.();
|
||||
timeoutRef.current = window.setTimeout(
|
||||
() => setIsCopied(false),
|
||||
timeout
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
onError?.(error as Error);
|
||||
}
|
||||
}, [code, onCopy, onError, timeout, isCopied]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const Icon = isCopied ? CheckIcon : CopyIcon;
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={cn("shrink-0", className)}
|
||||
onClick={copyToClipboard}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
{...props}
|
||||
>
|
||||
{children ?? <Icon size={14} />}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export type CodeBlockLanguageSelectorProps = ComponentProps<typeof Select>;
|
||||
|
||||
export const CodeBlockLanguageSelector = (
|
||||
props: CodeBlockLanguageSelectorProps
|
||||
) => <Select {...props} />;
|
||||
|
||||
export type CodeBlockLanguageSelectorTriggerProps = ComponentProps<
|
||||
typeof SelectTrigger
|
||||
>;
|
||||
|
||||
export const CodeBlockLanguageSelectorTrigger = ({
|
||||
className,
|
||||
...props
|
||||
}: CodeBlockLanguageSelectorTriggerProps) => (
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
"h-7 border-none bg-transparent px-2 text-xs shadow-none",
|
||||
className
|
||||
)}
|
||||
size="sm"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export type CodeBlockLanguageSelectorValueProps = ComponentProps<
|
||||
typeof SelectValue
|
||||
>;
|
||||
|
||||
export const CodeBlockLanguageSelectorValue = (
|
||||
props: CodeBlockLanguageSelectorValueProps
|
||||
) => <SelectValue {...props} />;
|
||||
|
||||
export type CodeBlockLanguageSelectorContentProps = ComponentProps<
|
||||
typeof SelectContent
|
||||
>;
|
||||
|
||||
export const CodeBlockLanguageSelectorContent = ({
|
||||
align = "end",
|
||||
...props
|
||||
}: CodeBlockLanguageSelectorContentProps) => (
|
||||
<SelectContent align={align} {...props} />
|
||||
);
|
||||
|
||||
export type CodeBlockLanguageSelectorItemProps = ComponentProps<
|
||||
typeof SelectItem
|
||||
>;
|
||||
|
||||
export const CodeBlockLanguageSelectorItem = (
|
||||
props: CodeBlockLanguageSelectorItemProps
|
||||
) => <SelectItem {...props} />;
|
||||
168
packages/ui/src/components/ai-elements/conversation.tsx
Normal file
168
packages/ui/src/components/ai-elements/conversation.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@code/ui/components/button";
|
||||
import { cn } from "@code/ui/lib/utils";
|
||||
import type { UIMessage } from "ai";
|
||||
import { ArrowDownIcon, DownloadIcon } from "lucide-react";
|
||||
import type { ComponentProps } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
|
||||
|
||||
export type ConversationProps = ComponentProps<typeof StickToBottom>;
|
||||
|
||||
export const Conversation = ({ className, ...props }: ConversationProps) => (
|
||||
<StickToBottom
|
||||
className={cn("relative flex-1 overflow-y-hidden", className)}
|
||||
initial="smooth"
|
||||
resize="smooth"
|
||||
role="log"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export type ConversationContentProps = ComponentProps<
|
||||
typeof StickToBottom.Content
|
||||
>;
|
||||
|
||||
export const ConversationContent = ({
|
||||
className,
|
||||
...props
|
||||
}: ConversationContentProps) => (
|
||||
<StickToBottom.Content
|
||||
className={cn("flex flex-col gap-8 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export type ConversationEmptyStateProps = ComponentProps<"div"> & {
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const ConversationEmptyState = ({
|
||||
className,
|
||||
title = "No messages yet",
|
||||
description = "Start a conversation to see messages here",
|
||||
icon,
|
||||
children,
|
||||
...props
|
||||
}: ConversationEmptyStateProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex size-full flex-col items-center justify-center gap-3 p-8 text-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? (
|
||||
<>
|
||||
{icon && <div className="text-muted-foreground">{icon}</div>}
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-medium text-sm">{title}</h3>
|
||||
{description && (
|
||||
<p className="text-muted-foreground text-sm">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export type ConversationScrollButtonProps = ComponentProps<typeof Button>;
|
||||
|
||||
export const ConversationScrollButton = ({
|
||||
className,
|
||||
...props
|
||||
}: ConversationScrollButtonProps) => {
|
||||
const { isAtBottom, scrollToBottom } = useStickToBottomContext();
|
||||
|
||||
const handleScrollToBottom = useCallback(() => {
|
||||
scrollToBottom();
|
||||
}, [scrollToBottom]);
|
||||
|
||||
return (
|
||||
!isAtBottom && (
|
||||
<Button
|
||||
className={cn(
|
||||
"absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full dark:bg-background dark:hover:bg-muted",
|
||||
className
|
||||
)}
|
||||
onClick={handleScrollToBottom}
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="outline"
|
||||
{...props}
|
||||
>
|
||||
<ArrowDownIcon className="size-4" />
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const getMessageText = (message: UIMessage): string =>
|
||||
message.parts
|
||||
.filter((part) => part.type === "text")
|
||||
.map((part) => part.text)
|
||||
.join("");
|
||||
|
||||
export type ConversationDownloadProps = Omit<
|
||||
ComponentProps<typeof Button>,
|
||||
"onClick"
|
||||
> & {
|
||||
messages: UIMessage[];
|
||||
filename?: string;
|
||||
formatMessage?: (message: UIMessage, index: number) => string;
|
||||
};
|
||||
|
||||
const defaultFormatMessage = (message: UIMessage): string => {
|
||||
const roleLabel =
|
||||
message.role.charAt(0).toUpperCase() + message.role.slice(1);
|
||||
return `**${roleLabel}:** ${getMessageText(message)}`;
|
||||
};
|
||||
|
||||
export const messagesToMarkdown = (
|
||||
messages: UIMessage[],
|
||||
formatMessage: (
|
||||
message: UIMessage,
|
||||
index: number
|
||||
) => string = defaultFormatMessage
|
||||
): string => messages.map((msg, i) => formatMessage(msg, i)).join("\n\n");
|
||||
|
||||
export const ConversationDownload = ({
|
||||
messages,
|
||||
filename = "conversation.md",
|
||||
formatMessage = defaultFormatMessage,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ConversationDownloadProps) => {
|
||||
const handleDownload = useCallback(() => {
|
||||
const markdown = messagesToMarkdown(messages, formatMessage);
|
||||
const blob = new Blob([markdown], { type: "text/markdown" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.append(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}, [messages, filename, formatMessage]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={cn(
|
||||
"absolute top-4 right-4 rounded-full dark:bg-background dark:hover:bg-muted",
|
||||
className
|
||||
)}
|
||||
onClick={handleDownload}
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="outline"
|
||||
{...props}
|
||||
>
|
||||
{children ?? <DownloadIcon className="size-4" />}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
357
packages/ui/src/components/ai-elements/message.tsx
Normal file
357
packages/ui/src/components/ai-elements/message.tsx
Normal file
@@ -0,0 +1,357 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@code/ui/components/button";
|
||||
import { ButtonGroup, ButtonGroupText } from "@code/ui/components/button-group";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@code/ui/components/tooltip";
|
||||
import { cn } from "@code/ui/lib/utils";
|
||||
import { cjk } from "@streamdown/cjk";
|
||||
import { code } from "@streamdown/code";
|
||||
import { math } from "@streamdown/math";
|
||||
import { mermaid } from "@streamdown/mermaid";
|
||||
import type { UIMessage } from "ai";
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
|
||||
import {
|
||||
createContext,
|
||||
memo,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Streamdown } from "streamdown";
|
||||
|
||||
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
|
||||
from: UIMessage["role"];
|
||||
};
|
||||
|
||||
export const Message = ({ className, from, ...props }: MessageProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
"group flex w-full max-w-[95%] flex-col gap-2",
|
||||
from === "user" ? "is-user ml-auto justify-end" : "is-assistant",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export type MessageContentProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const MessageContent = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: MessageContentProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
"is-user:dark flex w-fit min-w-0 max-w-full flex-col gap-2 overflow-hidden text-sm",
|
||||
"group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:bg-secondary group-[.is-user]:px-4 group-[.is-user]:py-3 group-[.is-user]:text-foreground",
|
||||
"group-[.is-assistant]:text-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export type MessageActionsProps = ComponentProps<"div">;
|
||||
|
||||
export const MessageActions = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: MessageActionsProps) => (
|
||||
<div className={cn("flex items-center gap-1", className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export type MessageActionProps = ComponentProps<typeof Button> & {
|
||||
tooltip?: string;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export const MessageAction = ({
|
||||
tooltip,
|
||||
children,
|
||||
label,
|
||||
variant = "ghost",
|
||||
size = "icon-sm",
|
||||
...props
|
||||
}: MessageActionProps) => {
|
||||
const button = (
|
||||
<Button size={size} type="button" variant={variant} {...props}>
|
||||
{children}
|
||||
<span className="sr-only">{label || tooltip}</span>
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (tooltip) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>{button}</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{tooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return button;
|
||||
};
|
||||
|
||||
interface MessageBranchContextType {
|
||||
currentBranch: number;
|
||||
totalBranches: number;
|
||||
goToPrevious: () => void;
|
||||
goToNext: () => void;
|
||||
branches: ReactElement[];
|
||||
setBranches: (branches: ReactElement[]) => void;
|
||||
}
|
||||
|
||||
const MessageBranchContext = createContext<MessageBranchContextType | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const useMessageBranch = () => {
|
||||
const context = useContext(MessageBranchContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"MessageBranch components must be used within MessageBranch"
|
||||
);
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export type MessageBranchProps = HTMLAttributes<HTMLDivElement> & {
|
||||
defaultBranch?: number;
|
||||
onBranchChange?: (branchIndex: number) => void;
|
||||
};
|
||||
|
||||
export const MessageBranch = ({
|
||||
defaultBranch = 0,
|
||||
onBranchChange,
|
||||
className,
|
||||
...props
|
||||
}: MessageBranchProps) => {
|
||||
const [currentBranch, setCurrentBranch] = useState(defaultBranch);
|
||||
const [branches, setBranches] = useState<ReactElement[]>([]);
|
||||
|
||||
const handleBranchChange = useCallback(
|
||||
(newBranch: number) => {
|
||||
setCurrentBranch(newBranch);
|
||||
onBranchChange?.(newBranch);
|
||||
},
|
||||
[onBranchChange]
|
||||
);
|
||||
|
||||
const goToPrevious = useCallback(() => {
|
||||
const newBranch =
|
||||
currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
|
||||
handleBranchChange(newBranch);
|
||||
}, [currentBranch, branches.length, handleBranchChange]);
|
||||
|
||||
const goToNext = useCallback(() => {
|
||||
const newBranch =
|
||||
currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
|
||||
handleBranchChange(newBranch);
|
||||
}, [currentBranch, branches.length, handleBranchChange]);
|
||||
|
||||
const contextValue = useMemo<MessageBranchContextType>(
|
||||
() => ({
|
||||
branches,
|
||||
currentBranch,
|
||||
goToNext,
|
||||
goToPrevious,
|
||||
setBranches,
|
||||
totalBranches: branches.length,
|
||||
}),
|
||||
[branches, currentBranch, goToNext, goToPrevious]
|
||||
);
|
||||
|
||||
return (
|
||||
<MessageBranchContext.Provider value={contextValue}>
|
||||
<div
|
||||
className={cn("grid w-full gap-2 [&>div]:pb-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
</MessageBranchContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const MessageBranchContent = ({
|
||||
children,
|
||||
...props
|
||||
}: MessageBranchContentProps) => {
|
||||
const { currentBranch, setBranches, branches } = useMessageBranch();
|
||||
const childrenArray = useMemo(
|
||||
() => (Array.isArray(children) ? children : [children]),
|
||||
[children]
|
||||
);
|
||||
|
||||
// Use useEffect to update branches when they change
|
||||
useEffect(() => {
|
||||
if (branches.length !== childrenArray.length) {
|
||||
setBranches(childrenArray);
|
||||
}
|
||||
}, [childrenArray, branches, setBranches]);
|
||||
|
||||
return childrenArray.map((branch, index) => (
|
||||
<div
|
||||
className={cn(
|
||||
"grid gap-2 overflow-hidden [&>div]:pb-0",
|
||||
index === currentBranch ? "block" : "hidden"
|
||||
)}
|
||||
key={branch.key}
|
||||
{...props}
|
||||
>
|
||||
{branch}
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
export type MessageBranchSelectorProps = ComponentProps<typeof ButtonGroup>;
|
||||
|
||||
export const MessageBranchSelector = ({
|
||||
className,
|
||||
...props
|
||||
}: MessageBranchSelectorProps) => {
|
||||
const { totalBranches } = useMessageBranch();
|
||||
|
||||
// Don't render if there's only one branch
|
||||
if (totalBranches <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ButtonGroup
|
||||
className={cn(
|
||||
"[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md",
|
||||
className
|
||||
)}
|
||||
orientation="horizontal"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export type MessageBranchPreviousProps = ComponentProps<typeof Button>;
|
||||
|
||||
export const MessageBranchPrevious = ({
|
||||
children,
|
||||
...props
|
||||
}: MessageBranchPreviousProps) => {
|
||||
const { goToPrevious, totalBranches } = useMessageBranch();
|
||||
|
||||
return (
|
||||
<Button
|
||||
aria-label="Previous branch"
|
||||
disabled={totalBranches <= 1}
|
||||
onClick={goToPrevious}
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronLeftIcon size={14} />}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export type MessageBranchNextProps = ComponentProps<typeof Button>;
|
||||
|
||||
export const MessageBranchNext = ({
|
||||
children,
|
||||
...props
|
||||
}: MessageBranchNextProps) => {
|
||||
const { goToNext, totalBranches } = useMessageBranch();
|
||||
|
||||
return (
|
||||
<Button
|
||||
aria-label="Next branch"
|
||||
disabled={totalBranches <= 1}
|
||||
onClick={goToNext}
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRightIcon size={14} />}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export type MessageBranchPageProps = HTMLAttributes<HTMLSpanElement>;
|
||||
|
||||
export const MessageBranchPage = ({
|
||||
className,
|
||||
...props
|
||||
}: MessageBranchPageProps) => {
|
||||
const { currentBranch, totalBranches } = useMessageBranch();
|
||||
|
||||
return (
|
||||
<ButtonGroupText
|
||||
className={cn(
|
||||
"border-none bg-transparent text-muted-foreground shadow-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{currentBranch + 1} of {totalBranches}
|
||||
</ButtonGroupText>
|
||||
);
|
||||
};
|
||||
|
||||
export type MessageResponseProps = ComponentProps<typeof Streamdown>;
|
||||
|
||||
const streamdownPlugins = { cjk, code, math, mermaid };
|
||||
|
||||
export const MessageResponse = memo(
|
||||
({ className, ...props }: MessageResponseProps) => (
|
||||
<Streamdown
|
||||
className={cn(
|
||||
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
|
||||
className
|
||||
)}
|
||||
plugins={streamdownPlugins}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.children === nextProps.children &&
|
||||
nextProps.isAnimating === prevProps.isAnimating
|
||||
);
|
||||
|
||||
MessageResponse.displayName = "MessageResponse";
|
||||
|
||||
export type MessageToolbarProps = ComponentProps<"div">;
|
||||
|
||||
export const MessageToolbar = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: MessageToolbarProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
"mt-4 flex w-full items-center justify-between gap-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
173
packages/ui/src/components/ai-elements/tool.tsx
Normal file
173
packages/ui/src/components/ai-elements/tool.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
"use client";
|
||||
|
||||
import { Badge } from "@code/ui/components/badge";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@code/ui/components/collapsible";
|
||||
import { cn } from "@code/ui/lib/utils";
|
||||
import type { DynamicToolUIPart, ToolUIPart } from "ai";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ChevronDownIcon,
|
||||
CircleIcon,
|
||||
ClockIcon,
|
||||
WrenchIcon,
|
||||
XCircleIcon,
|
||||
} from "lucide-react";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import { isValidElement } from "react";
|
||||
|
||||
import { CodeBlock } from "./code-block";
|
||||
|
||||
export type ToolProps = ComponentProps<typeof Collapsible>;
|
||||
|
||||
export const Tool = ({ className, ...props }: ToolProps) => (
|
||||
<Collapsible
|
||||
className={cn("group not-prose mb-4 w-full rounded-md border", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export type ToolPart = ToolUIPart | DynamicToolUIPart;
|
||||
|
||||
export type ToolHeaderProps = {
|
||||
title?: string;
|
||||
className?: string;
|
||||
} & (
|
||||
| { type: ToolUIPart["type"]; state: ToolUIPart["state"]; toolName?: never }
|
||||
| {
|
||||
type: DynamicToolUIPart["type"];
|
||||
state: DynamicToolUIPart["state"];
|
||||
toolName: string;
|
||||
}
|
||||
);
|
||||
|
||||
const statusLabels: Record<ToolPart["state"], string> = {
|
||||
"approval-requested": "Awaiting Approval",
|
||||
"approval-responded": "Responded",
|
||||
"input-available": "Running",
|
||||
"input-streaming": "Pending",
|
||||
"output-available": "Completed",
|
||||
"output-denied": "Denied",
|
||||
"output-error": "Error",
|
||||
};
|
||||
|
||||
const statusIcons: Record<ToolPart["state"], ReactNode> = {
|
||||
"approval-requested": <ClockIcon className="size-4 text-yellow-600" />,
|
||||
"approval-responded": <CheckCircleIcon className="size-4 text-blue-600" />,
|
||||
"input-available": <ClockIcon className="size-4 animate-pulse" />,
|
||||
"input-streaming": <CircleIcon className="size-4" />,
|
||||
"output-available": <CheckCircleIcon className="size-4 text-green-600" />,
|
||||
"output-denied": <XCircleIcon className="size-4 text-orange-600" />,
|
||||
"output-error": <XCircleIcon className="size-4 text-red-600" />,
|
||||
};
|
||||
|
||||
export const getStatusBadge = (status: ToolPart["state"]) => (
|
||||
<Badge className="gap-1.5 rounded-full text-xs" variant="secondary">
|
||||
{statusIcons[status]}
|
||||
{statusLabels[status]}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
export const ToolHeader = ({
|
||||
className,
|
||||
title,
|
||||
type,
|
||||
state,
|
||||
toolName,
|
||||
...props
|
||||
}: ToolHeaderProps) => {
|
||||
const derivedName =
|
||||
type === "dynamic-tool" ? toolName : type.split("-").slice(1).join("-");
|
||||
|
||||
return (
|
||||
<CollapsibleTrigger
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between gap-4 p-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<WrenchIcon className="size-4 text-muted-foreground" />
|
||||
<span className="font-medium text-sm">{title ?? derivedName}</span>
|
||||
{getStatusBadge(state)}
|
||||
</div>
|
||||
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
|
||||
</CollapsibleTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
export type ToolContentProps = ComponentProps<typeof CollapsibleContent>;
|
||||
|
||||
export const ToolContent = ({ className, ...props }: ToolContentProps) => (
|
||||
<CollapsibleContent
|
||||
className={cn(
|
||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 space-y-4 p-4 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export type ToolInputProps = ComponentProps<"div"> & {
|
||||
input: ToolPart["input"];
|
||||
};
|
||||
|
||||
export const ToolInput = ({ className, input, ...props }: ToolInputProps) => (
|
||||
<div className={cn("space-y-2 overflow-hidden", className)} {...props}>
|
||||
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||
Parameters
|
||||
</h4>
|
||||
<div className="rounded-md bg-muted/50">
|
||||
<CodeBlock code={JSON.stringify(input, null, 2)} language="json" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export type ToolOutputProps = ComponentProps<"div"> & {
|
||||
output: ToolPart["output"];
|
||||
errorText: ToolPart["errorText"];
|
||||
};
|
||||
|
||||
export const ToolOutput = ({
|
||||
className,
|
||||
output,
|
||||
errorText,
|
||||
...props
|
||||
}: ToolOutputProps) => {
|
||||
if (!(output || errorText)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let Output = <div>{output as ReactNode}</div>;
|
||||
|
||||
if (typeof output === "object" && !isValidElement(output)) {
|
||||
Output = (
|
||||
<CodeBlock code={JSON.stringify(output, null, 2)} language="json" />
|
||||
);
|
||||
} else if (typeof output === "string") {
|
||||
Output = <CodeBlock code={output} language="json" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-2", className)} {...props}>
|
||||
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||
{errorText ? "Error" : "Result"}
|
||||
</h4>
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-x-auto rounded-md text-xs [&_table]:w-full",
|
||||
errorText
|
||||
? "bg-destructive/10 text-destructive"
|
||||
: "bg-muted/50 text-foreground"
|
||||
)}
|
||||
>
|
||||
{errorText && <div>{errorText}</div>}
|
||||
{Output}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
51
packages/ui/src/components/badge.tsx
Normal file
51
packages/ui/src/components/badge.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { mergeProps } from "@base-ui/react/merge-props";
|
||||
import { useRender } from "@base-ui/react/use-render";
|
||||
import { cn } from "@code/ui/lib/utils";
|
||||
import { cva } from "class-variance-authority";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-none border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
||||
{
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
outline:
|
||||
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const Badge = ({
|
||||
className,
|
||||
variant = "default",
|
||||
render,
|
||||
...props
|
||||
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) =>
|
||||
useRender({
|
||||
defaultTagName: "span",
|
||||
props: mergeProps<"span">(
|
||||
{
|
||||
className: cn(badgeVariants({ variant }), className),
|
||||
},
|
||||
props
|
||||
),
|
||||
render,
|
||||
state: {
|
||||
slot: "badge",
|
||||
variant,
|
||||
},
|
||||
});
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
82
packages/ui/src/components/button-group.tsx
Normal file
82
packages/ui/src/components/button-group.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { mergeProps } from "@base-ui/react/merge-props";
|
||||
import { useRender } from "@base-ui/react/use-render";
|
||||
import { Separator } from "@code/ui/components/separator";
|
||||
import { cn } from "@code/ui/lib/utils";
|
||||
import { cva } from "class-variance-authority";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
|
||||
const buttonGroupVariants = cva(
|
||||
"m-0 flex min-w-0 w-fit items-stretch rounded-none border-0 p-0 *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-none [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
||||
{
|
||||
defaultVariants: {
|
||||
orientation: "horizontal",
|
||||
},
|
||||
variants: {
|
||||
orientation: {
|
||||
horizontal:
|
||||
"*:data-slot:rounded-r-none [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0",
|
||||
vertical:
|
||||
"flex-col *:data-slot:rounded-b-none [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0",
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const ButtonGroup = ({
|
||||
className,
|
||||
orientation,
|
||||
...props
|
||||
}: React.ComponentProps<"fieldset"> &
|
||||
VariantProps<typeof buttonGroupVariants>) => (
|
||||
<fieldset
|
||||
data-slot="button-group"
|
||||
data-orientation={orientation}
|
||||
className={cn(buttonGroupVariants({ orientation }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const ButtonGroupText = ({
|
||||
className,
|
||||
render,
|
||||
...props
|
||||
}: useRender.ComponentProps<"div">) =>
|
||||
useRender({
|
||||
defaultTagName: "div",
|
||||
props: mergeProps<"div">(
|
||||
{
|
||||
className: cn(
|
||||
"flex items-center gap-2 rounded-none border bg-muted px-2.5 text-xs font-medium [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
),
|
||||
},
|
||||
props
|
||||
),
|
||||
render,
|
||||
state: {
|
||||
slot: "button-group-text",
|
||||
},
|
||||
});
|
||||
|
||||
const ButtonGroupSeparator = ({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Separator>) => (
|
||||
<Separator
|
||||
data-slot="button-group-separator"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"relative self-stretch bg-input data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export {
|
||||
ButtonGroup,
|
||||
ButtonGroupSeparator,
|
||||
ButtonGroupText,
|
||||
buttonGroupVariants,
|
||||
};
|
||||
17
packages/ui/src/components/collapsible.tsx
Normal file
17
packages/ui/src/components/collapsible.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible";
|
||||
|
||||
const Collapsible = ({ ...props }: CollapsiblePrimitive.Root.Props) => (
|
||||
<CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||
);
|
||||
|
||||
const CollapsibleTrigger = ({
|
||||
...props
|
||||
}: CollapsiblePrimitive.Trigger.Props) => (
|
||||
<CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />
|
||||
);
|
||||
|
||||
const CollapsibleContent = ({ ...props }: CollapsiblePrimitive.Panel.Props) => (
|
||||
<CollapsiblePrimitive.Panel data-slot="collapsible-content" {...props} />
|
||||
);
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
363
packages/ui/src/components/mobile-chat.tsx
Normal file
363
packages/ui/src/components/mobile-chat.tsx
Normal file
@@ -0,0 +1,363 @@
|
||||
import { cn } from "@code/ui/lib/utils";
|
||||
import { ArrowUp, GripVertical, LoaderCircle } from "lucide-react";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
|
||||
const MobileViewport = ({ className, ...props }: ComponentProps<"div">) => (
|
||||
<div
|
||||
data-slot="mobile-viewport"
|
||||
className={cn(
|
||||
"relative mx-auto h-[100dvh] min-h-0 w-full max-w-[390px] overflow-hidden bg-background text-foreground sm:border-x sm:border-border/70",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const MobileChatMark = ({ className, ...props }: ComponentProps<"span">) => (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
data-slot="mobile-chat-mark"
|
||||
className={cn(
|
||||
"inline-flex size-6 shrink-0 items-center justify-center rounded-md bg-[#f1f1ef] text-[#171716]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<GripVertical className="size-3.5" strokeWidth={2.2} />
|
||||
</span>
|
||||
);
|
||||
|
||||
interface MobileChatHeaderProps extends Omit<
|
||||
ComponentProps<"header">,
|
||||
"children"
|
||||
> {
|
||||
active?: boolean;
|
||||
label?: string;
|
||||
statusLabel: string;
|
||||
}
|
||||
|
||||
const MobileChatHeader = ({
|
||||
active = false,
|
||||
className,
|
||||
label = "Zopu",
|
||||
statusLabel,
|
||||
...props
|
||||
}: MobileChatHeaderProps) => (
|
||||
<header
|
||||
data-slot="mobile-chat-header"
|
||||
className={cn(
|
||||
"flex h-[78px] shrink-0 items-center border-b border-[#e9e9e7] bg-[#fefefe] px-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex w-full items-center gap-3">
|
||||
<MobileChatMark className="size-7 rounded-[7px]" />
|
||||
<h1 className="text-[17px] leading-none font-semibold tracking-[-0.015em] text-[#171716]">
|
||||
{label}
|
||||
</h1>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"ml-auto size-1.5 rounded-full bg-[#b9bbb8]",
|
||||
active && "bg-[#17c777]"
|
||||
)}
|
||||
/>
|
||||
<span className="sr-only" aria-live="polite">
|
||||
{statusLabel}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
interface MobileChatMessageProps extends Omit<
|
||||
ComponentProps<"article">,
|
||||
"role"
|
||||
> {
|
||||
sender: "assistant" | "user";
|
||||
}
|
||||
|
||||
const MobileChatMessage = ({
|
||||
className,
|
||||
sender,
|
||||
...props
|
||||
}: MobileChatMessageProps) => (
|
||||
<article
|
||||
data-sender={sender}
|
||||
data-slot="mobile-chat-message"
|
||||
className={cn(
|
||||
"flex w-full min-w-0",
|
||||
sender === "user" ? "justify-end" : "justify-start",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
interface MobileChatBubbleProps extends Omit<ComponentProps<"div">, "role"> {
|
||||
sender: "assistant" | "user";
|
||||
}
|
||||
|
||||
const MobileChatBubble = ({
|
||||
className,
|
||||
sender,
|
||||
...props
|
||||
}: MobileChatBubbleProps) => (
|
||||
<div
|
||||
data-sender={sender}
|
||||
data-slot="mobile-chat-bubble"
|
||||
className={cn(
|
||||
"min-w-0 text-[15px] tracking-[-0.012em] wrap-break-word",
|
||||
sender === "user"
|
||||
? "max-w-[260px] rounded-[20px] bg-[#0d0d0c] px-[14px] py-[11px] leading-[18px] text-[#fafafa]"
|
||||
: "w-full leading-[18px] text-[#232321]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
interface MobileChatAssistantLabelProps extends ComponentProps<"div"> {
|
||||
label?: string;
|
||||
state?: ReactNode;
|
||||
}
|
||||
|
||||
const MobileChatAssistantLabel = ({
|
||||
className,
|
||||
label = "Zopu",
|
||||
state,
|
||||
...props
|
||||
}: MobileChatAssistantLabelProps) => (
|
||||
<div
|
||||
data-slot="mobile-chat-assistant-label"
|
||||
className={cn(
|
||||
"mb-3 flex items-center gap-2 text-[14px] leading-5 text-[#6f706e]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<MobileChatMark className="size-5 rounded-[5px] [&_svg]:size-3" />
|
||||
<span>{label}</span>
|
||||
{state}
|
||||
</div>
|
||||
);
|
||||
|
||||
interface MobileChatToolCallProps extends ComponentProps<"div"> {
|
||||
detail?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
status: string;
|
||||
tone?: "error" | "neutral" | "success";
|
||||
toolName: string;
|
||||
}
|
||||
|
||||
const MobileChatToolCall = ({
|
||||
children,
|
||||
className,
|
||||
detail,
|
||||
icon,
|
||||
status,
|
||||
tone = "neutral",
|
||||
toolName,
|
||||
...props
|
||||
}: MobileChatToolCallProps) => {
|
||||
let content: ReactNode;
|
||||
if (children) {
|
||||
content = <div className="mt-2 grid gap-2">{children}</div>;
|
||||
} else if (detail) {
|
||||
content = (
|
||||
<div className="mt-2 flex items-start gap-2">
|
||||
<MobileChatMark className="mt-0.5 size-4 rounded-[4px] [&_svg]:size-2.5" />
|
||||
<p className="line-clamp-2 min-w-0 text-[11px] leading-[15px] text-[#555653]">
|
||||
{detail}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="mobile-chat-tool-call"
|
||||
className={cn(
|
||||
"mb-3 overflow-hidden rounded-[16px] bg-[#f4f4f2] px-3 py-3 text-[#171716]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex min-h-5 items-center gap-2">
|
||||
<span className="flex size-5 shrink-0 items-center justify-center rounded-[5px] bg-[#e8e8e5] text-[#171716]">
|
||||
{icon ?? <GripVertical className="size-3" strokeWidth={2.2} />}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate text-[12px] leading-4 font-semibold">
|
||||
{toolName}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full bg-[#e6e7e4] px-2 py-1 text-[9px] leading-none font-medium text-[#454643]",
|
||||
tone === "success" && "bg-[#dff5e8] text-[#174b32]",
|
||||
tone === "error" && "bg-[#f8dfdc] text-[#7b2821]"
|
||||
)}
|
||||
>
|
||||
{status}
|
||||
</span>
|
||||
</div>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface MobileChatToolResultProps extends ComponentProps<"div"> {
|
||||
icon?: ReactNode;
|
||||
source?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const MobileChatToolResult = ({
|
||||
className,
|
||||
icon,
|
||||
source,
|
||||
title,
|
||||
...props
|
||||
}: MobileChatToolResultProps) => (
|
||||
<div
|
||||
data-slot="mobile-chat-tool-result"
|
||||
className={cn("flex min-w-0 items-start gap-2", className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-[4px] bg-[#e8e8e5] text-[#171716]">
|
||||
{icon ?? <GripVertical className="size-2.5" strokeWidth={2.2} />}
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-[11px] leading-[14px] font-medium text-[#292a28]">
|
||||
{title}
|
||||
</span>
|
||||
{source ? (
|
||||
<span className="block truncate text-[9px] leading-[12px] text-[#9a9b98]">
|
||||
{source}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
interface MobileChatComposerDockProps extends ComponentProps<"div"> {
|
||||
errorMessage?: string;
|
||||
statusMessage?: ReactNode;
|
||||
}
|
||||
|
||||
const MobileChatComposerDock = ({
|
||||
children,
|
||||
className,
|
||||
errorMessage,
|
||||
statusMessage,
|
||||
...props
|
||||
}: MobileChatComposerDockProps) => {
|
||||
const message = errorMessage ?? statusMessage;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="mobile-chat-composer-dock"
|
||||
className={cn(
|
||||
"shrink-0 border-t border-[#e8e8e6] bg-[#fefefe] pb-[env(safe-area-inset-bottom)]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{message ? (
|
||||
<div
|
||||
id={errorMessage ? "composer-error" : undefined}
|
||||
className={cn(
|
||||
"px-3 pt-2 text-center text-[10px] leading-4 text-[#777875]",
|
||||
errorMessage && "text-destructive"
|
||||
)}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
) : null}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface MobileChatComposerProps extends Omit<
|
||||
ComponentProps<"form">,
|
||||
"children"
|
||||
> {
|
||||
busy?: boolean;
|
||||
canSend: boolean;
|
||||
errorMessage?: string;
|
||||
statusMessage?: ReactNode;
|
||||
textareaProps: ComponentProps<"textarea">;
|
||||
}
|
||||
|
||||
const MobileChatComposer = ({
|
||||
busy = false,
|
||||
canSend,
|
||||
className,
|
||||
errorMessage,
|
||||
statusMessage,
|
||||
textareaProps,
|
||||
...props
|
||||
}: MobileChatComposerProps) => {
|
||||
const { className: textareaClassName, ...inputProps } = textareaProps;
|
||||
|
||||
return (
|
||||
<MobileChatComposerDock
|
||||
errorMessage={errorMessage}
|
||||
statusMessage={statusMessage}
|
||||
>
|
||||
<form
|
||||
aria-label="Send a message"
|
||||
className={cn("w-full px-3 pt-[11px] pb-3", className)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-end gap-2.5">
|
||||
<textarea
|
||||
aria-label="Message Zopu"
|
||||
placeholder="Message Zopu..."
|
||||
rows={1}
|
||||
className={cn(
|
||||
"min-h-10 max-h-28 flex-1 resize-none rounded-[20px] border-0 bg-[#f4f4f2] px-4 py-[10px] text-[15px] leading-5 text-[#232321] outline-none placeholder:text-center placeholder:text-[#b6b7b4] focus-visible:ring-2 focus-visible:ring-[#171716]/15 disabled:cursor-not-allowed disabled:opacity-60",
|
||||
textareaClassName
|
||||
)}
|
||||
{...inputProps}
|
||||
/>
|
||||
<button
|
||||
aria-label="Send message"
|
||||
className="inline-flex size-10 shrink-0 items-center justify-center rounded-full bg-[#0d0d0c] text-[#fafafa] transition-transform active:scale-[0.96] disabled:bg-[#dededb] disabled:text-[#9b9c99]"
|
||||
disabled={!canSend}
|
||||
type="submit"
|
||||
>
|
||||
{busy ? (
|
||||
<LoaderCircle className="size-[18px] animate-spin" />
|
||||
) : (
|
||||
<ArrowUp className="size-[18px]" strokeWidth={2.2} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</MobileChatComposerDock>
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
MobileChatAssistantLabel,
|
||||
MobileChatBubble,
|
||||
MobileChatComposer,
|
||||
MobileChatComposerDock,
|
||||
MobileChatHeader,
|
||||
MobileChatMark,
|
||||
MobileChatMessage,
|
||||
MobileChatToolResult,
|
||||
MobileChatToolCall,
|
||||
MobileViewport,
|
||||
};
|
||||
export type {
|
||||
MobileChatAssistantLabelProps,
|
||||
MobileChatBubbleProps,
|
||||
MobileChatComposerProps,
|
||||
MobileChatComposerDockProps,
|
||||
MobileChatHeaderProps,
|
||||
MobileChatMessageProps,
|
||||
MobileChatToolCallProps,
|
||||
MobileChatToolResultProps,
|
||||
};
|
||||
8
packages/ui/src/components/mobile-product.tsx
Normal file
8
packages/ui/src/components/mobile-product.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
export {
|
||||
MobileAssistantChatScreen,
|
||||
MobileExpandedWorkScreen,
|
||||
MobileHomeScreen,
|
||||
MobileWorkListScreen,
|
||||
MobileWorkUnitDetailScreen,
|
||||
} from "./mobile-workspace/index";
|
||||
export type { MobileWorkspaceScreenProps } from "./mobile-workspace/index";
|
||||
910
packages/ui/src/components/mobile-workspace-screens.tsx
Normal file
910
packages/ui/src/components/mobile-workspace-screens.tsx
Normal file
@@ -0,0 +1,910 @@
|
||||
import { cn } from "@code/ui/lib/utils";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowUp,
|
||||
Check,
|
||||
ChevronRight,
|
||||
FileText,
|
||||
ListChecks,
|
||||
Mic,
|
||||
MoreHorizontal,
|
||||
Paperclip,
|
||||
Plus,
|
||||
Sparkles,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import type { FormEvent, ReactNode } from "react";
|
||||
|
||||
export type MobileWorkspaceVariant =
|
||||
| "character-pass"
|
||||
| "expanded-work-unit"
|
||||
| "home-expanded-in-place"
|
||||
| "vertical-work-stack"
|
||||
| "work-units-home";
|
||||
|
||||
export interface MobileWorkspaceRendererProps {
|
||||
composerValue: string;
|
||||
onBack?: () => void;
|
||||
onComposerChange: (value: string) => void;
|
||||
onComposerSubmit: (event: FormEvent<HTMLFormElement>) => void;
|
||||
onOpenAssistant?: () => void;
|
||||
onOpenUnit?: () => void;
|
||||
onViewWork?: () => void;
|
||||
statusMessage?: string;
|
||||
variant: MobileWorkspaceVariant;
|
||||
}
|
||||
|
||||
const BrandGlyph = ({ className }: { className?: string }) => (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"inline-flex h-[23px] w-[9px] items-center justify-center bg-black text-[4px] leading-none font-bold tracking-[-0.08em] text-white [writing-mode:vertical-rl]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
ZOPU
|
||||
</span>
|
||||
);
|
||||
|
||||
interface ProductHeaderProps {
|
||||
avatar?: boolean;
|
||||
letterLogo?: boolean;
|
||||
onAssistant?: () => void;
|
||||
profileInset?: boolean;
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
const ProductHeader = ({
|
||||
avatar = false,
|
||||
letterLogo = false,
|
||||
onAssistant,
|
||||
profileInset = false,
|
||||
subtitle = "Workspace is active",
|
||||
}: ProductHeaderProps) => (
|
||||
<header className="flex h-20 shrink-0 items-center bg-white px-4">
|
||||
<div className="flex size-10 items-center justify-center rounded-[14px] bg-[#c8ff00] text-[20px] font-bold text-black">
|
||||
{letterLogo ? "Z" : <BrandGlyph />}
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h1 className="text-[20px] leading-6 font-semibold tracking-[-0.03em] text-[#0b0b0a]">
|
||||
Zopu
|
||||
</h1>
|
||||
<p className="mt-0.5 flex items-center gap-2 text-[13px] leading-4 text-[#7c7772]">
|
||||
<span className="size-1.5 rounded-full bg-[#64ad1f]" />
|
||||
{subtitle}
|
||||
</p>
|
||||
</div>
|
||||
{avatar ? (
|
||||
<button
|
||||
aria-label="Open profile"
|
||||
className={cn(
|
||||
"ml-auto flex size-[42px] items-center justify-center rounded-full bg-[#0b0b0a] text-[13px] font-semibold text-white",
|
||||
profileInset && "mr-[31px]"
|
||||
)}
|
||||
type="button"
|
||||
>
|
||||
YM
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
aria-label="Open Zopu assistant"
|
||||
className="ml-auto flex size-10 items-center justify-center rounded-full bg-[#f2f3ef]"
|
||||
onClick={onAssistant}
|
||||
type="button"
|
||||
>
|
||||
<BrandGlyph className="h-[20px] w-[8px]" />
|
||||
</button>
|
||||
<button
|
||||
aria-label="More options"
|
||||
className="ml-2 flex size-10 items-center justify-center rounded-full bg-[#f2f3ef]"
|
||||
type="button"
|
||||
>
|
||||
<MoreHorizontal className="size-5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
|
||||
interface ComposerProps {
|
||||
contextLabel?: string;
|
||||
deviceHomeIndicator?: boolean;
|
||||
hint?: string;
|
||||
homeIndicator?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
|
||||
placeholder: string;
|
||||
sendBlue?: boolean;
|
||||
statusMessage?: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const ReferenceComposer = ({
|
||||
contextLabel,
|
||||
deviceHomeIndicator = false,
|
||||
hint,
|
||||
homeIndicator = false,
|
||||
onChange,
|
||||
onSubmit,
|
||||
placeholder,
|
||||
sendBlue = false,
|
||||
statusMessage,
|
||||
value,
|
||||
}: ComposerProps) => (
|
||||
<footer
|
||||
className={cn(
|
||||
"relative shrink-0 border-t border-[#eff0ec] bg-white px-[14px]",
|
||||
contextLabel ? "h-[144px] pt-[13px]" : "h-[120px] pt-[14px]",
|
||||
!homeIndicator && !contextLabel && "h-[100px]",
|
||||
deviceHomeIndicator && "h-[125px]"
|
||||
)}
|
||||
>
|
||||
{contextLabel ? (
|
||||
<div className="mb-2 flex h-6 w-fit items-center gap-2 rounded-full bg-[#dbe6ff] px-2.5 text-[10px] font-semibold text-[#315dc0]">
|
||||
<span className="size-1.5 rounded-full bg-[#315dc0]" />
|
||||
{contextLabel}
|
||||
<X className="size-3" />
|
||||
</div>
|
||||
) : null}
|
||||
<form
|
||||
className="flex h-[60px] items-center rounded-[30px] bg-[#f2f3ef] p-2"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<button
|
||||
aria-label="Add attachment"
|
||||
className="flex size-11 shrink-0 items-center justify-center rounded-full bg-white text-[#5e5a56]"
|
||||
type="button"
|
||||
>
|
||||
<Plus className="size-5" strokeWidth={1.8} />
|
||||
</button>
|
||||
<label className="ml-2 min-w-0 flex-1">
|
||||
<span className="sr-only">{placeholder}</span>
|
||||
<textarea
|
||||
className="block h-5 w-full resize-none overflow-hidden bg-transparent text-[14px] leading-5 text-[#282622] outline-none placeholder:text-[#b3afab]"
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
placeholder={placeholder}
|
||||
rows={1}
|
||||
value={value}
|
||||
/>
|
||||
{hint || statusMessage ? (
|
||||
<span className="block truncate text-[10px] leading-4 text-[#c0bcb7]">
|
||||
{statusMessage ?? hint}
|
||||
</span>
|
||||
) : null}
|
||||
</label>
|
||||
<button
|
||||
aria-label="Send"
|
||||
className={cn(
|
||||
"ml-2 flex size-11 shrink-0 items-center justify-center rounded-full text-white",
|
||||
sendBlue ? "bg-[#315dc0]" : "bg-black",
|
||||
!value.trim() && "opacity-100"
|
||||
)}
|
||||
type="submit"
|
||||
>
|
||||
<ArrowUp className="size-5" strokeWidth={2.2} />
|
||||
</button>
|
||||
</form>
|
||||
{homeIndicator ? (
|
||||
<span className="absolute bottom-[13px] left-1/2 h-[5px] w-[120px] -translate-x-1/2 rounded-full bg-black" />
|
||||
) : null}
|
||||
</footer>
|
||||
);
|
||||
|
||||
const WorkFeedScreen = ({
|
||||
onChange,
|
||||
onOpenAssistant,
|
||||
onOpen,
|
||||
onSubmit,
|
||||
onViewWork,
|
||||
statusMessage,
|
||||
value,
|
||||
}: ComposerProps & {
|
||||
onOpen?: () => void;
|
||||
onOpenAssistant?: () => void;
|
||||
onViewWork?: () => void;
|
||||
}) => (
|
||||
<div className="mx-auto flex h-[min(100dvh,844px)] w-full max-w-[390px] flex-col overflow-hidden bg-white text-[#0b0b0a]">
|
||||
<ProductHeader onAssistant={onOpenAssistant} />
|
||||
<main className="min-h-0 flex-1 bg-[#f7f8f5] px-4 pt-4">
|
||||
<h2 className="text-[29px] leading-9 font-semibold tracking-[-0.05em]">
|
||||
Good morning.
|
||||
</h2>
|
||||
<p className="text-[14px] leading-5 text-[#807b76]">
|
||||
Your work is moving. Two things need you.
|
||||
</p>
|
||||
<div className="mt-2 grid h-[55px] grid-cols-3 gap-2">
|
||||
<div className="rounded-[18px] bg-white px-2.5 py-2">
|
||||
<p className="text-[9px] font-semibold text-[#aaa6a1]">ACTIVE</p>
|
||||
<p className="text-[17px] leading-5 font-semibold">4 units</p>
|
||||
</div>
|
||||
<div className="rounded-[18px] bg-[#fff0e8] px-2.5 py-2 text-[#9a3215]">
|
||||
<p className="text-[9px] font-semibold">NEEDS YOU</p>
|
||||
<p className="text-[17px] leading-5 font-semibold">2 blockers</p>
|
||||
</div>
|
||||
<div className="rounded-[18px] bg-[#e8f7ef] px-2.5 py-2 text-[#23633d]">
|
||||
<p className="text-[9px] font-semibold">SHIPPED</p>
|
||||
<p className="text-[17px] leading-5 font-semibold">3 this week</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-[18px] flex items-center">
|
||||
<h3 className="text-[14px] font-semibold">Now</h3>
|
||||
<button
|
||||
className="ml-auto text-[12px] text-[#807b76]"
|
||||
onClick={onViewWork}
|
||||
type="button"
|
||||
>
|
||||
View all 6
|
||||
</button>
|
||||
</div>
|
||||
<article className="mt-3 h-[190px] rounded-[27px] bg-[#e8efff] p-[14px] text-[#14265f]">
|
||||
<div className="flex items-center">
|
||||
<span className="flex size-[26px] items-center justify-center rounded-[9px] bg-white text-[#315dc0]">
|
||||
↗
|
||||
</span>
|
||||
<div className="ml-2">
|
||||
<p className="text-[10px] font-semibold text-[#315dc0]">
|
||||
IN PROGRESS · 68%
|
||||
</p>
|
||||
<p className="text-[10px] text-[#60718e]">
|
||||
Updated 4 min ago by Zopu
|
||||
</p>
|
||||
</div>
|
||||
<ChevronRight className="ml-auto size-4 text-[#315dc0]" />
|
||||
</div>
|
||||
<h3 className="mt-3 text-[20px] leading-6 font-semibold tracking-[-0.035em]">
|
||||
Launch the new onboarding flow
|
||||
</h3>
|
||||
<p className="mt-1 overflow-hidden text-[13px] leading-5 whitespace-nowrap text-[#526482]">
|
||||
First-run flow mapped. Zopu is drafting the welcome sequence.
|
||||
</p>
|
||||
<div className="mt-3 h-[7px] rounded-full bg-[#315dc0]/15">
|
||||
<div className="h-full w-[68%] rounded-full bg-[#315dc0]" />
|
||||
</div>
|
||||
<div className="mt-[10px] flex h-[42px] gap-2">
|
||||
<div className="min-w-0 flex-1 rounded-[14px] bg-white px-2 py-1.5">
|
||||
<p className="text-[9px] font-semibold text-[#8a97b5]">
|
||||
NEXT ACTION
|
||||
</p>
|
||||
<p className="truncate text-[12px] font-semibold">
|
||||
Review the welcome copy
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
className="rounded-[14px] bg-[#c8ff00] px-4 text-[12px] font-semibold text-black"
|
||||
onClick={onOpen}
|
||||
type="button"
|
||||
>
|
||||
Open unit →
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
<article className="mt-[14px] h-[116px] rounded-[25px] bg-[#fff0e8] p-[14px] text-[#8a2e13]">
|
||||
<div className="flex items-center text-[10px] font-semibold text-[#c53c08]">
|
||||
<span className="mr-2 size-2 rounded-full bg-[#ff7620]" />
|
||||
WAITING ON YOU
|
||||
<ChevronRight className="ml-auto size-4" />
|
||||
</div>
|
||||
<h3 className="mt-[13px] text-[18px] leading-5 font-semibold">
|
||||
Fix GitHub authentication bug
|
||||
</h3>
|
||||
<div className="mt-2 flex items-center">
|
||||
<div>
|
||||
<p className="text-[12px]">Blocked by missing Safari repro logs</p>
|
||||
<p className="text-[10px] text-[#b3aaa5]">
|
||||
Zopu can continue as soon as they’re attached.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
className="ml-auto rounded-[13px] bg-white px-5 py-2 text-[11px] font-semibold"
|
||||
type="button"
|
||||
>
|
||||
Add logs
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
<div className="mt-[14px] grid h-[94px] grid-cols-2 gap-[10px]">
|
||||
<article className="rounded-[24px] bg-[#f1e8ff] p-3 text-[#3e0b74]">
|
||||
<div className="flex text-[10px] font-semibold text-[#7137ef]">
|
||||
RESEARCHING
|
||||
<span className="ml-auto font-normal">42%</span>
|
||||
</div>
|
||||
<h3 className="mt-3 text-[15px] font-semibold">
|
||||
Pricing page rewrite
|
||||
</h3>
|
||||
<p className="mt-1 text-[11px] text-[#7f7190]">
|
||||
Comparing 8 competitors
|
||||
</p>
|
||||
</article>
|
||||
<article className="rounded-[24px] bg-[#e8f7ef] p-3 text-[#105c35]">
|
||||
<div className="flex text-[10px] font-semibold text-[#238050]">
|
||||
READY
|
||||
<Check className="ml-auto size-3" />
|
||||
</div>
|
||||
<h3 className="mt-3 text-[15px] font-semibold">Q3 launch brief</h3>
|
||||
<p className="mt-1 text-[11px] text-[#668678]">
|
||||
Draft ready for review
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="h-[100px] shrink-0 border-t border-[#eff0ec] bg-white px-3 pt-[10px]">
|
||||
<div className="mb-[7px] flex h-5 w-fit items-center gap-2 rounded-full bg-[#f1f2ef] px-2 text-[10px] font-semibold text-[#5f5b57]">
|
||||
<span className="size-1.5 rounded-full bg-[#c8ff00]" />
|
||||
GLOBAL MODE
|
||||
</div>
|
||||
<form
|
||||
className="flex h-[52px] items-center rounded-[26px] bg-[#f2f3ef] p-2"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<button
|
||||
aria-label="Add attachment"
|
||||
className="flex size-9 items-center justify-center rounded-full bg-white"
|
||||
type="button"
|
||||
>
|
||||
<Plus className="size-5" />
|
||||
</button>
|
||||
<input
|
||||
className="min-w-0 flex-1 bg-transparent px-2 text-[14px] outline-none placeholder:text-[#b3afab]"
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
placeholder="Ask anything or create new work..."
|
||||
value={value}
|
||||
/>
|
||||
<button
|
||||
aria-label="Send"
|
||||
className="flex size-9 items-center justify-center rounded-full bg-black text-white"
|
||||
type="submit"
|
||||
>
|
||||
<ArrowUp className="size-5" />
|
||||
</button>
|
||||
</form>
|
||||
{statusMessage ? <span className="sr-only">{statusMessage}</span> : null}
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
|
||||
const DeviceFrame402 = ({ children }: { children: ReactNode }) => (
|
||||
<div className="mx-auto h-[min(100dvh,855px)] w-full max-w-[402px] overflow-hidden bg-black p-[5px] pb-0">
|
||||
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-white">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ExpandedWorkUnitContent = ({
|
||||
compact = false,
|
||||
}: {
|
||||
compact?: boolean;
|
||||
}) => (
|
||||
<article
|
||||
className={cn(
|
||||
"rounded-[30px] bg-[#e8efff] p-5 text-[#14265f]",
|
||||
compact ? "h-[500px]" : "h-[610px]"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="rounded-full bg-[#d6e2ff] px-2 py-1 text-[10px] font-semibold text-[#315dc0]">
|
||||
IN PROGRESS
|
||||
</span>
|
||||
<span className="ml-auto text-[11px] text-[#6e83b4]">68%</span>
|
||||
</div>
|
||||
<h1
|
||||
className={cn(
|
||||
"font-semibold tracking-[-0.045em]",
|
||||
compact
|
||||
? "mt-4 text-[27px] leading-[30px]"
|
||||
: "mt-5 text-[29px] leading-[31px]"
|
||||
)}
|
||||
>
|
||||
Launch onboarding flow
|
||||
</h1>
|
||||
<p
|
||||
className={cn(
|
||||
"text-[14px] leading-5 text-[#60718e]",
|
||||
compact ? "mt-3" : "mt-[43px]"
|
||||
)}
|
||||
>
|
||||
Create a calm first-run experience that helps new users reach their first
|
||||
meaningful outcome.
|
||||
</p>
|
||||
<div className="mt-[14px] h-[7px] rounded-full bg-[#315dc0]/15">
|
||||
<div className="h-full w-[68%] rounded-full bg-[#315dc0]" />
|
||||
</div>
|
||||
<div className="mt-1.5 flex text-[10px] font-semibold text-[#315dc0]">
|
||||
ON TRACK
|
||||
<span className="ml-auto font-normal text-[#6e83b4]">
|
||||
{compact ? "4m ago" : "Updated 4m ago"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-[18px] border-t border-[#315dc0]/15 pt-[14px]">
|
||||
<p className="text-[10px] font-semibold text-[#6e83b4]">CURRENT STATE</p>
|
||||
<p className="mt-1 text-[14px] leading-5 text-[#33405c]">
|
||||
{compact
|
||||
? "Flow mapped. Welcome copy and the activation checklist are being drafted."
|
||||
: "The first-run flow is mapped. Zopu is drafting the welcome sequence and activation checklist."}
|
||||
</p>
|
||||
</div>
|
||||
<section
|
||||
className={cn(
|
||||
"flex items-center rounded-[19px] bg-white p-3",
|
||||
compact ? "mt-5" : "mt-[35px]"
|
||||
)}
|
||||
>
|
||||
<span className="flex size-10 shrink-0 items-center justify-center rounded-[13px] bg-[#c8ff00] text-black">
|
||||
<Check className="size-5" />
|
||||
</span>
|
||||
<div className="ml-3 min-w-0">
|
||||
<p className="text-[10px] font-semibold text-[#6e83b4]">
|
||||
NEXT MILESTONE
|
||||
</p>
|
||||
<p className="truncate text-[14px] font-semibold">
|
||||
Review the welcome copy
|
||||
</p>
|
||||
<p className="text-[10px] text-[#6e83b4]">
|
||||
Ready for your feedback · about 5 min
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<div className="mt-[14px] grid h-[78px] grid-cols-2 gap-[10px]">
|
||||
<div className="rounded-[19px] bg-[#dce6fc] p-3">
|
||||
<p className="text-[10px] font-semibold text-[#6e83b4]">ARTIFACTS</p>
|
||||
<strong className="mt-1 block text-xl">2</strong>
|
||||
<span className="text-[10px] text-[#60718e]">
|
||||
Flow map · Copy draft
|
||||
</span>
|
||||
</div>
|
||||
<div className="rounded-[19px] bg-[#dce6fc] p-3">
|
||||
<p className="text-[10px] font-semibold text-[#6e83b4]">ACTIVITY</p>
|
||||
<strong className="mt-1 block text-xl">12</strong>
|
||||
<span className="text-[10px] text-[#60718e]">Actions this week</span>
|
||||
</div>
|
||||
</div>
|
||||
{compact ? (
|
||||
<div className="mt-[14px] flex items-center gap-3">
|
||||
<span className="flex size-[30px] items-center justify-center rounded-[10px] bg-white text-[#315dc0]">
|
||||
Z
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold text-[#33405c]">
|
||||
Activation checklist updated
|
||||
</p>
|
||||
<p className="text-[10px] text-[#6e83b4]">Zopu · 4 minutes ago</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="mt-[14px] flex text-[10px] font-semibold text-[#6e83b4]">
|
||||
RECENT ACTIVITY
|
||||
<span className="ml-auto font-normal text-[#315dc0]">View all</span>
|
||||
</div>
|
||||
<div className="mt-3 grid gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex size-7 items-center justify-center rounded-[9px] bg-white text-[#315dc0]">
|
||||
Z
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold text-[#33405c]">
|
||||
Zopu updated the activation checklist
|
||||
</p>
|
||||
<p className="text-[10px] text-[#6e83b4]">4 minutes ago</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex size-7 items-center justify-center rounded-[9px] bg-white text-[#315dc0]">
|
||||
↗
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold text-[#33405c]">
|
||||
Welcome copy draft was attached
|
||||
</p>
|
||||
<p className="text-[10px] text-[#6e83b4]">18 minutes ago</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
|
||||
const WorkUnitScreen = ({
|
||||
onBack,
|
||||
onChange,
|
||||
onSubmit,
|
||||
statusMessage,
|
||||
value,
|
||||
}: ComposerProps & { onBack?: () => void }) => (
|
||||
<DeviceFrame402>
|
||||
<header className="flex h-20 shrink-0 items-center bg-white px-4">
|
||||
<button
|
||||
aria-label="Back to work units"
|
||||
className="flex size-10 items-center justify-center rounded-full bg-[#fbfcf9]"
|
||||
onClick={onBack}
|
||||
type="button"
|
||||
>
|
||||
<ArrowLeft className="size-5" />
|
||||
</button>
|
||||
<div className="min-w-0 flex-1 text-center">
|
||||
<p className="text-[16px] font-semibold">Work unit</p>
|
||||
<p className="text-[11px] text-[#807b76]">FLOW-08 · In progress</p>
|
||||
</div>
|
||||
<button
|
||||
aria-label="More work unit actions"
|
||||
className="flex size-10 items-center justify-center rounded-full bg-[#fbfcf9]"
|
||||
type="button"
|
||||
>
|
||||
<MoreHorizontal className="size-5" />
|
||||
</button>
|
||||
</header>
|
||||
<main className="min-h-0 flex-1 bg-[#f7f8f5] px-[17px] pt-4">
|
||||
<ExpandedWorkUnitContent />
|
||||
</main>
|
||||
<ReferenceComposer
|
||||
contextLabel="LAUNCH ONBOARDING FLOW"
|
||||
hint="Message stays in this card’s context"
|
||||
onChange={onChange}
|
||||
onSubmit={onSubmit}
|
||||
placeholder="Ask about this work unit..."
|
||||
sendBlue
|
||||
statusMessage={statusMessage}
|
||||
value={value}
|
||||
/>
|
||||
</DeviceFrame402>
|
||||
);
|
||||
|
||||
const VerticalStack = ({
|
||||
onChange,
|
||||
onOpen,
|
||||
onSubmit,
|
||||
statusMessage,
|
||||
value,
|
||||
}: ComposerProps & { onOpen?: () => void }) => (
|
||||
<DeviceFrame402>
|
||||
<ProductHeader
|
||||
avatar
|
||||
letterLogo
|
||||
profileInset
|
||||
subtitle="4 active work units"
|
||||
/>
|
||||
<main className="relative min-h-0 flex-1 bg-[#f7f8f5] px-5 pt-8">
|
||||
<div className="flex items-center">
|
||||
<h2 className="text-[29px] leading-9 font-semibold tracking-[-0.05em]">
|
||||
Work in motion
|
||||
</h2>
|
||||
<span className="ml-auto rounded-full bg-white px-3 py-1.5 text-[11px] font-semibold text-[#5f5b57]">
|
||||
MON 20
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-[14px] leading-5 text-[#807b76]">
|
||||
Scroll vertically. Tap a card to expand it in place.
|
||||
</p>
|
||||
<div className="mt-[34px] flex text-[12px]">
|
||||
<span className="font-semibold">ACTIVE WORK</span>
|
||||
<span className="ml-auto text-[#807b76]">4 units</span>
|
||||
</div>
|
||||
<section className="absolute inset-x-5 top-[142px] h-[500px]">
|
||||
<article className="absolute inset-x-[13px] top-0 h-[150px] rounded-[28px] bg-[#f1e8ff] p-5 text-[#3e0b74]">
|
||||
<span className="text-[10px] font-semibold text-[#7137ef]">
|
||||
RESEARCHING
|
||||
</span>
|
||||
<span className="float-right text-[10px] text-[#9a7ac5]">
|
||||
WORK-402
|
||||
</span>
|
||||
<strong className="mt-4 block text-[22px]">
|
||||
Rewrite pricing page
|
||||
</strong>
|
||||
</article>
|
||||
<article className="absolute inset-x-[7px] top-[70px] h-[150px] rounded-[28px] bg-[#fff0e8] p-5 text-[#8a2e13]">
|
||||
<span className="text-[10px] font-semibold text-[#c53c08]">
|
||||
BLOCKED
|
||||
</span>
|
||||
<span className="float-right text-[10px] text-[#c68168]">BUG-12</span>
|
||||
<strong className="mt-4 block text-[22px]">
|
||||
Fix GitHub authentication
|
||||
</strong>
|
||||
</article>
|
||||
<article className="absolute inset-x-1 top-[142px] h-[150px] rounded-[28px] bg-[#e8f7ef] p-5 text-[#105c35]">
|
||||
<span className="text-[10px] font-semibold text-[#238050]">
|
||||
READY FOR REVIEW
|
||||
</span>
|
||||
<span className="float-right text-[10px] text-[#61a17e]">
|
||||
BRIEF-03
|
||||
</span>
|
||||
<strong className="mt-4 block text-[22px]">Q3 launch brief</strong>
|
||||
</article>
|
||||
<article className="absolute inset-x-px top-[218px] h-[285px] rounded-[30px] bg-[#e8efff] p-5 text-[#14265f]">
|
||||
<div className="flex items-center">
|
||||
<span className="rounded-full bg-[#d6e2ff] px-2 py-1 text-[10px] font-semibold text-[#315dc0]">
|
||||
IN PROGRESS
|
||||
</span>
|
||||
<span className="ml-auto text-[10px] text-[#6e83b4]">FLOW-08</span>
|
||||
</div>
|
||||
<h3 className="mt-4 text-[24px] leading-[27px] font-semibold tracking-[-0.04em]">
|
||||
Launch onboarding flow
|
||||
</h3>
|
||||
<p className="mt-2 text-[13px] text-[#60718e]">
|
||||
Welcome copy and activation checklist are being drafted.
|
||||
</p>
|
||||
<div className="mt-4 h-[7px] rounded-full bg-[#315dc0]/15">
|
||||
<div className="h-full w-[68%] rounded-full bg-[#315dc0]" />
|
||||
</div>
|
||||
<div className="mt-2 flex text-[10px] font-semibold text-[#315dc0]">
|
||||
68% COMPLETE
|
||||
<span className="ml-auto font-normal text-[#6e83b4]">On track</span>
|
||||
</div>
|
||||
<button
|
||||
className="mt-5 flex w-full items-center border-t border-[#315dc0]/15 pt-4 text-left"
|
||||
onClick={onOpen}
|
||||
type="button"
|
||||
>
|
||||
<span className="flex size-10 items-center justify-center rounded-[14px] bg-white text-[#315dc0]">
|
||||
↗
|
||||
</span>
|
||||
<span className="ml-3">
|
||||
<span className="block text-[10px] font-semibold text-[#6e83b4]">
|
||||
NEXT MILESTONE
|
||||
</span>
|
||||
<span className="block text-[14px] font-semibold">
|
||||
Review welcome copy
|
||||
</span>
|
||||
<span className="block text-[10px] text-[#6e83b4]">
|
||||
2 artifacts · Updated 4 minutes ago
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
<ReferenceComposer
|
||||
deviceHomeIndicator
|
||||
hint="Use @ to reference a work unit"
|
||||
homeIndicator
|
||||
onChange={onChange}
|
||||
onSubmit={onSubmit}
|
||||
placeholder="Ask Zopu or create new work..."
|
||||
statusMessage={statusMessage}
|
||||
value={value}
|
||||
/>
|
||||
</DeviceFrame402>
|
||||
);
|
||||
|
||||
const HomeExpanded = ({
|
||||
onChange,
|
||||
onOpen,
|
||||
onSubmit,
|
||||
statusMessage,
|
||||
value,
|
||||
}: ComposerProps & { onOpen?: () => void }) => (
|
||||
<DeviceFrame402>
|
||||
<ProductHeader
|
||||
avatar
|
||||
letterLogo
|
||||
profileInset
|
||||
subtitle="4 active work units"
|
||||
/>
|
||||
<main className="relative min-h-0 flex-1 bg-[#f7f8f5] px-5 pt-[27px]">
|
||||
<div className="flex items-center">
|
||||
<h2 className="text-[29px] leading-9 font-semibold tracking-[-0.05em]">
|
||||
Work in motion
|
||||
</h2>
|
||||
<span className="ml-auto rounded-full bg-white px-3 py-1.5 text-[11px] font-semibold text-[#5f5b57]">
|
||||
MON 20
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-[14px] leading-5 text-[#807b76]">
|
||||
One work unit is expanded in the stack.
|
||||
</p>
|
||||
<div className="absolute inset-x-[31px] top-[97px] h-24 rounded-[28px] bg-[#f1e8ff] px-4 pt-4 text-[10px] font-semibold text-[#7137ef]">
|
||||
RESEARCHING
|
||||
</div>
|
||||
<div className="absolute inset-x-[26px] top-[117px] h-24 rounded-[28px] bg-[#fff0e8] px-4 pt-4 text-[10px] font-semibold text-[#c53c08]">
|
||||
BLOCKED
|
||||
</div>
|
||||
<div className="absolute inset-x-[17px] top-[139px]">
|
||||
<ExpandedWorkUnitContent compact />
|
||||
<button
|
||||
aria-label="Open launch onboarding flow"
|
||||
className="absolute inset-0 rounded-[30px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#315dc0]"
|
||||
onClick={onOpen}
|
||||
type="button"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
<ReferenceComposer
|
||||
contextLabel="LAUNCH ONBOARDING FLOW"
|
||||
hint="Chat is scoped to the selected card"
|
||||
onChange={onChange}
|
||||
onSubmit={onSubmit}
|
||||
placeholder="Ask about this work unit..."
|
||||
sendBlue
|
||||
statusMessage={statusMessage}
|
||||
value={value}
|
||||
/>
|
||||
</DeviceFrame402>
|
||||
);
|
||||
|
||||
const CharacterChat = ({
|
||||
onChange,
|
||||
onSubmit,
|
||||
statusMessage,
|
||||
value,
|
||||
}: ComposerProps) => (
|
||||
<div className="mx-auto h-[min(100dvh,858px)] w-full max-w-[390px] overflow-hidden bg-black">
|
||||
<main className="relative h-full overflow-hidden rounded-[30px] bg-white px-4 text-[#11110f]">
|
||||
<header className="absolute inset-x-4 top-8 flex items-center">
|
||||
<span className="flex size-9 items-center justify-center rounded-[13px] bg-[#c8ff00]">
|
||||
<Sparkles className="size-5" />
|
||||
</span>
|
||||
<div className="ml-3">
|
||||
<h1 className="text-[18px] leading-5 font-semibold">Zopu</h1>
|
||||
<p className="mt-0.5 flex items-center gap-2 text-[12px] text-[#55524f]">
|
||||
<span className="size-1.5 rounded-full bg-[#18c973]" />
|
||||
Ready to help
|
||||
</p>
|
||||
</div>
|
||||
<MoreHorizontal className="ml-auto size-5" />
|
||||
</header>
|
||||
<div className="absolute top-[106px] left-4 rounded-full bg-[#f4f4f2] px-2.5 py-1.5 text-[10px] text-[#aaa7a3]">
|
||||
TODAY • 9:41 AM
|
||||
</div>
|
||||
<section className="absolute inset-x-4 top-[148px]">
|
||||
<div className="flex items-center gap-2 text-[14px] text-[#55524f]">
|
||||
<span className="flex size-6 items-center justify-center rounded-[8px] bg-[#f4f4f2]">
|
||||
<Sparkles className="size-4" />
|
||||
</span>
|
||||
<strong>Zopu</strong>
|
||||
<span className="text-[#b5b2ae]">• a fresh start</span>
|
||||
</div>
|
||||
<p className="mt-3 text-[17px] leading-5">
|
||||
Good morning. What are we making clearer today?
|
||||
</p>
|
||||
<div className="mt-4 flex gap-7 text-[12px] font-semibold text-[#55524f]">
|
||||
<button className="flex items-center gap-2" type="button">
|
||||
<FileText className="size-4" />
|
||||
Summarize
|
||||
</button>
|
||||
<button className="flex items-center gap-2" type="button">
|
||||
<ListChecks className="size-4" />
|
||||
Plan
|
||||
</button>
|
||||
<button className="flex items-center gap-2" type="button">
|
||||
◉ Explore
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<div className="absolute top-[278px] right-4 max-w-[275px] rounded-[22px] bg-[#0c0c0b] px-4 py-3 text-[17px] leading-6 text-white">
|
||||
Turn my messy notes into a focused plan.
|
||||
</div>
|
||||
<p className="absolute top-[344px] right-4 text-[10px] text-[#b5b2ae]">
|
||||
9:42 AM <span className="text-[#18c973]">✓</span>
|
||||
</p>
|
||||
<section className="absolute inset-x-4 top-[374px]">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex size-7 items-center justify-center rounded-[9px] bg-[#c8ff00]">
|
||||
<Sparkles className="size-4" />
|
||||
</span>
|
||||
<strong className="text-[14px]">Zopu</strong>
|
||||
<span className="rounded-full bg-[#f1f1ef] px-2 py-1 text-[9px] text-[#aaa7a3]">
|
||||
THINKING PARTNER
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-3 text-[16px] leading-[19px]">
|
||||
Absolutely. I’ll turn the noise into a small set of next steps, then
|
||||
leave room for the unexpected.
|
||||
</p>
|
||||
<section className="mt-px h-[127px] rounded-[22px] bg-[#f4f4f2] p-[14px]">
|
||||
<div className="flex items-center">
|
||||
<span className="flex size-8 items-center justify-center rounded-[10px] bg-[#c8ff00]">
|
||||
↗
|
||||
</span>
|
||||
<div className="ml-3">
|
||||
<h2 className="text-[15px] leading-4 font-semibold">
|
||||
Weekly reset
|
||||
</h2>
|
||||
<p className="text-[10px] text-[#b5b2ae]">
|
||||
3 focus areas · drafted just now
|
||||
</p>
|
||||
</div>
|
||||
<ChevronRight className="ml-auto size-4" />
|
||||
</div>
|
||||
<ul className="mt-2 space-y-1 text-[13px] leading-[18px]">
|
||||
<li>
|
||||
<span className="mr-2 text-[#c8ff00]">●</span>
|
||||
Finish the launch brief
|
||||
<span className="ml-2 text-[10px] text-[#b5b2ae]">today</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="mr-2">●</span>
|
||||
Block two hours for deep work
|
||||
<span className="ml-2 text-[10px] text-[#b5b2ae]">Tue</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="mr-2 text-[#b5b2ae]">●</span>
|
||||
Leave an hour for cleanup
|
||||
<span className="ml-2 text-[10px] text-[#b5b2ae]">Fri</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
<form
|
||||
className="absolute inset-x-3 bottom-[34px] flex h-[53px] items-center rounded-[27px] bg-[#f4f4f2] p-2"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<button
|
||||
aria-label="Attach file"
|
||||
className="flex size-10 items-center justify-center rounded-full bg-white"
|
||||
type="button"
|
||||
>
|
||||
<Paperclip className="size-5" />
|
||||
</button>
|
||||
<input
|
||||
className="min-w-0 flex-1 bg-transparent px-2 text-[14px] outline-none placeholder:text-[#b5b2ae]"
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
placeholder="Ask anything..."
|
||||
value={value}
|
||||
/>
|
||||
<button
|
||||
aria-label="Record voice message"
|
||||
className="flex size-10 items-center justify-center rounded-full bg-white"
|
||||
type="button"
|
||||
>
|
||||
<Mic className="size-5" />
|
||||
</button>
|
||||
<button
|
||||
aria-label="Send"
|
||||
className="ml-1 flex size-10 items-center justify-center rounded-full bg-black text-white"
|
||||
type="submit"
|
||||
>
|
||||
<ArrowUp className="size-5" />
|
||||
</button>
|
||||
</form>
|
||||
<p className="absolute inset-x-0 bottom-[13px] text-center text-[9px] text-[#b5b2ae]">
|
||||
{statusMessage ?? "Press Enter to send · Shift + Enter for a new line"}
|
||||
</p>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const MobileWorkspaceScreenRenderer = ({
|
||||
composerValue,
|
||||
onBack,
|
||||
onComposerChange,
|
||||
onComposerSubmit,
|
||||
onOpenAssistant,
|
||||
onOpenUnit,
|
||||
onViewWork,
|
||||
statusMessage,
|
||||
variant,
|
||||
}: MobileWorkspaceRendererProps) => {
|
||||
const composerProps = {
|
||||
onChange: onComposerChange,
|
||||
onSubmit: onComposerSubmit,
|
||||
placeholder: "",
|
||||
statusMessage,
|
||||
value: composerValue,
|
||||
};
|
||||
|
||||
if (variant === "character-pass") {
|
||||
return <CharacterChat {...composerProps} />;
|
||||
}
|
||||
if (variant === "expanded-work-unit") {
|
||||
return <WorkUnitScreen {...composerProps} onBack={onBack} />;
|
||||
}
|
||||
if (variant === "home-expanded-in-place") {
|
||||
return <HomeExpanded {...composerProps} onOpen={onOpenUnit} />;
|
||||
}
|
||||
if (variant === "vertical-work-stack") {
|
||||
return <VerticalStack {...composerProps} onOpen={onOpenUnit} />;
|
||||
}
|
||||
if (variant === "work-units-home") {
|
||||
return (
|
||||
<WorkFeedScreen
|
||||
{...composerProps}
|
||||
onOpen={onOpenUnit}
|
||||
onOpenAssistant={onOpenAssistant}
|
||||
onViewWork={onViewWork}
|
||||
/>
|
||||
);
|
||||
}
|
||||
variant satisfies never;
|
||||
return null;
|
||||
};
|
||||
6
packages/ui/src/components/mobile-workspace/index.ts
Normal file
6
packages/ui/src/components/mobile-workspace/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { MobileAssistantChatScreen } from "./mobile-assistant-chat-screen";
|
||||
export { MobileExpandedWorkScreen } from "./mobile-expanded-work-screen";
|
||||
export { MobileHomeScreen } from "./mobile-home-screen";
|
||||
export { MobileWorkListScreen } from "./mobile-work-list-screen";
|
||||
export { MobileWorkUnitDetailScreen } from "./mobile-work-unit-detail-screen";
|
||||
export type { MobileWorkspaceScreenProps } from "./types";
|
||||
@@ -0,0 +1,6 @@
|
||||
import { MobileWorkspaceScreenRenderer } from "../mobile-workspace-screens";
|
||||
import type { MobileWorkspaceScreenProps } from "./types";
|
||||
|
||||
export const MobileAssistantChatScreen = (
|
||||
props: MobileWorkspaceScreenProps
|
||||
) => <MobileWorkspaceScreenRenderer {...props} variant="character-pass" />;
|
||||
@@ -0,0 +1,6 @@
|
||||
import { MobileWorkspaceScreenRenderer } from "../mobile-workspace-screens";
|
||||
import type { MobileWorkspaceScreenProps } from "./types";
|
||||
|
||||
export const MobileExpandedWorkScreen = (props: MobileWorkspaceScreenProps) => (
|
||||
<MobileWorkspaceScreenRenderer {...props} variant="home-expanded-in-place" />
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { MobileWorkspaceScreenRenderer } from "../mobile-workspace-screens";
|
||||
import type { MobileWorkspaceScreenProps } from "./types";
|
||||
|
||||
export const MobileHomeScreen = (props: MobileWorkspaceScreenProps) => (
|
||||
<MobileWorkspaceScreenRenderer {...props} variant="work-units-home" />
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { MobileWorkspaceScreenRenderer } from "../mobile-workspace-screens";
|
||||
import type { MobileWorkspaceScreenProps } from "./types";
|
||||
|
||||
export const MobileWorkListScreen = (props: MobileWorkspaceScreenProps) => (
|
||||
<MobileWorkspaceScreenRenderer {...props} variant="vertical-work-stack" />
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { MobileWorkspaceScreenRenderer } from "../mobile-workspace-screens";
|
||||
import type { MobileWorkspaceScreenProps } from "./types";
|
||||
|
||||
export const MobileWorkUnitDetailScreen = (
|
||||
props: MobileWorkspaceScreenProps
|
||||
) => <MobileWorkspaceScreenRenderer {...props} variant="expanded-work-unit" />;
|
||||
6
packages/ui/src/components/mobile-workspace/types.ts
Normal file
6
packages/ui/src/components/mobile-workspace/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { MobileWorkspaceRendererProps } from "../mobile-workspace-screens";
|
||||
|
||||
export type MobileWorkspaceScreenProps = Omit<
|
||||
MobileWorkspaceRendererProps,
|
||||
"variant"
|
||||
>;
|
||||
183
packages/ui/src/components/select.tsx
Normal file
183
packages/ui/src/components/select.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
"use client";
|
||||
|
||||
import { Select as SelectPrimitive } from "@base-ui/react/select";
|
||||
import { cn } from "@code/ui/lib/utils";
|
||||
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectGroup = ({ className, ...props }: SelectPrimitive.Group.Props) => (
|
||||
<SelectPrimitive.Group
|
||||
data-slot="select-group"
|
||||
className={cn("scroll-my-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const SelectValue = ({ className, ...props }: SelectPrimitive.Value.Props) => (
|
||||
<SelectPrimitive.Value
|
||||
data-slot="select-value"
|
||||
className={cn("flex flex-1 text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const SelectTrigger = ({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: SelectPrimitive.Trigger.Props & {
|
||||
size?: "sm" | "default";
|
||||
}) => (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"flex w-fit items-center justify-between gap-1.5 rounded-none border border-input bg-transparent py-2 pr-2 pl-2.5 text-xs whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-none *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon
|
||||
render={
|
||||
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
|
||||
}
|
||||
/>
|
||||
</SelectPrimitive.Trigger>
|
||||
);
|
||||
|
||||
const SelectScrollUpButton = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) => (
|
||||
<SelectPrimitive.ScrollUpArrow
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon />
|
||||
</SelectPrimitive.ScrollUpArrow>
|
||||
);
|
||||
|
||||
const SelectScrollDownButton = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) => (
|
||||
<SelectPrimitive.ScrollDownArrow
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon />
|
||||
</SelectPrimitive.ScrollDownArrow>
|
||||
);
|
||||
|
||||
const SelectContent = ({
|
||||
className,
|
||||
children,
|
||||
side = "bottom",
|
||||
sideOffset = 4,
|
||||
align = "center",
|
||||
alignOffset = 0,
|
||||
alignItemWithTrigger = true,
|
||||
...props
|
||||
}: SelectPrimitive.Popup.Props &
|
||||
Pick<
|
||||
SelectPrimitive.Positioner.Props,
|
||||
"align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
|
||||
>) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Positioner
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
alignItemWithTrigger={alignItemWithTrigger}
|
||||
className="isolate z-50"
|
||||
>
|
||||
<SelectPrimitive.Popup
|
||||
data-slot="select-content"
|
||||
data-align-trigger={alignItemWithTrigger}
|
||||
className={cn(
|
||||
"relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-none bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none 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-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}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.List>{children}</SelectPrimitive.List>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Popup>
|
||||
</SelectPrimitive.Positioner>
|
||||
</SelectPrimitive.Portal>
|
||||
);
|
||||
|
||||
const SelectLabel = ({
|
||||
className,
|
||||
...props
|
||||
}: SelectPrimitive.GroupLabel.Props) => (
|
||||
<SelectPrimitive.GroupLabel
|
||||
data-slot="select-label"
|
||||
className={cn("px-2 py-2 text-xs text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const SelectItem = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SelectPrimitive.Item.Props) => (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default items-center gap-2 rounded-none py-2 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.ItemText className="flex flex-1 shrink-0 gap-2 whitespace-nowrap">
|
||||
{children}
|
||||
</SelectPrimitive.ItemText>
|
||||
<SelectPrimitive.ItemIndicator
|
||||
render={
|
||||
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
|
||||
}
|
||||
>
|
||||
<CheckIcon className="pointer-events-none" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</SelectPrimitive.Item>
|
||||
);
|
||||
|
||||
const SelectSeparator = ({
|
||||
className,
|
||||
...props
|
||||
}: SelectPrimitive.Separator.Props) => (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("pointer-events-none -mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
};
|
||||
@@ -76,7 +76,7 @@
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-sans: "Inter Variable", sans-serif;
|
||||
--font-sans: "Inter", sans-serif;
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
|
||||
Reference in New Issue
Block a user