mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-06-22 04:11:55 +10:00
chore: lint using react-doctor, update translations, dynamic imports
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^3.0.78",
|
||||
"@ai-sdk/google": "^3.0.75",
|
||||
"@ai-sdk/google": "^3.0.77",
|
||||
"@ai-sdk/openai": "^3.0.64",
|
||||
"@ai-sdk/openai-compatible": "^2.0.47",
|
||||
"@aws-sdk/client-s3": "^3.1051.0",
|
||||
@@ -35,7 +35,7 @@
|
||||
"@reactive-resume/resume": "workspace:*",
|
||||
"@reactive-resume/schema": "workspace:*",
|
||||
"@reactive-resume/utils": "workspace:*",
|
||||
"ai": "^6.0.185",
|
||||
"ai": "^6.0.187",
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-auth": "1.6.11",
|
||||
"drizzle-orm": "1.0.0-rc.3",
|
||||
|
||||
@@ -355,9 +355,8 @@ describe("agentService.messages.send", () => {
|
||||
});
|
||||
aiProvidersServiceMock.markUsed.mockResolvedValue(undefined);
|
||||
|
||||
const { convertToModelMessages, ToolLoopAgent } = await import("ai");
|
||||
const { agentStreamLifecycle } = await import("./streams");
|
||||
const { streamToEventIterator } = await import("@orpc/server");
|
||||
const [{ convertToModelMessages, ToolLoopAgent }, { agentStreamLifecycle }, { streamToEventIterator }] =
|
||||
await Promise.all([import("ai"), import("./streams"), import("@orpc/server")]);
|
||||
vi.mocked(convertToModelMessages).mockResolvedValue([
|
||||
{ role: "user", content: [{ type: "text", text: "Use this file" }] },
|
||||
]);
|
||||
@@ -435,10 +434,12 @@ describe("agentService.messages.send", () => {
|
||||
});
|
||||
aiProvidersServiceMock.markUsed.mockResolvedValue(undefined);
|
||||
|
||||
const { convertToModelMessages, ToolLoopAgent } = await import("ai");
|
||||
const { agentStreamLifecycle } = await import("./streams");
|
||||
const { buildAgentTools } = await import("./tools");
|
||||
const { streamToEventIterator } = await import("@orpc/server");
|
||||
const [
|
||||
{ convertToModelMessages, ToolLoopAgent },
|
||||
{ agentStreamLifecycle },
|
||||
{ buildAgentTools },
|
||||
{ streamToEventIterator },
|
||||
] = await Promise.all([import("ai"), import("./streams"), import("./tools"), import("@orpc/server")]);
|
||||
vi.mocked(convertToModelMessages).mockResolvedValue([
|
||||
{ role: "user", content: [{ type: "text", text: "Add a custom field" }] },
|
||||
]);
|
||||
@@ -577,9 +578,8 @@ describe("agentService.messages.send", () => {
|
||||
aiProvidersServiceMock.markUsed.mockResolvedValue(undefined);
|
||||
storageServiceMock.read.mockResolvedValue({ data: new TextEncoder().encode("hello"), contentType: "text/plain" });
|
||||
|
||||
const { convertToModelMessages, ToolLoopAgent } = await import("ai");
|
||||
const { agentStreamLifecycle } = await import("./streams");
|
||||
const { streamToEventIterator } = await import("@orpc/server");
|
||||
const [{ convertToModelMessages, ToolLoopAgent }, { agentStreamLifecycle }, { streamToEventIterator }] =
|
||||
await Promise.all([import("ai"), import("./streams"), import("@orpc/server")]);
|
||||
const streamMock = vi.fn(async () => ({
|
||||
toUIMessageStream: vi.fn(() => new ReadableStream()),
|
||||
}));
|
||||
@@ -743,9 +743,8 @@ describe("agentService.messages.send", () => {
|
||||
});
|
||||
aiProvidersServiceMock.markUsed.mockResolvedValue(undefined);
|
||||
|
||||
const { convertToModelMessages, ToolLoopAgent } = await import("ai");
|
||||
const { agentStreamLifecycle } = await import("./streams");
|
||||
const { streamToEventIterator } = await import("@orpc/server");
|
||||
const [{ convertToModelMessages, ToolLoopAgent }, { agentStreamLifecycle }, { streamToEventIterator }] =
|
||||
await Promise.all([import("ai"), import("./streams"), import("@orpc/server")]);
|
||||
vi.mocked(convertToModelMessages).mockResolvedValue([
|
||||
{ role: "user", content: [{ type: "text", text: "Change the name" }] },
|
||||
{
|
||||
@@ -903,9 +902,8 @@ describe("agentService.messages.send", () => {
|
||||
});
|
||||
aiProvidersServiceMock.markUsed.mockResolvedValue(undefined);
|
||||
|
||||
const { convertToModelMessages, ToolLoopAgent } = await import("ai");
|
||||
const { agentStreamLifecycle } = await import("./streams");
|
||||
const { streamToEventIterator } = await import("@orpc/server");
|
||||
const [{ convertToModelMessages, ToolLoopAgent }, { agentStreamLifecycle }, { streamToEventIterator }] =
|
||||
await Promise.all([import("ai"), import("./streams"), import("@orpc/server")]);
|
||||
vi.mocked(convertToModelMessages).mockResolvedValue([{ role: "user", content: [{ type: "text", text: "Retry" }] }]);
|
||||
class MockToolLoopAgent {
|
||||
stream = vi.fn(async () => ({ toUIMessageStream: vi.fn(() => new ReadableStream()) }));
|
||||
|
||||
@@ -187,7 +187,11 @@ type AgentToolPart = UIMessage["parts"][number] & {
|
||||
toolCallId?: string;
|
||||
};
|
||||
|
||||
function isAnsweredAskUserQuestionPart(part: UIMessage["parts"][number]): part is AgentToolPart {
|
||||
type AnsweredAskUserQuestionPart = AgentToolPart & {
|
||||
toolCallId: string;
|
||||
};
|
||||
|
||||
function isAnsweredAskUserQuestionPart(part: UIMessage["parts"][number]): part is AnsweredAskUserQuestionPart {
|
||||
const toolPart = part as AgentToolPart;
|
||||
return (
|
||||
toolPart.type === "tool-ask_user_question" &&
|
||||
@@ -197,9 +201,11 @@ function isAnsweredAskUserQuestionPart(part: UIMessage["parts"][number]): part i
|
||||
}
|
||||
|
||||
function mergeAskUserQuestionOutputs(existingMessage: UIMessage, incomingMessage: UIMessage): UIMessage {
|
||||
const answeredParts = new Map(
|
||||
incomingMessage.parts.filter(isAnsweredAskUserQuestionPart).map((part) => [part.toolCallId, part] as const),
|
||||
);
|
||||
const answeredParts = new Map<string, AgentToolPart>();
|
||||
|
||||
for (const part of incomingMessage.parts) {
|
||||
if (isAnsweredAskUserQuestionPart(part)) answeredParts.set(part.toolCallId, part);
|
||||
}
|
||||
|
||||
let didMerge = false;
|
||||
const parts = existingMessage.parts.map((part) => {
|
||||
@@ -580,6 +586,7 @@ async function repairLegacyAskUserQuestionAnswers(
|
||||
input: { threadId: string; userId: string },
|
||||
) {
|
||||
const nextRows = [...rows];
|
||||
const updates: Promise<unknown>[] = [];
|
||||
|
||||
for (let index = 0; index < nextRows.length - 1; index++) {
|
||||
const assistantRow = nextRows[index];
|
||||
@@ -598,21 +605,25 @@ async function repairLegacyAskUserQuestionAnswers(
|
||||
uiMessage: mergedMessage as unknown as AgentMessageRecord["uiMessage"],
|
||||
};
|
||||
|
||||
await db
|
||||
.update(schema.agentMessage)
|
||||
.set({
|
||||
status: "completed",
|
||||
uiMessage: mergedMessage as unknown as Record<string, unknown>,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(schema.agentMessage.id, assistantRow.id),
|
||||
eq(schema.agentMessage.threadId, input.threadId),
|
||||
eq(schema.agentMessage.userId, input.userId),
|
||||
updates.push(
|
||||
db
|
||||
.update(schema.agentMessage)
|
||||
.set({
|
||||
status: "completed",
|
||||
uiMessage: mergedMessage as unknown as Record<string, unknown>,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(schema.agentMessage.id, assistantRow.id),
|
||||
eq(schema.agentMessage.threadId, input.threadId),
|
||||
eq(schema.agentMessage.userId, input.userId),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(updates);
|
||||
|
||||
return nextRows;
|
||||
}
|
||||
|
||||
@@ -635,11 +646,13 @@ async function cleanupActiveRun(input: {
|
||||
}
|
||||
|
||||
function messageText(message: UIMessage) {
|
||||
return message.parts
|
||||
.filter((part) => part.type === "text")
|
||||
.map((part) => part.text)
|
||||
.join(" ")
|
||||
.trim();
|
||||
const textParts: string[] = [];
|
||||
|
||||
for (const part of message.parts) {
|
||||
if (part.type === "text") textParts.push(part.text);
|
||||
}
|
||||
|
||||
return textParts.join(" ").trim();
|
||||
}
|
||||
|
||||
function buildThreadTitle(message: UIMessage, fallback: string) {
|
||||
@@ -933,12 +946,14 @@ export const agentService = {
|
||||
|
||||
await getThread({ id: input.id, userId: input.userId });
|
||||
|
||||
await getStorageService().delete(`uploads/${input.userId}/agent/${input.id}`);
|
||||
await db.delete(schema.agentAttachment).where(eq(schema.agentAttachment.threadId, input.id));
|
||||
await db
|
||||
.update(schema.agentThread)
|
||||
.set({ status: "deleted", deletedAt: new Date() })
|
||||
.where(and(eq(schema.agentThread.id, input.id), eq(schema.agentThread.userId, input.userId)));
|
||||
await Promise.all([
|
||||
getStorageService().delete(`uploads/${input.userId}/agent/${input.id}`),
|
||||
db.delete(schema.agentAttachment).where(eq(schema.agentAttachment.threadId, input.id)),
|
||||
db
|
||||
.update(schema.agentThread)
|
||||
.set({ status: "deleted", deletedAt: new Date() })
|
||||
.where(and(eq(schema.agentThread.id, input.id), eq(schema.agentThread.userId, input.userId))),
|
||||
]);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -960,15 +975,17 @@ export const agentService = {
|
||||
throw new ORPCError("BAD_REQUEST", { message: "Agent messages must be user messages or tool results." });
|
||||
}
|
||||
|
||||
const runnableProvider = await aiProvidersService.getRunnableById({
|
||||
id: thread.aiProviderId,
|
||||
userId: input.userId,
|
||||
});
|
||||
const attachments = await getUnlinkedMessageAttachments({
|
||||
ids: input.attachmentIds ?? [],
|
||||
threadId: input.threadId,
|
||||
userId: input.userId,
|
||||
});
|
||||
const [runnableProvider, attachments] = await Promise.all([
|
||||
aiProvidersService.getRunnableById({
|
||||
id: thread.aiProviderId,
|
||||
userId: input.userId,
|
||||
}),
|
||||
getUnlinkedMessageAttachments({
|
||||
ids: input.attachmentIds ?? [],
|
||||
threadId: input.threadId,
|
||||
userId: input.userId,
|
||||
}),
|
||||
]);
|
||||
const runId = generateId();
|
||||
const streamId = generateId();
|
||||
const controller = new AbortController();
|
||||
@@ -1156,19 +1173,20 @@ export const agentService = {
|
||||
assertAgentEnvironment();
|
||||
await getThread({ id: input.threadId, userId: input.userId });
|
||||
|
||||
const [aggregate] = await db
|
||||
.select({ totalBytes: sql<number>`coalesce(sum(${schema.agentAttachment.size}), 0)` })
|
||||
.from(schema.agentAttachment)
|
||||
.where(
|
||||
and(eq(schema.agentAttachment.threadId, input.threadId), eq(schema.agentAttachment.userId, input.userId)),
|
||||
);
|
||||
|
||||
const [attachmentCount] = await db
|
||||
.select({ total: count() })
|
||||
.from(schema.agentAttachment)
|
||||
.where(
|
||||
and(eq(schema.agentAttachment.threadId, input.threadId), eq(schema.agentAttachment.userId, input.userId)),
|
||||
);
|
||||
const [[aggregate], [attachmentCount]] = await Promise.all([
|
||||
db
|
||||
.select({ totalBytes: sql<number>`coalesce(sum(${schema.agentAttachment.size}), 0)` })
|
||||
.from(schema.agentAttachment)
|
||||
.where(
|
||||
and(eq(schema.agentAttachment.threadId, input.threadId), eq(schema.agentAttachment.userId, input.userId)),
|
||||
),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(schema.agentAttachment)
|
||||
.where(
|
||||
and(eq(schema.agentAttachment.threadId, input.threadId), eq(schema.agentAttachment.userId, input.userId)),
|
||||
),
|
||||
]);
|
||||
|
||||
if ((attachmentCount?.total ?? 0) >= MAX_ATTACHMENTS_PER_MESSAGE) throw new ORPCError("BAD_REQUEST");
|
||||
if (input.data.byteLength > MAX_ATTACHMENT_BYTES) throw new ORPCError("BAD_REQUEST");
|
||||
|
||||
@@ -17,17 +17,17 @@ vi.mock("drizzle-orm", () => ({
|
||||
isNull: (value: unknown) => ({ type: "isNull", value }),
|
||||
}));
|
||||
|
||||
async function readStream(stream: ReadableStream<string>) {
|
||||
const reader = stream.getReader();
|
||||
const chunks: string[] = [];
|
||||
async function readStreamChunks(reader: ReadableStreamDefaultReader<string>, chunks: string[]): Promise<string[]> {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
chunks.push(value);
|
||||
}
|
||||
if (done) return chunks;
|
||||
|
||||
return chunks;
|
||||
chunks.push(value);
|
||||
return readStreamChunks(reader, chunks);
|
||||
}
|
||||
|
||||
function readStream(stream: ReadableStream<string>) {
|
||||
return readStreamChunks(stream.getReader(), []);
|
||||
}
|
||||
|
||||
function createRunStateDb(returningRows: unknown[] = []) {
|
||||
|
||||
@@ -75,17 +75,28 @@ export async function* subscribeResumeUpdated({ resumeId, userId, signal }: Subs
|
||||
try {
|
||||
await client.query(`LISTEN ${RESUME_UPDATED_CHANNEL}`);
|
||||
|
||||
while (!done) {
|
||||
const waitForNextEvent = async (): Promise<ResumeUpdatedEvent | null> => {
|
||||
if (done) return null;
|
||||
|
||||
const event = queue.shift();
|
||||
if (event) {
|
||||
yield event;
|
||||
continue;
|
||||
}
|
||||
if (event) return event;
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
wake = resolve;
|
||||
});
|
||||
|
||||
return waitForNextEvent();
|
||||
};
|
||||
|
||||
async function* streamEvents(): AsyncGenerator<ResumeUpdatedEvent> {
|
||||
const event = await waitForNextEvent();
|
||||
if (!event) return;
|
||||
|
||||
yield event;
|
||||
yield* streamEvents();
|
||||
}
|
||||
|
||||
yield* streamEvents();
|
||||
} finally {
|
||||
signal?.removeEventListener("abort", onAbort);
|
||||
client.off("notification", onNotification);
|
||||
|
||||
@@ -101,13 +101,11 @@ const fetchGitHubStarsOnce = async (): Promise<number | null> => {
|
||||
}
|
||||
};
|
||||
|
||||
const getGitHubStars = async (): Promise<number | null> => {
|
||||
for (let attempt = 0; attempt < GITHUB_REQUEST_MAX_ATTEMPTS; attempt++) {
|
||||
const stars = await fetchGitHubStarsOnce();
|
||||
if (stars !== null) return stars;
|
||||
}
|
||||
const getGitHubStars = async (attempt = 1): Promise<number | null> => {
|
||||
if (attempt > GITHUB_REQUEST_MAX_ATTEMPTS) return null;
|
||||
|
||||
return null;
|
||||
const stars = await fetchGitHubStarsOnce();
|
||||
return stars ?? getGitHubStars(attempt + 1);
|
||||
};
|
||||
|
||||
export const statisticsService = {
|
||||
|
||||
@@ -300,9 +300,8 @@ class S3StorageService implements StorageService {
|
||||
}
|
||||
|
||||
async delete(keyOrPrefix: string): Promise<boolean> {
|
||||
const client = await this.getClient();
|
||||
// Use list to find all matching keys (handles both single file and folder/prefix)
|
||||
const keys = await this.list(keyOrPrefix);
|
||||
const [client, keys] = await Promise.all([this.getClient(), this.list(keyOrPrefix)]);
|
||||
|
||||
if (keys.length === 0) return false;
|
||||
|
||||
|
||||
@@ -126,15 +126,21 @@ async function allocateUniqueUsername(email: string, preferredUsername?: string
|
||||
|
||||
if (!(await isUsernameTaken(baseUsername))) return baseUsername;
|
||||
|
||||
for (let index = 1; index <= 999; index += 1) {
|
||||
const candidate = appendUsernameSuffix(baseUsername, `-${index}`);
|
||||
if (await isUsernameTaken(candidate)) continue;
|
||||
return candidate;
|
||||
}
|
||||
const suffixedUsername = await findAvailableUsernameSuffix(baseUsername);
|
||||
if (suffixedUsername) return suffixedUsername;
|
||||
|
||||
return appendUsernameSuffix(baseUsername, `-${generateId().slice(0, 8).toLowerCase()}`);
|
||||
}
|
||||
|
||||
async function findAvailableUsernameSuffix(baseUsername: string, index = 1): Promise<string | null> {
|
||||
if (index > 999) return null;
|
||||
|
||||
const candidate = appendUsernameSuffix(baseUsername, `-${index}`);
|
||||
if (!(await isUsernameTaken(candidate))) return candidate;
|
||||
|
||||
return findAvailableUsernameSuffix(baseUsername, index + 1);
|
||||
}
|
||||
|
||||
interface OAuthProfile {
|
||||
email?: string | null;
|
||||
name?: string | null;
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
"./server": "./src/server.tsx",
|
||||
"./templates": "./src/templates/index.ts"
|
||||
},
|
||||
"imports": {
|
||||
"#react-pdf-renderer": "@react-pdf/renderer"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"test": "vitest run --passWithNoTests",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import type { Template } from "@reactive-resume/schema/templates";
|
||||
import type { SectionTitleResolver } from "./section-title";
|
||||
import { pdf } from "@react-pdf/renderer";
|
||||
import { createElement } from "react";
|
||||
import { ResumeDocument } from "./document";
|
||||
import { pdf } from "./renderer";
|
||||
|
||||
type CreateResumePdfBlobOptions = {
|
||||
data: ResumeData;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import type { ReactNode } from "react";
|
||||
import type { SectionTitleResolver } from "./section-title";
|
||||
import { createContext, useContext } from "react";
|
||||
import { createContext, use } from "react";
|
||||
|
||||
type RenderContextValue = ResumeData & {
|
||||
resolveSectionTitle?: SectionTitleResolver | undefined;
|
||||
@@ -20,7 +20,7 @@ export const RenderProvider = ({ data, resolveSectionTitle, children }: RenderPr
|
||||
};
|
||||
|
||||
export const useRender = (): RenderContextValue => {
|
||||
const context = useContext(RenderContext);
|
||||
const context = use(RenderContext);
|
||||
|
||||
if (!context) throw new Error("useRender must be called inside a <RenderProvider>.");
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ import type { Template } from "@reactive-resume/schema/templates";
|
||||
import type { Locale } from "@reactive-resume/utils/locale";
|
||||
import type { ComponentType } from "react";
|
||||
import type { SectionTitleResolver } from "./section-title";
|
||||
import { Document } from "@react-pdf/renderer";
|
||||
import { useMemo } from "react";
|
||||
import { RenderProvider } from "./context";
|
||||
import { registerFonts, resumeContentContainsCJK } from "./hooks/use-register-fonts";
|
||||
import { Document } from "./renderer";
|
||||
import { getTemplatePage } from "./templates";
|
||||
|
||||
export type TemplatePageProps = {
|
||||
@@ -22,8 +22,12 @@ export type ResumeDocumentProps = {
|
||||
resolveSectionTitle?: SectionTitleResolver | undefined;
|
||||
};
|
||||
|
||||
const getLayoutPageKey = (page: LayoutPage, pageIndex: number) =>
|
||||
`${page.fullWidth ? "full" : "split"}:${page.main.join(",")}:${page.sidebar.join(",")}:${pageIndex}`;
|
||||
|
||||
export const ResumeDocument = ({ data, template, resolveSectionTitle }: ResumeDocumentProps) => {
|
||||
const TemplatePageComponent = getTemplatePage(template);
|
||||
const creationDate = useMemo(() => new Date(), []);
|
||||
const hasCjkContent = useMemo(() => resumeContentContainsCJK(data), [data]);
|
||||
const typography = registerFonts(
|
||||
data.metadata.typography,
|
||||
@@ -40,7 +44,7 @@ export const ResumeDocument = ({ data, template, resolveSectionTitle }: ResumeDo
|
||||
<RenderProvider data={resumeData} resolveSectionTitle={resolveSectionTitle}>
|
||||
<Document
|
||||
pageMode="useNone"
|
||||
creationDate={new Date()}
|
||||
creationDate={creationDate}
|
||||
producer="Reactive Resume"
|
||||
title={resumeData.basics.name}
|
||||
author={resumeData.basics.name}
|
||||
@@ -49,7 +53,7 @@ export const ResumeDocument = ({ data, template, resolveSectionTitle }: ResumeDo
|
||||
language={resumeData.metadata.page.locale}
|
||||
>
|
||||
{resumeData.metadata.layout.pages.map((page, index) => (
|
||||
<TemplatePageComponent key={index} page={page} pageIndex={index} />
|
||||
<TemplatePageComponent key={getLayoutPageKey(page, index)} page={page} pageIndex={index} />
|
||||
))}
|
||||
</Document>
|
||||
</RenderProvider>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ResumeData, Typography } from "@reactive-resume/schema/resume/data";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { Font } from "@react-pdf/renderer";
|
||||
import { getWebFontSource } from "@reactive-resume/fonts";
|
||||
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
|
||||
import { Font } from "../renderer";
|
||||
|
||||
const typography = {
|
||||
body: {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { FontWeight } from "@reactive-resume/fonts";
|
||||
import type { ResumeData, Typography } from "@reactive-resume/schema/resume/data";
|
||||
import type { Locale } from "@reactive-resume/utils/locale";
|
||||
import { Font } from "@react-pdf/renderer";
|
||||
import { letters as cjkLetters } from "cjk-regex";
|
||||
import {
|
||||
getFont,
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
sortFontWeights,
|
||||
} from "@reactive-resume/fonts";
|
||||
import { isCJKLocale } from "@reactive-resume/utils/locale";
|
||||
import { Font } from "../renderer";
|
||||
|
||||
type FontWeightRange = {
|
||||
lowest: number;
|
||||
@@ -30,7 +30,13 @@ export type PdfTypography = Omit<Typography, "body" | "heading"> & {
|
||||
};
|
||||
|
||||
const getFontWeightRange = (fontWeights: string[]): FontWeightRange => {
|
||||
const numericWeights = fontWeights.map(Number).filter((weight) => Number.isFinite(weight));
|
||||
const numericWeights: number[] = [];
|
||||
|
||||
for (const fontWeight of fontWeights) {
|
||||
const numericWeight = Number(fontWeight);
|
||||
if (Number.isFinite(numericWeight)) numericWeights.push(numericWeight);
|
||||
}
|
||||
|
||||
if (numericWeights.length === 0) return { lowest: 400, highest: 700 };
|
||||
|
||||
const lowest = Math.min(...numericWeights);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export {
|
||||
Document,
|
||||
Font,
|
||||
Image,
|
||||
Link,
|
||||
Page,
|
||||
pdf,
|
||||
renderToBuffer,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from "#react-pdf-renderer";
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import type { Template } from "@reactive-resume/schema/templates";
|
||||
import type { SectionTitleResolver } from "./section-title";
|
||||
import { renderToBuffer } from "@react-pdf/renderer";
|
||||
import { createElement } from "react";
|
||||
import { ResumeDocument } from "./document";
|
||||
import { renderToBuffer } from "./renderer";
|
||||
|
||||
type CreateResumePdfFileOptions = {
|
||||
data: ResumeData;
|
||||
|
||||
@@ -7,10 +7,10 @@ import type {
|
||||
TemplateStyleContext,
|
||||
TemplateStyleSlots,
|
||||
} from "../shared/types";
|
||||
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
|
||||
import { Fragment, useMemo } from "react";
|
||||
import { rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { useRender } from "../../context";
|
||||
import { Image, Page, StyleSheet, View } from "../../renderer";
|
||||
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
|
||||
import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
@@ -69,16 +69,16 @@ export const AzurillPage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
rowGap: metrics.sectionGap,
|
||||
})}
|
||||
>
|
||||
{sidebarSections.map((section, index) => (
|
||||
<Fragment key={index}>
|
||||
{sidebarSections.map((section) => (
|
||||
<Fragment key={section}>
|
||||
<Section section={section} placement="sidebar" />
|
||||
</Fragment>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View style={composeStyles(styles.mainColumn, { rowGap: metrics.sectionGap })}>
|
||||
{mainSections.map((section, index) => (
|
||||
<Section key={index} section={section} placement="main" />
|
||||
{mainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Style } from "@react-pdf/types";
|
||||
import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateStyleSlots } from "../shared/types";
|
||||
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
|
||||
import { useMemo } from "react";
|
||||
import { rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { useRender } from "../../context";
|
||||
import { Image, Page, StyleSheet, View } from "../../renderer";
|
||||
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
|
||||
import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
@@ -43,9 +43,18 @@ const getBronzorSections = ({
|
||||
}) => {
|
||||
if (fullWidth) return mainSections;
|
||||
|
||||
return Array.from({ length: Math.max(mainSections.length, sidebarSections.length) }).flatMap((_, index) =>
|
||||
[sidebarSections[index], mainSections[index]].filter((section): section is string => Boolean(section)),
|
||||
);
|
||||
const sections: string[] = [];
|
||||
const sectionCount = Math.max(mainSections.length, sidebarSections.length);
|
||||
|
||||
for (let index = 0; index < sectionCount; index += 1) {
|
||||
const sidebarSection = sidebarSections[index];
|
||||
const mainSection = mainSections[index];
|
||||
|
||||
if (sidebarSection) sections.push(sidebarSection);
|
||||
if (mainSection) sections.push(mainSection);
|
||||
}
|
||||
|
||||
return sections;
|
||||
};
|
||||
|
||||
export const BronzorPage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Style } from "@react-pdf/types";
|
||||
import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
|
||||
import { Fragment, useMemo } from "react";
|
||||
import { rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { useRender } from "../../context";
|
||||
import { Image, Page, StyleSheet, View } from "../../renderer";
|
||||
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
|
||||
import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
@@ -60,8 +60,8 @@ export const ChikoritaPage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
>
|
||||
{showHeader && <Header styles={styles} />}
|
||||
|
||||
{mainSections.map((section, index) => (
|
||||
<Section key={index} section={section} placement="main" />
|
||||
{mainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
|
||||
@@ -79,8 +79,8 @@ export const ChikoritaPage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
rowGap: metrics.sectionGap,
|
||||
})}
|
||||
>
|
||||
{sidebarSections.map((section, index) => (
|
||||
<Fragment key={index}>
|
||||
{sidebarSections.map((section) => (
|
||||
<Fragment key={section}>
|
||||
<Section section={section} placement="sidebar" />
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Style } from "@react-pdf/types";
|
||||
import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateFeatures, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
|
||||
import { useMemo } from "react";
|
||||
import { parseColorString, rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { useRender } from "../../context";
|
||||
import { Image, Page, StyleSheet, View } from "../../renderer";
|
||||
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
|
||||
import { TemplateProvider } from "../shared/context";
|
||||
import { getFeaturedSummaryLayout } from "../shared/featured-summary";
|
||||
@@ -75,8 +75,8 @@ export const DitgarPage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
|
||||
{!page.fullWidth && (
|
||||
<View style={composeStyles(styles.sidebarContent, { rowGap: metrics.sectionGap })}>
|
||||
{regularSidebarSections.map((section, index) => (
|
||||
<Section key={index} section={section} placement="sidebar" />
|
||||
{regularSidebarSections.map((section) => (
|
||||
<Section key={section} section={section} placement="sidebar" />
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
@@ -91,8 +91,8 @@ export const DitgarPage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
)}
|
||||
|
||||
<View style={composeStyles(styles.mainContent, { rowGap: metrics.sectionGap })}>
|
||||
{regularMainSections.map((section, index) => (
|
||||
<Section key={index} section={section} placement="main" />
|
||||
{regularMainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Style } from "@react-pdf/types";
|
||||
import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
|
||||
import { Fragment, useMemo } from "react";
|
||||
import { rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { useRender } from "../../context";
|
||||
import { Image, Page, StyleSheet, View } from "../../renderer";
|
||||
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
|
||||
import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
@@ -66,8 +66,8 @@ export const DittoPage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
rowGap: metrics.sectionGap,
|
||||
})}
|
||||
>
|
||||
{sidebarSections.map((section, index) => (
|
||||
<Fragment key={index}>
|
||||
{sidebarSections.map((section) => (
|
||||
<Fragment key={section}>
|
||||
<Section section={section} placement="sidebar" />
|
||||
</Fragment>
|
||||
))}
|
||||
@@ -80,8 +80,8 @@ export const DittoPage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
rowGap: metrics.sectionGap,
|
||||
})}
|
||||
>
|
||||
{mainSections.map((section, index) => (
|
||||
<Section key={index} section={section} placement="main" />
|
||||
{mainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Style } from "@react-pdf/types";
|
||||
import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
|
||||
import { Fragment, useMemo } from "react";
|
||||
import { parseColorString, rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { useRender } from "../../context";
|
||||
import { Image, Page, StyleSheet, View } from "../../renderer";
|
||||
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
|
||||
import { TemplateProvider } from "../shared/context";
|
||||
import { getFeaturedSummaryLayout } from "../shared/featured-summary";
|
||||
@@ -70,8 +70,8 @@ export const GengarPage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
|
||||
{!page.fullWidth && (
|
||||
<View style={styles.sidebarContent}>
|
||||
{regularSidebarSections.map((section, index) => (
|
||||
<Fragment key={index}>
|
||||
{regularSidebarSections.map((section) => (
|
||||
<Fragment key={section}>
|
||||
<Section section={section} placement="sidebar" />
|
||||
</Fragment>
|
||||
))}
|
||||
@@ -88,8 +88,8 @@ export const GengarPage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
)}
|
||||
|
||||
<View style={composeStyles(styles.mainContent, { rowGap: metrics.sectionGap })}>
|
||||
{regularMainSections.map((section, index) => (
|
||||
<Section key={index} section={section} placement="main" />
|
||||
{regularMainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Style } from "@react-pdf/types";
|
||||
import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateFeatures, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
|
||||
import { useMemo } from "react";
|
||||
import { parseColorString, rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { useRender } from "../../context";
|
||||
import { Image, Page, StyleSheet, View } from "../../renderer";
|
||||
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
|
||||
import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
@@ -69,8 +69,8 @@ export const GlaliePage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
|
||||
{!page.fullWidth && (
|
||||
<View style={composeStyles(styles.sidebarContent, { rowGap: metrics.sectionGap })}>
|
||||
{sidebarSections.map((section, index) => (
|
||||
<Section key={index} section={section} placement="sidebar" />
|
||||
{sidebarSections.map((section) => (
|
||||
<Section key={section} section={section} placement="sidebar" />
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
@@ -79,8 +79,8 @@ export const GlaliePage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
|
||||
<View style={styles.mainColumn}>
|
||||
<View style={composeStyles(styles.mainContent, { rowGap: metrics.sectionGap })}>
|
||||
{mainSections.map((section, index) => (
|
||||
<Section key={index} section={section} placement="main" />
|
||||
{mainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Style } from "@react-pdf/types";
|
||||
import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
|
||||
import { useMemo } from "react";
|
||||
import { rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { useRender } from "../../context";
|
||||
import { Image, Page, StyleSheet, View } from "../../renderer";
|
||||
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
|
||||
import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
@@ -50,15 +50,15 @@ export const KakunaPage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
{showHeader && <Header styles={styles} />}
|
||||
|
||||
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
|
||||
{mainSections.map((section, index) => (
|
||||
<Section key={index} section={section} placement="main" />
|
||||
{mainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
|
||||
{!page.fullWidth && (
|
||||
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
|
||||
{sidebarSections.map((section, index) => (
|
||||
<Section key={index} section={section} placement="sidebar" />
|
||||
{sidebarSections.map((section) => (
|
||||
<Section key={section} section={section} placement="sidebar" />
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Style } from "@react-pdf/types";
|
||||
import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
|
||||
import { useMemo } from "react";
|
||||
import { rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { useRender } from "../../context";
|
||||
import { Image, Page, StyleSheet, View } from "../../renderer";
|
||||
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
|
||||
import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
@@ -49,15 +49,15 @@ export const LaprasPage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
{showHeader && <Header styles={styles} />}
|
||||
|
||||
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.gapY(1.5) })}>
|
||||
{mainSections.map((section, index) => (
|
||||
<Section key={index} section={section} placement="main" />
|
||||
{mainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
|
||||
{!page.fullWidth && (
|
||||
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.gapY(1.5) })}>
|
||||
{sidebarSections.map((section, index) => (
|
||||
<Section key={index} section={section} placement="sidebar" />
|
||||
{sidebarSections.map((section) => (
|
||||
<Section key={section} section={section} placement="sidebar" />
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Style } from "@react-pdf/types";
|
||||
import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
|
||||
import { useMemo } from "react";
|
||||
import { parseColorString, rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { useRender } from "../../context";
|
||||
import { Image, Page, StyleSheet, View } from "../../renderer";
|
||||
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
|
||||
import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
@@ -55,8 +55,8 @@ export const LeafishPage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
|
||||
<View style={styles.body}>
|
||||
<View style={composeStyles(styles.mainColumn, { rowGap: metrics.sectionGap })}>
|
||||
{mainSections.map((section, index) => (
|
||||
<Section key={index} section={section} placement="main" />
|
||||
{mainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
|
||||
@@ -67,8 +67,8 @@ export const LeafishPage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
rowGap: metrics.sectionGap,
|
||||
})}
|
||||
>
|
||||
{sidebarSections.map((section, index) => (
|
||||
<Section key={index} section={section} placement="sidebar" />
|
||||
{sidebarSections.map((section) => (
|
||||
<Section key={section} section={section} placement="sidebar" />
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Style } from "@react-pdf/types";
|
||||
import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateFeatures, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
|
||||
import { useMemo } from "react";
|
||||
import { rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { useRender } from "../../context";
|
||||
import { Image, Page, StyleSheet, View } from "../../renderer";
|
||||
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
|
||||
import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
@@ -54,15 +54,15 @@ export const MeowthPage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
{showHeader && <Header styles={styles} />}
|
||||
|
||||
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
|
||||
{mainSections.map((section, index) => (
|
||||
<Section key={index} section={section} placement="main" />
|
||||
{mainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
|
||||
{!page.fullWidth && (
|
||||
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
|
||||
{sidebarSections.map((section, index) => (
|
||||
<Section key={index} section={section} placement="sidebar" />
|
||||
{sidebarSections.map((section) => (
|
||||
<Section key={section} section={section} placement="sidebar" />
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Style } from "@react-pdf/types";
|
||||
import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
|
||||
import { useMemo } from "react";
|
||||
import { rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { useRender } from "../../context";
|
||||
import { Image, Page, StyleSheet, View } from "../../renderer";
|
||||
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
|
||||
import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
@@ -49,15 +49,15 @@ export const OnyxPage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
{showHeader && <Header styles={styles} />}
|
||||
|
||||
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
|
||||
{mainSections.map((section, index) => (
|
||||
<Section key={index} section={section} placement="main" />
|
||||
{mainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
|
||||
{!page.fullWidth && (
|
||||
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
|
||||
{sidebarSections.map((section, index) => (
|
||||
<Section key={index} section={section} placement="sidebar" />
|
||||
{sidebarSections.map((section) => (
|
||||
<Section key={section} section={section} placement="sidebar" />
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Style } from "@react-pdf/types";
|
||||
import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateFeatures, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
|
||||
import { useMemo } from "react";
|
||||
import { rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { useRender } from "../../context";
|
||||
import { Image, Page, StyleSheet, View } from "../../renderer";
|
||||
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
|
||||
import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
@@ -68,8 +68,8 @@ export const PikachuPage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
{showHeader && showSidebar && hasPicture && <Image src={picture.url} style={styles.picture} />}
|
||||
|
||||
<View style={composeStyles(styles.sidebarContent, { rowGap: metrics.sectionGap })}>
|
||||
{sidebarSections.map((section, index) => (
|
||||
<Section key={index} section={section} placement="sidebar" />
|
||||
{sidebarSections.map((section) => (
|
||||
<Section key={section} section={section} placement="sidebar" />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
@@ -84,8 +84,8 @@ export const PikachuPage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
)}
|
||||
|
||||
<View style={{ rowGap: metrics.sectionGap }}>
|
||||
{mainSections.map((section, index) => (
|
||||
<Section key={index} section={section} placement="main" />
|
||||
{mainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { Style } from "@react-pdf/types";
|
||||
import type { ReactNode } from "react";
|
||||
import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
|
||||
import { useMemo } from "react";
|
||||
import { rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { useRender } from "../../context";
|
||||
import { Image, Page, StyleSheet, View } from "../../renderer";
|
||||
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
|
||||
import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
@@ -51,15 +52,15 @@ export const RhyhornPage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
{showHeader && <Header styles={styles} />}
|
||||
|
||||
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
|
||||
{mainSections.map((section, index) => (
|
||||
<Section key={index} section={section} placement="main" />
|
||||
{mainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
|
||||
{!page.fullWidth && (
|
||||
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
|
||||
{sidebarSections.map((section, index) => (
|
||||
<Section key={index} section={section} placement="sidebar" />
|
||||
{sidebarSections.map((section) => (
|
||||
<Section key={section} section={section} placement="sidebar" />
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
@@ -71,32 +72,57 @@ export const RhyhornPage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
const Header = ({ styles }: { styles: RhyhornStyles }) => {
|
||||
const { basics, picture } = useRender();
|
||||
const hasPicture = hasTemplatePicture(picture);
|
||||
const contactItems = [
|
||||
basics.email && (
|
||||
<Link src={`mailto:${basics.email}`} style={styles.contactItemContent}>
|
||||
<Icon name="envelope" />
|
||||
<Text>{basics.email}</Text>
|
||||
</Link>
|
||||
),
|
||||
basics.phone && (
|
||||
<Link src={`tel:${basics.phone}`} style={styles.contactItemContent}>
|
||||
<Icon name="phone" />
|
||||
<Text>{basics.phone}</Text>
|
||||
</Link>
|
||||
),
|
||||
basics.location && (
|
||||
<View style={styles.contactItemContent}>
|
||||
<Icon name="map-pin" />
|
||||
<Text>{basics.location}</Text>
|
||||
</View>
|
||||
),
|
||||
basics.website.url && (
|
||||
<WebsiteContactItem key="website" website={basics.website} style={styles.contactItemContent} />
|
||||
),
|
||||
...basics.customFields.map((field) => (
|
||||
<CustomFieldContactItem key={field.id} field={field} style={styles.contactItemContent} />
|
||||
)),
|
||||
].filter(Boolean);
|
||||
const contactItems: { id: string; content: ReactNode }[] = [];
|
||||
|
||||
if (basics.email) {
|
||||
contactItems.push({
|
||||
id: "email",
|
||||
content: (
|
||||
<Link src={`mailto:${basics.email}`} style={styles.contactItemContent}>
|
||||
<Icon name="envelope" />
|
||||
<Text>{basics.email}</Text>
|
||||
</Link>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (basics.phone) {
|
||||
contactItems.push({
|
||||
id: "phone",
|
||||
content: (
|
||||
<Link src={`tel:${basics.phone}`} style={styles.contactItemContent}>
|
||||
<Icon name="phone" />
|
||||
<Text>{basics.phone}</Text>
|
||||
</Link>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (basics.location) {
|
||||
contactItems.push({
|
||||
id: "location",
|
||||
content: (
|
||||
<View style={styles.contactItemContent}>
|
||||
<Icon name="map-pin" />
|
||||
<Text>{basics.location}</Text>
|
||||
</View>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (basics.website.url) {
|
||||
contactItems.push({
|
||||
id: "website",
|
||||
content: <WebsiteContactItem website={basics.website} style={styles.contactItemContent} />,
|
||||
});
|
||||
}
|
||||
|
||||
contactItems.push(
|
||||
...basics.customFields.map((field) => ({
|
||||
id: `custom-${field.id}`,
|
||||
content: <CustomFieldContactItem field={field} style={styles.contactItemContent} />,
|
||||
})),
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.header}>
|
||||
@@ -109,13 +135,13 @@ const Header = ({ styles }: { styles: RhyhornStyles }) => {
|
||||
<View style={styles.contactList}>
|
||||
{contactItems.map((item, index) => (
|
||||
<View
|
||||
key={index}
|
||||
key={item.id}
|
||||
style={composeStyles(
|
||||
styles.contactItem,
|
||||
index === contactItems.length - 1 ? styles.contactItemLast : undefined,
|
||||
)}
|
||||
>
|
||||
{item}
|
||||
{item.content}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Style } from "@react-pdf/types";
|
||||
import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
|
||||
import { useMemo } from "react";
|
||||
import { rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { useRender } from "../../context";
|
||||
import { Image, Page, StyleSheet, View } from "../../renderer";
|
||||
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
|
||||
import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Style } from "@react-pdf/types";
|
||||
import type { CustomField } from "@reactive-resume/schema/resume/data";
|
||||
import type { IconName } from "phosphor-icons-react-pdf/dynamic";
|
||||
import { View } from "@react-pdf/renderer";
|
||||
import { View } from "../../renderer";
|
||||
import { getCustomFieldLinkUrl, getWebsiteDisplayText } from "./contact";
|
||||
import { Icon, Link, Text } from "./primitives";
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
TemplateStyleSlot,
|
||||
TemplateStyleSlots,
|
||||
} from "./types";
|
||||
import { createContext, useContext } from "react";
|
||||
import { createContext, use } from "react";
|
||||
|
||||
type TemplateContextValue = {
|
||||
styles: TemplateStyleSlots;
|
||||
@@ -50,11 +50,14 @@ type TemplateStyleContextValue = {
|
||||
colors: TemplateColorRoles;
|
||||
};
|
||||
|
||||
const EMPTY_FEATURE_STYLES: TemplateFeatureStyleSlots = {};
|
||||
const EMPTY_FEATURES: TemplateFeatures = {};
|
||||
|
||||
export const TemplateProvider = ({
|
||||
styles,
|
||||
featureStyles = {},
|
||||
featureStyles = EMPTY_FEATURE_STYLES,
|
||||
colors,
|
||||
features = {},
|
||||
features = EMPTY_FEATURES,
|
||||
children,
|
||||
}: TemplateProviderProps) => {
|
||||
return (
|
||||
@@ -73,7 +76,7 @@ export const TemplatePlacementProvider = ({
|
||||
};
|
||||
|
||||
const useTemplateContext = () => {
|
||||
const context = useContext(TemplateContext);
|
||||
const context = use(TemplateContext);
|
||||
|
||||
if (!context) throw new Error("useTemplateContext must be called inside a <TemplateProvider>.");
|
||||
|
||||
@@ -86,7 +89,7 @@ export const useTemplateFeature = (feature: keyof TemplateFeatures): boolean =>
|
||||
return features[feature] ?? false;
|
||||
};
|
||||
|
||||
export const useTemplatePlacement = () => useContext(TemplatePlacementContext);
|
||||
export const useTemplatePlacement = () => use(TemplatePlacementContext);
|
||||
|
||||
const useTemplateStyleContext = (): TemplateStyleContextValue => {
|
||||
const { colors } = useTemplateContext();
|
||||
|
||||
@@ -81,9 +81,14 @@ const filterExperienceRoles = <T extends HiddenItem>(item: T, sectionType?: stri
|
||||
};
|
||||
|
||||
export const filterItems = <T extends HiddenItem>(items: T[], sectionType?: string): T[] => {
|
||||
return items
|
||||
.filter((item) => !item.hidden && hasValidPrimaryTitle(item, sectionType))
|
||||
.map((item) => filterExperienceRoles(item, sectionType));
|
||||
const visibleItems: T[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
if (item.hidden || !hasValidPrimaryTitle(item, sectionType)) continue;
|
||||
visibleItems.push(filterExperienceRoles(item, sectionType));
|
||||
}
|
||||
|
||||
return visibleItems;
|
||||
};
|
||||
|
||||
export const hasVisibleItems = (section: ItemSectionLike, sectionType?: string): boolean => {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { Style } from "@react-pdf/types";
|
||||
import type { IconName } from "phosphor-icons-react-pdf/dynamic";
|
||||
import { View } from "@react-pdf/renderer";
|
||||
import { useRender } from "../../context";
|
||||
import { View } from "../../renderer";
|
||||
import { useTemplateIconSlot, useTemplateStyle } from "./context";
|
||||
import { getTemplateMetrics } from "./metrics";
|
||||
import { Icon } from "./primitives";
|
||||
import { composeStyles } from "./styles";
|
||||
|
||||
const LEVEL_ITEM_KEYS = ["level-1", "level-2", "level-3", "level-4", "level-5"] as const;
|
||||
|
||||
export const LevelDisplay = ({ level }: { level: number }) => {
|
||||
const data = useRender();
|
||||
const levelDesign = data.metadata.design.level;
|
||||
@@ -40,13 +42,13 @@ export const LevelDisplay = ({ level }: { level: number }) => {
|
||||
levelContainerStyle,
|
||||
)}
|
||||
>
|
||||
{Array.from({ length: 5 }).map((_, index) => {
|
||||
{LEVEL_ITEM_KEYS.map((itemKey, index) => {
|
||||
const isActive = index < level;
|
||||
|
||||
if (levelDesign.type === "icon") {
|
||||
return (
|
||||
<Icon
|
||||
key={index}
|
||||
key={itemKey}
|
||||
size={iconSize + 4}
|
||||
name={levelDesign.icon as IconName}
|
||||
style={{ opacity: isActive ? 1 : 0.35 }}
|
||||
@@ -57,7 +59,7 @@ export const LevelDisplay = ({ level }: { level: number }) => {
|
||||
if (levelDesign.type === "progress-bar") {
|
||||
return (
|
||||
<View
|
||||
key={index}
|
||||
key={itemKey}
|
||||
style={composeStyles(
|
||||
{
|
||||
flex: 1,
|
||||
@@ -92,7 +94,7 @@ export const LevelDisplay = ({ level }: { level: number }) => {
|
||||
|
||||
return (
|
||||
<View
|
||||
key={index}
|
||||
key={itemKey}
|
||||
style={composeStyles(
|
||||
{
|
||||
width,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Style } from "@react-pdf/types";
|
||||
import type { ComponentProps } from "react";
|
||||
import type { StyleInput } from "./styles";
|
||||
import { Link as PdfLink, Text as PdfText, View } from "@react-pdf/renderer";
|
||||
import { Icon as PhosphorIcon } from "phosphor-icons-react-pdf/dynamic";
|
||||
import { Link as PdfLink, Text as PdfText, View } from "../../renderer";
|
||||
import { useTemplateIconSlot, useTemplateStyle } from "./context";
|
||||
import { composeLinkStyles, composeStyles } from "./styles";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ReactElement } from "react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Text as PdfText } from "@react-pdf/renderer";
|
||||
import { renderHtml } from "react-pdf-html";
|
||||
import { Text as PdfText } from "../../renderer";
|
||||
import { normalizeRichTextHtml, richTextMarkClassName } from "./rich-text-html";
|
||||
|
||||
type PdfElement = ReactElement<{ children?: unknown; element?: { tag: string } }>;
|
||||
|
||||
@@ -29,12 +29,19 @@ const getTagName = (node: Node) => node.rawTagName.toLowerCase();
|
||||
const hasBlockDescendant = (node: Node): boolean =>
|
||||
node.childNodes.some((child) => child.nodeType === NodeType.ELEMENT_NODE && !isInlineNode(child));
|
||||
|
||||
const mergeClassNames = (...classNames: (string | undefined)[]): string =>
|
||||
classNames
|
||||
.flatMap((className) => className?.split(/\s+/) ?? [])
|
||||
.filter(Boolean)
|
||||
.filter((className, index, classNames) => classNames.indexOf(className) === index)
|
||||
.join(" ");
|
||||
const mergeClassNames = (...classNames: (string | undefined)[]): string => {
|
||||
const uniqueClassNames = new Set<string>();
|
||||
|
||||
for (const className of classNames) {
|
||||
if (!className) continue;
|
||||
|
||||
for (const part of className.split(/\s+/)) {
|
||||
if (part) uniqueClassNames.add(part);
|
||||
}
|
||||
}
|
||||
|
||||
return [...uniqueClassNames].join(" ");
|
||||
};
|
||||
|
||||
const normalizeMarkElements = (root: ReturnType<typeof parse>) => {
|
||||
for (const mark of root.querySelectorAll("mark")) {
|
||||
|
||||
@@ -83,13 +83,29 @@ const getRootElement = (element: RichTextSpacingElement): RichTextSpacingElement
|
||||
return root;
|
||||
};
|
||||
|
||||
const getTopLevelFlowElements = (root: RichTextSpacingElement): RichTextSpacingElement[] =>
|
||||
(root.childNodes ?? []).filter(isElementNode).flatMap((child) => {
|
||||
if (isRichTextTag(child, "p")) return [child];
|
||||
if (!isRichTextTag(child, "ul", "ol")) return [];
|
||||
const getTopLevelFlowElements = (root: RichTextSpacingElement): RichTextSpacingElement[] => {
|
||||
const flowElements: RichTextSpacingElement[] = [];
|
||||
|
||||
return (child.childNodes ?? []).filter(isElementNode).filter((listChild) => isRichTextTag(listChild, "li"));
|
||||
});
|
||||
for (const childNode of root.childNodes ?? []) {
|
||||
if (!isElementNode(childNode)) continue;
|
||||
const child = childNode;
|
||||
|
||||
if (isRichTextTag(child, "p")) {
|
||||
flowElements.push(child);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isRichTextTag(child, "ul", "ol")) continue;
|
||||
|
||||
for (const listChildNode of child.childNodes ?? []) {
|
||||
if (isElementNode(listChildNode) && isRichTextTag(listChildNode, "li")) {
|
||||
flowElements.push(listChildNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return flowElements;
|
||||
};
|
||||
|
||||
export const createRichTextProseSpacing = (bodyLineHeight: number | undefined): RichTextProseSpacing => {
|
||||
if (bodyLineHeight === undefined) return { paragraph: {}, listItem: {} };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Style } from "@react-pdf/types";
|
||||
import { Text as PdfText, View } from "@react-pdf/renderer";
|
||||
import { Html } from "react-pdf-html";
|
||||
import { Text as PdfText, View } from "../../renderer";
|
||||
import { useTemplateStyle } from "./context";
|
||||
import { safeTextStyle } from "./primitives";
|
||||
import { normalizeRichTextHtml, richTextMarkClassName } from "./rich-text-html";
|
||||
|
||||
@@ -19,10 +19,10 @@ import type { IconName } from "phosphor-icons-react-pdf/dynamic";
|
||||
import type { ReactNode } from "react";
|
||||
import type { StyleInput, TemplatePlacement } from "./styles";
|
||||
import type { CustomItemSection, ItemSection } from "./types";
|
||||
import { View } from "@react-pdf/renderer";
|
||||
import { Children, createContext, useContext } from "react";
|
||||
import { Children, createContext, isValidElement, use } from "react";
|
||||
import { match } from "ts-pattern";
|
||||
import { useRender } from "../../context";
|
||||
import { View } from "../../renderer";
|
||||
import { getResumeSectionTitle } from "../../section-title";
|
||||
import { getSectionItemRows, getSectionItemsLayout, shouldUseSectionTimeline } from "./columns";
|
||||
import { getWebsiteDisplayText } from "./contact";
|
||||
@@ -48,8 +48,25 @@ type SectionItemsContextValue = {
|
||||
};
|
||||
|
||||
const SectionItemsContext = createContext<SectionItemsContextValue>({ itemStyle: undefined, useTimeline: false });
|
||||
const SECTION_ITEM_PLACEHOLDER_KEYS = [
|
||||
"placeholder-1",
|
||||
"placeholder-2",
|
||||
"placeholder-3",
|
||||
"placeholder-4",
|
||||
"placeholder-5",
|
||||
"placeholder-6",
|
||||
] as const;
|
||||
|
||||
const useSectionItemsContext = () => useContext(SectionItemsContext);
|
||||
const useSectionItemsContext = () => use(SectionItemsContext);
|
||||
|
||||
const getChildKey = (child: ReactNode, fallbackIndex: number) => {
|
||||
return isValidElement(child) && child.key !== null ? String(child.key) : `child-${fallbackIndex}`;
|
||||
};
|
||||
|
||||
const getRowKey = (row: ReactNode[], rowIndex: number) => {
|
||||
const childKeys = row.map(getChildKey).join("|");
|
||||
return childKeys || `row-${rowIndex}`;
|
||||
};
|
||||
|
||||
const getVisibleItems = <T extends { hidden: boolean }>(section: ItemSection<T>, sectionType?: string): T[] => {
|
||||
if (!hasVisibleItems(section, sectionType)) return [];
|
||||
@@ -109,10 +126,10 @@ const SectionItems = ({ children, columns = 1 }: { children: ReactNode; columns?
|
||||
<SectionItemsContext.Provider value={context}>
|
||||
<View style={composeStyles(layout.containerStyle, sectionItemsStyle)}>
|
||||
{rows.map((row, rowIndex) => (
|
||||
<View key={rowIndex} style={composeStyles(layout.rowStyle)}>
|
||||
<View key={getRowKey(row, rowIndex)} style={composeStyles(layout.rowStyle)}>
|
||||
{row}
|
||||
{Array.from({ length: layout.columns - row.length }, (_, index) => (
|
||||
<View key={`placeholder-${index}`} style={composeStyles(layout.itemStyle)} />
|
||||
{SECTION_ITEM_PLACEHOLDER_KEYS.slice(0, layout.columns - row.length).map((placeholderKey) => (
|
||||
<View key={placeholderKey} style={composeStyles(layout.itemStyle)} />
|
||||
))}
|
||||
</View>
|
||||
))}
|
||||
|
||||
@@ -11,7 +11,7 @@ type FormItemContextValue = {
|
||||
const FormItemContext = React.createContext<FormItemContextValue>({ id: "", hasError: false });
|
||||
|
||||
function useFormItem() {
|
||||
return React.useContext(FormItemContext);
|
||||
return React.use(FormItemContext);
|
||||
}
|
||||
|
||||
type FormItemProps = React.ComponentProps<"div"> & { hasError?: boolean };
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type * as React from "react";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
|
||||
function Label({ className, ...props }: React.ComponentProps<"label">) {
|
||||
function Label({ className, htmlFor, ...props }: React.ComponentProps<"label">) {
|
||||
return (
|
||||
// biome-ignore lint/a11y/noLabelWithoutControl: label is a generic component
|
||||
<label
|
||||
htmlFor={htmlFor}
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex select-none items-center gap-2 font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50",
|
||||
|
||||
@@ -33,7 +33,7 @@ type SidebarContextProps = {
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
|
||||
|
||||
function useSidebarState() {
|
||||
const context = React.useContext(SidebarContext);
|
||||
const context = React.use(SidebarContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useSidebarState must be used within a SidebarProvider.");
|
||||
@@ -60,7 +60,7 @@ function SidebarProvider({
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen);
|
||||
const [_open, _setOpen] = React.useReducer((_state: boolean, nextOpen: boolean) => nextOpen, defaultOpen);
|
||||
const open = openProp ?? _open;
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { Slider as SliderPrimitive } from "@base-ui/react/slider";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
|
||||
const THUMB_POSITION_KEYS = ["single", "start", "end"] as const;
|
||||
|
||||
function Slider({ className, defaultValue, value, min = 0, max = 100, ...props }: SliderPrimitive.Root.Props) {
|
||||
const _values = Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max];
|
||||
const thumbDescriptors = _values.map((thumbValue, position) => ({
|
||||
key:
|
||||
_values.length === 1
|
||||
? THUMB_POSITION_KEYS[0]
|
||||
: (THUMB_POSITION_KEYS[position + 1] ?? `thumb-${position}-${thumbValue}`),
|
||||
}));
|
||||
|
||||
return (
|
||||
<SliderPrimitive.Root
|
||||
@@ -25,10 +33,10 @@ function Slider({ className, defaultValue, value, min = 0, max = 100, ...props }
|
||||
className="select-none bg-primary data-horizontal:h-full data-vertical:w-full"
|
||||
/>
|
||||
</SliderPrimitive.Track>
|
||||
{Array.from({ length: _values.length }, (_, index) => (
|
||||
{thumbDescriptors.map((thumb) => (
|
||||
<SliderPrimitive.Thumb
|
||||
data-slot="slider-thumb"
|
||||
key={index}
|
||||
key={thumb.key}
|
||||
className="relative block size-3 shrink-0 select-none rounded-full border border-ring bg-white ring-ring/50 transition-[color,box-shadow] after:absolute after:-inset-2 hover:ring-3 focus-visible:outline-hidden focus-visible:ring-3 active:ring-3 disabled:pointer-events-none disabled:opacity-50"
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -85,7 +85,7 @@ export function ConfirmDialogProvider({ children }: { children: React.ReactNode
|
||||
}
|
||||
|
||||
export function useConfirm() {
|
||||
const context = React.useContext(ConfirmContext);
|
||||
const context = React.use(ConfirmContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useConfirm must be used within a <ConfirmDialogProvider />.");
|
||||
|
||||
@@ -73,19 +73,18 @@ export function useCookie(
|
||||
|
||||
useEffect(() => {
|
||||
const cookie = getCookie(name);
|
||||
let nextValue: string | null;
|
||||
|
||||
if (cookie !== null) {
|
||||
setValue(cookie);
|
||||
return;
|
||||
nextValue = cookie;
|
||||
} else if (defaultValue === undefined) {
|
||||
nextValue = null;
|
||||
} else {
|
||||
if (canUseCookies()) Cookie.set(name, defaultValue, resolveCookieOptions(defaultOptions));
|
||||
nextValue = defaultValue;
|
||||
}
|
||||
|
||||
if (defaultValue === undefined) {
|
||||
setValue(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (canUseCookies()) Cookie.set(name, defaultValue, resolveCookieOptions(defaultOptions));
|
||||
setValue(defaultValue);
|
||||
setValue(nextValue);
|
||||
}, [name, defaultValue, defaultOptions]);
|
||||
|
||||
const updateCookie = useCallback(
|
||||
|
||||
@@ -49,10 +49,12 @@ export function PromptDialogProvider({ children }: { children: React.ReactNode }
|
||||
React.useEffect(() => {
|
||||
if (!state.open) return;
|
||||
|
||||
setTimeout(() => {
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
if (!inputRef.current) return;
|
||||
inputRef.current.focus();
|
||||
}, 0);
|
||||
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
}, [state.open]);
|
||||
|
||||
const prompt = React.useCallback(async (title: string, options?: PromptOptions): Promise<string | null> => {
|
||||
@@ -126,7 +128,7 @@ export function PromptDialogProvider({ children }: { children: React.ReactNode }
|
||||
}
|
||||
|
||||
export function usePrompt() {
|
||||
const context = React.useContext(PromptContext);
|
||||
const context = React.use(PromptContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("usePrompt must be used within a <PromptDialogProvider />.");
|
||||
|
||||
Reference in New Issue
Block a user