From 8da780c86848e474b23df97c29a5d6182b30bc65 Mon Sep 17 00:00:00 2001 From: Amruth Pillai Date: Tue, 26 May 2026 13:09:30 +0200 Subject: [PATCH] feat: update links for improved accessibility --- apps/server/package.json | 2 + apps/web/package.json | 2 + .../input/github-stars-button.test.tsx | 4 +- .../components/input/github-stars-button.tsx | 7 +- apps/web/src/components/ui/copyright.test.tsx | 2 +- apps/web/src/components/ui/copyright.tsx | 4 +- apps/web/src/dialogs/resume/import.tsx | 13 +- .../features/resume/preview/pdf-canvas.tsx | 9 +- apps/web/src/hooks/use-confirm.tsx | 4 +- apps/web/src/hooks/use-controlled-state.tsx | 14 +- apps/web/src/hooks/use-mobile.tsx | 29 +- apps/web/src/hooks/use-prompt.tsx | 4 +- .../web/src/routes/_home/-sections/donate.tsx | 4 +- apps/web/src/routes/_home/-sections/faq.tsx | 2 +- .../web/src/routes/_home/-sections/footer.tsx | 2 +- apps/web/src/routes/_home/index.tsx | 4 +- apps/web/src/routes/agent/$threadId.tsx | 1 + .../-sidebar/left/sections/picture.tsx | 9 +- .../right/sections/information.test.tsx | 2 +- .../-sidebar/right/sections/information.tsx | 12 +- .../right/sections/resume-analysis.tsx | 4 +- package.json | 1 + packages/api/src/features/agent/service.ts | 12 +- packages/email/package.json | 2 + packages/pdf/package.json | 2 + packages/pdf/src/context.tsx | 8 +- packages/pdf/src/templates/shared/context.tsx | 9 +- .../pdf/src/templates/shared/primitives.tsx | 8 +- .../src/templates/shared/rich-text-html.ts | 8 +- .../pdf/src/templates/shared/rich-text.tsx | 2 +- .../src/templates/shared/safe-text-style.ts | 8 + packages/ui/package.json | 2 + packages/ui/src/components/badge.test.tsx | 4 +- packages/ui/src/components/form.tsx | 3 +- packages/ui/src/components/input-group.tsx | 1 - packages/ui/src/components/sidebar.tsx | 1 + packages/ui/src/hooks/use-confirm.tsx | 4 +- .../ui/src/hooks/use-controlled-state.tsx | 14 +- packages/ui/src/hooks/use-mobile.tsx | 29 +- packages/ui/src/hooks/use-prompt.tsx | 4 +- pnpm-lock.yaml | 878 ++++++++++++++++-- pnpm-workspace.yaml | 3 +- turbo.json | 4 +- 43 files changed, 952 insertions(+), 189 deletions(-) create mode 100644 packages/pdf/src/templates/shared/safe-text-style.ts diff --git a/apps/server/package.json b/apps/server/package.json index 0334a3b5d..27c98c2e8 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -7,6 +7,7 @@ "dev": "tsx watch src/index.ts", "build": "tsdown", "start": "node dist/index.mjs", + "doctor": "react-doctor", "typecheck": "tsgo --noEmit", "test": "vitest run --passWithNoTests", "test:coverage": "vitest run --coverage --passWithNoTests", @@ -80,6 +81,7 @@ "@types/pg": "^8.20.0", "@types/react": "^19.2.15", "@typescript/native-preview": "7.0.0-dev.20260526.1", + "react-doctor": "^0.2.6", "tsdown": "^0.22.0", "tsx": "^4.22.3", "typescript": "^6.0.3", diff --git a/apps/web/package.json b/apps/web/package.json index dd6d60075..0518e98ef 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -8,6 +8,7 @@ "dev": "vite dev", "serve": "vite preview", "start": "vite preview", + "doctor": "react-doctor", "typecheck": "tsgo --noEmit", "test": "vitest run --passWithNoTests", "test:coverage": "vitest run --coverage --passWithNoTests", @@ -99,6 +100,7 @@ "@vitejs/plugin-react": "^6.0.2", "babel-plugin-macros": "^3.1.0", "babel-plugin-react-compiler": "^1.0.0", + "react-doctor": "^0.2.6", "typescript": "^6.0.3", "vite": "^8.0.14" } diff --git a/apps/web/src/components/input/github-stars-button.test.tsx b/apps/web/src/components/input/github-stars-button.test.tsx index 23188d8bb..9d32d0170 100644 --- a/apps/web/src/components/input/github-stars-button.test.tsx +++ b/apps/web/src/components/input/github-stars-button.test.tsx @@ -35,12 +35,12 @@ const renderButton = () => ); describe("GithubStarsButton", () => { - it("renders an anchor pointing at the project repo with rel=noopener and target=_blank", () => { + it("renders an anchor pointing at the project repo with rel=noopener noreferrer and target=_blank", () => { renderButton(); const link = screen.getByRole("button") as HTMLAnchorElement; expect(link.href).toBe("https://github.com/amruthpillai/reactive-resume"); expect(link.target).toBe("_blank"); - expect(link.rel).toBe("noopener"); + expect(link.rel).toBe("noopener noreferrer"); }); it("uses the no-count aria-label when star count hasn't loaded yet", () => { diff --git a/apps/web/src/components/input/github-stars-button.tsx b/apps/web/src/components/input/github-stars-button.tsx index 796544b6f..83aa67525 100644 --- a/apps/web/src/components/input/github-stars-button.tsx +++ b/apps/web/src/components/input/github-stars-button.tsx @@ -18,7 +18,12 @@ export function GithubStarsButton() { variant="outline" nativeButton={false} render={ - + MIT @@ -30,7 +30,7 @@ export function Copyright({ className, ...props }: Props) { A passion project by{" "} diff --git a/apps/web/src/dialogs/resume/import.tsx b/apps/web/src/dialogs/resume/import.tsx index 6e9d1da74..733c54fa9 100644 --- a/apps/web/src/dialogs/resume/import.tsx +++ b/apps/web/src/dialogs/resume/import.tsx @@ -6,7 +6,7 @@ import { DownloadSimpleIcon, FileIcon, UploadSimpleIcon } from "@phosphor-icons/ import { useStore } from "@tanstack/react-form"; import { useMutation, useQuery } from "@tanstack/react-query"; import { useNavigate } from "@tanstack/react-router"; -import { useEffect, useRef, useState } from "react"; +import { useRef, useState } from "react"; import { toast } from "sonner"; import z from "zod"; import { JSONResumeImporter } from "@reactive-resume/import/json-resume"; @@ -92,7 +92,6 @@ export function ImportResumeDialog(_: DialogProps<"resume.import">) { const navigate = useNavigate(); const closeDialog = useDialogStore((state) => state.closeDialog); - const prevTypeRef = useRef(""); const inputRef = useRef(null); const [isImporting, setIsImporting] = useState(false); @@ -207,12 +206,6 @@ export function ImportResumeDialog(_: DialogProps<"resume.import">) { const type = useStore(form.store, (s) => s.values.type); - useEffect(() => { - if (prevTypeRef.current === type) return; - prevTypeRef.current = type; - form.setFieldValue("file", undefined); - }, [form, type]); - const onSelectFile = () => { if (!inputRef.current) return; inputRef.current.click(); @@ -261,7 +254,9 @@ export function ImportResumeDialog(_: DialogProps<"resume.import">) { showClear={false} value={field.state.value} onValueChange={(value) => { - field.handleChange(value as ImportType); + const nextType = value as ImportType; + if (nextType !== field.state.value) form.setFieldValue("file", undefined); + field.handleChange(nextType); }} options={[ { diff --git a/apps/web/src/features/resume/preview/pdf-canvas.tsx b/apps/web/src/features/resume/preview/pdf-canvas.tsx index 350bc8752..496e99ec5 100644 --- a/apps/web/src/features/resume/preview/pdf-canvas.tsx +++ b/apps/web/src/features/resume/preview/pdf-canvas.tsx @@ -190,13 +190,8 @@ export function PdfCanvasPage({ ) : null} -
- +
+
); diff --git a/apps/web/src/hooks/use-confirm.tsx b/apps/web/src/hooks/use-confirm.tsx index 25360e08c..f9f743ace 100644 --- a/apps/web/src/hooks/use-confirm.tsx +++ b/apps/web/src/hooks/use-confirm.tsx @@ -64,8 +64,10 @@ export function ConfirmDialogProvider({ children }: { children: React.ReactNode setState((prev) => ({ ...prev, open: false, resolve: null })); }, [state.resolve]); + const contextValue = React.useMemo(() => ({ confirm }), [confirm]); + return ( - + {children} !open && handleCancel()}> diff --git a/apps/web/src/hooks/use-controlled-state.tsx b/apps/web/src/hooks/use-controlled-state.tsx index c127aae6b..a118aaef0 100644 --- a/apps/web/src/hooks/use-controlled-state.tsx +++ b/apps/web/src/hooks/use-controlled-state.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useState } from "react"; interface CommonControlledStateProps { value?: T; @@ -13,19 +13,17 @@ export function useControlledState( props: UseControlledStateProps, ): readonly [T, (next: T, ...args: Rest) => void] { const { value, defaultValue, onChange } = props; + const isControlled = value !== undefined; - const [state, setInternalState] = useState(value !== undefined ? value : (defaultValue as T)); - - useEffect(() => { - if (value !== undefined) setInternalState(value); - }, [value]); + const [internalState, setInternalState] = useState(value !== undefined ? value : (defaultValue as T)); + const state = isControlled ? (value as T) : internalState; const setState = useCallback( (next: T, ...args: Rest) => { - setInternalState(next); + if (!isControlled) setInternalState(next); onChange?.(next, ...args); }, - [onChange], + [isControlled, onChange], ); return [state, setState] as const; diff --git a/apps/web/src/hooks/use-mobile.tsx b/apps/web/src/hooks/use-mobile.tsx index 39fcce4c4..881d95af3 100644 --- a/apps/web/src/hooks/use-mobile.tsx +++ b/apps/web/src/hooks/use-mobile.tsx @@ -1,20 +1,25 @@ -import { useEffect, useState } from "react"; +import { useRef, useState, useSyncExternalStore } from "react"; const MOBILE_BREAKPOINT = 768; const MOBILE_QUERY = `(max-width: ${MOBILE_BREAKPOINT - 1}px)`; export function useIsMobile() { - const [isMobile, setIsMobile] = useState(undefined); + const [mediaQueryList] = useState(() => (typeof window === "undefined" ? null : window.matchMedia(MOBILE_QUERY))); + const latestMatchesRef = useRef(mediaQueryList?.matches ?? false); - useEffect(() => { - const mql = window.matchMedia(MOBILE_QUERY); - const onChange = (e: MediaQueryListEvent) => { - setIsMobile(e.matches); - }; - mql.addEventListener("change", onChange); - setIsMobile(mql.matches); - return () => mql.removeEventListener("change", onChange); - }, []); + return useSyncExternalStore( + (onStoreChange) => { + if (!mediaQueryList) return () => {}; - return !!isMobile; + const handleChange = (event: MediaQueryListEvent | { matches: boolean }) => { + latestMatchesRef.current = event.matches; + onStoreChange(); + }; + + mediaQueryList.addEventListener("change", handleChange); + return () => mediaQueryList.removeEventListener("change", handleChange); + }, + () => latestMatchesRef.current, + () => false, + ); } diff --git a/apps/web/src/hooks/use-prompt.tsx b/apps/web/src/hooks/use-prompt.tsx index 6dd85b885..5d8beefbf 100644 --- a/apps/web/src/hooks/use-prompt.tsx +++ b/apps/web/src/hooks/use-prompt.tsx @@ -102,8 +102,10 @@ export function PromptDialogProvider({ children }: { children: React.ReactNode } [handleConfirm], ); + const contextValue = React.useMemo(() => ({ prompt }), [prompt]); + return ( - + {children} !open && handleCancel()}> diff --git a/apps/web/src/routes/_home/-sections/donate.tsx b/apps/web/src/routes/_home/-sections/donate.tsx index 2ed818385..d91cb95ec 100644 --- a/apps/web/src/routes/_home/-sections/donate.tsx +++ b/apps/web/src/routes/_home/-sections/donate.tsx @@ -202,7 +202,7 @@ export const DonationBanner = () => ( nativeButton={false} className="h-11 gap-2 px-6" render={ -
+ + setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} diff --git a/apps/web/src/routes/_home/index.tsx b/apps/web/src/routes/_home/index.tsx index 86d11d6c2..0805f7d66 100644 --- a/apps/web/src/routes/_home/index.tsx +++ b/apps/web/src/routes/_home/index.tsx @@ -1,7 +1,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { createRootStructuredDataScript, getCanonicalRootUrl } from "@/libs/seo"; import { DonationBanner } from "./-sections/donate"; -import { FAQ } from "./-sections/faq"; +import { Faq } from "./-sections/faq"; import { Features } from "./-sections/features"; import { Footer } from "./-sections/footer"; import { Hero } from "./-sections/hero"; @@ -35,7 +35,7 @@ function RouteComponent() { - +
diff --git a/apps/web/src/routes/agent/$threadId.tsx b/apps/web/src/routes/agent/$threadId.tsx index 25045d8cb..8306cd533 100644 --- a/apps/web/src/routes/agent/$threadId.tsx +++ b/apps/web/src/routes/agent/$threadId.tsx @@ -994,6 +994,7 @@ function AgentChatComposer({ ref={fileInputRef} type="file" multiple + aria-label={t`Upload attachments`} className="hidden" onChange={(event) => onUploadFiles(event.target.files)} /> diff --git a/apps/web/src/routes/builder/$resumeId/-sidebar/left/sections/picture.tsx b/apps/web/src/routes/builder/$resumeId/-sidebar/left/sections/picture.tsx index 9c1fee485..2ddce0e7a 100644 --- a/apps/web/src/routes/builder/$resumeId/-sidebar/left/sections/picture.tsx +++ b/apps/web/src/routes/builder/$resumeId/-sidebar/left/sections/picture.tsx @@ -55,7 +55,14 @@ function PicturePreviewControls({ }) { return (
- +