Skip to content
Merged
4 changes: 3 additions & 1 deletion eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down Expand Up @@ -48,7 +50,7 @@ const eslintConfig = defineConfig([
},
},
},
oxlint.buildFromOxlintConfigFile("./oxlint.config.ts"),
oxlint.buildFromOxlintConfig(oxlintConfig),
]);

export default eslintConfig;
62 changes: 37 additions & 25 deletions src/components/description.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ const RECRUITER: TabValues = "recruiter";
function Description() {
const [tab, setTab] = useState<TabValues>(ABOUT);
const [hasInteracted, setHasInteracted] = useState(false);
const [shouldHighlight, setShouldHighlight] = useState(false);

return (
<Tabs
defaultValue={ABOUT}
value={tab}
onValueChange={(value: TabValues) => {
setHasInteracted(true);
setShouldHighlight(false);
setTab(value);
}}
>
Expand All @@ -33,26 +35,46 @@ function Description() {
</TabsList>

<motion.div layout="size" className="overflow-clip">
<TabsPanel value={ABOUT} className="px-1 py-3" key={`${ABOUT}-${tab}`}>
<AboutTab hasInteracted={hasInteracted} />
<TabsPanel
value={ABOUT}
className="flex flex-col gap-y-3 px-1 py-3 font-geist text-sm sm:text-base"
render={
<motion.div
layout
initial={hasInteracted && { opacity: 0.25, filter: "blur(4px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
/>
}
key={`${ABOUT}-${tab}`}
>
<AboutTab />
</TabsPanel>

<TabsPanel value={RECRUITER} className="px-1 py-3" key={`${RECRUITER}-${tab}`}>
<RecruiterTab />
<TabsPanel
value={RECRUITER}
className="flex flex-col gap-y-3 px-1 py-3 font-geist text-sm sm:text-base"
render={
<motion.div
layout
initial={{ opacity: 0.25, filter: "blur(4px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
onAnimationComplete={() => {
setShouldHighlight(true);
}}
/>
}
key={`${RECRUITER}-${tab}`}
>
<RecruiterTab shouldHighlight={shouldHighlight} />
</TabsPanel>
</motion.div>
</Tabs>
);
}

function AboutTab({ hasInteracted }: { hasInteracted: boolean }) {
function AboutTab() {
return (
<motion.div
layout
initial={hasInteracted && { opacity: 0.25, filter: "blur(4px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
className="flex flex-col gap-y-3 font-geist text-sm sm:text-base"
>
<>
<p>
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
Expand All @@ -67,23 +89,13 @@ function AboutTab({ hasInteracted }: { hasInteracted: boolean }) {
<strong className="font-[450] text-primary-highlight">🇸🇬 Singapore</strong> and love
exploring the city for new cafes and restaurants.
</p>
</motion.div>
</>
);
}

function RecruiterTab() {
const [shouldHighlight, setShouldHighlight] = useState(false);

function RecruiterTab({ shouldHighlight }: { shouldHighlight: boolean }) {
return (
<motion.div
layout
initial={{ opacity: 0.25, filter: "blur(4px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
onAnimationComplete={() => {
setShouldHighlight(true);
}}
className="flex flex-col gap-y-3 font-geist text-sm sm:text-base"
>
<>
<p>I am currently open to new employment opportunities and collaborations.</p>
<p>
As a design-minded engineer, I gravitate towards product-oriented engineering roles. My
Expand All @@ -109,7 +121,7 @@ function RecruiterTab() {
<span aria-hidden="true">{"->"}</span>
<CVButton />
</Highlighter>
</motion.div>
</>
);
}

Expand Down
10 changes: 5 additions & 5 deletions src/components/experience.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@ function Experience({ ref }: Pick<ComponentProps<"div">, "ref">) {
return (
<div ref={ref} className="flex flex-col gap-y-2">
<h2 className="text-lg font-semibold">Experience</h2>
<div className="grid max-w-md grid-cols-12 gap-y-1">
<dl className="grid max-w-md grid-cols-12 gap-y-1">
{EXPERIENCES.map(({ company, role }) => (
<Fragment key={`${company}${role}`}>
<span className="col-span-4 font-semibold tracking-wide text-primary-highlight">
<dt className="col-span-4 font-semibold tracking-wide text-primary-highlight">
{company}
</span>
<span className="col-span-8 text-muted-foreground">{role}</span>
</dt>
<dd className="col-span-8 text-muted-foreground">{role}</dd>
</Fragment>
))}
</div>
</dl>
</div>
);
}
Expand Down
10 changes: 4 additions & 6 deletions src/components/hero.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
function Hero() {
return (
<h1 className="text-3xl/9.5 font-bold tracking-tight sm:text-5xl/15">
I'm Jasper & I engineer
<br />
<span className="tracking-normal text-nowrap text-primary-hover italic underline underline-offset-4 sm:underline-offset-6">
<h1 className="text-hero font-bold tracking-tight text-balance">
<span className="block">I'm Jasper & I engineer</span>
<span className="block tracking-normal text-primary-hover italic underline underline-offset-4 sm:underline-offset-6">
design aspirations
</span>
<br />
into functional reality
<span className="block">into functional reality</span>
</h1>
);
}
Expand Down
11 changes: 6 additions & 5 deletions src/components/socials.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ function Socials({ ref }: Pick<ComponentProps<"div">, "ref">) {
return (
<div ref={ref} className="-mx-2 flex flex-col gap-y-4">
<div className="flex w-fit flex-col gap-y-0.5">
<p className="px-2 font-medium">Find me on</p>
<div className="flex flex-wrap items-center-safe gap-x-0.5">
<h2 className="px-2 font-medium">Find me on</h2>
<nav className="flex flex-wrap items-center-safe gap-x-0.5">
{SOCIALS_ARRAY.map(({ icon, label, href }) => (
<Fragment key={label}>
<Separator orientation="vertical" className="h-6 first:hidden" />
<Separator aria-hidden="true" orientation="vertical" className="h-6 first:hidden" />
<Button
variant="link"
size="sm"
Expand All @@ -58,11 +58,11 @@ function Socials({ ref }: Pick<ComponentProps<"div">, "ref">) {
</Button>
</Fragment>
))}
</div>
</nav>
</div>

<div className="flex w-fit flex-col gap-y-0.5">
<p className="px-2 font-medium">Or hit me up at</p>
<h2 className="px-2 font-medium">Or hit me up at</h2>
<Button
variant="link"
size="sm"
Expand All @@ -71,6 +71,7 @@ function Socials({ ref }: Pick<ComponentProps<"div">, "ref">) {
render={<a href={`mailto:${EMAIL}`} />}
>
<EnvelopeOpenHeart
aria-hidden="true"
className="group-hover/email:[--secondary-fill:var(--primary)] group-data-pressed/email:[--secondary-fill:var(--primary)]"
secondaryfill="var(--secondary-fill)"
/>
Expand Down
24 changes: 22 additions & 2 deletions src/components/ui/button.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -45,13 +47,31 @@ const buttonVariants = cva(
}
);

type ButtonProps = ButtonPrimitive.Props & VariantProps<typeof buttonVariants>;
type ButtonProps = ButtonPrimitive.Props &
VariantProps<typeof buttonVariants> &
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 (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
nativeButton={nativeButton}
render={render}
{...props}
/>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function TabsTab({ className, ...props }: TabsPrimitive.Tab.Props) {
return (
<TabsPrimitive.Tab
className={cn(
"relative flex h-9 shrink-0 grow cursor-pointer items-center justify-center gap-1.5 rounded-md border border-transparent px-[calc(--spacing(2.5)-1px)] text-base font-medium whitespace-nowrap transition-[color,background-color,box-shadow] outline-none hover:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring data-active:text-foreground data-disabled:pointer-events-none data-disabled:opacity-64 data-[orientation=vertical]:w-full data-[orientation=vertical]:justify-start sm:h-8 sm:text-sm [&_svg]:pointer-events-none [&_svg]:-mx-0.5 [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4",
"relative flex h-9 shrink-0 grow cursor-pointer items-center justify-center gap-1.5 rounded-full border border-transparent px-[calc(--spacing(2.5)-1px)] text-base font-medium whitespace-nowrap transition-[color,background-color,box-shadow] outline-none hover:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring data-active:text-foreground data-disabled:pointer-events-none data-disabled:opacity-64 data-[orientation=vertical]:w-full data-[orientation=vertical]:justify-start sm:h-8 sm:text-sm [&_svg]:pointer-events-none [&_svg]:-mx-0.5 [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4",
className
)}
data-slot="tabs-tab"
Expand Down
2 changes: 1 addition & 1 deletion src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const Route = createRootRoute({
<head>
<HeadContent />
</head>
<body className="relative grid scroll-smooth bg-background text-foreground antialiased">
<body className="relative grid scroll-smooth bg-background bg-[radial-gradient(ellipse_80%_60%_at_50%_0%,color-mix(in_oklch,var(--select)_20%,transparent),transparent_65%)] bg-fixed text-foreground antialiased">
{children}
<Scripts />
<Devtools enabled />
Expand Down
2 changes: 1 addition & 1 deletion src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function Page() {
<Description />
<MotionSocials layout="position" />
<MotionExperience layout="position" />
<MotionSeparator layout="position" className="-mx-1 my-1" />
<MotionSeparator aria-hidden="true" layout="position" className="-mx-1 my-1" />
</main>
</div>
</MotionConfig>
Expand Down
11 changes: 11 additions & 0 deletions src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
}

@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:
"Manrope", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol", "Noto Color Emoji";
Expand Down Expand Up @@ -57,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);

Expand Down Expand Up @@ -123,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 */

Expand Down Expand Up @@ -175,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 */

Expand Down