rewrite #16

Merged
imnyang merged 12 commits from rewrite into main 2026-09-09 14:38:23 +09:00
9 changed files with 257 additions and 108 deletions
Showing only changes of commit dd23a3c76e - Show all commits

wow
Some checks failed
/ print-content (push) Failing after 1m43s

암냥 2026-09-09 14:34:33 +09:00

BIN
public/about/vrchat.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View file

@ -1,12 +1,11 @@
---
import AboutRow from "@/components/AboutRow.astro";
import DDay from "./DDay.astro";
---
<section
id="about"
data-node-id="4:104"
class="min-h-full snap-start snap-always bg-background text-foreground"
class="min-h-[calc(100svh-64px)] snap-start bg-background text-foreground"
aria-labelledby="about-title"
>
<div class="grid min-h-full grid-cols-1 lg:grid-cols-2">
@ -14,10 +13,12 @@ import DDay from "./DDay.astro";
class="relative z-10 flex flex-col justify-center px-6 py-16 sm:px-10 lg:px-[clamp(52px,5.208vw,100px)] lg:py-[clamp(58px,9.26vh,100px)]"
>
<h2 id="about-title" class="max-w-xl text-3xl font-semibold tracking-tight sm:text-4xl">
👋
About<span class="text-foreground/35" aria-hidden="true">.</span>
</h2>
<p class="mt-6 max-w-lg text-base leading-8 text-foreground">
안녕하세요. 정보보안과 소프트웨어 엔지니어링을 공부하는 암냥입니다.
새로운 기술을 배우고, 머릿속의 아이디어를 실제로 작동하는 것으로
만드는 일을 좋아해요.
</p>
<section class="mt-16" aria-labelledby="experience-title">
<div class="mb-5 flex items-baseline justify-between gap-4">
@ -70,11 +71,11 @@ import DDay from "./DDay.astro";
</AboutRow>
</div>
</section>
<div class="mt-6 flex flex-row items-center text-sm text-foreground/60">
<div class="mt-8 flex flex-row items-center text-sm text-foreground/60">
<a
href="#experience"
class="flex flex-row items-center underline underline-offset-4"
><span>등등...</span>
class="flex flex-row items-center underline underline-offset-4 hover:text-foreground"
><span>전체 타임라인 보기</span>
<svg
aria-hidden="true"
class="h-4 text-foreground/40"
@ -89,21 +90,26 @@ import DDay from "./DDay.astro";
<path d="M7 7h10v10"></path>
</svg></a
>
<p class="ml-1"> 많은 여러가지의 활동을 해왔어요.</p>
</div>
</div>
<div
class="relative order-first h-[min(150vw,720px)] min-h-[600px] overflow-hidden lg:sticky lg:top-0 lg:order-none lg:h-[calc(100svh-64px)] lg:min-h-0 lg:self-start"
class="relative order-last h-[min(100vw,480px)] min-h-0 overflow-hidden bg-muted lg:sticky lg:top-0 lg:order-none lg:h-[calc(100svh-64px)] lg:min-h-0 lg:self-start"
aria-label="활동 사진"
>
<!-- <img
class="absolute inset-0 h-full w-full select-none pointer-events-none object-contain"
src="/side.avif"
alt="활동 사진 콜라주"
width="3840"
height="4320"
/> -->
<img
class="absolute inset-0 h-full w-full select-none object-cover object-center"
src="/about/vrchat.webp"
alt="VRChat에서 촬영한 캐릭터 스크린샷"
width="1920"
height="1080"
loading="lazy"
decoding="async"
/>
<div
class="pointer-events-none absolute inset-0 bg-gradient-to-t from-background/35 via-transparent to-black/5"
aria-hidden="true"
></div>
</div>
</div>
</section>

View file

@ -105,14 +105,14 @@ const groups = [
{
name: "Project Sekai KR",
value: "7354623251148069648",
href: "#",
copy: true,
icon: "project-sekai",
tone: "blue",
},
{
name: "Project Sekai JP",
value: "729310364018688008",
href: "#",
copy: true,
icon: "project-sekai",
tone: "blue",
},
@ -152,7 +152,7 @@ const paths: Record<string, string> = {
};
---
<section id="contact" class="contact-section" aria-labelledby="contact-title">
<section id="contact" class="contact-section snap-start" aria-labelledby="contact-title">
<div class="contact-content">
<header class="contact-heading">
<h2 id="contact-title">Contact<span aria-hidden="true">.</span></h2>
@ -174,6 +174,90 @@ const paths: Record<string, string> = {
<ul class="contact-list">
{group.items.map((item) => (
<li class="contact-row">
{"copy" in item ? (
<button
class="contact-link contact-copy-link"
type="button"
data-contact-copy={item.value}
aria-label={`${item.name} ${item.value} 복사`}
>
<span
class={`contact-icon tone-${item.tone}`}
aria-hidden="true"
>
{imageIcons[item.icon] ? (
<img
src={
imageIcons[item.icon]
.src
}
width="22"
height="22"
alt=""
class:list={{
"mono-icon":
imageIcons[
item.icon
].monochrome,
}}
/>
) : (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d={paths[item.icon]}
/>
</svg>
)}
</span>
<span class="contact-name">
{item.name}
</span>
<span class="contact-value">
{item.value}
</span>
<span
class="contact-copy-indicator"
aria-hidden="true"
>
<svg
class="copy-symbol"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect
x="8"
y="8"
width="12"
height="12"
rx="2"
/>
<path d="M16 8V4a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h4" />
</svg>
<svg
class="copied-symbol"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m5 12 4 4L19 6" />
</svg>
</span>
</button>
) : (
<a
class="contact-link"
href={item.href}
@ -244,6 +328,7 @@ const paths: Record<string, string> = {
</svg>
)}
</a>
)}
{"email" in item && (
<button
class="contact-copy"
@ -309,9 +394,7 @@ const paths: Record<string, string> = {
<style>
.contact-section {
--contact-top-spacing: clamp(48px, 7vw, 88px);
min-height: 100%;
/* A viewport-spanning snap area permits free scrolling through the links. */
scroll-snap-align: start;
min-height: calc(100svh - 64px);
padding: var(--contact-top-spacing) clamp(24px, 5.208vw, 100px) 32px;
}
.contact-content {
@ -320,10 +403,6 @@ const paths: Record<string, string> = {
}
.contact-heading {
display: flow-root;
/* Keep the snap target smaller than the viewport so entry stops at the title. */
scroll-snap-align: start;
scroll-snap-stop: always;
scroll-margin-top: var(--contact-top-spacing);
padding-bottom: 32px;
border-bottom: 1px solid var(--border);
}
@ -393,6 +472,15 @@ const paths: Record<string, string> = {
border-radius: 8px;
text-decoration: none;
}
.contact-copy-link {
width: 100%;
border: 0;
background: transparent;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
}
.contact-row:has(.contact-copy) .contact-link {
grid-template-columns: 22px auto minmax(0, 1fr);
padding-right: 0;
@ -463,6 +551,22 @@ const paths: Record<string, string> = {
color: var(--muted-foreground);
cursor: pointer;
}
.contact-copy-indicator {
display: grid;
place-items: center;
width: 18px;
height: 18px;
color: var(--muted-foreground);
transition: color 160ms;
}
.contact-copy-link:hover .contact-copy-indicator {
color: var(--foreground);
}
.contact-copy-indicator svg {
width: 17px;
height: 17px;
grid-area: 1 / 1;
}
.contact-copy:hover {
color: var(--foreground);
background: color-mix(in srgb, var(--foreground) 7%, transparent);
@ -472,10 +576,12 @@ const paths: Record<string, string> = {
height: 17px;
}
.copied-symbol,
.contact-copy[data-copied] .copy-symbol {
.contact-copy[data-copied] .copy-symbol,
.contact-copy-link[data-copied] .copy-symbol {
display: none;
}
.contact-copy[data-copied] .copied-symbol {
.contact-copy[data-copied] .copied-symbol,
.contact-copy-link[data-copied] .copied-symbol {
display: block;
color: #46916c;
}
@ -540,6 +646,10 @@ const paths: Record<string, string> = {
grid-column: 3;
grid-row: 1 / 3;
}
.contact-copy-link .contact-copy-indicator {
grid-column: 3;
grid-row: 1 / 3;
}
}
@media (prefers-reduced-motion: reduce) {
.contact-row,
@ -556,27 +666,30 @@ const paths: Record<string, string> = {
const contactStatus =
document.querySelector<HTMLElement>(".contact-status");
document
.querySelectorAll<HTMLButtonElement>("[data-contact-email]")
.querySelectorAll<HTMLButtonElement>(
"[data-contact-email], [data-contact-copy]",
)
.forEach((button) => {
let resetTimer: ReturnType<typeof setTimeout>;
button.addEventListener("click", async () => {
const email = button.dataset.contactEmail;
if (!email) return;
const value =
button.dataset.contactEmail ?? button.dataset.contactCopy;
if (!value) return;
try {
await navigator.clipboard.writeText(email);
await navigator.clipboard.writeText(value);
clearTimeout(resetTimer);
button.dataset.copied = "true";
button.setAttribute("aria-label", `${email} 복사 완료`);
button.setAttribute("aria-label", `${value} 복사 완료`);
if (contactStatus)
contactStatus.textContent = `${email} 주소를 복사했어요.`;
contactStatus.textContent = `${value}를 복사했어요.`;
resetTimer = setTimeout(() => {
delete button.dataset.copied;
button.setAttribute("aria-label", `${email} 복사`);
button.setAttribute("aria-label", `${value} 복사`);
}, 2000);
} catch {
if (contactStatus)
contactStatus.textContent =
"복사하지 못했어요. 이메일 주소를 직접 선택해 복사해주세요.";
"복사하지 못했어요. 값을 직접 선택해 복사해주세요.";
}
});
});

View file

@ -6,7 +6,7 @@ const roles = ["Broadcast Manager", "ARG Creator", "Developer"];
<section
id="projects"
class="min-h-full snap-start bg-background text-foreground"
class="min-h-[calc(100svh-64px)] snap-start bg-background text-foreground"
aria-labelledby="projects-title"
>
<div class="grid min-h-full grid-cols-1 lg:grid-cols-2">

View file

@ -26,7 +26,7 @@ const daysUntilYearEnd = Math.ceil(
<section
id="experience"
data-node-id="4:104"
class="min-h-full snap-start snap-always bg-background text-foreground"
class="min-h-[calc(100svh-64px)] snap-start bg-background text-foreground"
aria-labelledby="experience-title"
>
<div class="grid min-h-full grid-cols-1 lg:grid-cols-2">
@ -130,7 +130,7 @@ const daysUntilYearEnd = Math.ceil(
</div>
<div
class="relative order-first h-[min(150vw,720px)] min-h-[600px] overflow-hidden lg:sticky lg:top-0 lg:order-none lg:h-[calc(100svh-64px)] lg:min-h-0 lg:self-start"
class="relative order-last h-[min(100vw,480px)] min-h-0 overflow-hidden lg:sticky lg:top-0 lg:order-none lg:h-[calc(100svh-64px)] lg:min-h-0 lg:self-start"
aria-label="활동 사진"
>
<img

View file

@ -3,7 +3,7 @@ const navigationItems = [
{ label: "Home", href: "#home" },
{ label: "About", href: "#about" },
{ label: "Projects", href: "#projects" },
{ label: "Timeline", href: "#timeline" },
{ label: "Timeline", href: "#experience" },
{ label: "Contact", href: "#contact" },
] as const;

View file

@ -2,7 +2,7 @@
id="home"
data-node-id="1:3"
data-name="imnyang"
class="flex h-full min-h-0 snap-start snap-always flex-col overflow-hidden"
class="flex h-[calc(100svh-64px)] min-h-0 snap-start flex-col overflow-hidden"
>
<section
class="flex h-[250px] min-h-[250px] min-w-0 flex-none basis-[250px] items-center overflow-clip px-[clamp(26px,2.5vw,48px)] max-md:px-6"

View file

@ -80,3 +80,37 @@
--mono-filter: invert(1);
}
}
html {
scroll-behavior: smooth;
scroll-padding-top: 64px;
}
html::-webkit-scrollbar {
width: 19px;
height: 11px;
}
html::-webkit-scrollbar-track {
background: transparent;
}
html::-webkit-scrollbar-thumb {
background: var(--scrollbar);
border: 5px solid var(--background);
border-radius: 16px;
}
html::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-hover);
}
html {
scrollbar-color: var(--scrollbar) var(--background);
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}

View file

@ -56,18 +56,14 @@ import "@/globals.css";
target.offsetTop - (scrollContainer?.offsetTop ?? 0);
if (scrollContainer && window.location.hash) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
const initialTarget = document.querySelector<HTMLElement>(
window.location.hash,
);
if (initialTarget) {
scrollContainer.scrollTop =
getTargetScrollTop(initialTarget);
scrollContainer.scrollTop = getTargetScrollTop(initialTarget);
}
});
});
}
inPageLinks.forEach((link) => {