mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-06 02:54:52 +10:00
chore: update dependencies
This commit is contained in:
Vendored
+4
-1
@@ -27,5 +27,8 @@
|
||||
["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
|
||||
],
|
||||
"tailwindCSS.experimental.configFile": "src/styles/globals.css",
|
||||
"typescript.experimental.useTsgo": true
|
||||
"typescript.experimental.useTsgo": true,
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"#react-pdf-renderer": "@react-pdf/renderer"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^4.0.7",
|
||||
"@ai-sdk/anthropic": "^4.0.8",
|
||||
"@ai-sdk/google": "^4.0.8",
|
||||
"@ai-sdk/openai": "^4.0.7",
|
||||
"@ai-sdk/openai-compatible": "^3.0.5",
|
||||
@@ -46,7 +46,7 @@
|
||||
"@sindresorhus/slugify": "^3.0.0",
|
||||
"@t3-oss/env-core": "^0.13.11",
|
||||
"@uiw/color-convert": "^2.10.3",
|
||||
"ai": "^7.0.14",
|
||||
"ai": "^7.0.15",
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-auth": "1.6.23",
|
||||
"cjk-regex": "^3.4.0",
|
||||
@@ -56,7 +56,7 @@
|
||||
"es-toolkit": "^1.49.0",
|
||||
"fast-json-patch": "^3.1.1",
|
||||
"hono": "^4.12.27",
|
||||
"jsonrepair": "^3.14.1",
|
||||
"jsonrepair": "^3.15.0",
|
||||
"node-html-parser": "^8.0.4",
|
||||
"nodemailer": "^9.0.3",
|
||||
"ollama-ai-provider-v2": "^3.6.0",
|
||||
@@ -77,7 +77,7 @@
|
||||
"@types/node": "^26.1.0",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/react": "^19.2.17",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260703.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260704.1",
|
||||
"tsdown": "^0.22.3",
|
||||
"tsx": "^4.23.0",
|
||||
"typescript": "^6.0.3",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"lingui:extract": "lingui extract --clean --overwrite"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/react": "^4.0.15",
|
||||
"@ai-sdk/react": "^4.0.16",
|
||||
"@base-ui/react": "^1.6.0",
|
||||
"@better-auth/api-key": "^1.6.23",
|
||||
"@better-auth/infra": "^0.3.4",
|
||||
@@ -57,13 +57,13 @@
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@uiw/color-convert": "^2.10.3",
|
||||
"@uiw/react-color-colorful": "^2.10.3",
|
||||
"ai": "^7.0.14",
|
||||
"ai": "^7.0.15",
|
||||
"better-auth": "1.6.23",
|
||||
"cmdk": "^1.1.1",
|
||||
"drizzle-orm": "1.0.0-rc.4",
|
||||
"es-toolkit": "^1.49.0",
|
||||
"fuse.js": "^7.4.2",
|
||||
"immer": "^11.1.9",
|
||||
"immer": "^11.1.11",
|
||||
"js-cookie": "^3.0.8",
|
||||
"motion": "^12.42.2",
|
||||
"pdfjs-dist": "6.1.200",
|
||||
@@ -73,7 +73,7 @@
|
||||
"react-dom": "^19.2.7",
|
||||
"react-easy-crop": "^6.1.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resizable-panels": "^4.12.0",
|
||||
"react-resizable-panels": "^4.12.1",
|
||||
"react-window": "^2.2.7",
|
||||
"react-zoom-pan-pinch": "^4.0.3",
|
||||
"sonner": "^2.0.7",
|
||||
@@ -95,7 +95,7 @@
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260703.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260704.1",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
|
||||
@@ -1,32 +1,19 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import { i18n } from "@lingui/core";
|
||||
import { I18nProvider } from "@lingui/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { LoadingScreen } from "./loading-screen";
|
||||
|
||||
beforeAll(() => {
|
||||
i18n.loadAndActivate({ locale: "en", messages: {} });
|
||||
});
|
||||
|
||||
describe("LoadingScreen", () => {
|
||||
it("renders a spinner and the loading text", () => {
|
||||
render(
|
||||
<I18nProvider i18n={i18n}>
|
||||
<LoadingScreen />
|
||||
</I18nProvider>,
|
||||
);
|
||||
it("renders the Reactive Resume logo and spinner", () => {
|
||||
render(<LoadingScreen />);
|
||||
|
||||
expect(screen.getByText("Loading…")).toBeInTheDocument();
|
||||
expect(screen.getAllByAltText("Reactive Resume")).toHaveLength(2);
|
||||
expect(screen.getByLabelText("Loading")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("fills the viewport (fixed inset-0)", () => {
|
||||
const { container } = render(
|
||||
<I18nProvider i18n={i18n}>
|
||||
<LoadingScreen />
|
||||
</I18nProvider>,
|
||||
);
|
||||
const { container } = render(<LoadingScreen />);
|
||||
|
||||
const wrapper = container.firstChild as HTMLElement;
|
||||
expect(wrapper.className).toContain("fixed");
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { BrandIcon } from "@reactive-resume/ui/components/brand-icon";
|
||||
import { Spinner } from "@reactive-resume/ui/components/spinner";
|
||||
|
||||
export function LoadingScreen() {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex h-svh w-svw items-center justify-center gap-x-3 bg-background">
|
||||
<div className="fixed inset-0 z-50 flex h-svh w-svw flex-col items-center justify-center gap-y-6 bg-background">
|
||||
<BrandIcon variant="logo" className="size-12" />
|
||||
<Spinner className="size-6" />
|
||||
<p className="text-muted-foreground">
|
||||
<Trans>Loading…</Trans>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { Tabs, TabsList, TabsTrigger } from "@reactive-resume/ui/components/tabs
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@reactive-resume/ui/components/tooltip";
|
||||
import { downloadWithAnchor, generateFilename } from "@reactive-resume/utils/file";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
import { LoadingScreen } from "@/components/layout/loading-screen";
|
||||
import { createResumePdfBlob } from "@/features/resume/export/pdf-document";
|
||||
import { ResumePreview } from "@/features/resume/preview/preview";
|
||||
import { orpc } from "@/libs/orpc/client";
|
||||
@@ -242,13 +243,7 @@ function RouteComponent() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="grid h-svh place-items-center bg-background text-muted-foreground">
|
||||
<Trans>Loading agent workspace…</Trans>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (isLoading) return <LoadingScreen />;
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
|
||||
@@ -28,7 +28,14 @@ import { m } from "motion/react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { toast } from "sonner";
|
||||
import { Badge } from "@reactive-resume/ui/components/badge";
|
||||
import {
|
||||
Attachment,
|
||||
AttachmentContent,
|
||||
AttachmentGroup,
|
||||
AttachmentMedia,
|
||||
AttachmentTitle,
|
||||
} from "@reactive-resume/ui/components/attachment";
|
||||
import { Bubble, BubbleContent } from "@reactive-resume/ui/components/bubble";
|
||||
import { Button } from "@reactive-resume/ui/components/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -37,7 +44,16 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@reactive-resume/ui/components/dropdown-menu";
|
||||
import { ScrollArea } from "@reactive-resume/ui/components/scroll-area";
|
||||
import { Marker, MarkerContent, MarkerIcon } from "@reactive-resume/ui/components/marker";
|
||||
import { Message, MessageContent } from "@reactive-resume/ui/components/message";
|
||||
import {
|
||||
MessageScroller,
|
||||
MessageScrollerButton,
|
||||
MessageScrollerContent,
|
||||
MessageScrollerItem,
|
||||
MessageScrollerProvider,
|
||||
MessageScrollerViewport,
|
||||
} from "@reactive-resume/ui/components/message-scroller";
|
||||
import { Textarea } from "@reactive-resume/ui/components/textarea";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
import { useConfirm } from "@/hooks/use-confirm";
|
||||
@@ -65,6 +81,12 @@ type AssistantMarkdownProps = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
type FileAttachmentProps = {
|
||||
filename?: string | null;
|
||||
mediaType?: string | null;
|
||||
state?: "idle" | "uploading" | "processing" | "error" | "done";
|
||||
};
|
||||
|
||||
type MessagePartProps = {
|
||||
part: UIMessage["parts"][number];
|
||||
isUser: boolean;
|
||||
@@ -392,23 +414,47 @@ function AssistantMarkdown({ text }: AssistantMarkdownProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function FileAttachment({ filename, mediaType, state = "done" }: FileAttachmentProps) {
|
||||
return (
|
||||
<Attachment size="sm" state={state}>
|
||||
<AttachmentMedia>
|
||||
<FileIcon />
|
||||
</AttachmentMedia>
|
||||
<AttachmentContent>
|
||||
<AttachmentTitle>{filename || t`Attachment`}</AttachmentTitle>
|
||||
{mediaType ? <span className="sr-only">{mediaType}</span> : null}
|
||||
</AttachmentContent>
|
||||
</Attachment>
|
||||
);
|
||||
}
|
||||
|
||||
function MessagePart({ part, isUser, onAnswer, onRevert, isReverting, actionsById }: MessagePartProps) {
|
||||
if (part.type === "text") {
|
||||
return isUser ? (
|
||||
<div className="whitespace-pre-wrap leading-relaxed">{part.text}</div>
|
||||
) : (
|
||||
<AssistantMarkdown text={part.text} />
|
||||
return (
|
||||
<Bubble variant={isUser ? "default" : "ghost"} align={isUser ? "end" : "start"}>
|
||||
<BubbleContent>
|
||||
{isUser ? (
|
||||
<div className="whitespace-pre-wrap leading-relaxed">{part.text}</div>
|
||||
) : (
|
||||
<AssistantMarkdown text={part.text} />
|
||||
)}
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
);
|
||||
}
|
||||
|
||||
if (part.type === "reasoning") {
|
||||
return (
|
||||
<details className="rounded-md border bg-muted/40 px-3 py-2 text-sm">
|
||||
<summary className="cursor-pointer text-muted-foreground">
|
||||
<Trans>Thinking</Trans>
|
||||
</summary>
|
||||
<div className="mt-2 whitespace-pre-wrap">{part.text}</div>
|
||||
</details>
|
||||
<Bubble variant="outline" className="max-w-full">
|
||||
<BubbleContent className="w-full">
|
||||
<details className="text-sm">
|
||||
<summary className="cursor-pointer text-muted-foreground">
|
||||
<Trans>Thinking</Trans>
|
||||
</summary>
|
||||
<div className="mt-2 whitespace-pre-wrap">{part.text}</div>
|
||||
</details>
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -421,16 +467,20 @@ function MessagePart({ part, isUser, onAnswer, onRevert, isReverting, actionsByI
|
||||
const question = typeof input.question === "string" ? input.question : t`The agent needs your input.`;
|
||||
|
||||
return (
|
||||
<div className="space-y-3 rounded-md border bg-card p-3">
|
||||
<div className="font-medium">{question}</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{choices.map((choice) => (
|
||||
<Button key={choice} size="sm" variant="outline" onClick={() => onAnswer(part.toolCallId, choice)}>
|
||||
{choice}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Bubble variant="outline" className="max-w-full">
|
||||
<BubbleContent className="w-full">
|
||||
<div className="space-y-3">
|
||||
<div className="font-medium">{question}</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{choices.map((choice) => (
|
||||
<Button key={choice} size="sm" variant="outline" onClick={() => onAnswer(part.toolCallId, choice)}>
|
||||
{choice}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -442,33 +492,38 @@ function MessagePart({ part, isUser, onAnswer, onRevert, isReverting, actionsByI
|
||||
const actionId = typeof output?.actionId === "string" ? output.actionId : null;
|
||||
const action = actionId ? actionsById.get(actionId) : undefined;
|
||||
|
||||
return <PatchToolCard part={part} action={action} onRevert={onRevert} isReverting={isReverting} />;
|
||||
return (
|
||||
<Bubble variant="outline" className="max-w-full">
|
||||
<BubbleContent className="w-full">
|
||||
<PatchToolCard part={part} action={action} onRevert={onRevert} isReverting={isReverting} />
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
);
|
||||
}
|
||||
|
||||
if (part.type === "source-url") {
|
||||
const title = part.title?.trim() || null;
|
||||
|
||||
return (
|
||||
<a className="block text-primary text-sm underline" href={part.url} target="_blank" rel="noreferrer">
|
||||
{title ? (
|
||||
<>
|
||||
<span className="block truncate">{title}</span>
|
||||
<span className="block truncate text-muted-foreground">{part.url}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="block truncate">{part.url}</span>
|
||||
)}
|
||||
</a>
|
||||
<Bubble variant="ghost" className="max-w-full">
|
||||
<BubbleContent className="w-full">
|
||||
<a className="block text-primary text-sm underline" href={part.url} target="_blank" rel="noreferrer">
|
||||
{title ? (
|
||||
<>
|
||||
<span className="block truncate">{title}</span>
|
||||
<span className="block truncate text-muted-foreground">{part.url}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="block truncate">{part.url}</span>
|
||||
)}
|
||||
</a>
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
);
|
||||
}
|
||||
|
||||
if (part.type === "file") {
|
||||
return (
|
||||
<div className="flex max-w-full items-center gap-2 rounded-md border bg-background/20 px-2 py-1 text-sm">
|
||||
<FileIcon className="shrink-0" />
|
||||
<span className="truncate">{part.filename ?? part.url}</span>
|
||||
</div>
|
||||
);
|
||||
return <FileAttachment filename={part.filename ?? part.url} mediaType={part.mediaType} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -478,15 +533,8 @@ function ChatMessage({ message, onAnswer, onRevert, isReverting, actionsById }:
|
||||
const isUser = message.role === "user";
|
||||
|
||||
return (
|
||||
<div className={cn("flex", isUser ? "justify-end" : "justify-start")}>
|
||||
<div
|
||||
className={cn(
|
||||
"space-y-3 text-sm",
|
||||
isUser
|
||||
? "max-w-[86%] rounded-md bg-primary px-4 py-3 text-primary-foreground"
|
||||
: "w-full max-w-full py-1 text-foreground",
|
||||
)}
|
||||
>
|
||||
<Message align={isUser ? "end" : "start"}>
|
||||
<MessageContent className={cn(isUser ? "items-end" : "items-start")}>
|
||||
{message.parts.map((part) => (
|
||||
<MessagePart
|
||||
key={getMessagePartKey(message.id, part)}
|
||||
@@ -498,8 +546,8 @@ function ChatMessage({ message, onAnswer, onRevert, isReverting, actionsById }:
|
||||
actionsById={actionsById}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</MessageContent>
|
||||
</Message>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -822,50 +870,63 @@ function AgentChatMessages({
|
||||
onStarterSelect,
|
||||
}: AgentChatMessagesProps) {
|
||||
return (
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
<div className="mx-auto flex max-w-3xl flex-col gap-4 p-4">
|
||||
{messages.length === 0 ? (
|
||||
<div className="grid gap-6 py-12 text-center">
|
||||
<SparkleIcon className="mx-auto size-8 text-muted-foreground" />
|
||||
<h2 className="font-semibold text-2xl">
|
||||
<Trans>What do you want to do?</Trans>
|
||||
</h2>
|
||||
<StarterPromptMarquee onSelect={onStarterSelect} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{messages.map((message) => (
|
||||
<ChatMessage
|
||||
key={message.id}
|
||||
message={message}
|
||||
isReverting={isReverting}
|
||||
actionsById={actionsById}
|
||||
onAnswer={onAnswer}
|
||||
onRevert={onRevert}
|
||||
/>
|
||||
))}
|
||||
|
||||
{isStreaming ? (
|
||||
<div className="flex justify-start">
|
||||
<div className="rounded-md bg-muted px-4 py-3 text-muted-foreground text-sm">
|
||||
<Trans>Working…</Trans>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<div className="flex items-center justify-between gap-3 rounded-md border border-rose-300 bg-rose-50 p-3 text-rose-950 text-sm dark:bg-rose-950/20 dark:text-rose-200">
|
||||
<span>{error.message}</span>
|
||||
{!isReadOnly ? (
|
||||
<Button size="sm" variant="outline" type="button" onClick={onRetry}>
|
||||
<ArrowClockwiseIcon />
|
||||
<Trans>Retry</Trans>
|
||||
</Button>
|
||||
<MessageScrollerProvider autoScroll defaultScrollPosition="end">
|
||||
<MessageScroller className="min-h-0 flex-1">
|
||||
<MessageScrollerViewport>
|
||||
<MessageScrollerContent className="mx-auto flex w-full max-w-3xl flex-col gap-4 p-4">
|
||||
{messages.length === 0 ? (
|
||||
<div className="grid gap-6 py-12 text-center">
|
||||
<SparkleIcon className="mx-auto size-8 text-muted-foreground" />
|
||||
<h2 className="font-semibold text-2xl">
|
||||
<Trans>What do you want to do?</Trans>
|
||||
</h2>
|
||||
<StarterPromptMarquee onSelect={onStarterSelect} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{messages.map((message) => (
|
||||
<MessageScrollerItem key={message.id} messageId={message.id} scrollAnchor={message.role === "user"}>
|
||||
<ChatMessage
|
||||
message={message}
|
||||
isReverting={isReverting}
|
||||
actionsById={actionsById}
|
||||
onAnswer={onAnswer}
|
||||
onRevert={onRevert}
|
||||
/>
|
||||
</MessageScrollerItem>
|
||||
))}
|
||||
|
||||
{isStreaming ? (
|
||||
<MessageScrollerItem>
|
||||
<Marker className="w-fit rounded-md bg-muted px-4 py-3">
|
||||
<MarkerIcon>
|
||||
<SparkleIcon />
|
||||
</MarkerIcon>
|
||||
<MarkerContent>
|
||||
<Trans>Working…</Trans>
|
||||
</MarkerContent>
|
||||
</Marker>
|
||||
</MessageScrollerItem>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<MessageScrollerItem>
|
||||
<Marker className="items-center justify-between gap-3 rounded-md border border-rose-300 bg-rose-50 p-3 text-rose-950 dark:bg-rose-950/20 dark:text-rose-200">
|
||||
<MarkerContent>{error.message}</MarkerContent>
|
||||
{!isReadOnly ? (
|
||||
<Button size="sm" variant="outline" type="button" onClick={onRetry}>
|
||||
<ArrowClockwiseIcon />
|
||||
<Trans>Retry</Trans>
|
||||
</Button>
|
||||
) : null}
|
||||
</Marker>
|
||||
</MessageScrollerItem>
|
||||
) : null}
|
||||
</MessageScrollerContent>
|
||||
</MessageScrollerViewport>
|
||||
<MessageScrollerButton />
|
||||
</MessageScroller>
|
||||
</MessageScrollerProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -977,16 +1038,18 @@ function AgentChatComposer({
|
||||
onSend();
|
||||
}}
|
||||
>
|
||||
<div className="mx-auto max-w-3xl space-y-2">
|
||||
<div className="mx-auto flex max-w-3xl flex-col gap-2">
|
||||
{pendingAttachments.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<AttachmentGroup className="flex-wrap gap-2 overflow-visible py-0">
|
||||
{pendingAttachments.map((attachment) => (
|
||||
<Badge key={attachment.id} variant="secondary">
|
||||
<FileIcon />
|
||||
{attachment.filename}
|
||||
</Badge>
|
||||
<FileAttachment
|
||||
key={attachment.id}
|
||||
filename={attachment.filename}
|
||||
mediaType={attachment.mediaType}
|
||||
state={isUploading ? "uploading" : "done"}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</AttachmentGroup>
|
||||
) : null}
|
||||
|
||||
<div className="flex items-end gap-1 rounded-md border bg-card p-1.5">
|
||||
|
||||
@@ -33,8 +33,6 @@
|
||||
"bcrypt",
|
||||
"cjk-regex",
|
||||
"deepmerge-ts",
|
||||
"dompurify",
|
||||
"dotenv",
|
||||
"drizzle-zod",
|
||||
"es-toolkit",
|
||||
"fast-json-patch",
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@
|
||||
"lefthook": "^2.1.9",
|
||||
"npm-check-updates": "^22.2.9",
|
||||
"pg": "^8.22.0",
|
||||
"turbo": "^2.10.2",
|
||||
"turbo": "^2.10.3",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^4.1.9"
|
||||
}
|
||||
|
||||
@@ -23,12 +23,12 @@
|
||||
"@reactive-resume/schema": "workspace:*",
|
||||
"@reactive-resume/utils": "workspace:*",
|
||||
"deepmerge-ts": "^7.1.5",
|
||||
"jsonrepair": "^3.14.1",
|
||||
"jsonrepair": "^3.15.0",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@reactive-resume/config": "workspace:*",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260703.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260704.1",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"test:agent": "vitest run --reporter=agent --reporter=json --outputFile.json=reports/vitest-results.json --passWithNoTests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^4.0.7",
|
||||
"@ai-sdk/anthropic": "^4.0.8",
|
||||
"@ai-sdk/google": "^4.0.8",
|
||||
"@ai-sdk/openai": "^4.0.7",
|
||||
"@ai-sdk/openai-compatible": "^3.0.5",
|
||||
@@ -34,7 +34,7 @@
|
||||
"@reactive-resume/resume": "workspace:*",
|
||||
"@reactive-resume/schema": "workspace:*",
|
||||
"@reactive-resume/utils": "workspace:*",
|
||||
"ai": "^7.0.14",
|
||||
"ai": "^7.0.15",
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-auth": "1.6.23",
|
||||
"drizzle-orm": "1.0.0-rc.4",
|
||||
@@ -51,7 +51,7 @@
|
||||
"devDependencies": {
|
||||
"@reactive-resume/config": "workspace:*",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260703.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260704.1",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"@reactive-resume/config": "workspace:*",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/react": "^19.2.17",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260703.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260704.1",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"devDependencies": {
|
||||
"@reactive-resume/config": "workspace:*",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260703.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260704.1",
|
||||
"drizzle-kit": "1.0.0-rc.4",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@reactive-resume/config": "workspace:*",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260703.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260704.1",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ describe("renderSummary", () => {
|
||||
content: "<p>Hello</p>",
|
||||
hidden: true,
|
||||
columns: 1,
|
||||
keepTogether: false,
|
||||
startOnNewPage: false,
|
||||
};
|
||||
expect(renderSummary(summary, HEX)).toEqual([]);
|
||||
});
|
||||
@@ -37,6 +39,8 @@ describe("renderSummary", () => {
|
||||
content: "",
|
||||
hidden: false,
|
||||
columns: 1,
|
||||
keepTogether: false,
|
||||
startOnNewPage: false,
|
||||
};
|
||||
expect(renderSummary(summary, HEX)).toEqual([]);
|
||||
});
|
||||
@@ -48,6 +52,8 @@ describe("renderSummary", () => {
|
||||
content: "<p>Hello world</p>",
|
||||
hidden: false,
|
||||
columns: 1,
|
||||
keepTogether: false,
|
||||
startOnNewPage: false,
|
||||
};
|
||||
const paragraphs = renderSummary(summary, HEX);
|
||||
// One heading + the htmlToParagraphs output for one <p>.
|
||||
@@ -61,6 +67,8 @@ describe("renderSummary", () => {
|
||||
content: "<p>Hello world</p>",
|
||||
hidden: false,
|
||||
columns: 1,
|
||||
keepTogether: false,
|
||||
startOnNewPage: false,
|
||||
};
|
||||
const paragraphs = renderSummary(summary, HEX);
|
||||
expect(paragraphs.length).toBeGreaterThanOrEqual(1);
|
||||
@@ -73,6 +81,8 @@ const emptySection = <T extends SectionType>(type: T): ResumeData["sections"][T]
|
||||
icon: getDefaultSectionIconName(type),
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
keepTogether: false,
|
||||
startOnNewPage: false,
|
||||
items: [],
|
||||
}) as ResumeData["sections"][T];
|
||||
|
||||
@@ -99,6 +109,8 @@ describe("renderCustomSection", () => {
|
||||
icon: getDefaultSectionIconName("summary"),
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
keepTogether: false,
|
||||
startOnNewPage: false,
|
||||
items: [],
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"@reactive-resume/config": "workspace:*",
|
||||
"@types/nodemailer": "^8.0.1",
|
||||
"@types/react": "^19.2.17",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260703.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260704.1",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -21,7 +21,7 @@
|
||||
"devDependencies": {
|
||||
"@reactive-resume/config": "workspace:*",
|
||||
"@types/node": "^26.1.0",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260703.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260704.1",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@reactive-resume/config": "workspace:*",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260703.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260704.1",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@reactive-resume/config": "workspace:*",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260703.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260704.1",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@reactive-resume/config": "workspace:*",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260703.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260704.1",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^4.1.9"
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"@react-pdf/types": "^2.11.1",
|
||||
"@reactive-resume/config": "workspace:*",
|
||||
"@types/react": "^19.2.17",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260703.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260704.1",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@reactive-resume/config": "workspace:*",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260703.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260704.1",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@reactive-resume/config": "workspace:*",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260703.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260704.1",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,13 +23,14 @@
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"@phosphor-icons/web": "^2.1.2",
|
||||
"@reactive-resume/utils": "workspace:*",
|
||||
"@shadcn/react": "^0.2.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"js-cookie": "^3.0.8",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-resizable-panels": "^4.12.0",
|
||||
"react-resizable-panels": "^4.12.1",
|
||||
"shadcn": "^4.13.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tw-animate-css": "^1.4.0"
|
||||
@@ -41,7 +42,7 @@
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260703.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260704.1",
|
||||
"postcss": "^8.5.16",
|
||||
"tailwindcss": "^4.3.2",
|
||||
"typescript": "^6.0.3"
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import type * as React from "react";
|
||||
import { mergeProps } from "@base-ui/react/merge-props";
|
||||
import { useRender } from "@base-ui/react/use-render";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
import { Button } from "./button";
|
||||
|
||||
const attachmentVariants = cva(
|
||||
"group/attachment relative flex w-fit min-w-0 max-w-full shrink-0 flex-wrap rounded-xl border bg-card text-card-foreground transition-colors focus-within:ring-1 focus-within:ring-ring/50 has-[>a,>button]:hover:bg-muted/50 data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
default:
|
||||
"gap-2 text-sm has-data-[slot=attachment-media]:p-2 has-data-[slot=attachment-content]:px-2.5 has-data-[slot=attachment-content]:py-2",
|
||||
sm: "gap-2.5 text-xs has-data-[slot=attachment-media]:p-1.5 has-data-[slot=attachment-content]:px-2 has-data-[slot=attachment-content]:py-1.5",
|
||||
xs: "gap-1.5 rounded-lg text-xs has-data-[slot=attachment-media]:p-1 has-data-[slot=attachment-content]:px-1.5 has-data-[slot=attachment-content]:py-1",
|
||||
},
|
||||
orientation: {
|
||||
horizontal: "min-w-40 items-center",
|
||||
vertical: "w-24 flex-col has-data-[slot=attachment-content]:w-30",
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Attachment({
|
||||
className,
|
||||
state = "done",
|
||||
size = "default",
|
||||
orientation = "horizontal",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> &
|
||||
VariantProps<typeof attachmentVariants> & {
|
||||
state?: "idle" | "uploading" | "processing" | "error" | "done";
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="attachment"
|
||||
data-state={state}
|
||||
data-size={size}
|
||||
data-orientation={orientation}
|
||||
className={cn(attachmentVariants({ size, orientation }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const attachmentMediaVariants = cva(
|
||||
"relative flex aspect-square w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted text-foreground group-data-[orientation=vertical]/attachment:w-full group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[size=xs]/attachment:rounded-md group-data-[state=error]/attachment:bg-destructive/10 group-data-[state=error]/attachment:text-destructive group-data-[orientation=vertical]/attachment:*:data-[slot=spinner]:size-6! [&_svg:not([class*='size-'])]:size-4 group-data-[orientation=vertical]/attachment:[&_svg:not([class*='size-'])]:size-6 group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-3.5 [&_svg]:pointer-events-none",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
icon: "",
|
||||
image:
|
||||
"opacity-60 group-data-[state=done]/attachment:opacity-100 group-data-[state=idle]/attachment:opacity-100 *:[img]:aspect-square *:[img]:w-full *:[img]:object-cover",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "icon",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function AttachmentMedia({
|
||||
className,
|
||||
variant = "icon",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof attachmentMediaVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="attachment-media"
|
||||
data-variant={variant}
|
||||
className={cn(attachmentMediaVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AttachmentContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="attachment-content"
|
||||
className={cn(
|
||||
"min-w-0 max-w-full flex-1 leading-tight group-data-[orientation=vertical]/attachment:px-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AttachmentTitle({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="attachment-title"
|
||||
className={cn(
|
||||
"group-data-[state=processing]/attachment:shimmer group-data-[state=uploading]/attachment:shimmer block min-w-0 max-w-full truncate font-medium",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AttachmentDescription({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="attachment-description"
|
||||
className={cn(
|
||||
"mt-0.5 block min-w-0 max-w-full truncate text-muted-foreground text-xs group-data-[state=error]/attachment:text-destructive/80",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AttachmentActions({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="attachment-actions"
|
||||
className={cn(
|
||||
"relative z-20 flex shrink-0 items-center group-data-[orientation=vertical]/attachment:absolute group-data-[orientation=vertical]/attachment:top-3 group-data-[orientation=vertical]/attachment:right-3 group-data-[orientation=vertical]/attachment:gap-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AttachmentAction({ className, variant, size = "icon-xs", ...props }: React.ComponentProps<typeof Button>) {
|
||||
return (
|
||||
<Button
|
||||
data-slot="attachment-action"
|
||||
variant={variant ?? "ghost"}
|
||||
size={size}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AttachmentTrigger({ className, type, render, ...props }: useRender.ComponentProps<"button">) {
|
||||
return useRender({
|
||||
defaultTagName: "button",
|
||||
props: mergeProps<"button">(
|
||||
{
|
||||
type: type ?? "button",
|
||||
className: cn("absolute inset-0 z-10 outline-none", className),
|
||||
},
|
||||
props,
|
||||
),
|
||||
render,
|
||||
state: { slot: "attachment-trigger" },
|
||||
});
|
||||
}
|
||||
|
||||
function AttachmentGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="attachment-group"
|
||||
className={cn(
|
||||
"scroll-fade-x scrollbar-none flex min-w-0 snap-x snap-mandatory scroll-px-1 gap-3 overflow-x-auto overscroll-x-contain py-1 *:data-[slot=attachment]:flex-none *:data-[slot=attachment]:snap-start",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Attachment,
|
||||
AttachmentAction,
|
||||
AttachmentActions,
|
||||
AttachmentContent,
|
||||
AttachmentDescription,
|
||||
AttachmentGroup,
|
||||
AttachmentMedia,
|
||||
AttachmentTitle,
|
||||
AttachmentTrigger,
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import type * as React from "react";
|
||||
import { mergeProps } from "@base-ui/react/merge-props";
|
||||
import { useRender } from "@base-ui/react/use-render";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
|
||||
function BubbleGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return <div data-slot="bubble-group" className={cn("flex min-w-0 flex-col gap-2", className)} {...props} />;
|
||||
}
|
||||
|
||||
const bubbleVariants = cva(
|
||||
"group/bubble relative flex w-fit min-w-0 max-w-[80%] flex-col gap-1 data-[variant=ghost]:max-w-full data-[align=end]:self-end group-data-[align=end]/message:self-end",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"*:data-[slot=bubble-content]:bg-primary *:data-[slot=bubble-content]:text-primary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-primary/80",
|
||||
secondary:
|
||||
"*:data-[slot=bubble-content]:bg-secondary *:data-[slot=bubble-content]:text-secondary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-secondary/80",
|
||||
muted: "*:data-[slot=bubble-content]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted/80",
|
||||
tinted:
|
||||
"*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)] *:data-[slot=bubble-content]:text-foreground dark:*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.3_calc(c*0.4)_h)]",
|
||||
outline:
|
||||
"*:data-[slot=bubble-content]:border-border *:data-[slot=bubble-content]:bg-background [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground",
|
||||
ghost:
|
||||
"border-none *:data-[slot=bubble-content]:rounded-none *:data-[slot=bubble-content]:bg-transparent *:data-[slot=bubble-content]:p-0 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground",
|
||||
destructive:
|
||||
"*:data-[slot=bubble-content]:bg-destructive/10 *:data-[slot=bubble-content]:text-destructive [&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/20",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Bubble({
|
||||
variant = "default",
|
||||
align = "start",
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> &
|
||||
VariantProps<typeof bubbleVariants> & {
|
||||
align?: "start" | "end";
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="bubble"
|
||||
data-variant={variant}
|
||||
data-align={align}
|
||||
className={cn(bubbleVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BubbleContent({ className, render, ...props }: useRender.ComponentProps<"div">) {
|
||||
return useRender({
|
||||
defaultTagName: "div",
|
||||
props: mergeProps<"div">(
|
||||
{
|
||||
className: cn(
|
||||
"w-fit min-w-0 max-w-full overflow-hidden break-words rounded-xl border border-transparent px-3 py-2 text-sm leading-relaxed group-data-[align=end]/bubble:self-end [button,a]:outline-none [button,a]:transition-colors [button,a]:focus-visible:border-ring [button,a]:focus-visible:ring-3 [button,a]:focus-visible:ring-ring/50 [button]:text-left",
|
||||
className,
|
||||
),
|
||||
},
|
||||
props,
|
||||
),
|
||||
render,
|
||||
state: { slot: "bubble-content" },
|
||||
});
|
||||
}
|
||||
|
||||
const bubbleReactionsVariants = cva(
|
||||
"absolute z-10 flex w-fit shrink-0 items-center justify-center gap-1 rounded-full bg-muted px-1.5 py-0.5 text-sm ring-3 ring-card has-[button]:p-0",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "top-0 -translate-y-3/4",
|
||||
bottom: "bottom-0 translate-y-3/4",
|
||||
},
|
||||
align: {
|
||||
start: "left-3",
|
||||
end: "right-3",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "bottom",
|
||||
align: "end",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function BubbleReactions({
|
||||
side = "bottom",
|
||||
align = "end",
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
align?: "start" | "end";
|
||||
side?: "top" | "bottom";
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="bubble-reactions"
|
||||
data-align={align}
|
||||
data-side={side}
|
||||
className={cn(bubbleReactionsVariants({ side, align }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Bubble, BubbleContent, BubbleGroup, BubbleReactions };
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import { mergeProps } from "@base-ui/react/merge-props";
|
||||
import { useRender } from "@base-ui/react/use-render";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
|
||||
const markerVariants = cva(
|
||||
"group/marker relative flex min-h-4 w-full items-center gap-2 text-left text-muted-foreground text-sm [&_svg:not([class*='size-'])]:size-4 [a]:underline [a]:underline-offset-3 [a]:hover:text-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "",
|
||||
separator:
|
||||
"before:me-1 before:h-px before:min-w-0 before:flex-1 before:bg-border after:ms-1 after:h-px after:min-w-0 after:flex-1 after:bg-border",
|
||||
border: "border-border border-b pb-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Marker({
|
||||
className,
|
||||
variant = "default",
|
||||
render,
|
||||
...props
|
||||
}: useRender.ComponentProps<"div"> & VariantProps<typeof markerVariants>) {
|
||||
return useRender({
|
||||
defaultTagName: "div",
|
||||
props: mergeProps<"div">(
|
||||
{
|
||||
className: cn(markerVariants({ variant }), className),
|
||||
},
|
||||
props,
|
||||
),
|
||||
render,
|
||||
state: { slot: "marker", variant },
|
||||
});
|
||||
}
|
||||
|
||||
function MarkerIcon({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="marker-icon"
|
||||
aria-hidden="true"
|
||||
className={cn("size-4 shrink-0 [&_svg:not([class*='size-'])]:size-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MarkerContent({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="marker-content"
|
||||
className={cn(
|
||||
"min-w-0 break-words group-data-[variant=separator]/marker:flex-none group-data-[variant=separator]/marker:text-center *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Marker, MarkerContent, MarkerIcon, markerVariants };
|
||||
@@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import type * as React from "react";
|
||||
import { ArrowDownIcon } from "@phosphor-icons/react";
|
||||
import {
|
||||
MessageScroller as MessageScrollerPrimitive,
|
||||
useMessageScroller,
|
||||
useMessageScrollerScrollable,
|
||||
useMessageScrollerVisibility,
|
||||
} from "@shadcn/react/message-scroller";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
import { Button } from "./button";
|
||||
|
||||
function MessageScrollerProvider(props: React.ComponentProps<typeof MessageScrollerPrimitive.Provider>) {
|
||||
return <MessageScrollerPrimitive.Provider {...props} />;
|
||||
}
|
||||
|
||||
function MessageScroller({ className, ...props }: React.ComponentProps<typeof MessageScrollerPrimitive.Root>) {
|
||||
return (
|
||||
<MessageScrollerPrimitive.Root
|
||||
data-slot="message-scroller"
|
||||
className={cn("group/message-scroller relative flex size-full min-h-0 flex-col overflow-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MessageScrollerViewport({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MessageScrollerPrimitive.Viewport>) {
|
||||
return (
|
||||
<MessageScrollerPrimitive.Viewport
|
||||
data-slot="message-scroller-viewport"
|
||||
className={cn(
|
||||
"scroll-fade-b scrollbar-thin scrollbar-gutter-stable data-autoscrolling:scrollbar-none size-full min-h-0 min-w-0 overflow-y-auto overscroll-contain contain-content",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MessageScrollerContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MessageScrollerPrimitive.Content>) {
|
||||
return (
|
||||
<MessageScrollerPrimitive.Content
|
||||
data-slot="message-scroller-content"
|
||||
className={cn("flex h-max min-h-full flex-col gap-8", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MessageScrollerItem({
|
||||
className,
|
||||
scrollAnchor = false,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MessageScrollerPrimitive.Item>) {
|
||||
return (
|
||||
<MessageScrollerPrimitive.Item
|
||||
data-slot="message-scroller-item"
|
||||
scrollAnchor={scrollAnchor}
|
||||
className={cn("min-w-0 shrink-0 [contain-intrinsic-size:auto_10rem] [content-visibility:auto]", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MessageScrollerButton({
|
||||
direction = "end",
|
||||
className,
|
||||
children,
|
||||
render,
|
||||
variant = "secondary",
|
||||
size = "icon-sm",
|
||||
...props
|
||||
}: React.ComponentProps<typeof MessageScrollerPrimitive.Button> &
|
||||
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
|
||||
return (
|
||||
<MessageScrollerPrimitive.Button
|
||||
data-slot="message-scroller-button"
|
||||
data-direction={direction}
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
direction={direction}
|
||||
className={cn(
|
||||
"absolute inset-s-1/2 -translate-x-1/2 border-border bg-background text-foreground transition-[translate,scale,opacity] duration-200 hover:bg-muted hover:text-foreground data-[direction=end]:data-[active=false]:translate-y-full data-[direction=start]:data-[active=false]:-translate-y-full data-[active=false]:pointer-events-none data-[direction=start]:top-4 data-[direction=end]:bottom-4 data-[active=true]:translate-y-0 data-[active=false]:scale-95 data-[active=true]:scale-100 data-[active=false]:opacity-0 data-[active=true]:opacity-100 data-[active=false]:duration-400 data-[active=false]:ease-[cubic-bezier(0.7,0,0.84,0)] data-[active=true]:ease-[cubic-bezier(0.23,1,0.32,1)] rtl:translate-x-1/2 data-[direction=start]:[&_svg]:rotate-180",
|
||||
className,
|
||||
)}
|
||||
render={render ?? <Button variant={variant} size={size} />}
|
||||
{...props}
|
||||
>
|
||||
{children ?? (
|
||||
<>
|
||||
<ArrowDownIcon />
|
||||
<span className="sr-only">{direction === "end" ? "Scroll to end" : "Scroll to start"}</span>
|
||||
</>
|
||||
)}
|
||||
</MessageScrollerPrimitive.Button>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
MessageScroller,
|
||||
MessageScrollerButton,
|
||||
MessageScrollerContent,
|
||||
MessageScrollerItem,
|
||||
MessageScrollerProvider,
|
||||
MessageScrollerViewport,
|
||||
useMessageScroller,
|
||||
useMessageScrollerScrollable,
|
||||
useMessageScrollerVisibility,
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import type * as React from "react";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
|
||||
function MessageGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return <div data-slot="message-group" className={cn("flex min-w-0 flex-col gap-2", className)} {...props} />;
|
||||
}
|
||||
|
||||
function Message({ className, align = "start", ...props }: React.ComponentProps<"div"> & { align?: "start" | "end" }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="message"
|
||||
data-align={align}
|
||||
className={cn(
|
||||
"group/message relative flex w-full min-w-0 gap-2 text-sm data-[align=end]:flex-row-reverse",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MessageAvatar({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="message-avatar"
|
||||
className={cn(
|
||||
"flex w-fit min-w-8 shrink-0 items-center justify-center self-end overflow-hidden rounded-full bg-muted group-has-data-[slot=message-footer]/message:-translate-y-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MessageContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="message-content"
|
||||
className={cn(
|
||||
"flex w-full min-w-0 flex-col gap-2.5 break-words group-data-[align=end]/message:*:data-slot:self-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MessageHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="message-header"
|
||||
className={cn(
|
||||
"flex min-w-0 max-w-full items-center px-3 font-medium text-muted-foreground text-xs group-has-data-[variant=ghost]/message:px-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MessageFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="message-footer"
|
||||
className={cn(
|
||||
"flex min-w-0 max-w-full items-center px-3 font-medium text-muted-foreground text-xs group-has-data-[variant=ghost]/message:px-0 group-data-[align=end]/message:justify-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Message, MessageAvatar, MessageContent, MessageFooter, MessageGroup, MessageHeader };
|
||||
@@ -37,7 +37,7 @@
|
||||
"devDependencies": {
|
||||
"@reactive-resume/config": "workspace:*",
|
||||
"@types/node": "^26.1.0",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260703.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260704.1",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+426
-460
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@
|
||||
"@reactive-resume/config": "workspace:*",
|
||||
"@reactive-resume/env": "workspace:*",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260703.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260704.1",
|
||||
"drizzle-orm": "1.0.0-rc.4",
|
||||
"pg": "^8.22.0",
|
||||
"tsx": "^4.23.0"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"ui": "tui",
|
||||
"ui": "stream",
|
||||
"boundaries": {
|
||||
"dependencies": {
|
||||
"deny": ["web", "server"]
|
||||
|
||||
Reference in New Issue
Block a user