chore: update dependencies

This commit is contained in:
Amruth Pillai
2026-07-04 19:06:31 +02:00
parent 44fa2badb4
commit d87c6758ab
33 changed files with 1199 additions and 625 deletions
+4 -1
View File
@@ -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"
}
}
+4 -4
View File
@@ -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",
+5 -5
View File
@@ -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>
);
}
+2 -7
View File
@@ -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">
-2
View File
@@ -33,8 +33,6 @@
"bcrypt",
"cjk-regex",
"deepmerge-ts",
"dompurify",
"dotenv",
"drizzle-zod",
"es-toolkit",
"fast-json-patch",
+1 -1
View File
@@ -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"
}
+2 -2
View File
@@ -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"
}
}
+3 -3
View File
@@ -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"
}
}
+1 -1
View File
@@ -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"
}
}
+1 -1
View File
@@ -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"
}
+1 -1
View File
@@ -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: [],
};
+1 -1
View File
@@ -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"
}
}
+1 -1
View File
@@ -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"
}
}
+1 -1
View File
@@ -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"
}
}
+1 -1
View File
@@ -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"
}
}
+1 -1
View File
@@ -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"
}
+1 -1
View File
@@ -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"
}
}
+1 -1
View File
@@ -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"
}
}
+1 -1
View File
@@ -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"
}
}
+3 -2
View File
@@ -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"
+182
View File
@@ -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,
};
+115
View File
@@ -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 };
+64
View File
@@ -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,
};
+74
View File
@@ -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 };
+1 -1
View File
@@ -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"
}
}
+426 -460
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"ui": "stream",
"boundaries": {
"dependencies": {
"deny": ["web", "server"]