From 2f95dc7d22e0c316ab79b31aeedf742e99701907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jasper=20=E5=BC=B5?= Date: Sat, 4 Apr 2026 17:01:41 +0800 Subject: [PATCH 1/9] refactor: use fluid typography and block spans in Hero component Replace fixed responsive text sizes with a fluid clamp-based `text-hero` token, and swap `
` line breaks for `block` spans with `text-balance` for better reflow. --- src/components/hero.tsx | 10 ++++------ src/styles/globals.css | 3 +++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/hero.tsx b/src/components/hero.tsx index a464aa6..82bf019 100644 --- a/src/components/hero.tsx +++ b/src/components/hero.tsx @@ -1,13 +1,11 @@ function Hero() { return ( -

- I'm Jasper & I engineer -
- +

+ I'm Jasper & I engineer + design aspirations -
- into functional reality + into functional reality

); } diff --git a/src/styles/globals.css b/src/styles/globals.css index 4cdd1e0..c8b142c 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -25,6 +25,9 @@ } @theme { + --text-hero: clamp(1.875rem, 4vw + 1rem, 3rem); + --text-hero--line-height: 1.25; + --font-sans: "Manrope", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; From 878f25ba35cb7d7267bb317ad8085c2df48d0995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jasper=20=E5=BC=B5?= Date: Sat, 4 Apr 2026 17:10:22 +0800 Subject: [PATCH 2/9] docs: add comments to fluid hero typography tokens --- src/styles/globals.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/styles/globals.css b/src/styles/globals.css index c8b142c..d3ed000 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -25,7 +25,9 @@ } @theme { + /* fluid: text-3xl (1.875rem) β†’ text-5xl (3rem), scaling via viewport width, capped at ~800px */ --text-hero: clamp(1.875rem, 4vw + 1rem, 3rem); + /* 1.25Γ— font-size, tracks fluid size */ --text-hero--line-height: 1.25; --font-sans: From 8f13cdc9207d016d2a4a132475c216e769abd327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jasper=20=E5=BC=B5?= Date: Sat, 4 Apr 2026 21:59:59 +0800 Subject: [PATCH 3/9] style: change tab border radius from rounded-md to rounded-full --- src/components/ui/tabs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx index af6fa75..64232e4 100644 --- a/src/components/ui/tabs.tsx +++ b/src/components/ui/tabs.tsx @@ -54,7 +54,7 @@ function TabsTab({ className, ...props }: TabsPrimitive.Tab.Props) { return ( Date: Sun, 5 Apr 2026 15:06:50 +0800 Subject: [PATCH 4/9] refactor: lift animation and highlight state up from tab subcomponents Moves motion animation props and shouldHighlight state from AboutTab/RecruiterTab into the parent TabsPanel render prop, enabling layout-aware animations at the panel level. --- src/components/description.tsx | 66 +++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 25 deletions(-) diff --git a/src/components/description.tsx b/src/components/description.tsx index 5f7cc5b..976d2c7 100644 --- a/src/components/description.tsx +++ b/src/components/description.tsx @@ -13,6 +13,7 @@ const RECRUITER: TabValues = "recruiter"; function Description() { const [tab, setTab] = useState(ABOUT); const [hasInteracted, setHasInteracted] = useState(false); + const [shouldHighlight, setShouldHighlight] = useState(false); return ( - - + + } + key={`${ABOUT}-${tab}`} + > + - - + { + setShouldHighlight(true); + }} + /> + } + key={`${RECRUITER}-${tab}`} + ref={() => { + return () => { + setShouldHighlight(false); + }; + }} + > + ); } -function AboutTab({ hasInteracted }: { hasInteracted: boolean }) { +function AboutTab() { return ( - + <>

A design-minded engineer focused on building intuitive and user-friendly web experiences across the stack to improve how people interact with digital products and make them @@ -67,23 +93,13 @@ function AboutTab({ hasInteracted }: { hasInteracted: boolean }) { πŸ‡ΈπŸ‡¬ Singapore and love exploring the city for new cafes and restaurants.

-
+ ); } -function RecruiterTab() { - const [shouldHighlight, setShouldHighlight] = useState(false); - +function RecruiterTab({ shouldHighlight }: { shouldHighlight: boolean }) { return ( - { - setShouldHighlight(true); - }} - className="flex flex-col gap-y-3 font-geist text-sm sm:text-base" - > + <>

I am currently open to new employment opportunities and collaborations.

As a design-minded engineer, I gravitate towards product-oriented engineering roles. My @@ -109,7 +125,7 @@ function RecruiterTab() { - + ); } From cd3a29a8c0744f84d0a9a90934bc78b4bd6066e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jasper=20=E5=BC=B5?= Date: Sun, 5 Apr 2026 15:07:20 +0800 Subject: [PATCH 5/9] feat: add radial gradient background using new select color token Introduces an orange-tinted select color token for light and dark themes and applies it as a fixed radial gradient on the body background. --- src/routes/__root.tsx | 2 +- src/styles/globals.css | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 9351dd4..b0d55e2 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -44,7 +44,7 @@ export const Route = createRootRoute({ - + {children} diff --git a/src/styles/globals.css b/src/styles/globals.css index d3ed000..028434e 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -62,6 +62,8 @@ --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); + --color-select: var(--select); + --color-muted: var(--muted); --color-muted-foreground: var(--muted-foreground); @@ -128,6 +130,8 @@ --accent: oklch(0.9257 0.014973 93.1764); /* Gold 4 */ --accent-foreground: oklch(0.2434 0.0079 96.31); /* Sand 12 */ + --select: oklch(0.8904 0.106551 70.8359); /* Orange 5 */ + --muted: oklch(0.9099 0.0038 95.91); /* Sand 5 */ --muted-foreground: oklch(0.4979 0.0079 106.68); /* Sand 11 */ @@ -180,6 +184,8 @@ --accent: oklch(0.2908 0.0095 86.76); /* Gold 4 */ --accent-foreground: oklch(0.9483 0.0026 106.45); /* Sand 12 */ + --select: oklch(0.3346 0.0868 58.09); /* Orange 5 */ + --muted: oklch(0.3124 0.0044 100.73); /* Sand 5 */ --muted-foreground: oklch(0.7666 0.009135 97.3831); /* Sand 11 */ From 3e0d2a5cb5055844883fe70853b3023b4ff71031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jasper=20=E5=BC=B5?= Date: Sun, 5 Apr 2026 15:07:51 +0800 Subject: [PATCH 6/9] feat: support custom render element in Button via useRender Uses Base UI's useRender hook to allow Button to render as any element (e.g. Link, anchor) when nativeButton is false, enabling semantic rendering for navigation use cases. --- src/components/ui/button.tsx | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index d758866..5d379b6 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,4 +1,6 @@ import { Button as ButtonPrimitive } from "@base-ui/react/button"; +import { mergeProps } from "@base-ui/react/merge-props"; +import { useRender } from "@base-ui/react/use-render"; import type { VariantProps } from "class-variance-authority"; import { cva } from "class-variance-authority"; @@ -45,13 +47,31 @@ const buttonVariants = cva( } ); -type ButtonProps = ButtonPrimitive.Props & VariantProps; +type ButtonProps = ButtonPrimitive.Props & + VariantProps & + useRender.ComponentProps<"button">; + +function Button({ className, variant, size, nativeButton, render, ...props }: ButtonProps) { + const renderResult = useRender({ + enabled: nativeButton === false, + defaultTagName: "button", + render, + props: mergeProps( + { "data-slot": "button", className: cn(buttonVariants({ variant, size, className })) }, + props + ), + }); + + if (nativeButton === false) { + return renderResult; + } -function Button({ className, variant, size, ...props }: ButtonProps) { return ( ); From 4a1c1e24b202e7a32104459269751a9cd7ae7846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jasper=20=E5=BC=B5?= Date: Sun, 5 Apr 2026 15:08:09 +0800 Subject: [PATCH 7/9] chore: switch oxlint ESLint integration to use imported config object --- eslint.config.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/eslint.config.ts b/eslint.config.ts index 461a75f..9b5d47a 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -12,6 +12,8 @@ import eslintPluginUnicorn from "eslint-plugin-unicorn"; import { defineConfig, globalIgnores } from "eslint/config"; import tseslint from "typescript-eslint"; +import oxlintConfig from "./oxlint.config"; + const gitignorePath = path.join(import.meta.dirname, ".gitignore"); const eslintConfig = defineConfig([ @@ -48,7 +50,7 @@ const eslintConfig = defineConfig([ }, }, }, - oxlint.buildFromOxlintConfigFile("./oxlint.config.ts"), + oxlint.buildFromOxlintConfig(oxlintConfig), ]); export default eslintConfig; From 1cdfad5d624f3c60e4aa19f7a519bd6356c193c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jasper=20=E5=BC=B5?= Date: Sun, 5 Apr 2026 16:32:17 +0800 Subject: [PATCH 8/9] a11y: improve semantic HTML and ARIA attributes Use dl/dt/dd for experience list, nav for socials, h2 for section headings, and aria-hidden on decorative separators and icons. --- src/components/experience.tsx | 10 +++++----- src/components/socials.tsx | 11 ++++++----- src/routes/index.tsx | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/components/experience.tsx b/src/components/experience.tsx index 098deb6..d31d8df 100644 --- a/src/components/experience.tsx +++ b/src/components/experience.tsx @@ -22,16 +22,16 @@ function Experience({ ref }: Pick, "ref">) { return (

Experience

-
+
{EXPERIENCES.map(({ company, role }) => ( - +
{company} - - {role} +
+
{role}
))} -
+
); } diff --git a/src/components/socials.tsx b/src/components/socials.tsx index b826224..48c03c8 100644 --- a/src/components/socials.tsx +++ b/src/components/socials.tsx @@ -39,11 +39,11 @@ function Socials({ ref }: Pick, "ref">) { return (
-

Find me on

-
+

Find me on

+
+
From dc8745f74f52a6080fd03d62fc1f7548f40b2f2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jasper=20=E5=BC=B5?= Date: Sun, 5 Apr 2026 18:26:04 +0800 Subject: [PATCH 9/9] refactor: reset shouldHighlight in onValueChange instead of ref cleanup Move setShouldHighlight(false) to the tab change handler where it belongs, removing the fragile ref callback cleanup pattern. --- src/components/description.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/description.tsx b/src/components/description.tsx index 976d2c7..5f6871b 100644 --- a/src/components/description.tsx +++ b/src/components/description.tsx @@ -21,6 +21,7 @@ function Description() { value={tab} onValueChange={(value: TabValues) => { setHasInteracted(true); + setShouldHighlight(false); setTab(value); }} > @@ -63,11 +64,6 @@ function Description() { /> } key={`${RECRUITER}-${tab}`} - ref={() => { - return () => { - setShouldHighlight(false); - }; - }} >