@@ -2,9 +2,18 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<meta name="theme-color" content="#e9e9e4" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Julien Valverde — full-stack engineer and product builder working on Effect, developer tools, and useful vertical software."
|
||||
/>
|
||||
<meta property="og:title" content="Julien Valverde — Full-stack engineer & product builder" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Open-source tools, production software, and systems currently taking shape."
|
||||
/>
|
||||
<title>Julien Valverde — Full-stack engineer & product builder</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
"react-dom": "^19.2.6",
|
||||
"react-i18next": "^17.0.8",
|
||||
"react-icons": "^5.6.0",
|
||||
"react-shadow": "^20.6.0",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tailwindcss": "^4.3.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { ArrowUpRight } from "lucide-react"
|
||||
|
||||
interface ProjectCardProps {
|
||||
readonly index: string
|
||||
readonly title: string
|
||||
readonly category: string
|
||||
readonly description: string
|
||||
readonly tags: ReadonlyArray<string>
|
||||
readonly href?: string
|
||||
readonly linkLabel?: string
|
||||
readonly variant?: "light" | "dark" | "outline"
|
||||
}
|
||||
|
||||
export function ProjectCard({
|
||||
index,
|
||||
title,
|
||||
category,
|
||||
description,
|
||||
tags,
|
||||
href,
|
||||
linkLabel = "View project",
|
||||
variant = "light",
|
||||
}: ProjectCardProps) {
|
||||
return (
|
||||
<article className={`project-card project-card--${variant}`}>
|
||||
<div className="project-card__topline">
|
||||
<span>{index}</span>
|
||||
<span>{category}</span>
|
||||
</div>
|
||||
|
||||
<div className="project-card__body">
|
||||
<h3>{title}</h3>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
|
||||
<div className="project-card__footer">
|
||||
<ul className="tag-list" aria-label={`${title} technologies and themes`}>
|
||||
{tags.map(tag => <li key={tag}>{tag}</li>)}
|
||||
</ul>
|
||||
|
||||
{href
|
||||
? (
|
||||
<a className="project-card__link" href={href} target="_blank" rel="noreferrer">
|
||||
{linkLabel}
|
||||
<ArrowUpRight aria-hidden="true" />
|
||||
</a>
|
||||
)
|
||||
: <span className="project-card__status">In development <span aria-hidden="true">●</span></span>}
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
interface SectionHeadingProps {
|
||||
readonly eyebrow: string
|
||||
readonly title: string
|
||||
readonly description?: string
|
||||
}
|
||||
|
||||
export function SectionHeading({ eyebrow, title, description }: SectionHeadingProps) {
|
||||
return (
|
||||
<header className="section-heading">
|
||||
<p className="section-heading__eyebrow">{eyebrow}</p>
|
||||
<div className="section-heading__body">
|
||||
<h2>{title}</h2>
|
||||
{description && <p>{description}</p>}
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
export function Win98Status() {
|
||||
return (
|
||||
<div className="win98-host">
|
||||
<div className="win98-window">
|
||||
<div className="win98-title-bar">
|
||||
<div className="win98-title-bar__text">julien.exe — System status</div>
|
||||
<div className="win98-title-bar__controls" aria-hidden="true">
|
||||
<span className="win98-control win98-control--minimize" />
|
||||
<span className="win98-control win98-control--maximize" />
|
||||
<span className="win98-control win98-control--close" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="win98-body">
|
||||
<div className="win98-copy">
|
||||
<strong>Building software that earns its place.</strong>
|
||||
Full-stack product engineering with a taste for typed systems, sharp interfaces, and useful tools.
|
||||
</div>
|
||||
|
||||
<ul className="win98-status-list">
|
||||
<li><span>Location</span><span>Chambéry, FR</span></li>
|
||||
<li><span>Current mode</span><span>Shipping</span></li>
|
||||
<li><span>Signal</span><span>Available</span></li>
|
||||
</ul>
|
||||
|
||||
<div className="win98-status-bar">
|
||||
<p>Effect / TypeScript</p>
|
||||
<p>5 projects loaded</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+911
-47
@@ -1,58 +1,922 @@
|
||||
/** biome-ignore-all lint/suspicious/noUnknownAtRules: Tailwind */
|
||||
/** biome-ignore-all lint/style/noDescendingSpecificity: Component selectors intentionally share a single stylesheet */
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "@fontsource/work-sans";
|
||||
@import "@fontsource/work-sans/400.css";
|
||||
@import "@fontsource/work-sans/500.css";
|
||||
@import "@fontsource/work-sans/600.css";
|
||||
@import "@fontsource/work-sans/700.css";
|
||||
@import "@fontsource/work-sans/800.css";
|
||||
|
||||
@font-face {
|
||||
font-family: "Pixelated MS Sans Serif";
|
||||
src: url("/98.css/fonts/converted/ms_sans_serif.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Pixelated MS Sans Serif";
|
||||
src: url("/98.css/fonts/converted/ms_sans_serif_bold.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: hsl(220 23% 95%);
|
||||
--secondary-background: oklch(100% 0 0);
|
||||
--foreground: oklch(0% 0 0);
|
||||
--main-foreground: oklch(0% 0 0);
|
||||
--main: hsl(220 23% 95%);
|
||||
--border: oklch(0% 0 0);
|
||||
--ring: oklch(0% 0 0);
|
||||
--overlay: oklch(0% 0 0 / 0.8);
|
||||
--shadow: 4px 4px 0px 0px var(--border);
|
||||
--chart-1: #FF7A05;
|
||||
--chart-2: #0099FF;
|
||||
--chart-3: #FFBF00;
|
||||
--chart-4: #00D696;
|
||||
--chart-5: #7A83FF;
|
||||
--chart-active-dot: #000;
|
||||
--paper: #e9e9e4;
|
||||
--ink: #0b0b0b;
|
||||
--white: #fff;
|
||||
--gray: #c9c9c4;
|
||||
--line: 2px solid var(--ink);
|
||||
--shadow: 7px 7px 0 var(--ink);
|
||||
color: var(--ink);
|
||||
background: var(--paper);
|
||||
font-family: "Work Sans", Arial, sans-serif;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-main: var(--main);
|
||||
--color-background: var(--background);
|
||||
--color-secondary-background: var(--secondary-background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-main-foreground: var(--main-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-overlay: var(--overlay);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
|
||||
--spacing-boxShadowX: 4px;
|
||||
--spacing-boxShadowY: 4px;
|
||||
--spacing-reverseBoxShadowX: -4px;
|
||||
--spacing-reverseBoxShadowY: -4px;
|
||||
--radius-base: 5px;
|
||||
--shadow-shadow: var(--shadow);
|
||||
--font-weight-base: 500;
|
||||
--font-weight-heading: 700;
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply text-foreground font-base bg-background;
|
||||
font-family: "Work Sans", sans-serif;
|
||||
}
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6{
|
||||
@apply font-heading;
|
||||
body {
|
||||
min-width: 320px;
|
||||
margin: 0;
|
||||
overflow-x: clip;
|
||||
background:
|
||||
linear-gradient(90deg, rgb(0 0 0 / 4%) 1px, transparent 1px),
|
||||
linear-gradient(rgb(0 0 0 / 4%) 1px, transparent 1px),
|
||||
var(--paper);
|
||||
background-size: 32px 32px;
|
||||
}
|
||||
|
||||
body,
|
||||
a,
|
||||
button {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
button:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::selection {
|
||||
color: var(--white);
|
||||
background: var(--ink);
|
||||
}
|
||||
|
||||
.site-header {
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 88px;
|
||||
padding: 16px clamp(20px, 4vw, 64px);
|
||||
border-bottom: var(--line);
|
||||
background: rgb(233 233 228 / 92%);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.site-mark {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.site-mark > span:first-child {
|
||||
display: grid;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
place-items: center;
|
||||
border: var(--line);
|
||||
background: var(--ink);
|
||||
color: var(--white);
|
||||
font-size: 18px;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.site-header nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: clamp(20px, 3vw, 48px);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.site-header nav a {
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.site-header nav a:hover,
|
||||
.site-header nav a:focus-visible {
|
||||
border-color: var(--ink);
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.55fr) minmax(320px, 0.65fr);
|
||||
min-height: calc(100svh - 88px);
|
||||
border-bottom: var(--line);
|
||||
}
|
||||
|
||||
.hero__intro {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
padding: clamp(42px, 7vw, 104px) clamp(20px, 4vw, 64px) 48px;
|
||||
border-right: var(--line);
|
||||
}
|
||||
|
||||
.hero__kicker,
|
||||
.section-heading__eyebrow,
|
||||
.repo-section__eyebrow {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
max-width: 1000px;
|
||||
margin: 44px 0;
|
||||
font-size: clamp(68px, 10.5vw, 180px);
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.085em;
|
||||
line-height: 0.76;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.hero h1 span {
|
||||
display: block;
|
||||
color: var(--paper);
|
||||
-webkit-text-stroke: clamp(1.5px, 0.16vw, 3px) var(--ink);
|
||||
text-indent: 0.22em;
|
||||
}
|
||||
|
||||
.hero__footer {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.hero__footer > p {
|
||||
max-width: 570px;
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: clamp(17px, 1.5vw, 22px);
|
||||
font-weight: 500;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.brutal-button {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
min-height: 54px;
|
||||
padding: 0 20px;
|
||||
border: var(--line);
|
||||
background: var(--white);
|
||||
box-shadow: var(--shadow);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
transition: transform 120ms ease, box-shadow 120ms ease;
|
||||
}
|
||||
|
||||
.brutal-button svg {
|
||||
width: 17px;
|
||||
}
|
||||
|
||||
.brutal-button:hover,
|
||||
.brutal-button:focus-visible {
|
||||
box-shadow: 2px 2px 0 var(--ink);
|
||||
transform: translate(5px, 5px);
|
||||
}
|
||||
|
||||
.hero__status {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: clamp(32px, 5vw, 72px);
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle, var(--ink) 1px, transparent 1.5px) 0 0 / 18px 18px,
|
||||
var(--gray);
|
||||
}
|
||||
|
||||
.hero__status::before {
|
||||
position: absolute;
|
||||
width: 230px;
|
||||
height: 230px;
|
||||
border: 3px solid var(--ink);
|
||||
border-radius: 50%;
|
||||
content: "";
|
||||
inset: -90px -80px auto auto;
|
||||
}
|
||||
|
||||
.hero__stamp {
|
||||
position: absolute;
|
||||
right: -39px;
|
||||
bottom: 82px;
|
||||
padding: 7px 16px;
|
||||
border: var(--line);
|
||||
background: var(--paper);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.16em;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.win98-host {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: min(100%, 430px);
|
||||
margin: auto;
|
||||
filter: drop-shadow(10px 10px 0 rgb(0 0 0 / 80%));
|
||||
transform: rotate(1.5deg);
|
||||
transition: transform 180ms ease;
|
||||
}
|
||||
|
||||
.win98-host:hover {
|
||||
transform: rotate(0);
|
||||
}
|
||||
|
||||
.win98-window {
|
||||
width: 100%;
|
||||
padding: 3px;
|
||||
background: #c0c0c0;
|
||||
box-shadow:
|
||||
inset -1px -1px #0a0a0a,
|
||||
inset 1px 1px #dfdfdf,
|
||||
inset -2px -2px #808080,
|
||||
inset 2px 2px #fff;
|
||||
color: #111;
|
||||
font-family: "Pixelated MS Sans Serif", Arial, sans-serif;
|
||||
-webkit-font-smoothing: none;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.win98-title-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 20px;
|
||||
padding: 3px 2px 3px 3px;
|
||||
background: linear-gradient(90deg, #111, #666);
|
||||
}
|
||||
|
||||
.win98-title-bar__text {
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.win98-title-bar__controls {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.win98-control {
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 14px;
|
||||
background-color: #c0c0c0;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
box-shadow:
|
||||
inset -1px -1px #0a0a0a,
|
||||
inset 1px 1px #fff,
|
||||
inset -2px -2px #808080,
|
||||
inset 2px 2px #dfdfdf;
|
||||
}
|
||||
|
||||
.win98-control--minimize {
|
||||
background-image: url("/98.css/icon/minimize.svg");
|
||||
}
|
||||
|
||||
.win98-control--maximize {
|
||||
background-image: url("/98.css/icon/maximize.svg");
|
||||
}
|
||||
|
||||
.win98-control--close {
|
||||
background-image: url("/98.css/icon/close.svg");
|
||||
}
|
||||
|
||||
.win98-body {
|
||||
margin: 16px 12px 12px;
|
||||
}
|
||||
|
||||
.win98-copy {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.win98-copy strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.win98-status-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin: 16px 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.win98-status-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.win98-status-list span:last-child {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.win98-status-bar {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.win98-status-bar p {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
padding: 3px 5px;
|
||||
box-shadow: inset -1px -1px #dfdfdf, inset 1px 1px #808080;
|
||||
}
|
||||
|
||||
.ticker {
|
||||
overflow: hidden;
|
||||
border-bottom: var(--line);
|
||||
background: var(--ink);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.ticker > div {
|
||||
display: flex;
|
||||
width: max-content;
|
||||
align-items: center;
|
||||
gap: 26px;
|
||||
padding: 14px 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
animation: marquee 24s linear infinite;
|
||||
}
|
||||
|
||||
.ticker i {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@keyframes marquee {
|
||||
to {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.work-section,
|
||||
.upcoming-section {
|
||||
padding: clamp(76px, 10vw, 150px) clamp(20px, 4vw, 64px);
|
||||
border-bottom: var(--line);
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(160px, 0.38fr) minmax(0, 1fr);
|
||||
gap: clamp(28px, 5vw, 90px);
|
||||
max-width: 1440px;
|
||||
margin: 0 auto clamp(48px, 7vw, 100px);
|
||||
}
|
||||
|
||||
.section-heading__eyebrow {
|
||||
padding-top: 14px;
|
||||
border-top: var(--line);
|
||||
}
|
||||
|
||||
.section-heading__body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(220px, 0.42fr);
|
||||
gap: 40px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.section-heading h2,
|
||||
.repo-section h2 {
|
||||
margin: 0;
|
||||
font-size: clamp(46px, 6.2vw, 100px);
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.065em;
|
||||
line-height: 0.9;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.section-heading__body > p {
|
||||
max-width: 420px;
|
||||
margin: 0 0 5px;
|
||||
font-size: 17px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.project-grid,
|
||||
.upcoming-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
gap: clamp(22px, 3vw, 44px);
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.project-grid .project-card:first-child {
|
||||
grid-column: span 7;
|
||||
}
|
||||
|
||||
.project-grid .project-card:nth-child(2) {
|
||||
grid-column: span 5;
|
||||
margin-top: 90px;
|
||||
}
|
||||
|
||||
.project-grid .project-card:nth-child(3) {
|
||||
grid-column: 3 / span 8;
|
||||
}
|
||||
|
||||
.upcoming-grid .project-card {
|
||||
grid-column: span 6;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 480px;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: clamp(24px, 3.5vw, 52px);
|
||||
border: var(--line);
|
||||
background: var(--white);
|
||||
box-shadow: var(--shadow);
|
||||
transition: transform 160ms ease, box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
.project-card:hover {
|
||||
box-shadow: 11px 11px 0 var(--ink);
|
||||
transform: translate(-4px, -4px);
|
||||
}
|
||||
|
||||
.project-card--dark {
|
||||
background: var(--ink);
|
||||
box-shadow: 7px 7px 0 #777;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.project-card--dark:hover {
|
||||
box-shadow: 11px 11px 0 #777;
|
||||
}
|
||||
|
||||
.project-card--outline {
|
||||
background:
|
||||
repeating-linear-gradient(-45deg, transparent 0 12px, rgb(0 0 0 / 4%) 12px 24px),
|
||||
var(--paper);
|
||||
}
|
||||
|
||||
.project-card__topline {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid currentColor;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.project-card__body {
|
||||
margin: 70px 0;
|
||||
}
|
||||
|
||||
.project-card h3 {
|
||||
margin: 0 0 28px;
|
||||
font-size: clamp(46px, 6vw, 92px);
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.075em;
|
||||
line-height: 0.86;
|
||||
}
|
||||
|
||||
.project-card__body p {
|
||||
max-width: 680px;
|
||||
margin: 0;
|
||||
font-size: clamp(16px, 1.35vw, 20px);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.project-card__footer {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.tag-list li {
|
||||
padding: 6px 9px;
|
||||
border: 1px solid currentColor;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.project-card__link,
|
||||
.project-card__status {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.project-card__link {
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 2px solid currentColor;
|
||||
}
|
||||
|
||||
.project-card__link svg {
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
.project-card__link:hover svg {
|
||||
transform: translate(2px, -2px);
|
||||
}
|
||||
|
||||
.project-card__status span {
|
||||
animation: blink 1.5s steps(1) infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
.upcoming-section {
|
||||
background: var(--gray);
|
||||
}
|
||||
|
||||
.about-section {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 0.35fr) minmax(0, 1fr);
|
||||
min-height: 720px;
|
||||
border-bottom: var(--line);
|
||||
}
|
||||
|
||||
.about-section__label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: clamp(32px, 5vw, 72px);
|
||||
border-right: var(--line);
|
||||
background: var(--ink);
|
||||
color: var(--white);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.about-section__label span:first-child {
|
||||
font-size: clamp(76px, 11vw, 180px);
|
||||
letter-spacing: -0.09em;
|
||||
line-height: 0.8;
|
||||
}
|
||||
|
||||
.about-section__copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: clamp(60px, 8vw, 120px);
|
||||
padding: clamp(58px, 9vw, 140px) clamp(24px, 7vw, 110px);
|
||||
}
|
||||
|
||||
.about-section__lead {
|
||||
max-width: 1100px;
|
||||
margin: 0;
|
||||
font-size: clamp(38px, 5.2vw, 82px);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.055em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.about-section__columns {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: clamp(32px, 6vw, 100px);
|
||||
max-width: 950px;
|
||||
font-size: 17px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.about-section__columns p {
|
||||
margin: 0;
|
||||
padding-top: 16px;
|
||||
border-top: var(--line);
|
||||
}
|
||||
|
||||
.repo-section {
|
||||
padding: clamp(76px, 10vw, 150px) clamp(20px, 4vw, 64px);
|
||||
border-bottom: var(--line);
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
.repo-section__eyebrow {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.repo-section h2 {
|
||||
margin-bottom: clamp(50px, 7vw, 100px);
|
||||
}
|
||||
|
||||
.repo-links {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
.repo-links > a {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
padding: clamp(22px, 3vw, 38px);
|
||||
border: var(--line);
|
||||
box-shadow: 5px 5px 0 var(--ink);
|
||||
font-size: clamp(16px, 2vw, 25px);
|
||||
font-weight: 700;
|
||||
transition: transform 120ms ease, box-shadow 120ms ease;
|
||||
}
|
||||
|
||||
.repo-links > a:hover,
|
||||
.repo-links > a:focus-visible {
|
||||
box-shadow: 1px 1px 0 var(--ink);
|
||||
transform: translate(4px, 4px);
|
||||
}
|
||||
|
||||
.repo-links svg {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.repo-links span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.repo-links small {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.site-footer {
|
||||
display: grid;
|
||||
grid-template-columns: 0.7fr 1.5fr auto;
|
||||
gap: 40px;
|
||||
align-items: end;
|
||||
min-height: 540px;
|
||||
padding: clamp(44px, 7vw, 100px) clamp(20px, 4vw, 64px);
|
||||
background: var(--ink);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.site-footer > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.site-footer p {
|
||||
margin: 0;
|
||||
font-size: clamp(56px, 8vw, 130px);
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.075em;
|
||||
line-height: 0.78;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.site-footer > a {
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px solid currentColor;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 3px solid currentColor;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 1050px) {
|
||||
.hero {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero__intro {
|
||||
min-height: 740px;
|
||||
border-right: 0;
|
||||
border-bottom: var(--line);
|
||||
}
|
||||
|
||||
.hero__status {
|
||||
min-height: 560px;
|
||||
}
|
||||
|
||||
.project-grid .project-card:first-child,
|
||||
.project-grid .project-card:nth-child(2),
|
||||
.project-grid .project-card:nth-child(3) {
|
||||
grid-column: span 12;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.project-grid .project-card:nth-child(3) {
|
||||
width: calc(100% - 100px);
|
||||
margin-left: 100px;
|
||||
}
|
||||
|
||||
.section-heading__body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.site-header {
|
||||
min-height: 74px;
|
||||
}
|
||||
|
||||
.site-mark > span:last-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.site-header nav {
|
||||
gap: 16px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.site-header nav a:nth-child(2) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.site-header nav a:nth-child(3) {
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.site-header nav a:nth-child(3)::after {
|
||||
content: "Code";
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.hero__intro {
|
||||
min-height: 700px;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: clamp(56px, 16.5vw, 84px);
|
||||
}
|
||||
|
||||
.hero__footer {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.brutal-button {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.hero__status {
|
||||
min-height: 480px;
|
||||
padding: 42px 26px;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.section-heading__body {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.section-heading__body > p {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
min-height: 520px;
|
||||
}
|
||||
|
||||
.project-card__body {
|
||||
margin: 52px 0;
|
||||
}
|
||||
|
||||
.project-card__footer {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.project-grid .project-card:nth-child(3) {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.upcoming-grid .project-card {
|
||||
grid-column: span 12;
|
||||
}
|
||||
|
||||
.about-section {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.about-section__label {
|
||||
min-height: 220px;
|
||||
border-right: 0;
|
||||
border-bottom: var(--line);
|
||||
}
|
||||
|
||||
.about-section__columns,
|
||||
.repo-links {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.site-footer {
|
||||
grid-template-columns: 1fr;
|
||||
min-height: 640px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.site-footer p {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.site-footer > a {
|
||||
justify-self: start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
.ticker > div,
|
||||
.project-card__status span {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.brutal-button,
|
||||
.win98-host,
|
||||
.project-card,
|
||||
.repo-links > a {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,6 @@ export const Route = createRootRoute({
|
||||
function RootComponent() {
|
||||
return <>
|
||||
<Outlet />
|
||||
<TanStackRouterDevtools />
|
||||
{import.meta.env.DEV && <TanStackRouterDevtools />}
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { ArrowDown, ArrowUpRight, Code2, GitBranch } from "lucide-react"
|
||||
import { ProjectCard } from "@/components/portfolio/ProjectCard"
|
||||
import { SectionHeading } from "@/components/portfolio/SectionHeading"
|
||||
import { Win98Status } from "@/components/portfolio/Win98Status"
|
||||
|
||||
|
||||
export const Route = createFileRoute("/_main/")({
|
||||
@@ -6,5 +10,175 @@ export const Route = createFileRoute("/_main/")({
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <p className="text-center">Coming soon!</p>
|
||||
return (
|
||||
<main>
|
||||
<header className="site-header">
|
||||
<a className="site-mark" href="#top" aria-label="Julien Valverde, back to top">
|
||||
<span>JV</span>
|
||||
<span>Full-stack engineer<br />& product builder</span>
|
||||
</a>
|
||||
|
||||
<nav aria-label="Main navigation">
|
||||
<a href="#work">Work</a>
|
||||
<a href="#about">About</a>
|
||||
<a href="#repositories">Repositories</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section className="hero" id="top">
|
||||
<div className="hero__intro">
|
||||
<p className="hero__kicker">Independent engineer · Chambéry, France</p>
|
||||
<h1>
|
||||
I build
|
||||
<span>useful</span>
|
||||
systems.
|
||||
</h1>
|
||||
|
||||
<div className="hero__footer">
|
||||
<p>
|
||||
From open-source primitives to vertical products — software with strong types,
|
||||
clear interfaces, and a reason to exist.
|
||||
</p>
|
||||
<a className="brutal-button" href="#work">
|
||||
Explore selected work
|
||||
<ArrowDown aria-hidden="true" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside className="hero__status" aria-label="Developer status">
|
||||
<div className="hero__stamp" aria-hidden="true">EST. 2019 / WEB</div>
|
||||
<Win98Status />
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<div className="ticker" aria-hidden="true">
|
||||
<div>
|
||||
<span>Effect</span><i>✦</i>
|
||||
<span>TypeScript</span><i>✦</i>
|
||||
<span>Product engineering</span><i>✦</i>
|
||||
<span>Open source</span><i>✦</i>
|
||||
<span>Effect</span><i>✦</i>
|
||||
<span>TypeScript</span><i>✦</i>
|
||||
<span>Product engineering</span><i>✦</i>
|
||||
<span>Open source</span><i>✦</i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="work-section" id="work">
|
||||
<SectionHeading
|
||||
eyebrow="01 / Selected work"
|
||||
title="Tools for developers. Products for people."
|
||||
description="A selection of open-source libraries, production software, and what is currently taking shape."
|
||||
/>
|
||||
|
||||
<div className="project-grid">
|
||||
<ProjectCard
|
||||
index="01"
|
||||
title="effect-lens"
|
||||
category="Open source / library"
|
||||
description="An effectful Lens for focusing, subscribing to, and updating nested state—without losing typed failures or requirements along the way."
|
||||
tags={["Effect", "State", "TypeScript"]}
|
||||
href="https://www.npmjs.com/package/effect-lens"
|
||||
linkLabel="View on npm"
|
||||
variant="dark"
|
||||
/>
|
||||
<ProjectCard
|
||||
index="02"
|
||||
title="effect-view"
|
||||
category="Open source / React"
|
||||
description="A small bridge for writing React function components with Effect, designed around composable programs instead of scattered side effects."
|
||||
tags={["Effect", "React", "DX"]}
|
||||
href="https://www.npmjs.com/package/effect-view"
|
||||
linkLabel="View on npm"
|
||||
variant="light"
|
||||
/>
|
||||
<ProjectCard
|
||||
index="03"
|
||||
title="Comme le Chef"
|
||||
category="SaaS / Food tech"
|
||||
description="An online recipe workspace for composing and balancing culinary creations, tracking nutrition and allergens, and generating production documents."
|
||||
tags={["Product", "SaaS", "Food tech"]}
|
||||
href="https://commedeschef.com/"
|
||||
linkLabel="Visit website"
|
||||
variant="outline"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="upcoming-section">
|
||||
<SectionHeading
|
||||
eyebrow="02 / On the workbench"
|
||||
title="Two new systems are booting."
|
||||
description="Early-stage products in active development. More details will appear when there is something worth showing."
|
||||
/>
|
||||
|
||||
<div className="upcoming-grid">
|
||||
<ProjectCard
|
||||
index="04"
|
||||
title="FLOWpti"
|
||||
category="Upcoming / Logistics"
|
||||
description="Delivery route optimization for transport professionals—turning operational constraints into clearer, more efficient routes."
|
||||
tags={["Routing", "Optimization", "B2B"]}
|
||||
/>
|
||||
<ProjectCard
|
||||
index="05"
|
||||
title="Sentinel"
|
||||
category="Upcoming / Open source"
|
||||
description="A self-hosted telemetry and observability stack that synchronizes OBD-II vehicle data for visualization, diagnostics, and remote insight."
|
||||
tags={["OBD-II", "Telemetry", "Self-hosted"]}
|
||||
variant="dark"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="about-section" id="about">
|
||||
<div className="about-section__label">
|
||||
<span>03</span>
|
||||
<span>About</span>
|
||||
</div>
|
||||
<div className="about-section__copy">
|
||||
<p className="about-section__lead">
|
||||
I’m Julien Valverde, a full-stack engineer and entrepreneur based in the French Alps.
|
||||
</p>
|
||||
<div className="about-section__columns">
|
||||
<p>
|
||||
I like the difficult middle: where domain problems, product decisions,
|
||||
interface design, and systems engineering all meet.
|
||||
</p>
|
||||
<p>
|
||||
My current work spans typed functional programming, React, operational
|
||||
software, optimization, and self-hosted infrastructure.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="repo-section" id="repositories">
|
||||
<p className="repo-section__eyebrow">Source code, experiments, and works in progress</p>
|
||||
<h2>Follow the commits.</h2>
|
||||
<div className="repo-links">
|
||||
<a href="https://git.valverde.cloud/" target="_blank" rel="noreferrer">
|
||||
<GitBranch aria-hidden="true" />
|
||||
<span><small>Primary forge</small>git.valverde.cloud</span>
|
||||
<ArrowUpRight aria-hidden="true" />
|
||||
</a>
|
||||
<a href="https://github.com/Thiladev" target="_blank" rel="noreferrer">
|
||||
<Code2 aria-hidden="true" />
|
||||
<span><small>Public collaboration</small>github.com/Thiladev</span>
|
||||
<ArrowUpRight aria-hidden="true" />
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer className="site-footer">
|
||||
<div>
|
||||
<span>Julien Valverde</span>
|
||||
<span>© {new Date().getFullYear()}</span>
|
||||
</div>
|
||||
<p>Build it well.<br />Make it useful.</p>
|
||||
<a href="#top">Back to top ↑</a>
|
||||
</footer>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,109 +1,10 @@
|
||||
import { createFileRoute, Outlet } from "@tanstack/react-router"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { DiGit } from "react-icons/di"
|
||||
import { FaCode } from "react-icons/fa"
|
||||
import root from "react-shadow"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
|
||||
|
||||
export const Route = createFileRoute("/_main")({
|
||||
component: WIPLayout,
|
||||
component: MainLayout,
|
||||
})
|
||||
|
||||
function WIPLayout() {
|
||||
return (
|
||||
<div className="mx-auto w-[750px] max-w-full px-5 pt-28 pb-10 text-foreground">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Outlet />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
function SharedLayout() {
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
return <>
|
||||
<div className="flex flex-row items-center justify-center gap-2 p-2">
|
||||
<Button
|
||||
variant={i18n.language !== "en" ? "default" : "noShadow"}
|
||||
onClick={() => i18n.changeLanguage("en")}
|
||||
>
|
||||
🇬🇧 English
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={i18n.language !== "fr" ? "default" : "noShadow"}
|
||||
onClick={() => i18n.changeLanguage("fr")}
|
||||
>
|
||||
🇫🇷 Français
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="container mt-20 mr-auto ml-auto">
|
||||
<div className="mr-auto ml-auto flex w-fit flex-row items-start justify-around gap-10">
|
||||
<root.div>
|
||||
<link rel="stylesheet" href="/98.css/dist/98.css" />
|
||||
|
||||
<div className="window" style={{ width: 300 }}>
|
||||
<div className="title-bar">
|
||||
<div className="title-bar-text">Julien Valverde</div>
|
||||
</div>
|
||||
<div className="window-body">
|
||||
<p>There's so much room for activities!</p>
|
||||
</div>
|
||||
</div>
|
||||
</root.div>
|
||||
|
||||
<div>
|
||||
<h1>Ingénieur Full Stack • Entrepreneur</h1>
|
||||
|
||||
<p className="mt-2">
|
||||
I am passionate about integrating functionality and design in applications to create intuitive, user-friendly experiences.
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex w-fit flex-row items-center justify-center gap-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button asChild>
|
||||
<a href="https://git.valverde.cloud"><DiGit /> {t($ => $.gitRepos.title, { ns: "nav" })}</a>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent><p>{t($ => $.gitRepos.tooltip, { ns: "nav" })}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button asChild>
|
||||
<a href="https://git.valverde.cloud/thilawyn/website"><FaCode /> {t($ => $.sourceCode.title, { ns: "nav" })}</a>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent><p>{t($ => $.sourceCode.tooltip, { ns: "nav" })}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<Button>Resumé</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto w-[750px] max-w-full px-5 pt-28 pb-10 text-foreground">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Outlet />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
function MainLayout() {
|
||||
return <Outlet />
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user