Skip to content

Commit ddbcf18

Browse files
fix(translations): update retroFps label to HELLRUN
1 parent 98d38ea commit ddbcf18

7 files changed

Lines changed: 3119 additions & 457 deletions

File tree

src/app/globals.css

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1876,7 +1876,7 @@ textarea {
18761876
}
18771877

18781878
.section-featured {
1879-
background: linear-gradient(180deg, rgba(15, 12, 8, 0.95) 0%, rgba(12, 9, 6, 0.95) 100%);
1879+
background: var(--content-bg);
18801880
min-height: 0;
18811881
position: relative;
18821882
overflow-x: visible;
@@ -1885,8 +1885,10 @@ textarea {
18851885
padding-bottom: clamp(56px, 8vw, 96px);
18861886
}
18871887

1888-
:root[data-theme="light"] .section-featured {
1889-
background: linear-gradient(180deg, rgba(250, 240, 222, 0.96) 0%, rgba(242, 226, 200, 0.96) 100%);
1888+
.section-featured + .section,
1889+
.section-featured ~ .section,
1890+
.section-featured ~ .site-footer {
1891+
background: var(--content-bg);
18901892
}
18911893

18921894
.featured-layout {
@@ -2025,7 +2027,11 @@ textarea {
20252027
top: 0;
20262028
left: 50%;
20272029
width: var(--featured-card-width);
2028-
transform: translateX(calc(-50% + var(--featured-offset)))
2030+
transform: translateX(
2031+
calc(
2032+
-50% + var(--featured-offset) + var(--featured-center-nudge, 0px)
2033+
)
2034+
)
20292035
scale(var(--featured-scale));
20302036
opacity: var(--featured-opacity);
20312037
z-index: var(--featured-z);
@@ -2040,11 +2046,13 @@ textarea {
20402046
width: var(--featured-card-width);
20412047
border: none;
20422048
background: transparent;
2043-
z-index: 3;
2049+
z-index: 1;
20442050
cursor: pointer;
20452051
padding: 0;
20462052
left: 50%;
2047-
transform: translateX(calc(-50% + var(--featured-hit-offset)));
2053+
transform: translateX(
2054+
calc(-50% + var(--featured-hit-offset) + var(--featured-center-nudge, 0px))
2055+
);
20482056
}
20492057

20502058
.featured-hit-prev {
@@ -3094,16 +3102,21 @@ textarea {
30943102
.featured-viewer {
30953103
min-height: 0;
30963104
overflow: visible;
3105+
width: 100vw;
3106+
margin-left: calc(50% - 50vw);
30973107
}
30983108

30993109
.featured-slider {
3100-
--featured-card-width: min(440px, 82vw);
3101-
--featured-offset-1: 26vw;
3102-
--featured-offset-2: 52vw;
3110+
width: 100vw;
3111+
--featured-card-width: min(380px, 86vw);
3112+
--featured-offset-1: 22vw;
3113+
--featured-offset-2: 44vw;
3114+
--featured-center-nudge: 0px;
31033115
}
31043116

31053117
.featured-track {
3106-
min-height: clamp(280px, 42vh, 480px);
3118+
min-height: clamp(300px, 42vh, 520px);
3119+
padding: 6px 0 18px;
31073120
}
31083121

31093122
.featured-item {
@@ -3121,10 +3134,16 @@ textarea {
31213134
}
31223135

31233136
.featured-card {
3124-
padding: 22px;
3137+
padding: 20px;
3138+
min-height: clamp(240px, 34vh, 320px);
31253139
box-shadow: 0 14px 32px rgba(0, 0, 0, 0.35);
31263140
}
31273141

3142+
.featured-card-header {
3143+
align-items: start;
3144+
flex-wrap: wrap;
3145+
}
3146+
31283147
.featured-summary {
31293148
display: block;
31303149
-webkit-line-clamp: unset;
@@ -3146,7 +3165,33 @@ textarea {
31463165
}
31473166

31483167
.featured-dots {
3149-
margin-top: 16px;
3168+
margin-top: 0;
3169+
}
3170+
3171+
.featured-controls {
3172+
display: grid;
3173+
grid-template-columns: 40px 1fr 40px;
3174+
align-items: center;
3175+
justify-items: center;
3176+
gap: 12px;
3177+
width: min(520px, 92vw);
3178+
margin: 0 auto;
3179+
}
3180+
3181+
.featured-controls .featured-dots {
3182+
margin: 0;
3183+
gap: 8px;
3184+
}
3185+
3186+
.featured-dot {
3187+
width: 10px;
3188+
height: 10px;
3189+
border-width: 2px;
3190+
}
3191+
3192+
.featured-arrow {
3193+
width: 38px;
3194+
height: 38px;
31503195
}
31513196

31523197
.featured-ghost {

src/app/page.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,17 @@ export async function generateMetadata({
2929
};
3030
}
3131

32-
export default async function Home() {
32+
export default async function Home({
33+
searchParams,
34+
}: {
35+
searchParams?: Promise<{ dev?: string; debug?: string }>;
36+
}) {
3337
const content = await getContent();
34-
return <HomeClient content={content} />;
38+
const params = searchParams ? await searchParams : undefined;
39+
const debugEnabled =
40+
params?.dev === "1" ||
41+
params?.dev === "true" ||
42+
params?.debug === "1" ||
43+
params?.debug === "true";
44+
return <HomeClient content={content} debugEnabled={debugEnabled} />;
3545
}

src/components/HomeClient.tsx

Lines changed: 87 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ function useSmoothScrollProgress(
5757
) {
5858
const progressRef = useRef(0);
5959
const targetRef = useRef(0);
60+
const lastTargetRef = useRef(0);
61+
const lastScrollAtRef = useRef(0);
6062
const rafRef = useRef<number | null>(null);
6163
const onUpdateRef = useRef<typeof onUpdate>(onUpdate);
6264

@@ -67,56 +69,97 @@ function useSmoothScrollProgress(
6769
useEffect(() => {
6870
let active = true;
6971
let resizeObserver: ResizeObserver | null = null;
72+
let running = false;
7073

7174
const computeTarget = () => {
7275
if (!ref.current) {
7376
targetRef.current = 0;
7477
return;
7578
}
7679
const rect = ref.current.getBoundingClientRect();
80+
if (!Number.isFinite(rect.top) || rect.height <= 1) {
81+
return;
82+
}
7783
const total = rect.height - window.innerHeight;
7884
const raw = total > 0 ? Math.min(Math.max(-rect.top / total, 0), 1) : 0;
85+
const now =
86+
typeof performance !== "undefined" ? performance.now() : Date.now();
87+
const sinceScroll = now - lastScrollAtRef.current;
88+
if (sinceScroll > 140 && Math.abs(raw - lastTargetRef.current) > 0.35) {
89+
targetRef.current = lastTargetRef.current;
90+
return;
91+
}
92+
lastTargetRef.current = raw;
7993
targetRef.current = raw;
8094
};
8195

8296
const epsilon = 0.0008;
8397
const smoothing = 0.12;
8498

8599
const tick = () => {
86-
if (!active) {
100+
if (!active || document.hidden) {
101+
running = false;
102+
rafRef.current = null;
87103
return;
88104
}
89105
const diff = targetRef.current - progressRef.current;
90106
if (Math.abs(diff) < epsilon) {
91107
progressRef.current = targetRef.current;
92-
} else {
93-
progressRef.current += diff * smoothing;
108+
onUpdateRef.current?.(progressRef.current);
109+
running = false;
110+
rafRef.current = null;
111+
return;
94112
}
113+
progressRef.current += diff * smoothing;
95114
onUpdateRef.current?.(progressRef.current);
96115
rafRef.current = window.requestAnimationFrame(tick);
97116
};
98117

118+
const startLoop = () => {
119+
if (running) {
120+
return;
121+
}
122+
running = true;
123+
rafRef.current = window.requestAnimationFrame(tick);
124+
};
125+
99126
computeTarget();
100-
rafRef.current = window.requestAnimationFrame(tick);
127+
startLoop();
101128

102-
const schedule = () => {
129+
const schedule = (fromScroll = false) => {
130+
if (fromScroll) {
131+
lastScrollAtRef.current =
132+
typeof performance !== "undefined" ? performance.now() : Date.now();
133+
}
103134
computeTarget();
135+
startLoop();
104136
};
105137

106-
window.addEventListener("scroll", schedule, { passive: true });
107-
window.addEventListener("resize", schedule);
138+
const handleScroll: EventListener = () => schedule(true);
139+
const handleResize: EventListener = () => schedule(false);
140+
window.addEventListener("scroll", handleScroll, { passive: true });
141+
window.addEventListener("resize", handleResize);
142+
const handleVisibility = () => {
143+
if (!document.hidden) {
144+
schedule();
145+
}
146+
};
147+
document.addEventListener("visibilitychange", handleVisibility);
108148

109149
if (typeof ResizeObserver !== "undefined") {
110-
resizeObserver = new ResizeObserver(schedule);
150+
const handleResizeObserver: ResizeObserverCallback = () =>
151+
schedule(false);
152+
resizeObserver = new ResizeObserver(handleResizeObserver);
111153
if (ref.current) {
112154
resizeObserver.observe(ref.current);
113155
}
114156
}
115157

116158
return () => {
117159
active = false;
118-
window.removeEventListener("scroll", schedule);
119-
window.removeEventListener("resize", schedule);
160+
window.removeEventListener("scroll", handleScroll);
161+
window.removeEventListener("resize", handleResize);
162+
document.removeEventListener("visibilitychange", handleVisibility);
120163
resizeObserver?.disconnect();
121164
resizeObserver = null;
122165
if (rafRef.current !== null) {
@@ -459,6 +502,8 @@ export default function HomeClient({
459502
heroActive: true,
460503
});
461504
const debugLastRef = useRef(0);
505+
const revealRef = useRef(0);
506+
const fadeRef = useRef(1);
462507
const featuredIndex = useMemo(
463508
() => profile.sections.indexOf("featured"),
464509
[profile.sections],
@@ -488,9 +533,12 @@ export default function HomeClient({
488533
if (!shellRef.current) {
489534
return;
490535
}
536+
if (typeof document !== "undefined" && document.hidden) {
537+
return;
538+
}
491539
const activeId = activeSectionRef.current ?? "home";
492540
const activeIndex = profile.sections.indexOf(activeId);
493-
const forceOpaque = featuredIndex >= 0 && activeIndex > featuredIndex;
541+
const forceOpaque = featuredIndex >= 0 && activeIndex >= featuredIndex;
494542
const clamp = (val: number, min: number, max: number) =>
495543
Math.min(max, Math.max(min, val));
496544
let reveal = 0;
@@ -500,22 +548,37 @@ export default function HomeClient({
500548
featuredRef.current;
501549
if (featuredEl) {
502550
const rect = featuredEl.getBoundingClientRect();
503-
const elementCenter = rect.top + rect.height / 2;
504-
const offsetCenter = elementCenter - window.innerHeight;
505-
const triggerStart = window.innerHeight * 0.8;
506-
const triggerEnd = window.innerHeight * 0.5;
507-
if (triggerStart !== triggerEnd) {
508-
reveal = clamp(
509-
(triggerStart - offsetCenter) / (triggerStart - triggerEnd),
510-
0,
511-
1,
512-
);
551+
if (Number.isFinite(rect.top) && rect.height > 1) {
552+
const elementCenter = rect.top + rect.height / 2;
553+
const offsetCenter = elementCenter - window.innerHeight;
554+
const triggerStart = window.innerHeight * 0.8;
555+
const triggerEnd = window.innerHeight * 0.5;
556+
if (triggerStart !== triggerEnd) {
557+
reveal = clamp(
558+
(triggerStart - offsetCenter) / (triggerStart - triggerEnd),
559+
0,
560+
1,
561+
);
562+
}
563+
if (rect.top <= window.innerHeight * 0.1) {
564+
reveal = 1;
565+
}
566+
} else {
567+
reveal = revealRef.current;
513568
}
514569
} else {
515570
reveal = clamp((value - 0.85) / 0.12, 0, 1);
516571
}
517572
reveal = forceOpaque ? 1 : reveal;
518-
const fade = forceOpaque ? 0 : 1 - reveal;
573+
let fade = forceOpaque ? 0 : 1 - reveal;
574+
if (!Number.isFinite(reveal)) {
575+
reveal = revealRef.current;
576+
}
577+
if (!Number.isFinite(fade)) {
578+
fade = fadeRef.current;
579+
}
580+
revealRef.current = reveal;
581+
fadeRef.current = fade;
519582
shellRef.current.style.setProperty("--hero-fade", String(fade));
520583
shellRef.current.style.setProperty("--content-reveal", String(reveal));
521584
const nextHeroActive = reveal < 0.98;
@@ -1488,9 +1551,9 @@ export default function HomeClient({
14881551
"# Kinin Portfolio\n\nStatus: active\nStack: Next.js, Three.js, WebGL\n",
14891552
},
14901553
{
1491-
path: `${projectsDir}/retro-os/README.md`,
1554+
path: `${projectsDir}/kinin-os/README.md`,
14921555
content:
1493-
"# Retro OS\n\nExperimenting with terminal UX and CRT shaders.\n",
1556+
"# Kinin OS\n\nExperimenting with terminal UX and CRT shaders.\n",
14941557
},
14951558
{
14961559
path: `${homeDir}/.bashrc`,

src/components/Markdown.tsx

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,36 @@ export default function Markdown({
2727
return -1;
2828
})();
2929
const renderInline = (text: string) => {
30-
const parts: Array<string | { label: string; href: string }> = [];
31-
const regex = /\[([^\]]+)\]\(([^)]+)\)/g;
30+
const parts: Array<
31+
| { type: "text"; value: string }
32+
| { type: "link"; label: string; href: string }
33+
| { type: "bold"; value: string }
34+
> = [];
35+
const regex = /\[([^\]]+)\]\(([^)]+)\)|\*\*([^*]+)\*\*|__([^_]+)__/g;
3236
let lastIndex = 0;
3337
let match: RegExpExecArray | null;
3438
while ((match = regex.exec(text)) !== null) {
3539
if (match.index > lastIndex) {
36-
parts.push(text.slice(lastIndex, match.index));
40+
parts.push({ type: "text", value: text.slice(lastIndex, match.index) });
41+
}
42+
if (match[1] && match[2]) {
43+
parts.push({ type: "link", label: match[1], href: match[2] });
44+
} else if (match[3]) {
45+
parts.push({ type: "bold", value: match[3] });
46+
} else if (match[4]) {
47+
parts.push({ type: "bold", value: match[4] });
3748
}
38-
parts.push({ label: match[1], href: match[2] });
3949
lastIndex = match.index + match[0].length;
4050
}
4151
if (lastIndex < text.length) {
42-
parts.push(text.slice(lastIndex));
52+
parts.push({ type: "text", value: text.slice(lastIndex) });
4353
}
4454
return parts.map((part, index) => {
45-
if (typeof part === "string") {
46-
return <span key={index}>{part}</span>;
55+
if (part.type === "text") {
56+
return <span key={index}>{part.value}</span>;
57+
}
58+
if (part.type === "bold") {
59+
return <strong key={index}>{part.value}</strong>;
4760
}
4861
return (
4962
<a key={index} href={part.href} target="_blank" rel="noreferrer">

0 commit comments

Comments
 (0)