This commit is contained in:
Amruth Pillai
2026-01-24 23:00:54 +01:00
parent 1419c16d04
commit be84f0cca6
74 changed files with 2329 additions and 1035 deletions
+91 -32
View File
@@ -1,10 +1,15 @@
import { Trans } from "@lingui/react/macro";
import { IconContext, type IconProps } from "@phosphor-icons/react";
import { useMemo } from "react";
import { ArrowRightIcon, IconContext, type IconProps, WarningIcon } from "@phosphor-icons/react";
import { type RefObject, useMemo, useRef, useState } from "react";
import { match } from "ts-pattern";
import { useResizeObserver } from "usehooks-ts";
import type z from "zod";
import { pageDimensionsAsPixels } from "@/schema/page";
import type { pageLayoutSchema } from "@/schema/resume/data";
import type { Template } from "@/schema/templates";
import { sanitizeCss } from "@/utils/sanitize";
import { cn } from "@/utils/style";
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
import { useCSSVariables } from "./hooks/use-css-variables";
import { useWebfonts } from "./hooks/use-webfonts";
import styles from "./preview.module.css";
@@ -27,11 +32,6 @@ export type ExtendedIconProps = IconProps & {
hidden?: boolean;
};
type Props = React.ComponentProps<"div"> & {
pageClassName?: string;
showPageNumbers?: boolean;
};
const CSS_RULE_SPLIT_PATTERN = /\n(?=\s*[.#a-zA-Z])/;
const CSS_SELECTOR_PATTERN = /^([^{]+)(\{)/;
@@ -53,13 +53,17 @@ function getTemplateComponent(template: Template) {
.exhaustive();
}
export const ResumePreview = ({ showPageNumbers, pageClassName, className, ...props }: Props) => {
type Props = React.ComponentProps<"div"> & {
pageClassName?: string;
showPageNumbers?: boolean;
};
export const ResumePreview = ({ showPageNumbers = false, pageClassName, className, ...props }: Props) => {
const picture = useResumeStore((state) => state.resume.data.picture);
const metadata = useResumeStore((state) => state.resume.data.metadata);
useWebfonts(metadata.typography);
const style = useCSSVariables({ picture, metadata });
const totalNumberOfPages = metadata.layout.pages.length;
const iconProps = useMemo<ExtendedIconProps>(() => {
return {
@@ -94,36 +98,91 @@ export const ResumePreview = ({ showPageNumbers, pageClassName, className, ...pr
return scoped;
}, [metadata.css.enabled, metadata.css.value]);
const TemplateComponent = useMemo(() => getTemplateComponent(metadata.template), [metadata.template]);
return (
<IconContext.Provider value={iconProps}>
{/** biome-ignore lint/security/noDangerouslySetInnerHtml: CSS is sanitized with sanitizeCss */}
{scopedCSS && <style dangerouslySetInnerHTML={{ __html: scopedCSS }} />}
<div style={style} className={cn("resume-preview-container", className)} {...props}>
{metadata.layout.pages.map((pageLayout, pageIndex) => {
const pageNumber = pageIndex + 1;
return (
<div key={pageIndex} data-page-index={pageIndex} className="relative">
{showPageNumbers && totalNumberOfPages > 1 && (
<div className="absolute -top-6 left-0">
<span className="font-medium text-foreground text-xs">
<Trans>
Page {pageNumber} of {totalNumberOfPages}
</Trans>
</span>
</div>
)}
<div className={cn(`page page-${pageIndex}`, styles.page, pageClassName)}>
<TemplateComponent pageIndex={pageIndex} pageLayout={pageLayout} />
</div>
</div>
);
})}
{metadata.layout.pages.map((pageLayout, pageIndex) => (
<PageContainer
key={pageIndex}
pageIndex={pageIndex}
pageLayout={pageLayout}
pageClassName={pageClassName}
showPageNumbers={showPageNumbers}
/>
))}
</div>
</IconContext.Provider>
);
};
type PageContainerProps = {
pageIndex: number;
pageLayout: z.infer<typeof pageLayoutSchema>;
pageClassName?: string;
showPageNumbers?: boolean;
};
function PageContainer({ pageIndex, pageLayout, pageClassName, showPageNumbers = false }: PageContainerProps) {
const pageRef = useRef<HTMLDivElement>(null);
const [pageHeight, setPageHeight] = useState<number>(0);
const metadata = useResumeStore((state) => state.resume.data.metadata);
const pageNumber = useMemo(() => pageIndex + 1, [pageIndex]);
const maxPageHeight = useMemo(() => pageDimensionsAsPixels[metadata.page.format].height, [metadata.page.format]);
const totalNumberOfPages = useMemo(() => metadata.layout.pages.length, [metadata.layout.pages]);
const TemplateComponent = useMemo(() => getTemplateComponent(metadata.template), [metadata.template]);
useResizeObserver({
ref: pageRef as RefObject<HTMLDivElement>,
onResize: (size) => {
if (!size.height) return;
setPageHeight(size.height);
},
});
return (
<div data-page-index={pageIndex} className="relative">
{showPageNumbers && totalNumberOfPages > 1 && (
<div className="absolute -top-6 left-0">
<span className="font-medium text-foreground text-xs">
<Trans>
Page {pageNumber} of {totalNumberOfPages}
</Trans>
</span>
</div>
)}
<div ref={pageRef} className={cn(`page page-${pageIndex}`, styles.page, pageClassName)}>
<TemplateComponent pageIndex={pageIndex} pageLayout={pageLayout} />
</div>
{pageHeight > maxPageHeight && (
<div className="absolute top-full left-0 mt-4">
<a
rel="noopener"
target="_blank"
className="group/link"
href="https://docs.rxresu.me/guides/fitting-content-on-a-page"
>
<Alert className="max-w-sm text-yellow-600">
<WarningIcon color="currentColor" />
<AlertTitle>
<Trans>
The content is too tall for this page, this may cause undesirable results when exporting to PDF.
</Trans>
</AlertTitle>
<AlertDescription className="text-xs underline-offset-2 group-hover/link:underline">
<Trans>Learn more about how to fit content on a page</Trans>
<ArrowRightIcon color="currentColor" className="ml-1 inline size-3" />
</AlertDescription>
</Alert>
</a>
</div>
)}
</div>
);
}
+12 -10
View File
@@ -36,21 +36,23 @@ export function DitgarTemplate({ pageIndex, pageLayout }: TemplateProps) {
return (
<div className="template-ditgar page-content">
{/* Sidebar Background */}
{!fullWidth && (
{(!fullWidth || isFirstPage) && (
<div className="page-sidebar-background absolute inset-y-0 left-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20" />
)}
<div className="flex">
<aside data-layout="sidebar" className="sidebar group z-10 flex w-(--page-sidebar-width) shrink-0 flex-col">
{isFirstPage && <Header />}
{(!fullWidth || isFirstPage) && (
<aside data-layout="sidebar" className="sidebar group z-10 flex w-(--page-sidebar-width) shrink-0 flex-col">
{isFirstPage && <Header />}
<div className="flex-1 space-y-4 px-(--page-margin-x) pt-(--page-margin-y)">
{sidebar.map((section) => {
const Component = getSectionComponent(section, { sectionClassName });
return <Component key={section} id={section} />;
})}
</div>
</aside>
<div className="flex-1 space-y-4 px-(--page-margin-x) pt-(--page-margin-y)">
{sidebar.map((section) => {
const Component = getSectionComponent(section, { sectionClassName });
return <Component key={section} id={section} />;
})}
</div>
</aside>
)}
<main data-layout="main" className={cn("main group z-10", !fullWidth ? "col-span-2" : "col-span-3")}>
{isFirstPage && <SummaryComponent id="summary" />}
+1 -1
View File
@@ -3,7 +3,7 @@ import type * as React from "react";
import { cn } from "@/utils/style";
const alertVariants = cva(
"group/alert relative grid w-full gap-0.5 rounded-lg border px-4 py-3 text-left text-sm has-data-[slot=alert-action]:relative has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 has-data-[slot=alert-action]:pr-18 *:[svg:not([class*='size-'])]:size-4 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current",
"group/alert relative grid w-full gap-1 rounded-lg border px-4 py-3 text-left text-sm has-data-[slot=alert-action]:relative has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 has-data-[slot=alert-action]:pr-18 *:[svg:not([class*='size-'])]:size-4 *:[svg]:row-span-2 *:[svg]:translate-y-1 *:[svg]:text-current",
{
variants: {
variant: {