mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-06-22 04:11:55 +10:00
Merge branch 'main' of github.com:amruthpillai/reactive-resume
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export const appVersion = typeof __APP_VERSION__ === "undefined" ? "0.0.0" : __APP_VERSION__;
|
||||
@@ -4,6 +4,7 @@ import { onError } from "@orpc/client";
|
||||
import { createRouterClient } from "@orpc/server";
|
||||
import router from "@reactive-resume/api/routers";
|
||||
import { MCP_TOOL_NAME, registerPrompts, registerResources, registerTools } from "@reactive-resume/mcp";
|
||||
import { appVersion } from "../app-version";
|
||||
import { getRequestLocale } from "../rpc/locale";
|
||||
|
||||
function createRequestClient(request: Request): RouterClient<typeof router> {
|
||||
@@ -25,7 +26,7 @@ export async function createMcpServer(request: Request) {
|
||||
const server = new McpServer(
|
||||
{
|
||||
name: "reactive-resume",
|
||||
version: __APP_VERSION__,
|
||||
version: appVersion,
|
||||
title: "Reactive Resume",
|
||||
websiteUrl: "https://rxresu.me",
|
||||
description:
|
||||
|
||||
@@ -8,6 +8,7 @@ import { downloadResumePdfProcedure } from "@reactive-resume/api/features/resume
|
||||
import router from "@reactive-resume/api/routers";
|
||||
import { env } from "@reactive-resume/env/server";
|
||||
import { resumeDataSchema } from "@reactive-resume/schema/resume/data";
|
||||
import { appVersion } from "../app-version";
|
||||
import { mergeResponseHeaders } from "../http/headers";
|
||||
import { getRequestLocale } from "../rpc/locale";
|
||||
|
||||
@@ -44,7 +45,7 @@ export async function handleOpenApi(request: Request) {
|
||||
const spec = await openAPIGenerator.generate(openAPIRouter, {
|
||||
info: {
|
||||
title: "Reactive Resume",
|
||||
version: __APP_VERSION__,
|
||||
version: appVersion,
|
||||
description: "Reactive Resume API",
|
||||
license: { name: "MIT", url: "https://github.com/amruthpillai/reactive-resume/blob/main/LICENSE" },
|
||||
contact: { name: "Amruth Pillai", email: "hello@amruthpillai.com", url: "https://amruthpillai.com" },
|
||||
|
||||
@@ -2,6 +2,7 @@ import { oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata } fr
|
||||
import { auth } from "@reactive-resume/auth/config";
|
||||
import { env } from "@reactive-resume/env/server";
|
||||
import { buildMcpServerCard } from "@reactive-resume/mcp/server-card";
|
||||
import { appVersion } from "../app-version";
|
||||
|
||||
const oauthAuthorizationServerHandler = oauthProviderAuthServerMetadata(auth);
|
||||
const openIdConfigurationHandler = oauthProviderOpenIdConfigMetadata(auth);
|
||||
@@ -11,7 +12,7 @@ export function handleWellKnownFallback() {
|
||||
}
|
||||
|
||||
export function handleMcpServerCard() {
|
||||
return Response.json(buildMcpServerCard(__APP_VERSION__), {
|
||||
return Response.json(buildMcpServerCard(appVersion), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "public, max-age=60, stale-while-revalidate=120",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import z from "zod";
|
||||
import { resumeDataSchema } from "@reactive-resume/schema/resume/data";
|
||||
import { appVersion } from "../app-version";
|
||||
|
||||
export function handleSchemaJson() {
|
||||
const resumeDataJSONSchema = z.toJSONSchema(resumeDataSchema);
|
||||
@@ -12,7 +13,7 @@ export function handleSchemaJson() {
|
||||
"Surrogate-Control": "max-age=86400",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"X-Robots-Tag": "index, follow",
|
||||
ETag: __APP_VERSION__,
|
||||
ETag: appVersion,
|
||||
Vary: "Accept",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -71,6 +71,21 @@ describe("LevelDisplay", () => {
|
||||
expect(wrapper.getAttribute("aria-label")).toContain("4");
|
||||
});
|
||||
|
||||
it("applies custom decoration and icon sizes in pixels", () => {
|
||||
const { container } = render(
|
||||
<LevelDisplay type="circle" icon="star" level={2} decorationSizePx={18} iconSizePx={20} />,
|
||||
);
|
||||
|
||||
const shapes = container.querySelectorAll("div[data-active]");
|
||||
expect(shapes[0]).toHaveStyle({ width: "18px", height: "18px" });
|
||||
|
||||
const { container: iconContainer } = render(
|
||||
<LevelDisplay type="icon" icon="star" level={2} decorationSizePx={16} />,
|
||||
);
|
||||
const icon = iconContainer.querySelector("i");
|
||||
expect(icon).toHaveStyle({ fontSize: "16px", width: "16px", height: "16px" });
|
||||
});
|
||||
|
||||
it("merges extra className into the wrapper", () => {
|
||||
const { container } = render(<LevelDisplay type="circle" icon="star" level={1} className="extra" />);
|
||||
const wrapper = container.firstChild as HTMLElement;
|
||||
|
||||
@@ -3,14 +3,35 @@ import type z from "zod";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
|
||||
type Props = z.infer<typeof levelDesignSchema> & React.ComponentProps<"div"> & { level: number };
|
||||
type Props = z.infer<typeof levelDesignSchema> &
|
||||
React.ComponentProps<"div"> & {
|
||||
level: number;
|
||||
decorationSizePx?: number | undefined;
|
||||
iconSizePx?: number | undefined;
|
||||
};
|
||||
|
||||
const LEVEL_ITEM_KEYS = ["level-1", "level-2", "level-3", "level-4", "level-5"] as const;
|
||||
|
||||
export function LevelDisplay({ icon, type, level, className, ...props }: Props) {
|
||||
const defaultDecorationClassName = "size-2.5";
|
||||
|
||||
export function LevelDisplay({ icon, type, level, className, decorationSizePx, iconSizePx, ...props }: Props) {
|
||||
if (level === 0) return null;
|
||||
if (type === "hidden" || icon === "") return null;
|
||||
|
||||
const decorationStyle =
|
||||
decorationSizePx === undefined
|
||||
? undefined
|
||||
: ({ width: decorationSizePx, height: decorationSizePx } satisfies React.CSSProperties);
|
||||
const resolvedIconSizePx = iconSizePx ?? decorationSizePx;
|
||||
const iconStyle =
|
||||
resolvedIconSizePx === undefined
|
||||
? undefined
|
||||
: ({
|
||||
fontSize: resolvedIconSizePx,
|
||||
width: resolvedIconSizePx,
|
||||
height: resolvedIconSizePx,
|
||||
} satisfies React.CSSProperties);
|
||||
|
||||
return (
|
||||
<div
|
||||
role="img"
|
||||
@@ -29,8 +50,10 @@ export function LevelDisplay({ icon, type, level, className, ...props }: Props)
|
||||
<div
|
||||
key={itemKey}
|
||||
data-active={isActive}
|
||||
style={decorationStyle}
|
||||
className={cn(
|
||||
"h-2.5 flex-1 border border-(--page-primary-color) border-x-0 first:border-l last:border-r",
|
||||
"flex-1 border border-(--page-primary-color) border-x-0 first:border-l last:border-r",
|
||||
decorationSizePx === undefined && "h-2.5",
|
||||
isActive && "bg-(--page-primary-color)",
|
||||
)}
|
||||
/>
|
||||
@@ -41,7 +64,13 @@ export function LevelDisplay({ icon, type, level, className, ...props }: Props)
|
||||
return (
|
||||
<i
|
||||
key={itemKey}
|
||||
className={cn("ph size-2.5 text-(--page-primary-color)", `ph-${icon}`, !isActive && "opacity-40")}
|
||||
style={iconStyle}
|
||||
className={cn(
|
||||
"ph text-(--page-primary-color)",
|
||||
resolvedIconSizePx === undefined && defaultDecorationClassName,
|
||||
`ph-${icon}`,
|
||||
!isActive && "opacity-40",
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -50,8 +79,10 @@ export function LevelDisplay({ icon, type, level, className, ...props }: Props)
|
||||
<div
|
||||
key={itemKey}
|
||||
data-active={isActive}
|
||||
style={decorationStyle}
|
||||
className={cn(
|
||||
"size-2.5 border border-(--page-primary-color)",
|
||||
"border border-(--page-primary-color)",
|
||||
decorationSizePx === undefined && defaultDecorationClassName,
|
||||
isActive && "bg-(--page-primary-color)",
|
||||
type === "circle" && "rounded-full",
|
||||
type === "rectangle" && "w-7",
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Trans } from "@lingui/react/macro";
|
||||
import { useStore } from "@tanstack/react-form";
|
||||
import { AnimatePresence, m } from "motion/react";
|
||||
import { colorDesignSchema, levelDesignSchema } from "@reactive-resume/schema/resume/data";
|
||||
import { resolveLevelDisplaySizes } from "@reactive-resume/schema/resume/level-display-sizes";
|
||||
import { resolveStyleRuleFontSize } from "@reactive-resume/schema/resume/style-rules";
|
||||
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
|
||||
import { Input } from "@reactive-resume/ui/components/input";
|
||||
import { Separator } from "@reactive-resume/ui/components/separator";
|
||||
@@ -276,6 +278,13 @@ function LevelSectionForm() {
|
||||
|
||||
const previewType = useStore(form.store, (s) => s.values.type);
|
||||
const previewIcon = useStore(form.store, (s) => s.values.icon);
|
||||
const iconFontSize = resolveStyleRuleFontSize(resume.data, { slot: "icon" });
|
||||
const levelFontSize = resolveStyleRuleFontSize(resume.data, { slot: "level" });
|
||||
const { decorationSize, levelIconExplicitSize } = resolveLevelDisplaySizes({
|
||||
bodyFontSize: resume.data.metadata.typography.body.fontSize,
|
||||
iconFontSize,
|
||||
levelFontSize,
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
@@ -294,7 +303,14 @@ function LevelSectionForm() {
|
||||
style={{ "--page-primary-color": colors.primary, backgroundColor: colors.background } as React.CSSProperties}
|
||||
className="flex items-center justify-center rounded-md p-6"
|
||||
>
|
||||
<LevelDisplay level={3} type={previewType} icon={previewIcon} className="w-full max-w-[220px] justify-center" />
|
||||
<LevelDisplay
|
||||
level={3}
|
||||
type={previewType}
|
||||
icon={previewIcon}
|
||||
decorationSizePx={decorationSize}
|
||||
iconSizePx={levelIconExplicitSize}
|
||||
className="w-full max-w-[220px] justify-center"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
Reference in New Issue
Block a user