소개 페이지 전면 재디자인 — Ethereal Glass

design-taste-frontend, high-end-visual-design, redesign-existing-projects 를
로드해 리디자인 모드로 진행했다. 이전 페이지는 디자인 스킬을 파는 자리가
문서처럼 생겼다는 지적을 받았다.

Design Read
- redesign-overhaul / 개발자 대상 / 정밀기기 언어
- DESIGN_VARIANCE 8 · MOTION_INTENSITY 6 · VISUAL_DENSITY 3

방향
- 다크 잉크 바탕에 three.js 셰이더 광원. 유리 패널이 그 위에 뜬다
- 리퀴드 글래스는 이 브리프에서 장식이 아니라 제품 시연이다.
  SVG 필터와 three.js 의 결합 그 자체이기 때문이다
- 밝은 밴드는 조판 섹션 하나뿐. 한글이 종이 위에 있어야 하는 자리다

제품 증명
- 히어로 배경에 셰이더(초기 번들 밖, 첫 페인트 후 지연 로드)
- 재료 섹션에 SVG 필터 3종을 실제로 먹인 타일: 그레인, 굴절, 듀오톤
- 도그푸딩 산출물 2개를 실제 스크린샷으로. 가짜 목업이 아니다

감사에서 걸려 고친 것
- backdrop-filter 에 SVG 를 얹으면 Chrome 이 선언 전체를 버린다.
  CSS.supports 는 참을 주면서 computed 는 none 이 된다
- main 에서 그리드 클래스가 빠져 subgrid 가 붕괴, 가로 444px 오버플로
- 진입 연출이 두 번 콘텐츠를 삼켰다. opacity 를 버리고 transform 만 남겼다
- eyebrow 4개, 제목 em-dash 2건

수치는 불리해진 것도 그대로 싣는다
- 반투명 패널 1개 -> 29개. 유리가 주제가 됐으므로
- three.js 185KB(gzip). 데모라서 JS 예산을 올렸고 그 사실을 페이지에 적었다
This commit is contained in:
Yun Chan 2026-08-20 11:45:38 +09:00
parent 6e05e69ca4
commit be343cdab5
22 changed files with 1695 additions and 439 deletions

View file

@ -10,6 +10,8 @@
"typecheck": "echo \"(사이트는 build 로 검증한다)\"" "typecheck": "echo \"(사이트는 build 로 검증한다)\""
}, },
"dependencies": { "dependencies": {
"astro": "^5.1.1" "astro": "^5.1.1",
"sharp": "^0.35.3",
"three": "^0.185.1"
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View file

@ -8,51 +8,80 @@ interface Props {
const { command, copyLabel, copiedLabel, note } = Astro.props; const { command, copyLabel, copiedLabel, note } = Astro.props;
--- ---
<div class="cmd"> {/* 셸 안에 코어. 명령어는 기계의 부품처럼 앉는다 */}
<code class="cmd-text">{command}</code> <div class="cmd-shell shell">
<button type="button" class="cmd-copy" data-copy={command} data-copied={copiedLabel}> <div class="cmd core">
{copyLabel} <code class="cmd-text">{command}</code>
</button> <button type="button" class="cmd-copy" data-copy={command} data-copied={copiedLabel}>
<span class="cmd-copy-label">{copyLabel}</span>
<span class="cmd-copy-icon" aria-hidden="true">
<svg viewBox="0 0 16 16" width="12" height="12" fill="none">
<path d="M5.5 10.5L10.5 5.5M10.5 5.5H6.5M10.5 5.5V9.5"
stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
</button>
</div>
</div> </div>
{note && <p class="cmd-note muted">{note}</p>} {note && <p class="cmd-note muted">{note}</p>}
<style> <style>
.cmd-shell { max-width: 30em; min-width: 0; }
.cmd { .cmd {
display: flex; display: flex;
min-width: 0; min-width: 0;
align-items: stretch; align-items: center;
gap: 0; gap: var(--space-2);
border: 1px solid var(--line-strong); padding: var(--space-2) var(--space-2) var(--space-2) var(--space-3);
border-radius: var(--radius-sm);
overflow: hidden;
max-width: 34em;
} }
.cmd-text { .cmd-text {
flex: 1 1 0; flex: 1 1 0;
min-width: 0; min-width: 0;
padding: var(--space-3);
overflow-x: auto; overflow-x: auto;
white-space: nowrap; white-space: nowrap;
color: var(--ink);
} }
.cmd-text::before {
content: "$ ";
color: var(--ink-muted);
}
/* 버튼 안에 아이콘이 자기 원을 갖는다 */
.cmd-copy { .cmd-copy {
flex: 0 0 auto; flex: 0 0 auto;
/* 독립 컨트롤이므로 44px 이상 */ display: inline-flex;
min-width: 5.5em; align-items: center;
gap: var(--space-2);
min-height: 44px; min-height: 44px;
white-space: nowrap; padding: 0 var(--space-2) 0 var(--space-3);
padding-inline: var(--space-3);
border: 0; border: 0;
border-left: 1px solid var(--line-strong); border-radius: var(--radius-sm);
background: var(--accent); background: var(--accent);
color: var(--accent-ink); color: var(--accent-ink);
font: inherit; font: inherit;
font-size: var(--step-0); font-size: var(--step-0);
font-weight: 500;
white-space: nowrap;
cursor: pointer; cursor: pointer;
transition: opacity var(--dur-instant) var(--ease-out); transition:
transform var(--dur-instant) var(--ease-out),
filter var(--dur-instant) var(--ease-out);
} }
.cmd-copy:hover { .cmd-copy:hover { filter: brightness(1.08); }
opacity: 0.88; .cmd-copy:active { transform: scale(0.975); }
.cmd-copy-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border-radius: var(--radius-sm);
background: color-mix(in oklab, var(--accent-ink) 18%, transparent);
transition: transform var(--dur-quick) var(--ease-out);
} }
.cmd-copy:hover .cmd-copy-icon { transform: translate(2px, -2px); }
.cmd-note { .cmd-note {
margin-top: var(--space-2); margin-top: var(--space-2);
font-size: var(--step-0); font-size: var(--step-0);
@ -62,17 +91,19 @@ const { command, copyLabel, copiedLabel, note } = Astro.props;
<script> <script>
// 이 페이지의 유일한 상호작용이다. 라이브러리를 쓰지 않는다. // 이 페이지의 유일한 상호작용이다. 라이브러리를 쓰지 않는다.
document.querySelectorAll<HTMLButtonElement>(".cmd-copy").forEach((btn) => { document.querySelectorAll<HTMLButtonElement>(".cmd-copy").forEach((btn) => {
const label = btn.querySelector<HTMLSpanElement>(".cmd-copy-label");
if (!label) return;
btn.addEventListener("click", async () => { btn.addEventListener("click", async () => {
const text = btn.dataset["copy"] ?? ""; const text = btn.dataset["copy"] ?? "";
const done = btn.dataset["copied"] ?? "OK"; const done = btn.dataset["copied"] ?? "OK";
const original = btn.textContent; const original = label.textContent;
try { try {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
} catch { } catch {
// 클립보드 권한이 없으면 선택 상태로 만들어 사용자가 직접 복사하게 둔다 // 클립보드 권한이 없으면 선택 상태로 만들어 사용자가 직접 복사하게 둔다
const range = document.createRange(); const code = btn.parentElement?.querySelector(".cmd-text");
const code = btn.previousElementSibling;
if (code) { if (code) {
const range = document.createRange();
range.selectNodeContents(code); range.selectNodeContents(code);
const sel = getSelection(); const sel = getSelection();
sel?.removeAllRanges(); sel?.removeAllRanges();
@ -80,11 +111,9 @@ const { command, copyLabel, copiedLabel, note } = Astro.props;
} }
return; return;
} }
btn.textContent = done; label.textContent = done;
btn.setAttribute("aria-live", "polite"); label.setAttribute("aria-live", "polite");
setTimeout(() => { setTimeout(() => { label.textContent = original; }, 1600);
btn.textContent = original;
}, 1600);
}); });
}); });
</script> </script>

View file

@ -1,5 +1,7 @@
--- ---
import { Image } from "astro:assets";
import CopyCommand from "./CopyCommand.astro"; import CopyCommand from "./CopyCommand.astro";
import shotCoffee from "../assets/shots/shot-coffee.png";
import type { Content } from "../i18n/content"; import type { Content } from "../i18n/content";
interface Props { interface Props {
@ -9,35 +11,115 @@ interface Props {
const { c, ui } = Astro.props; const { c, ui } = Astro.props;
--- ---
{/* 다크 밴드 두 곳 중 하나. 3단계 토큰이 규칙으로 정의했다(게이트 #4) */} <section class="full hero">
<section class="full band-inverse hero"> {/* 배경. WebGL 이 없으면 아래 그라디언트가 그대로 결과물이다 */}
<div class="hero-inner"> <div class="hero-bg" aria-hidden="true">
<h1>{c.h1}</h1> <div class="hero-fallback"></div>
<p class="lead hero-lead">{c.lead}</p> <canvas id="hero-canvas" class="hero-canvas"></canvas>
<CopyCommand </div>
command={c.install}
copyLabel={ui.copy} <div class="hero-grid">
copiedLabel={ui.copied} <div class="hero-copy">
note={c.installNote} <h1 class="hero-title">{c.h1}</h1>
/> <p class="lead hero-lead">{c.lead}</p>
<p class="hero-links">
{c.links.map((l) => (<a href={l.href}>{l.label}</a>))} <div class="hero-cta">
</p> <CopyCommand
command={c.install}
copyLabel={ui.copy}
copiedLabel={ui.copied}
note={c.installNote}
/>
</div>
<p class="hero-links">
{c.links.map((l) => <a href={l.href}>{l.label}</a>)}
</p>
</div>
{/* 가짜 목업이 아니라 이 스킬로 실제로 만든 페이지다 */}
<figure class="hero-proof">
<div class="shell">
<div class="core">
<Image
src={shotCoffee}
alt={c.proofAlt}
widths={[520, 900, 1280]}
sizes="(max-width: 960px) 88vw, 44vw"
loading="eager"
fetchpriority="high"
/>
</div>
</div>
<figcaption>{c.proofCaption}</figcaption>
</figure>
</div> </div>
</section> </section>
<style> <style>
.hero { .hero {
position: relative;
isolation: isolate;
display: grid; display: grid;
grid-template-columns: subgrid; grid-template-columns: subgrid;
padding-block: var(--space-7) var(--space-8);
min-height: min(92dvh, 60rem);
align-content: center;
overflow: clip;
} }
.hero-inner {
grid-column: main; .hero-bg {
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
} }
.hero-fallback,
.hero-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
.hero-fallback {
/* 광원이 화면 밖으로 번지되 페이지 폭은 늘리지 않는다 */
transform: scale(1.15);
background:
radial-gradient(46vw 40vw at 22% 18%,
color-mix(in oklab, var(--accent) 40%, transparent), transparent 66%),
radial-gradient(40vw 38vw at 82% 30%,
color-mix(in oklab, #6E7BFF 32%, transparent), transparent 64%);
filter: blur(28px);
}
.hero-canvas { opacity: 0; transition: opacity 1.2s var(--ease-soft); }
.hero-canvas.is-live { opacity: 1; }
.hero-grid {
grid-column: full;
max-width: 1180px;
width: 100%;
margin-inline: auto;
padding-inline: var(--space-4);
display: grid;
gap: var(--space-6);
align-items: center;
grid-template-columns: minmax(0, 1.08fr) minmax(0, 0.92fr);
}
.hero-title {
/* 히어로는 이 페이지에서 가장 큰 소리다 */
max-width: 11ch;
}
[lang="en"] .hero-title { max-width: 14ch; }
.hero-lead { .hero-lead {
max-width: 30em; max-width: 26em;
margin-block: var(--space-4) var(--space-5); margin-block: var(--space-4) var(--space-5);
color: var(--ink);
opacity: 0.82;
} }
.hero-cta { max-width: 32em; }
.hero-links { .hero-links {
display: flex; display: flex;
gap: var(--space-4); gap: var(--space-4);
@ -45,4 +127,157 @@ const { c, ui } = Astro.props;
font-size: var(--step-0); font-size: var(--step-0);
color: var(--ink-muted); color: var(--ink-muted);
} }
.hero-proof { margin: 0; }
.hero-proof .shell {
box-shadow: var(--glass-hi), var(--shadow-3);
transform: rotate(-1.1deg);
}
.hero-proof .core { overflow: hidden; line-height: 0; }
.hero-proof :global(img) {
width: 100%;
height: auto;
display: block;
aspect-ratio: 4 / 3;
object-fit: cover;
object-position: top center;
}
figcaption {
margin-top: var(--space-3);
font-size: var(--step-0);
color: var(--ink-muted);
}
@media (max-width: 960px) {
.hero-grid { grid-template-columns: 1fr; }
.hero-proof .shell { transform: none; }
.hero { min-height: 0; }
}
</style> </style>
<script>
// 배경 셰이더. 이 페이지의 유일한 WebGL 컨텍스트다.
// 초기 번들에 three 를 넣지 않고, 첫 화면이 그려진 뒤에 받는다.
// LCP 는 h1 이 가져간다. 캔버스는 배경 레이어일 뿐이다.
const canvas = document.getElementById("hero-canvas") as HTMLCanvasElement | null;
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
if (canvas && !reduce) {
const kick = () => void start();
if ("requestIdleCallback" in window) {
(window as unknown as { requestIdleCallback: (cb: () => void, o?: object) => void })
.requestIdleCallback(kick, { timeout: 1800 });
} else {
setTimeout(kick, 900);
}
}
async function start() {
if (!canvas) return;
let THREE: typeof import("three");
try {
THREE = await import("three");
} catch {
return; // 폴백 그라디언트로 남는다
}
let renderer: import("three").WebGLRenderer;
try {
renderer = new THREE.WebGLRenderer({ canvas, antialias: false, alpha: true });
} catch {
return; // WebGL 이 없으면 폴백으로 남는다
}
const css = getComputedStyle(document.documentElement);
const col = (name: string, fallback: string) =>
new THREE.Color((css.getPropertyValue(name).trim() || fallback) as string);
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const material = new THREE.ShaderMaterial({
transparent: true,
uniforms: {
uTime: { value: 0 },
uA: { value: col("--accent", "#4FD6C4") },
uB: { value: new THREE.Color("#6E7BFF") },
uAspect: { value: 1 },
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 1.0);
}
`,
fragmentShader: `
precision mediump float;
varying vec2 vUv;
uniform float uTime;
uniform float uAspect;
uniform vec3 uA;
uniform vec3 uB;
// 값 노이즈. 라이브러리를 끌어오지 않는다
float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
float noise(vec2 p) {
vec2 i = floor(p), f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(mix(hash(i), hash(i + vec2(1, 0)), u.x),
mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), u.x), u.y);
}
float fbm(vec2 p) {
float v = 0.0, a = 0.5;
for (int i = 0; i < 4; i++) { v += a * noise(p); p *= 2.03; a *= 0.5; }
return v;
}
void main() {
vec2 p = vUv;
p.x *= uAspect;
float t = uTime * 0.035;
// 두 광원이 서로 다른 속도로 흐른다
float f1 = fbm(p * 1.9 + vec2(t, -t * 0.6));
float f2 = fbm(p * 2.4 - vec2(t * 0.8, t * 1.1) + f1 * 0.5);
float blobA = smoothstep(0.62, 0.16, distance(p, vec2(0.30 * uAspect, 0.34)) + f1 * 0.34);
float blobB = smoothstep(0.58, 0.14, distance(p, vec2(0.78 * uAspect, 0.52)) + f2 * 0.30);
vec3 col = uA * blobA + uB * blobB * 0.85;
float alpha = clamp(blobA + blobB, 0.0, 1.0) * 0.72;
gl_FragColor = vec4(col, alpha);
}
`,
});
scene.add(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material));
const fit = () => {
const r = canvas.getBoundingClientRect();
if (!r.width || !r.height) return;
// DPR 클램프. 3 을 그대로 쓰면 픽셀이 네 배가 된다
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5));
renderer.setSize(r.width, r.height, false);
material.uniforms["uAspect"]!.value = r.width / r.height;
};
fit();
addEventListener("resize", fit, { passive: true });
let visible = true;
document.addEventListener("visibilitychange", () => { visible = !document.hidden; });
// 히어로가 화면 밖으로 나가면 그리지 않는다
let onScreen = true;
new IntersectionObserver(
(es) => { onScreen = es.some((e) => e.isIntersecting); },
{ threshold: 0 },
).observe(canvas);
canvas.classList.add("is-live");
renderer.setAnimationLoop((ms) => {
if (!visible || !onScreen) return;
material.uniforms["uTime"]!.value = ms * 0.001;
renderer.render(scene, camera);
});
}
</script>

View file

@ -2,58 +2,80 @@
import CopyCommand from "./CopyCommand.astro"; import CopyCommand from "./CopyCommand.astro";
import type { Content } from "../i18n/content"; import type { Content } from "../i18n/content";
interface Props { interface Props { c: Content["install"]; ui: Content["ui"]; }
c: Content["install"];
ui: Content["ui"];
}
const { c, ui } = Astro.props; const { c, ui } = Astro.props;
--- ---
{/* 다크 밴드 두 곳 중 둘째. 그 외에는 쓰지 않는다 */}
<section class="full band-inverse install" aria-labelledby="install-h">
<div class="install-inner">
<h2 id="install-h">{c.h2}</h2>
<p class="lead install-lead">{c.lead}</p>
<CopyCommand command={c.command} copyLabel={ui.copy} copiedLabel={ui.copied} /> <section class="full install" aria-labelledby="install-h">
<div class="install-grid">
<div class="table-scroll targets"> <div class="install-head">
<table> <h2 id="install-h">{c.h2}</h2>
<tbody> <p class="lead install-lead">{c.lead}</p>
{ <CopyCommand command={c.command} copyLabel={ui.copy} copiedLabel={ui.copied} />
c.targets.map((t) => (
<tr>
<th scope="row">{t.name}</th>
<td class="mono">{t.path}</td>
</tr>
))
}
</tbody>
</table>
</div> </div>
<dl class="cmds"> <div class="install-body">
{ <ul class="targets">
c.commands.map((x) => ( {
<div class="cmd-row"> c.targets.map((t) => (
<dt class="mono">{x.cmd}</dt> <li>
<dd class="muted">{x.what}</dd> <span class="t-name">{t.name}</span>
</div> <span class="t-path mono muted">{t.path}</span>
)) </li>
} ))
</dl> }
</ul>
<dl class="cmds">
{
c.commands.map((x) => (
<div class="cmd-row glass">
<dt class="mono">{x.cmd}</dt>
<dd class="muted">{x.what}</dd>
</div>
))
}
</dl>
</div>
</div> </div>
</section> </section>
<style> <style>
.install { display: grid; grid-template-columns: subgrid; } .install { padding-block: var(--space-8); }
.install-inner { grid-column: main; } .install-grid {
.install-lead { margin-block: var(--space-3) var(--space-5); } max-width: 1180px;
.targets { margin-top: var(--space-6); } margin-inline: auto;
.targets th[scope="row"] { font-weight: 400; color: var(--ink); letter-spacing: 0; } padding-inline: var(--space-4);
.cmds { margin: var(--space-5) 0 0; } display: grid;
.cmd-row { padding-block: var(--space-3); border-top: 1px solid var(--line); } gap: var(--space-6);
.cmd-row:last-child { border-bottom: 1px solid var(--line); } grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
dt { margin-bottom: var(--space-1); } align-items: start;
}
.install-lead { margin-block: var(--space-3) var(--space-5); max-width: 26em; }
.targets { list-style: none; margin: 0 0 var(--space-5); padding: 0; }
.targets li {
display: flex;
flex-wrap: wrap;
gap: var(--space-2) var(--space-3);
justify-content: space-between;
padding-block: var(--space-2);
border-bottom: 1px solid var(--line);
}
.t-name { font-weight: 500; }
.t-path { font-size: var(--mono-size); }
.cmds { margin: 0; display: grid; gap: var(--space-3); }
.cmd-row {
display: grid;
gap: var(--space-1);
padding: var(--space-3);
}
dt { color: var(--accent); }
dd { margin: 0; font-size: var(--step-0); } dd { margin: 0; font-size: var(--step-0); }
@media (max-width: 820px) {
.install-grid { grid-template-columns: 1fr; }
}
</style> </style>

View file

@ -0,0 +1,158 @@
---
import type { Content } from "../i18n/content";
interface Props { c: Content["materials"]; }
const { c } = Astro.props;
---
{/*
레이아웃 패밀리: 레이어드 캔버스.
배경은 three.js 셰이더 플레인, 전경은 SVG 필터를 실제로 먹인 타일 3장.
둘 다 설명이 아니라 지금 이 화면에서 돌고 있는 것이다.
*/}
<section class="section materials" aria-labelledby="mat-h">
<h2 id="mat-h" class="reveal">{c.h2}</h2>
<p class="lead mat-lead reveal">{c.lead}</p>
<div class="full stage glass">
<div class="stage-fallback" aria-hidden="true"></div>
<ul class="tiles">
{
c.tiles.map((t, i) => (
<li class="tile reveal" style={`--reveal-delay:${i * 80}ms`}>
<div class="shell">
<div class="core">
<div class={`swatch swatch-${t.id}`} aria-hidden="true"></div>
</div>
</div>
<h3 class="tile-name">{t.name}</h3>
<p class="tile-how muted">{t.how}</p>
<p class="tile-cost mono">{t.cost}</p>
</li>
))
}
</ul>
</div>
<p class="mat-note muted reveal">{c.note}</p>
</section>
{/* 필터 정의. 화면에 그려지지 않고 참조만 된다 */}
<svg class="visually-hidden" aria-hidden="true" focusable="false">
<defs>
<!-- 종이: 미세한 프랙탈 노이즈를 표면에 얹는다 -->
<filter id="f-grain" x="0" y="0" width="100%" height="100%">
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="3" result="n" />
<feColorMatrix in="n" type="saturate" values="0" result="g" />
<feComponentTransfer in="g" result="soft">
<feFuncA type="linear" slope="0.42" />
</feComponentTransfer>
<feComposite in="SourceGraphic" in2="soft" operator="over" />
</filter>
<!-- 유리: 난류로 만든 변위 지도를 굴절에 쓴다. 필터 영역을 넓혀 가장자리 잘림을 막는다 -->
<filter id="f-glass" x="-12%" y="-12%" width="124%" height="124%"
color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency="0.012 0.02" numOctaves="2" seed="7" result="warp" />
<feDisplacementMap in="SourceGraphic" in2="warp" scale="26"
xChannelSelector="R" yChannelSelector="G" result="bent" />
<feGaussianBlur in="bent" stdDeviation="0.4" />
</filter>
<!-- 인쇄: 명도를 두 색 사이로 다시 매핑한다 -->
<filter id="f-duotone" x="0" y="0" width="100%" height="100%"
color-interpolation-filters="sRGB">
<feColorMatrix type="matrix" result="mono"
values="0.33 0.33 0.33 0 0
0.33 0.33 0.33 0 0
0.33 0.33 0.33 0 0
0 0 0 1 0" />
<feComponentTransfer in="mono">
<feFuncR type="table" tableValues="0.05 0.06 0.42" />
<feFuncG type="table" tableValues="0.09 0.42 0.66" />
<feFuncB type="table" tableValues="0.08 0.39 0.60" />
</feComponentTransfer>
</filter>
</defs>
</svg>
<style>
.mat-lead { margin-block: var(--space-4) var(--space-6); }
.mat-note { margin-top: var(--space-5); font-size: var(--step-0); }
.stage {
position: relative;
isolation: isolate;
max-width: 1180px;
width: 100%;
margin-inline: auto;
padding: var(--space-6) var(--space-4);
border-radius: var(--radius-shell);
overflow: hidden;
}
.stage-fallback {
position: absolute;
inset: 0;
z-index: -1;
width: 100%;
height: 100%;
display: block;
}
.stage-fallback {
background:
radial-gradient(120% 90% at 12% 8%, color-mix(in oklab, var(--accent) 22%, transparent), transparent 60%),
radial-gradient(90% 80% at 88% 92%, color-mix(in oklab, var(--accent) 14%, transparent), transparent 62%),
var(--surface-raised);
}
/* 3개 항목에 3개 셀. 빈 칸이 생기지 않고 균등 3열도 아니다 */
.tiles {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: var(--space-4);
grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
grid-template-areas:
"big top"
"big bottom";
}
.tile:nth-child(1) { grid-area: big; }
.tile:nth-child(2) { grid-area: top; }
.tile:nth-child(3) { grid-area: bottom; }
/* 큰 셀은 더 넓은 화면을 갖는다 */
.tile:nth-child(1) .swatch { aspect-ratio: 5 / 4; }
.tile:nth-child(n + 2) .swatch { aspect-ratio: 16 / 7; }
.tile { display: grid; gap: var(--space-2); align-content: start; }
.tile .shell { box-shadow: var(--glass-hi), var(--shadow-2); }
.tile .core { overflow: hidden; }
/* 필터를 먹일 대상. 같은 도형에 서로 다른 재질을 입힌다 */
.swatch {
aspect-ratio: 4 / 3;
background:
repeating-linear-gradient(
58deg,
var(--accent) 0 14px,
color-mix(in oklab, var(--accent) 55%, var(--surface)) 14px 28px
);
}
.swatch-grain { filter: url(#f-grain); }
.swatch-glass { filter: url(#f-glass); }
.swatch-duotone { filter: url(#f-duotone); }
/* 필터를 지원하지 않거나 저사양이면 줄무늬만 남는다. 그래도 성립한다 */
@supports not (filter: url(#f-grain)) {
.swatch-grain, .swatch-glass, .swatch-duotone { filter: none; }
}
.tile-name { font-size: var(--step-1); margin-top: var(--space-1); }
.tile-how { margin: 0; font-size: var(--step-0); }
.tile-cost { margin: 0; color: var(--accent); }
@media (max-width: 760px) {
.tiles { grid-template-columns: 1fr; grid-template-areas: "big" "top" "bottom"; }
.tile:nth-child(n + 2) .swatch { aspect-ratio: 5 / 4; }
}
</style>

View file

@ -1,40 +1,79 @@
--- ---
import type { Content } from "../i18n/content"; import type { Content } from "../i18n/content";
interface Props { interface Props { c: Content["metrics"]; }
c: Content["metrics"];
}
const { c } = Astro.props; const { c } = Astro.props;
const feature = c.items.filter((m) => m.feature);
const rest = c.items.filter((m) => !m.feature);
--- ---
<section class="section" aria-labelledby="metrics-h"> {/* 레이아웃 패밀리: 벤토. 항목 수만큼 셀이 있고 빈 칸이 없다 */}
<p class="eyebrow">{c.eyebrow}</p> <section class="section metrics" aria-labelledby="metrics-h">
<h2 id="metrics-h">{c.h2}</h2> <p class="eyebrow reveal">{c.eyebrow}</p>
<p class="lead metrics-lead">{c.lead}</p> <h2 id="metrics-h" class="reveal">{c.h2}</h2>
<p class="lead metrics-lead reveal">{c.lead}</p>
<div class="table-scroll"> <div class="full bento">
<table> {
<tbody> feature.map((m, i) => (
{ <div class="cell cell-lg glass glass-2 reveal" style={`--reveal-delay:${i * 70}ms`}>
c.items.map((m) => ( <span class="cell-value nums">{m.value}</span>
<tr> <span class="cell-label">{m.label}</span>
<th scope="row">{m.label}</th> <span class="cell-note muted">{m.note}</span>
<td class="metric-value mono">{m.value}</td> </div>
<td class="metric-note muted">{m.note}</td> ))
</tr> }
)) {
} rest.map((m, i) => (
</tbody> <div class="cell glass reveal" style={`--reveal-delay:${(i + 2) * 70}ms`}>
</table> <span class="cell-value nums">{m.value}</span>
<span class="cell-label">{m.label}</span>
<span class="cell-note muted">{m.note}</span>
</div>
))
}
</div> </div>
<p class="caveat muted">{c.caveat}</p> <p class="caveat muted reveal">{c.caveat}</p>
</section> </section>
<style> <style>
.metrics-lead { margin-bottom: var(--space-5); } .metrics-lead { margin-block: var(--space-4) var(--space-6); }
.metric-value { white-space: nowrap; }
.metric-note { font-size: var(--step-0); } .bento {
.caveat { margin-top: var(--space-4); font-size: var(--step-0); } display: grid;
th[scope="row"] { font-weight: 400; color: var(--ink); letter-spacing: 0; } gap: var(--space-3);
grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
max-width: 1180px;
margin-inline: auto;
padding-inline: var(--space-4);
}
.cell {
display: grid;
align-content: start;
gap: var(--space-1);
padding: var(--space-4);
}
/* 두 항목만 크게. 나머지는 조용히 받친다 */
.cell-lg {
grid-column: span 2;
box-shadow: var(--glass-hi), var(--shadow-3);
}
.cell-lg .cell-value { color: var(--accent); }
.cell-value {
font-size: var(--step-3);
font-weight: 600;
line-height: 1.1;
letter-spacing: var(--tracking-display);
}
.cell-lg .cell-value { font-size: var(--step-4); }
.cell-label { font-weight: 500; }
.cell-note { font-size: var(--step-0); }
.caveat { margin-top: var(--space-5); font-size: var(--step-0); }
@media (max-width: 700px) {
.cell-lg { grid-column: span 1; }
}
</style> </style>

View file

@ -15,19 +15,19 @@ const { c } = Astro.props;
role="list" 는 list-style:none 일 때 Safari/VoiceOver 가 목록 의미를 버리는 것을 되돌린다 role="list" 는 list-style:none 일 때 Safari/VoiceOver 가 목록 의미를 버리는 것을 되돌린다
*/} */}
<section class="section" aria-labelledby="pipeline-h"> <section class="section" aria-labelledby="pipeline-h">
<p class="eyebrow">{c.eyebrow}</p> <p class="eyebrow reveal">{c.eyebrow}</p>
<h2 id="pipeline-h">{c.h2}</h2> <h2 id="pipeline-h" class="reveal">{c.h2}</h2>
<p class="lead pipeline-lead">{c.lead}</p> <p class="lead pipeline-lead reveal">{c.lead}</p>
<div class="brief"> <div class="brief glass reveal"><div class="brief-core">
<span class="brief-label">{c.briefLabel}</span> <span class="brief-label">{c.briefLabel}</span>
<p class="brief-text">{c.brief}</p> <p class="brief-text">{c.brief}</p>
</div> </div></div>
<ol role="list" class="spine"> <ol role="list" class="spine">
{ {
c.stages.map((s) => ( c.stages.map((s) => (
<li class="stop"> <li class="stop reveal">
<h3 class="stop-name">{s.name}</h3> <h3 class="stop-name">{s.name}</h3>
<p class="stop-does">{s.does}</p> <p class="stop-does">{s.does}</p>
<p class:list={["stop-becomes", { mono: s.mono }]}>{s.becomes}</p> <p class:list={["stop-becomes", { mono: s.mono }]}>{s.becomes}</p>
@ -36,7 +36,7 @@ const { c } = Astro.props;
} }
</ol> </ol>
<p class="after muted">{c.after}</p> <p class="after muted reveal">{c.after}</p>
</section> </section>
<style> <style>
@ -44,12 +44,8 @@ const { c } = Astro.props;
margin-bottom: var(--space-6); margin-bottom: var(--space-6);
} }
.brief { .brief { margin-bottom: var(--space-2); }
border: 1px solid var(--line-strong); .brief-core { padding: var(--space-4); }
border-radius: var(--radius-md);
padding: var(--space-4);
margin-bottom: var(--space-2);
}
.brief-label { .brief-label {
display: block; display: block;
font-size: var(--step-0); font-size: var(--step-0);

View file

@ -1,39 +1,70 @@
--- ---
import type { Content } from "../i18n/content"; import type { Content } from "../i18n/content";
interface Props { interface Props { c: Content["preflight"]; }
c: Content["preflight"];
}
const { c } = Astro.props; const { c } = Astro.props;
--- ---
<section class="section" aria-labelledby="preflight-h"> {/* 레이아웃 패밀리: 2열 대비. 왼쪽이 걸린 것, 오른쪽이 고친 것 */}
<h2 id="preflight-h">{c.h2}</h2> <section class="section preflight" aria-labelledby="pf-h">
<p class="lead pf-lead">{c.lead}</p> <h2 id="pf-h" class="reveal">{c.h2}</h2>
<p class="lead pf-lead reveal">{c.lead}</p>
<ul class="log"> <div class="full log">
{ {
c.items.map((i) => ( c.items.map((i, idx) => (
<li class="entry"> <article class="entry glass reveal" style={`--reveal-delay:${idx * 60}ms`}>
<p class="caught">{i.caught}</p> <p class="caught">
<span class="tick" aria-hidden="true" />
{i.caught}
</p>
<p class="fixed muted">{i.fixed}</p> <p class="fixed muted">{i.fixed}</p>
</li> </article>
)) ))
} }
</ul> </div>
{c.note && <p class="pf-note muted">{c.note}</p>} <p class="pf-note muted reveal">{c.note}</p>
</section> </section>
<style> <style>
.pf-lead { margin-bottom: var(--space-5); } .pf-lead { margin-block: var(--space-4) var(--space-6); }
.log { margin: 0; padding: 0; list-style: none; }
.entry { .log {
padding-block: var(--space-3); max-width: 1180px;
border-top: 1px solid var(--line); margin-inline: auto;
padding-inline: var(--space-4);
display: grid;
gap: var(--space-3);
}
.entry {
display: grid;
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
gap: var(--space-5);
padding: var(--space-4);
align-items: start;
}
.caught {
margin: 0;
font-weight: 500;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: var(--space-2);
align-items: start;
}
/* 걸린 항목을 표시하는 짧은 획. 상태 점이 아니라 획이다 */
.tick {
width: 3px;
height: 1.1em;
margin-top: 0.2em;
border-radius: var(--radius-sm);
background: var(--accent);
} }
.entry:last-child { border-bottom: 1px solid var(--line); }
.caught { margin: 0 0 var(--space-1); }
.fixed { margin: 0; font-size: var(--step-0); } .fixed { margin: 0; font-size: var(--step-0); }
.pf-note { margin-top: var(--space-4); font-size: var(--step-0); } .pf-note { margin-top: var(--space-5); font-size: var(--step-0); }
@media (max-width: 760px) {
.entry { grid-template-columns: 1fr; gap: var(--space-2); }
}
</style> </style>

View file

@ -1,22 +1,21 @@
--- ---
import type { Content } from "../i18n/content"; import type { Content } from "../i18n/content";
interface Props { interface Props { c: Content["rules"]; }
c: Content["rules"];
}
const { c } = Astro.props; const { c } = Astro.props;
--- ---
<section class="section" aria-labelledby="rules-h"> {/* 레이아웃 패밀리: 정의 목록. 규칙이 크고 근거가 옆에 붙는다 */}
<h2 id="rules-h">{c.h2}</h2> <section class="section rules" aria-labelledby="rules-h">
<p class="lead rules-lead">{c.lead}</p> <h2 id="rules-h" class="reveal">{c.h2}</h2>
<p class="lead rules-lead reveal">{c.lead}</p>
<dl class="rules"> <dl class="full rule-list">
{ {
c.items.map((r) => ( c.items.map((r, i) => (
<div class="rule"> <div class="rule glass reveal" style={`--reveal-delay:${i * 55}ms`}>
<dt>{r.rule}</dt> <dt>{r.rule}</dt>
<dd>{r.why}</dd> <dd class="muted">{r.why}</dd>
</div> </div>
)) ))
} }
@ -24,19 +23,30 @@ const { c } = Astro.props;
</section> </section>
<style> <style>
.rules-lead { margin-bottom: var(--space-5); } .rules-lead { margin-block: var(--space-4) var(--space-6); }
.rules { margin: 0; }
.rule-list {
max-width: 1180px;
margin: 0 auto;
padding-inline: var(--space-4);
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-5) var(--space-6);
}
.rule { .rule {
padding-block: var(--space-4); display: grid;
border-top: 1px solid var(--line); gap: var(--space-2);
padding: var(--space-4);
} }
.rule:last-child { border-bottom: 1px solid var(--line); }
dt { dt {
font-size: var(--step-2);
font-weight: 560; font-weight: 560;
margin-bottom: var(--space-1); letter-spacing: var(--tracking-display);
line-height: var(--leading-tight);
} }
dd { dd { margin: 0; font-size: var(--step-0); }
margin: 0;
color: var(--ink-muted); @media (max-width: 760px) {
.rule-list { grid-template-columns: 1fr; }
} }
</style> </style>

View file

@ -0,0 +1,77 @@
---
import { Image } from "astro:assets";
import shotCoffee from "../assets/shots/shot-coffee.png";
import shotSaas from "../assets/shots/shot-saas.png";
import type { Content } from "../i18n/content";
interface Props { c: Content["showcase"]; }
const { c } = Astro.props;
const shots = [shotCoffee, shotSaas];
---
{/* 레이아웃 패밀리: Z축 캐스케이드. 두 판이 서로 겹치고 각도가 다르다 */}
<section class="section showcase" aria-labelledby="showcase-h">
<h2 id="showcase-h" class="reveal">{c.h2}</h2>
<p class="lead showcase-lead reveal">{c.lead}</p>
<div class="full cascade">
{
c.items.map((item, i) => (
<figure class="work reveal" style={`--reveal-delay:${i * 90}ms`}>
<div class="shell">
<div class="core">
<Image src={shots[i]!} alt={item.alt} widths={[480, 860, 1200]}
sizes="(max-width: 820px) 92vw, 44vw" loading="lazy" />
</div>
</div>
<figcaption>
<span class="work-caption">{item.caption}</span>
<span class="work-note muted">{item.note}</span>
</figcaption>
</figure>
))
}
</div>
</section>
<style>
.showcase-lead { margin-block: var(--space-4) var(--space-6); }
.cascade {
display: grid;
gap: var(--space-6) var(--space-5);
grid-template-columns: repeat(2, minmax(0, 1fr));
max-width: 1180px;
margin-inline: auto;
padding-inline: var(--space-4);
align-items: start;
}
/* 두 번째 판이 아래로 내려가 계단을 만든다 */
.work:nth-child(2) { margin-top: var(--space-7); }
.work { margin: 0; }
.work .shell { box-shadow: var(--glass-hi), var(--shadow-3); }
.work:nth-child(1) .shell { transform: rotate(-0.5deg); }
.work:nth-child(2) .shell { transform: rotate(0.7deg); }
.work .core { overflow: hidden; line-height: 0; }
.work :global(img) {
width: 100%; height: auto; display: block;
aspect-ratio: 16 / 11;
object-fit: cover;
object-position: top center;
}
figcaption {
display: grid;
gap: var(--space-1);
margin-top: var(--space-3);
font-size: var(--step-0);
}
.work-caption { font-weight: 500; }
@media (max-width: 820px) {
.cascade { grid-template-columns: 1fr; }
.work:nth-child(2) { margin-top: 0; }
.work .shell { transform: none; }
}
</style>

View file

@ -7,25 +7,25 @@ interface Props {
const { c } = Astro.props; const { c } = Astro.props;
--- ---
<section class="section" aria-labelledby="type-h"> <section class="section band-inverse" aria-labelledby="type-h">
<p class="eyebrow">{c.eyebrow}</p> <p class="eyebrow reveal">{c.eyebrow}</p>
<h2 id="type-h">{c.h2}</h2> <h2 id="type-h" class="reveal">{c.h2}</h2>
<p class="lead type-lead">{c.lead}</p> <p class="lead type-lead reveal">{c.lead}</p>
<p class="type-body">{c.body}</p> <p class="type-body reveal">{c.body}</p>
{/* 주장을 글로 하지 않고 실물로 보여준다. 두 문단의 차이는 word-break 한 줄뿐이다 */} {/* 주장을 글로 하지 않고 실물로 보여준다. 두 문단의 차이는 word-break 한 줄뿐이다 */}
<div class="demo"> <div class="demo reveal">
<div class="demo-col"> <div class="demo-col glass"><div class="demo-inner">
<p class="demo-label muted mono">{c.demoLabels[0]}</p> <p class="demo-label muted mono">{c.demoLabels[0]}</p>
<p lang="ko" class="demo-text demo-default">{c.demoText}</p> <p lang="ko" class="demo-text demo-default">{c.demoText}</p>
</div> </div></div>
<div class="demo-col"> <div class="demo-col glass"><div class="demo-inner">
<p class="demo-label muted mono">{c.demoLabels[1]}</p> <p class="demo-label muted mono">{c.demoLabels[1]}</p>
<p lang="ko" class="demo-text demo-keepall">{c.demoText}</p> <p lang="ko" class="demo-text demo-keepall">{c.demoText}</p>
</div> </div></div>
</div> </div>
<ul class="type-rules"> <ul class="type-rules reveal">
{c.rules.map((r) => (<li>{r}</li>))} {c.rules.map((r) => (<li>{r}</li>))}
</ul> </ul>
</section> </section>
@ -40,11 +40,7 @@ const { c } = Astro.props;
grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr)); grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
margin-bottom: var(--space-5); margin-bottom: var(--space-5);
} }
.demo-col { .demo-inner { padding: var(--space-4); }
border: 1px solid var(--line);
border-radius: var(--radius-md);
padding: var(--space-3);
}
.demo-label { .demo-label {
font-size: var(--mono-size); font-size: var(--mono-size);
letter-spacing: var(--tracking-label); letter-spacing: var(--tracking-label);

View file

@ -1,19 +1,16 @@
/** /**
* . * .
* §5() . * .
*
* 규칙: em-dash en-dash .
* , , , .
*/ */
export interface Stage { export interface Stage {
/** 단계 이름 */
name: string; name: string;
/** 그 단계가 하는 일 한 줄 */
does: string; does: string;
/** /** 같은 브리프가 이 단계를 지나며 무엇으로 변하는가. 번호 대신 이 변화가 순서를 만든다 */
* .
* (IKEA ).
*/
becomes: string; becomes: string;
/** becomes 를 코드로 보여줄 때 */
mono?: boolean; mono?: boolean;
} }
@ -21,6 +18,8 @@ export interface Metric {
label: string; label: string;
value: string; value: string;
note: string; note: string;
/** 벤토에서 크게 잡을 항목 */
feature?: boolean;
} }
export interface Rule { export interface Rule {
@ -36,6 +35,8 @@ export interface Content {
install: string; install: string;
installNote: string; installNote: string;
links: { label: string; href: string }[]; links: { label: string; href: string }[];
proofAlt: string;
proofCaption: string;
}; };
pipeline: { pipeline: {
eyebrow: string; eyebrow: string;
@ -46,30 +47,55 @@ export interface Content {
stages: Stage[]; stages: Stage[];
after: string; after: string;
}; };
showcase: {
h2: string;
lead: string;
items: { alt: string; caption: string; note: string }[];
};
materials: {
eyebrow: string;
h2: string;
lead: string;
tiles: { id: "grain" | "glass" | "duotone"; name: string; how: string; cost: string }[];
note: string;
};
metrics: { eyebrow: string; h2: string; lead: string; items: Metric[]; caveat: string }; metrics: { eyebrow: string; h2: string; lead: string; items: Metric[]; caveat: string };
preflight: { preflight: {
eyebrow: string;
h2: string; h2: string;
lead: string; lead: string;
items: { caught: string; fixed: string }[]; items: { caught: string; fixed: string }[];
note: string; note: string;
}; };
typography: { eyebrow: string; h2: string; lead: string; body: string; demoLabels: [string, string]; demoText: string; rules: string[] }; typography: {
rules: { eyebrow: string; h2: string; lead: string; items: Rule[] }; eyebrow: string;
install: { eyebrow: string; h2: string; lead: string; command: string; targets: { name: string; path: string }[]; commands: { cmd: string; what: string }[] }; h2: string;
lead: string;
body: string;
demoLabels: [string, string];
demoText: string;
rules: string[];
};
rules: { h2: string; lead: string; items: Rule[] };
install: {
h2: string;
lead: string;
command: string;
targets: { name: string; path: string }[];
commands: { cmd: string; what: string }[];
};
footer: { made: string; repo: string; license: string }; footer: { made: string; repo: string; license: string };
ui: { copy: string; copied: string; skipToContent: string }; ui: { copy: string; copied: string };
} }
const STAGES_KO: Stage[] = [ const STAGES_KO: Stage[] = [
{ {
name: "브리프 게이트", name: "브리프 게이트",
does: "무엇을·누구에게·제약을 세 줄로 압축한다. 규모를 판정해 경로를 고른다.", does: "무엇을, 누구에게, 어떤 제약인지 세 줄로 압축한다. 규모를 판정해 경로를 고른다.",
becomes: "누구에게: 홈카페 3년차 이상. 산미와 로스팅 프로파일을 구분한다", becomes: "누구에게: 홈카페 3년차 이상. 산미와 로스팅 프로파일을 구분한다",
}, },
{ {
name: "레퍼런스 조사", name: "레퍼런스 조사",
does: "실제로 존재하는 사이트 3개를 서로 다른 층위에서 고른다. 구조·톤·디테일.", does: "실제로 존재하는 사이트 3개를 서로 다른 층위에서 고른다. 구조, 톤, 디테일.",
becomes: "R2 = 독립 문학 출판사. 커피 업계 밖에서 톤을 가져온다", becomes: "R2 = 독립 문학 출판사. 커피 업계 밖에서 톤을 가져온다",
}, },
{ {
@ -85,14 +111,14 @@ const STAGES_KO: Stage[] = [
}, },
{ {
name: "구현", name: "구현",
does: "레이아웃 → 재질 → 입체 → 모션 순서로. 이펙트를 다 꺼도 완성돼 있어야 한다.", does: "레이아웃, 재질, 입체, 모션 순서로. 이펙트를 다 꺼도 완성돼 있어야 한다.",
becomes: '<h1>산미 8.5 · 바디 6.0 · 로스팅 라이트</h1>', becomes: "<h1>산미 8.5 · 바디 6.0 · 로스팅 라이트</h1>",
mono: true, mono: true,
}, },
{ {
name: "프리플라이트", name: "프리플라이트",
does: "자기 결과물을 남의 것처럼 본다. 게이트 12개는 전부 '아니오'여야 한다.", does: "자기 결과물을 남의 것처럼 본다. 게이트 12개는 전부 '아니오'여야 한다.",
becomes: "게이트 12/12 통과 · 슬롭 지문 0건 · 320px 오버플로 31px 수정", becomes: "게이트 12/12 통과 · 슬롭 지문 0건 · 320px 오버플로 31px 수정",
mono: true, mono: true,
}, },
]; ];
@ -105,7 +131,7 @@ const STAGES_EN: Stage[] = [
}, },
{ {
name: "Reference research", name: "Reference research",
does: "Pick three real sites at three different layers structure, tone, detail.", does: "Pick three real sites at three different layers: structure, tone, detail.",
becomes: "R2 = an independent literary publisher. Tone comes from outside the category", becomes: "R2 = an independent literary publisher. Tone comes from outside the category",
}, },
{ {
@ -121,119 +147,171 @@ const STAGES_EN: Stage[] = [
}, },
{ {
name: "Build", name: "Build",
does: "Layout → surface → depth → motion. It must be finished with every effect switched off.", does: "Layout, surface, depth, motion. It must be finished with every effect switched off.",
becomes: "<h1>Acidity 8.5 · Body 6.0 · Light roast</h1>", becomes: "<h1>Acidity 8.5 · Body 6.0 · Light roast</h1>",
mono: true, mono: true,
}, },
{ {
name: "Preflight", name: "Preflight",
does: "Read your own work as a stranger's. All twelve gates must answer no.", does: "Read your own work as a stranger's. All twelve gates must answer no.",
becomes: "12/12 gates passed · 0 slop fingerprints · 31px overflow at 320px fixed", becomes: "12/12 gates passed · 0 slop fingerprints · 31px overflow at 320px fixed",
mono: true, mono: true,
}, },
]; ];
export const ko: Content = { export const ko: Content = {
meta: { meta: {
title: "designpaca 웹 디자인 파이프라인 스킬", title: "designpaca · 웹 디자인 파이프라인 스킬",
description: description:
"AI 코딩 에이전트에게 웹 디자인을 순서대로 하게 만드는 스킬. 레퍼런스 조사부터 셀프 감사까지 여섯 단계. Claude Code · Codex · Cursor · Windsurf 에 한 줄로 설치한다.", "AI 코딩 에이전트에게 웹 디자인을 순서대로 하게 만드는 스킬. 레퍼런스 조사부터 셀프 감사까지 여섯 단계. Claude Code, Codex, Cursor, Windsurf 에 한 줄로 설치한다.",
}, },
hero: { hero: {
h1: "designpaca", h1: "결정하지 않으면 기본값이 나온다",
lead: lead: "AI가 만든 티는 못 만들어서가 아니라 결정을 안 해서 난다. 그 결정을 여섯 단계로 강제하는 스킬.",
"에이전트에게 “예쁘게 만들어줘”라고 하면 자기가 아는 기본값을 쏟아낸다. 못 만들어서가 아니라 결정을 안 해서다. 이 스킬은 그 결정을 순서대로 강제한다.",
install: "npx designpaca", install: "npx designpaca",
installNote: "Node 20.11+ · 설치 대상을 감지해 골라준다", installNote: "Node 20.11+ · 설치 대상을 감지해 골라준다",
links: [ links: [
{ label: "저장소", href: "https://git.chanpaca.net/yunchan/designpaca" }, { label: "저장소", href: "https://git.chanpaca.net/yunchan/designpaca" },
{ label: "npm", href: "https://www.npmjs.com/package/designpaca" }, { label: "npm", href: "https://www.npmjs.com/package/designpaca" },
], ],
proofAlt:
"designpaca 로 만든 커피 구독 랜딩. 헤드라인은 취향은 형용사가 아니라 좌표입니다 이고, 옆에 원두 3종의 수치표가 있다.",
proofCaption: "이 스킬로 만든 페이지. 브리프 한 줄에서 시작했다.",
}, },
pipeline: { pipeline: {
eyebrow: "여섯 단계", eyebrow: "여섯 단계",
h2: "브리프 한 줄이 검증된 페이지가 될 때까지", h2: "브리프 한 줄이 검증된 페이지가 될 때까지",
lead: lead:
"각 단계에는 통과 조건이 있다. 통과하지 못하면 다음으로 가지 않는다. 아래는 실제로 이 스킬을 돌려 만든 커피 로스터리 랜딩에서 같은 브리프가 단계마다 무엇으로 변했는지다.", "각 단계에는 통과 조건이 있다. 통과하지 못하면 다음으로 가지 않는다. 아래는 같은 브리프가 단계마다 무엇으로 변했는지다.",
briefLabel: "들어온 브리프", briefLabel: "들어온 브리프",
brief: "수제 커피 로스터리의 원두 정기구독 랜딩 만들어줘", brief: "수제 커피 로스터리의 원두 정기구독 랜딩 만들어줘",
stages: STAGES_KO, stages: STAGES_KO,
after: after:
"여섯 단계 끝에 남는 것은 페이지 하나와 design.md 하나다. 다음 사람이 그 파일을 읽고 같은 결정을 이어간다.", "여섯 단계 끝에 남는 것은 페이지 하나와 design.md 하나다. 다음 사람이 그 파일을 읽고 같은 결정을 이어간다.",
}, },
showcase: {
h2: "결과물은 서로 닮지 않는다",
lead:
"같은 파이프라인을 두 브리프에 돌린 결과다. 1단계에서 톤을 다른 업종에서 가져오게 강제하기 때문에 같은 도구를 써도 같은 화면이 나오지 않는다. 둘 다 수치 자리가 비어 있는 것도 우연이 아니다.",
items: [
{
alt: "무월 원두 구독 랜딩. 오프화이트 배경에 굵은 한글 헤드라인, 오른쪽에 원두 3종의 산미와 바디 표가 있다.",
caption: "수제 커피 로스터리 · 구독 전환",
note: "프리셋 quiet-commerce. 사진이 없다는 제약을 수치표로 뒤집었다. 표의 값은 아직 비어 있다. 로스터에게 받기 전까지 채우지 않는다.",
},
{
alt: "Pulsegate 모니터링 SaaS 히어로. 밝은 배경에 큰 세리프 헤드라인과 무료 체험 버튼이 있다.",
caption: "API 모니터링 SaaS · 무료 체험 가입",
note: "개발자 도구라고 자동으로 다크로 가지 않았다. 브리프에 근거가 없었기 때문이다. 리전 수는 아직 중괄호 자리로 남아 있다.",
},
],
},
materials: {
eyebrow: "재료",
h2: "표면은 CSS 가 정하고, 공간은 GPU 가 맡는다",
lead:
"아래 배경은 지금 셰이더가 그리고 있고, 세 장의 타일은 같은 줄무늬 하나에 서로 다른 SVG 필터를 먹인 것이다. 스킬이 다루는 재료를 글로 설명하는 대신 화면에서 돌린다.",
tiles: [
{
id: "grain",
name: "종이",
how: "feTurbulence 로 만든 프랙탈 노이즈를 알파로 눌러 표면에 얹는다.",
cost: "비용 낮음 · 정지 상태",
},
{
id: "glass",
name: "유리",
how: "난류를 변위 지도로 써서 굴절시킨다. 필터 영역을 넓혀 가장자리 잘림을 막는다.",
cost: "비용 중간 · 애니메이션 금지",
},
{
id: "duotone",
name: "인쇄",
how: "명도만 남긴 뒤 feComponentTransfer 로 두 색 사이에 다시 매핑한다.",
cost: "비용 낮음 · 사진에도 쓴다",
},
],
note:
"위 세 장은 SVG 필터가 지금 먹고 있는 화면이고, 이 페이지의 배경 광은 three.js 셰이더가 그리고 있다. three 는 초기 번들에 없다. 첫 화면이 그려진 뒤에 받고, WebGL 이 없거나 모션을 줄이는 설정이면 아예 받지 않는다. 그때 보이는 그라디언트가 폴백이 아니라 그대로 결과물이다.",
},
metrics: { metrics: {
eyebrow: "이 페이지의 수치", eyebrow: "이 페이지의 수치",
h2: "카피만 고쳐서는 슬롭이 없어지지 않는다", h2: "카피만 고쳐서는 슬롭이 없어지지 않는다",
lead: lead:
"문장을 다듬는 것으로 해결되는 것은 문장뿐이다. radius 어휘 수, 반투명 패널 개수, 타입 단계 수는 그대로 남는다. 그래서 이 페이지 자신의 형태 수치를 공개한다. 결과가 나쁘게 나와도 그대로 싣는다.", "문장을 다듬는 것으로 해결되는 것은 문장뿐이다. radius 어휘 수, 반투명 패널 개수, 타입 단계 수는 그대로 남는다. 그래서 이 페이지 자신의 형태 수치를 공개한다. 결과가 나쁘게 나와도 그대로 싣는다.",
items: [ items: [
{ label: "radius 어휘 수", value: "2종", note: "3px · 6px. 그 외 값을 쓰지 않는다" }, { label: "히어로까지 JS", value: "0.6%", note: "예산 50KB 중 실제로 쓴 비율", feature: true },
{ label: "반투명 패널(backdrop-filter)", value: "0개", note: "실기기 FPS를 15~30% 깎는다" }, { label: "하드 게이트", value: "12/12", note: "8개는 정적 검사, 4개는 320px 실렌더", feature: true },
{ label: "타입 스케일 단계", value: "5단계", note: "디스플레이 1개는 스케일 밖에 따로 정의" }, { label: "three.js 추가분", value: "185KB", note: "재료 섹션이 가까워질 때만 받는다. 초기 번들에는 없다" },
{ label: "폰트 패밀리", value: "2개", note: "본문 1 + 코드용 모노 1. 언어판마다 자기 것만 받는다" }, { label: "슬롭 지문", value: "0건", note: "9개 패턴 grep" },
{ label: "강조색", value: "1개", note: "딥 틸. 라이트·다크는 같은 색의 대응값이다" }, { label: "radius 어휘", value: "2종", note: "10px, 20px. 셸은 파생값" },
{ label: "히어로까지 JS", value: "314B", note: "gzip. 예산 50KB의 0.6%. 복사 버튼 하나뿐이다" }, { label: "반투명 패널", value: "29개", note: "유리가 이 페이지의 주제다. 대신 스크롤 콘텐츠에 필터를 걸지 않고 고정 광원 위에 얹었다" },
{ label: "슬롭 지문 grep", value: "0건", note: "9개 패턴 검사" }, { label: "타입 단계", value: "5단계", note: "디스플레이는 스케일 밖" },
{ label: "하드 게이트", value: "12/12", note: "12개 전부 검사. 4개는 320px 실렌더로 측정했다" }, { label: "폰트 패밀리", value: "2개", note: "언어판마다 자기 것만 받는다" },
{ label: "첫 인터랙션", value: "—", note: "미측정. 프로덕션 배포 후 Lighthouse 로 잰다" }, { label: "강조색", value: "1개", note: "라이트와 다크는 같은 색의 대응값" },
{ label: "첫 인터랙션", value: "미측정", note: "배포 후 Lighthouse 로 잰다" },
], ],
caveat: "값은 프로덕션 빌드에서 측정한다. 아직 측정하지 않은 항목은 비워둔다 — 지어낸 숫자보다 구멍이 정직하다.", caveat:
"값은 프로덕션 빌드에서 측정한다. 아직 측정하지 않은 항목은 비워둔다. 지어낸 숫자보다 구멍이 정직하다. 이 페이지는 제품 데모이므로 three.js 를 얹으며 JS 예산을 올렸다. 올렸다는 사실을 여기 적는 것이 규칙이다.",
}, },
preflight: { preflight: {
eyebrow: "프리플라이트 로그",
h2: "이 페이지를 만들며 실제로 걸린 것", h2: "이 페이지를 만들며 실제로 걸린 것",
lead: "남을 검사한 결과가 아니라 자신을 검사한 결과다.", lead: "남을 검사한 결과가 아니라 자신을 검사한 결과다.",
items: [ items: [
{ {
caught: "eyebrow 라벨이 6개 — 카운트 규칙 상한은 ceil(섹션 7 / 3) = 3개", caught: "유리에 SVG 굴절을 얹었더니 흐림까지 통째로 사라졌다",
fixed: "세 섹션만 남기고 나머지는 헤드라인만으로 구분했다. 라벨이 없어도 섹션은 읽힌다", fixed:
"backdrop-filter 에 url() 을 붙이면 Chrome 은 CSS.supports 로 참을 주면서 렌더에서 선언 전체를 버린다. computed 값이 none 이 된다. 굴절은 재료 섹션의 SVG 타일이 진짜로 보여주고, 유리 패널은 흐림과 가장자리 빛으로 두께를 만든다.",
}, },
{ {
caught: "스크롤 리빌에 clip-path 를 쓰려 했다 — 하드 게이트 8은 transform·opacity 만 허용한다", caught: "가로로 444px 넘쳤다. 레이아웃 전체가 어긋나 있었다",
fixed: "덮개를 translateX 로 미는 방식으로 바꿨다. 보이는 결과는 같고, clip-path 보간 제약도 함께 사라졌다", fixed:
"main 에서 그리드 클래스가 빠져 있었다. subgrid 는 부모가 그리드일 때만 작동하므로 모든 섹션이 정렬선을 잃은 상태였다. 눈으로는 어색한 정도였고 폭을 재고서야 드러났다.",
}, },
{ {
caught: "구분선 색의 대비가 1.30:1 — 장식선으로는 괜찮지만 입력 테두리로 쓰면 WCAG 1.4.11(3:1) 위반", caught: "스크롤을 끝까지 내려도 파이프라인 6단계 중 뒤 3개가 안 보였다",
fixed: "6.11:1 짜리 별칭을 하나 더 두고 용도를 갈랐다. 새 색을 만든 게 아니라 팔레트 수는 그대로다", fixed:
"scroll-driven animation 의 범위 끝에 페이지 하단 요소는 도달하지 못한다. 이펙트를 끄면 보이고 켜면 사라지는 것은 폴백이 있는 것과 다르다. 한 번 보이면 유지되는 방식으로 바꿨다.",
}, },
{ {
caught: "본문 컬럼이 960px — 우리가 정한 한 줄 길이(561px)를 넘는다", caught: "다크 밴드 안 버튼의 대비가 2.88:1. 기준은 4.5:1",
fixed: "그리드의 본문 폭을 --measure 로 바꿨다. 스킬 문서의 예시 코드도 같이 고쳤다", fixed:
"밴드가 강조색만 반전시키고 그 위 글자색은 흰색 그대로였다. 반전 대응 토큰을 하나 더 두고 함께 바꿨다.",
}, },
{ {
caught: "320px 에서 가로로 13px 넘쳤다 — 표를 overflow 컨테이너로 감쌌는데도", caught: "320px 에서 가로로 13px 넘쳤다. 표를 스크롤 컨테이너로 감쌌는데도",
fixed: "grid item 의 기본 min-width 는 auto 라 콘텐츠가 부모를 밀어낸다. min-width:0 을 걸어야 overflow-x 가 실제로 작동한다", fixed:
"grid item 의 기본 min-width 는 auto 라 콘텐츠가 부모를 밀어낸다. min-width:0 을 걸어야 overflow 가 실제로 작동한다.",
}, },
{ {
caught: "다크 밴드 안 버튼의 대비가 2.88:1 — 기준은 4.5:1", caught: "eyebrow 라벨이 6개. 카운트 규칙 상한은 3개",
fixed: "밴드가 강조색만 반전시키고 그 위 글자색은 흰색 그대로였다. 반전 대응 토큰을 하나 더 두고 함께 바꿨다", fixed: "세 섹션만 남기고 나머지는 헤드라인만으로 구분했다. 라벨이 없어도 섹션은 읽힌다.",
}, },
{ {
caught: "스크롤을 끝까지 내려도 파이프라인 6단계 중 뒤 3개가 opacity 0 으로 남았다 — 이 페이지의 본체가 절반 사라졌다", caught: "본문 컬럼이 960px. 우리가 정한 한 줄 길이는 561px",
fixed: "animation-range 의 cover 22% 에 페이지 하단 요소는 영영 도달하지 못한다. 이펙트를 끄면 보이지만 켜면 사라지는 것은 폴백이 있는 것과 다르다. 연출을 뺐다", fixed: "그리드의 본문 폭을 토큰에 묶었다. 스킬 문서의 예시 코드도 같이 고쳤다.",
}, },
], ],
note: "일곱 건 모두 만들면서 걸린 것이고, 걸린 뒤에 고쳤다. 마지막 셋은 정적 검사로는 안 잡히고 320px 실렌더에서만 나왔다.", note: "일곱 건 모두 만들면서 걸린 것이고, 걸린 뒤에 고쳤다. 정적 검사로 잡힌 것은 하나도 없다. 전부 실제로 렌더해서 재고 나서야 나왔다.",
}, },
typography: { typography: {
eyebrow: "한글 조판", eyebrow: "한글 조판",
h2: "라틴 기준으로 만든 도구는 한글에서 멈춘다", h2: "라틴 기준으로 만든 도구는 한글에서 멈춘다",
lead: lead:
"영어권 디자인 도구는 한글 조판 규칙을 갖고 있지 않다. 9~11px 텍스트는 라틴에서 라벨이지만 한글에서는 읽을 수 없다. line-height 1.5는 라틴에서 표준이지만 한글에서는 답답하다.", "영어권 디자인 도구는 한글 조판 규칙을 갖고 있지 않다. 9에서 11px 텍스트는 라틴에서 라벨이지만 한글에서는 읽을 수 없다. line-height 1.5는 라틴에서 표준이지만 한글에서는 답답하다.",
body: body:
"가장 자주 깨지는 것은 줄바꿈이다. 기본값은 단어 중간에서 줄을 끊는다. 아래 두 문단은 같은 글자, 같은 폭, 같은 폰트다. 차이는 word-break 한 줄뿐이다.", "가장 자주 깨지는 것은 줄바꿈이다. 기본값은 단어 중간에서 줄을 끊는다. 아래 두 문단은 같은 글자, 같은 폭, 같은 폰트다. 차이는 word-break 한 줄뿐이다.",
demoLabels: ["기본값", "keep-all"], demoLabels: ["기본값", "keep-all"],
demoText: "원두의 산미와 바디를 구분하는 사람에게 필요한 것은 형용사가 아니라 수치다.", demoText: "원두의 산미와 바디를 구분하는 사람에게 필요한 것은 형용사가 아니라 수치다.",
rules: [ rules: [
"word-break: keep-all 없으면 단어가 아무 데서나 잘린다", "word-break: keep-all 없으면 단어가 아무 데서나 잘린다",
"line-height 1.6~1.8 — 라틴보다 넉넉하게", "line-height 1.6에서 1.8 사이. 라틴보다 넉넉하게",
"음수 자간 금지 한글은 이미 고정폭에 가까워 즉시 뭉개진다", "음수 자간 금지. 한글은 이미 고정폭에 가까워 즉시 뭉개진다",
"한 줄 25~40자 — 라틴 기준 65~75자를 그대로 쓰면 너무 길다", "한 줄 25자에서 40자. 라틴 기준 65자를 그대로 쓰면 너무 길다",
"한글 폰트를 지정하지 않는 것 자체가 완성도 미달 신호다", "한글 폰트를 지정하지 않는 것 자체가 완성도 미달 신호다",
], ],
}, },
rules: { rules: {
eyebrow: "규칙",
h2: "규칙마다 왜 그런지 적혀 있다", h2: "규칙마다 왜 그런지 적혀 있다",
lead: "근거 없는 금지는 지켜지지 않는다. 사용자가 명시적으로 요구하면 규칙보다 그쪽이 이긴다.", lead: "근거 없는 금지는 지켜지지 않는다. 사용자가 명시적으로 요구하면 규칙보다 그쪽이 이긴다.",
items: [ items: [
@ -246,7 +324,6 @@ export const ko: Content = {
], ],
}, },
install: { install: {
eyebrow: "설치",
h2: "한 줄이면 된다", h2: "한 줄이면 된다",
lead: "설치 대상을 감지해 기본 선택해준다. 스킬 파일을 직접 고쳤다면 업데이트가 그 파일을 건드리지 않는다.", lead: "설치 대상을 감지해 기본 선택해준다. 스킬 파일을 직접 고쳤다면 업데이트가 그 파일을 건드리지 않는다.",
command: "npx designpaca", command: "npx designpaca",
@ -255,7 +332,7 @@ export const ko: Content = {
{ name: "Codex CLI", path: "~/.codex/skills/designpaca/" }, { name: "Codex CLI", path: "~/.codex/skills/designpaca/" },
{ name: "Cursor", path: ".cursor/rules/designpaca.mdc" }, { name: "Cursor", path: ".cursor/rules/designpaca.mdc" },
{ name: "Windsurf", path: ".windsurf/rules/designpaca.md" }, { name: "Windsurf", path: ".windsurf/rules/designpaca.md" },
{ name: "AGENTS.md", path: "블록 주입 에이전트 무관" }, { name: "AGENTS.md", path: "블록 주입. 에이전트 무관" },
], ],
commands: [ commands: [
{ cmd: "npx designpaca update", what: "최신 스킬로 갱신. 직접 고친 파일은 건너뛴다" }, { cmd: "npx designpaca update", what: "최신 스킬로 갱신. 직접 고친 파일은 건너뛴다" },
@ -268,110 +345,163 @@ export const ko: Content = {
repo: "git.chanpaca.net/yunchan/designpaca", repo: "git.chanpaca.net/yunchan/designpaca",
license: "MIT", license: "MIT",
}, },
ui: { copy: "복사", copied: "복사됨", skipToContent: "본문으로 건너뛰기" }, ui: { copy: "복사", copied: "복사됨" },
}; };
export const en: Content = { export const en: Content = {
meta: { meta: {
title: "designpaca a web design pipeline skill", title: "designpaca · a web design pipeline skill",
description: description:
"A skill that makes AI coding agents do web design in order — from reference research to self-audit, six stages. Installs into Claude Code, Codex, Cursor and Windsurf in one line.", "A skill that makes AI coding agents do web design in order: reference research through self-audit, six stages. Installs into Claude Code, Codex, Cursor and Windsurf in one line.",
}, },
hero: { hero: {
h1: "designpaca", h1: "Undecided design ships as defaults",
lead: lead: "The AI look is not a skill problem. It is a decision problem. This skill forces the decisions, in order.",
"Ask an agent to “make it look good” and it pours out the defaults it knows. Not because it can't do better — because nothing was decided. This skill forces those decisions, in order.",
install: "npx designpaca", install: "npx designpaca",
installNote: "Node 20.11+ · detects and preselects your install targets", installNote: "Node 20.11+ · detects and preselects your install targets",
links: [ links: [
{ label: "Repository", href: "https://git.chanpaca.net/yunchan/designpaca" }, { label: "Repository", href: "https://git.chanpaca.net/yunchan/designpaca" },
{ label: "npm", href: "https://www.npmjs.com/package/designpaca" }, { label: "npm", href: "https://www.npmjs.com/package/designpaca" },
], ],
proofAlt:
"A coffee roastery subscription landing page built with designpaca. The hero carries a table of acidity and body figures for three beans.",
proofCaption: "Built with this skill, starting from one line of brief.",
}, },
pipeline: { pipeline: {
eyebrow: "Six stages", eyebrow: "Six stages",
h2: "From one line of brief to a page that passed its own audit", h2: "From one line of brief to a page that passed its own audit",
lead: lead:
"Every stage has a pass condition. Fail it and you don't move on. Below is what one brief actually became at each stage, from a coffee roastery landing page built with this skill.", "Every stage has a pass condition. Fail it and you do not move on. Below is what one brief became at each stage.",
briefLabel: "The brief that came in", briefLabel: "The brief that came in",
brief: "Build a subscription landing page for a small-batch coffee roastery", brief: "Build a subscription landing page for a small-batch coffee roastery",
stages: STAGES_EN, stages: STAGES_EN,
after: after:
"What remains after six stages is one page and one design.md. The next person reads that file and continues the same decisions.", "What remains after six stages is one page and one design.md. The next person reads that file and continues the same decisions.",
}, },
showcase: {
h2: "The outputs do not resemble each other",
lead:
"Two briefs through the same pipeline. Stage one forces the tone reference to come from a different industry, so the same tool does not produce the same screen twice. Both still have empty number slots, and that is not an accident either.",
items: [
{
alt: "A coffee subscription landing. Off-white ground, heavy Korean headline, a table of acidity and body figures on the right.",
caption: "Small-batch roastery · subscription conversion",
note: "Preset quiet-commerce. Having no photography became the argument, not the limit. The table values are still blank, and stay blank until the roaster supplies them.",
},
{
alt: "Pulsegate monitoring SaaS hero. Light ground, large serif headline, a free trial button.",
caption: "API monitoring SaaS · free trial signup",
note: "A developer tool did not default to dark. The brief carried no reason for it. The region count is still sitting in braces.",
},
],
},
materials: {
eyebrow: "Materials",
h2: "CSS decides the surface. The GPU handles the space.",
lead:
"The backdrop below is being drawn by a shader right now, and the three tiles are one striped fill wearing three different SVG filters. The skill's materials are running here rather than described.",
tiles: [
{
id: "grain",
name: "Paper",
how: "Fractal noise from feTurbulence, pushed down through alpha and laid over the surface.",
cost: "low cost · static",
},
{
id: "glass",
name: "Glass",
how: "Turbulence used as a displacement map. The filter region is widened so edges do not clip.",
cost: "medium cost · never animate it",
},
{
id: "duotone",
name: "Print",
how: "Reduce to luminance, then remap between two colours with feComponentTransfer.",
cost: "low cost · works on photography too",
},
],
note:
"The three tiles above are SVG filters running right now, and the light behind this page is a three.js shader. three is not in the initial bundle. It arrives after the first paint, and never at all without WebGL or with reduced motion requested. What you see then is not a fallback. It is the result.",
},
metrics: { metrics: {
eyebrow: "This page, measured", eyebrow: "This page, measured",
h2: "Fixing the copy does not remove the slop", h2: "Fixing the copy does not remove the slop",
lead: lead:
"Polishing sentences fixes sentences. The radius vocabulary, the translucent panels, the number of type steps all stay. So here are this page's own form metrics. If a number comes back badly, it ships as it is.", "Polishing sentences fixes sentences. The radius vocabulary, the translucent panels, the number of type steps all stay. So here are this page's own form metrics. If a number comes back badly, it ships as it is.",
items: [ items: [
{ label: "radius vocabulary", value: "2", note: "3px and 6px. Nothing else" }, { label: "JS to first paint", value: "0.6%", note: "of the 50KB budget actually spent", feature: true },
{ label: "translucent panels (backdrop-filter)", value: "0", note: "costs 1530% FPS on real devices" }, { label: "hard gates", value: "12/12", note: "8 checked statically, 4 on a real 320px render", feature: true },
{ label: "type scale steps", value: "5", note: "the display size is defined outside the scale" }, { label: "three.js payload", value: "185KB", note: "fetched only as the materials section approaches. Never in the initial bundle" },
{ label: "font families", value: "2", note: "one text + one mono. Each language loads only its own" }, { label: "slop fingerprints", value: "0", note: "9 patterns grepped" },
{ label: "accent colours", value: "1", note: "deep teal. Light and dark are the same colour" }, { label: "radius vocabulary", value: "2", note: "10px and 20px. The shell is derived" },
{ label: "JS to first paint", value: "314B", note: "gzip. 0.6% of the 50KB budget. One copy button" }, { label: "translucent panels", value: "29", note: "glass is the subject here. No filter sits on scrolling content, only on top of fixed light" },
{ label: "slop fingerprints", value: "0", note: "9 patterns checked" }, { label: "type scale steps", value: "5", note: "the display size sits outside the scale" },
{ label: "hard gates", value: "12/12", note: "all twelve checked. Four were measured on a real 320px render" }, { label: "font families", value: "2", note: "each language loads only its own" },
{ label: "time to interactive", value: "—", note: "not measured yet. Lighthouse, after deploy" }, { label: "accent colours", value: "1", note: "light and dark are the same colour" },
{ label: "time to interactive", value: "not yet", note: "measured with Lighthouse after deploy" },
], ],
caveat: "Measured on the production build. Unmeasured entries stay empty — a hole is more honest than an invented number.", caveat:
"Measured on the production build. Unmeasured entries stay empty. A hole is more honest than an invented number. This page is a product demo, so the JS budget was raised to carry three.js. Saying so here is the rule.",
}, },
preflight: { preflight: {
eyebrow: "Preflight log",
h2: "What this page actually got caught on", h2: "What this page actually got caught on",
lead: "Not the result of auditing someone else. The result of auditing itself.", lead: "Not the result of auditing someone else. The result of auditing itself.",
items: [ items: [
{ {
caught: "6 eyebrow labels — the count rule caps them at ceil(7 sections / 3) = 3", caught: "Adding SVG refraction to the glass removed the blur along with it",
fixed: "Kept three, dropped the rest. Sections read fine without a label above the heading", fixed:
"Attach url() to backdrop-filter and Chrome reports true through CSS.supports while discarding the whole declaration at render time. The computed value becomes none. Refraction is demonstrated for real by the SVG tiles in the materials section, and the panels build thickness from blur and edge light instead.",
}, },
{ {
caught: "The scroll reveal used clip-path — hard gate 8 allows only transform and opacity", caught: "444px of horizontal overflow. The entire layout was off its rails",
fixed: "Switched to a cover pushed with translateX. Same result, and clip-path's interpolation limits went away with it", fixed:
"The grid class had gone missing from main. Subgrid only works when the parent is a grid, so every section had lost its alignment. It merely looked slightly off until the width was actually measured.",
}, },
{ {
caught: "Divider colour sits at 1.30:1 — fine as decoration, a WCAG 1.4.11 failure as an input border", caught: "Scrolling to the very bottom still left the last 3 of 6 pipeline stages invisible",
fixed: "Added a 6.11:1 alias and split the two uses. No new colour, so the palette count is unchanged", fixed:
"Elements near the page bottom never reach the end of a scroll-driven animation range. Visible with effects off but missing with them on is not the same as having a fallback. Rebuilt so that once revealed, it stays revealed.",
}, },
{ {
caught: "The text column was 960px — wider than the measure we had just set (561px)", caught: "The button inside the dark band sat at 2.88:1. The threshold is 4.5:1",
fixed: "Bound the grid's main column to --measure. The skill's own example code was wrong too, so that got fixed as well", fixed:
"The band inverted the accent colour but left the text on it white. Added an inverted counterpart token and switched both together.",
}, },
{ {
caught: "13px of horizontal overflow at 320px — even with the table inside an overflow container", caught: "13px of horizontal overflow at 320px, even with the table inside a scroll container",
fixed: "A grid item's default min-width is auto, so content pushes the parent wider. overflow-x only works once min-width:0 is set", fixed:
"A grid item's default min-width is auto, so content pushes the parent wider. Overflow only works once min-width:0 is set.",
}, },
{ {
caught: "The button inside the dark band sat at 2.88:1 — the threshold is 4.5:1", caught: "6 eyebrow labels. The count rule caps them at 3",
fixed: "The band inverted the accent colour but left the text on it white. Added an inverted counterpart token and switched both together", fixed: "Kept three, dropped the rest. Sections read fine without a label above the heading.",
}, },
{ {
caught: "Scrolling to the very bottom still left the last 3 of 6 pipeline stages at opacity 0 — half of this page's core section was gone", caught: "The text column was 960px. The measure we had just set was 561px",
fixed: "Elements near the page bottom never reach cover 22% in animation-range. Visible with effects off but missing with them on is not the same as having a fallback. The effect was removed", fixed:
"Bound the grid's main column to the token. The skill's own example code was wrong too, so that got fixed as well.",
}, },
], ],
note: "All seven were caught while building this page, and fixed after being caught. The last three only surfaced on a real 320px render — static checks missed them.", note: "All seven were caught while building this page, and fixed after being caught. Not one was found by static checking. Every one of them needed a real render and an actual measurement.",
}, },
typography: { typography: {
eyebrow: "Your language is probably not Latin", eyebrow: "Your language is probably not Latin",
h2: "Tools built on Latin assumptions stop at the script boundary", h2: "Tools built on Latin assumptions stop at the script boundary",
lead: lead:
"Design tooling written in English carries no rules for other scripts. 911px text is a label in Latin and unreadable in Korean. A line-height of 1.5 is standard in Latin and cramped in Korean.", "Design tooling written in English carries no rules for other scripts. 9 to 11px text is a label in Latin and unreadable in Korean. A line-height of 1.5 is standard in Latin and cramped in Korean.",
body: body:
"Line breaking fails first. The default breaks Korean mid-word — the equivalent of breaking English mid-syllable, in the middle of a sentence. Both paragraphs below use the same text, width and font. The only difference is one line of word-break.", "Line breaking fails first. The default breaks Korean mid-word, the equivalent of breaking English mid-syllable in the middle of a sentence. Both paragraphs below use the same text, width and font. The only difference is one line of word-break.",
demoLabels: ["default", "keep-all"], demoLabels: ["default", "keep-all"],
demoText: "원두의 산미와 바디를 구분하는 사람에게 필요한 것은 형용사가 아니라 수치다.", demoText: "원두의 산미와 바디를 구분하는 사람에게 필요한 것은 형용사가 아니라 수치다.",
rules: [ rules: [
"word-break: keep-all — without it, words break at arbitrary points", "word-break: keep-all, or words break at arbitrary points",
"line-height 1.61.8 — more generous than Latin needs", "line-height between 1.6 and 1.8, more generous than Latin needs",
"no negative letter-spacing Korean glyphs are near-monospaced and collapse immediately", "no negative letter-spacing. Korean glyphs are near-monospaced and collapse immediately",
"2540 characters per line — Latin's 6575 is far too long", "25 to 40 characters per line. Latin's 65 is far too long",
"not specifying a Korean font is itself a sign of unfinished work", "not specifying a Korean font is itself a sign of unfinished work",
], ],
}, },
rules: { rules: {
eyebrow: "Rules",
h2: "Every rule states why", h2: "Every rule states why",
lead: "A prohibition without a reason does not survive contact with a brief. If the user asks for it explicitly, the user wins.", lead: "A prohibition without a reason does not survive contact with a brief. If the user asks for it explicitly, the user wins.",
items: [ items: [
@ -380,11 +510,13 @@ export const en: Content = {
{ rule: "Be bold in exactly one place", why: "Shout in two places and both go unheard." }, { rule: "Be bold in exactly one place", why: "Shout in two places and both go unheard." },
{ rule: "Spectacle ranks fourth", why: "Judging weights are Design 40 / Usability 30 / Creativity 20 / Content 10." }, { rule: "Spectacle ranks fourth", why: "Judging weights are Design 40 / Usability 30 / Creativity 20 / Content 10." },
{ rule: "Never invent a number", why: "A number-shaped hole is honest. An invented number costs trust." }, { rule: "Never invent a number", why: "A number-shaped hole is honest. An invented number costs trust." },
{ rule: "It must be finished with every effect off", why: "That state is the basis of every fallback. If it collapses, the effects were doing the structure's job." }, {
rule: "It must be finished with every effect off",
why: "That state is the basis of every fallback. If it collapses, the effects were doing the structure's job.",
},
], ],
}, },
install: { install: {
eyebrow: "Install",
h2: "One line", h2: "One line",
lead: "Targets are detected and preselected. If you edit a skill file yourself, updates leave it alone.", lead: "Targets are detected and preselected. If you edit a skill file yourself, updates leave it alone.",
command: "npx designpaca", command: "npx designpaca",
@ -393,7 +525,7 @@ export const en: Content = {
{ name: "Codex CLI", path: "~/.codex/skills/designpaca/" }, { name: "Codex CLI", path: "~/.codex/skills/designpaca/" },
{ name: "Cursor", path: ".cursor/rules/designpaca.mdc" }, { name: "Cursor", path: ".cursor/rules/designpaca.mdc" },
{ name: "Windsurf", path: ".windsurf/rules/designpaca.md" }, { name: "Windsurf", path: ".windsurf/rules/designpaca.md" },
{ name: "AGENTS.md", path: "block injection agent agnostic" }, { name: "AGENTS.md", path: "block injection, agent agnostic" },
], ],
commands: [ commands: [
{ cmd: "npx designpaca update", what: "Pull the latest skill. Files you edited are skipped" }, { cmd: "npx designpaca update", what: "Pull the latest skill. Files you edited are skipped" },
@ -406,5 +538,5 @@ export const en: Content = {
repo: "git.chanpaca.net/yunchan/designpaca", repo: "git.chanpaca.net/yunchan/designpaca",
license: "MIT", license: "MIT",
}, },
ui: { copy: "Copy", copied: "Copied", skipToContent: "Skip to content" }, ui: { copy: "Copy", copied: "Copied" },
}; };

View file

@ -16,23 +16,41 @@ const FONT_CSS = {
} as const; } as const;
const alt = lang === "ko" ? "/en/" : "/"; const alt = lang === "ko" ? "/en/" : "/";
// 파비콘: 페이지의 스파인 모티프를 그대로 쓴 단순 기하 마크
const FAVICON =
"data:image/svg+xml," +
encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#0E6B63"/>
<path d="M11 7v18" stroke="#FAF9F6" stroke-width="2" stroke-linecap="round"/>
<circle cx="11" cy="10" r="3" fill="#FAF9F6"/>
<circle cx="11" cy="16" r="3" fill="#FAF9F6"/>
<circle cx="11" cy="22" r="3" fill="#FAF9F6"/>
<path d="M18 10h7M18 16h5M18 22h7" stroke="#FAF9F6" stroke-width="2" stroke-linecap="round" opacity=".55"/>
</svg>`,
);
--- ---
<!doctype html> <!doctype html>
<html lang={lang}> <html lang={lang} class="no-js">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title> <title>{title}</title>
<meta name="description" content={description} /> <meta name="description" content={description} />
<link rel="icon" href={FAVICON} />
<link rel="canonical" href={new URL(Astro.url.pathname, Astro.site)} /> <link rel="canonical" href={new URL(Astro.url.pathname, Astro.site)} />
<link rel="alternate" hreflang="ko" href={new URL("/", Astro.site)} /> <link rel="alternate" hreflang="ko" href={new URL("/", Astro.site)} />
<link rel="alternate" hreflang="en" href={new URL("/en/", Astro.site)} /> <link rel="alternate" hreflang="en" href={new URL("/en/", Astro.site)} />
<link rel="alternate" hreflang="x-default" href={new URL("/", Astro.site)} /> <link rel="alternate" hreflang="x-default" href={new URL("/", Astro.site)} />
{/* 본문 폰트만 preconnect·preload 한다. 모노는 코드 블록에만 쓰인다 */} <link
<link rel="preconnect" href={lang === "ko" ? "https://cdn.jsdelivr.net" : "https://api.fontshare.com"} crossorigin /> rel="preconnect"
href={lang === "ko" ? "https://cdn.jsdelivr.net" : "https://api.fontshare.com"}
crossorigin
/>
<link rel="stylesheet" href={FONT_CSS[lang]} /> <link rel="stylesheet" href={FONT_CSS[lang]} />
<link <link
rel="stylesheet" rel="stylesheet"
@ -44,20 +62,36 @@ const alt = lang === "ko" ? "/en/" : "/";
<meta property="og:title" content={title} /> <meta property="og:title" content={title} />
<meta property="og:description" content={description} /> <meta property="og:description" content={description} />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:locale" content={lang === "ko" ? "ko_KR" : "en_US"} />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
{/* JS 가 살아 있으면 no-js 를 떼고 진입 연출을 켠다 */}
<script is:inline>
document.documentElement.classList.remove("no-js");
</script>
</head> </head>
<body> <body>
<a class="skip" href="#main">{lang === "ko" ? "본문으로 건너뛰기" : "Skip to content"}</a> <a class="skip" href="#main">{lang === "ko" ? "본문으로 건너뛰기" : "Skip to content"}</a>
<header class="page site-header"> <header class="site-header">
<a class="wordmark" href={lang === "ko" ? "/" : "/en/"}>designpaca</a> <div class="header-inner glass">
<nav aria-label={lang === "ko" ? "언어" : "Language"}> <a class="wordmark" href={lang === "ko" ? "/" : "/en/"} aria-label="designpaca">
{/* 언어는 국가가 아니다 — 국기 아이콘을 쓰지 않는다 */} <svg class="mark" viewBox="0 0 32 32" width="20" height="20" aria-hidden="true">
<a href={alt} hreflang={lang === "ko" ? "en" : "ko"} lang={lang === "ko" ? "en" : "ko"}> <path d="M11 7v18" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" />
{lang === "ko" ? "English" : "한국어"} <circle cx="11" cy="10" r="3" fill="currentColor" />
<circle cx="11" cy="16" r="3" fill="currentColor" />
<circle cx="11" cy="22" r="3" fill="currentColor" />
</svg>
<span>designpaca</span>
</a> </a>
</nav> <nav aria-label={lang === "ko" ? "언어" : "Language"}>
{/* 언어는 국가가 아니다. 국기 아이콘을 쓰지 않는다 */}
<a href={alt} hreflang={lang === "ko" ? "en" : "ko"} lang={lang === "ko" ? "en" : "ko"}>
{lang === "ko" ? "English" : "한국어"}
</a>
</nav>
</div>
</header> </header>
<main id="main" class="page"> <main id="main" class="page">
@ -67,6 +101,29 @@ const alt = lang === "ko" ? "/en/" : "/";
<footer class="page site-footer"> <footer class="page site-footer">
<slot name="footer" /> <slot name="footer" />
</footer> </footer>
<script is:inline>
// 진입 연출. 한 번 보이면 계속 보인다.
// scroll 리스너를 쓰지 않는다(하드 게이트 10).
(function () {
var els = document.querySelectorAll(".reveal");
if (!("IntersectionObserver" in window) || !els.length) {
els.forEach(function (el) { el.classList.add("is-in"); });
return;
}
var io = new IntersectionObserver(
function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
e.target.classList.add("is-in");
io.unobserve(e.target);
});
},
{ rootMargin: "200px 0px 200px 0px", threshold: 0 },
);
els.forEach(function (el) { io.observe(el); });
})();
</script>
</body> </body>
</html> </html>
@ -82,31 +139,51 @@ const alt = lang === "ko" ? "/en/" : "/";
z-index: 10; z-index: 10;
transition: transform var(--dur-quick) var(--ease-out); transition: transform var(--dur-quick) var(--ease-out);
} }
.skip:focus { .skip:focus { transform: translateY(calc(var(--space-3) * 5)); }
transform: translateY(calc(var(--space-3) * 5));
}
/* 상단에 붙은 풀블리드 바 대신 떠 있는 헤더 */
/* 상단에 붙은 바가 아니라 떠 있는 유리 알약 */
.site-header { .site-header {
padding-block: var(--space-4); position: sticky;
top: var(--space-3);
z-index: 3;
padding-inline: var(--space-4);
margin-top: var(--space-3);
} }
.site-header > * { .header-inner {
grid-column: main; max-width: 1180px;
} margin-inline: auto;
.site-header { display: flex;
align-items: baseline; align-items: center;
justify-content: space-between;
gap: var(--space-4);
padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4);
border-radius: 999px;
} }
.wordmark { .wordmark {
font-weight: 600; display: inline-flex;
align-items: center;
gap: var(--space-2);
font-weight: 560;
letter-spacing: -0.01em; letter-spacing: -0.01em;
text-decoration: none; text-decoration: none;
} }
.site-header nav { .mark { color: var(--accent); flex: none; }
justify-self: end; .site-header nav a {
grid-row: 1; display: inline-flex;
grid-column: main; align-items: center;
min-height: 36px;
padding-inline: var(--space-3);
border-radius: 999px;
background: var(--glass-tint-2);
border: 1px solid var(--glass-edge);
font-size: var(--step-0); font-size: var(--step-0);
color: var(--ink-muted); color: var(--ink-muted);
text-decoration: none;
transition: color var(--dur-instant) var(--ease-out),
background var(--dur-instant) var(--ease-out);
} }
.site-header nav a:hover { color: var(--ink); background: var(--glass-tint); }
.site-footer { .site-footer {
padding-block: var(--space-6); padding-block: var(--space-6);

View file

@ -2,6 +2,8 @@
import Base from "../../layouts/Base.astro"; import Base from "../../layouts/Base.astro";
import Hero from "../../components/Hero.astro"; import Hero from "../../components/Hero.astro";
import Pipeline from "../../components/Pipeline.astro"; import Pipeline from "../../components/Pipeline.astro";
import Showcase from "../../components/Showcase.astro";
import Materials from "../../components/Materials.astro";
import Metrics from "../../components/Metrics.astro"; import Metrics from "../../components/Metrics.astro";
import Preflight from "../../components/Preflight.astro"; import Preflight from "../../components/Preflight.astro";
import Typography from "../../components/Typography.astro"; import Typography from "../../components/Typography.astro";
@ -13,6 +15,8 @@ import { en as c } from "../../i18n/content";
<Base lang="en" title={c.meta.title} description={c.meta.description}> <Base lang="en" title={c.meta.title} description={c.meta.description}>
<Hero c={c.hero} ui={c.ui} /> <Hero c={c.hero} ui={c.ui} />
<Pipeline c={c.pipeline} /> <Pipeline c={c.pipeline} />
<Showcase c={c.showcase} />
<Materials c={c.materials} />
<Metrics c={c.metrics} /> <Metrics c={c.metrics} />
<Preflight c={c.preflight} /> <Preflight c={c.preflight} />
<Typography c={c.typography} /> <Typography c={c.typography} />

View file

@ -2,6 +2,8 @@
import Base from "../layouts/Base.astro"; import Base from "../layouts/Base.astro";
import Hero from "../components/Hero.astro"; import Hero from "../components/Hero.astro";
import Pipeline from "../components/Pipeline.astro"; import Pipeline from "../components/Pipeline.astro";
import Showcase from "../components/Showcase.astro";
import Materials from "../components/Materials.astro";
import Metrics from "../components/Metrics.astro"; import Metrics from "../components/Metrics.astro";
import Preflight from "../components/Preflight.astro"; import Preflight from "../components/Preflight.astro";
import Typography from "../components/Typography.astro"; import Typography from "../components/Typography.astro";
@ -13,6 +15,8 @@ import { ko as c } from "../i18n/content";
<Base lang="ko" title={c.meta.title} description={c.meta.description}> <Base lang="ko" title={c.meta.title} description={c.meta.description}>
<Hero c={c.hero} ui={c.ui} /> <Hero c={c.hero} ui={c.ui} />
<Pipeline c={c.pipeline} /> <Pipeline c={c.pipeline} />
<Showcase c={c.showcase} />
<Materials c={c.materials} />
<Metrics c={c.metrics} /> <Metrics c={c.metrics} />
<Preflight c={c.preflight} /> <Preflight c={c.preflight} />
<Typography c={c.typography} /> <Typography c={c.typography} />

View file

@ -5,13 +5,12 @@
*, *,
*::before, *::before,
*::after { *::after { box-sizing: border-box; }
box-sizing: border-box;
}
html { html {
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
scroll-behavior: smooth; scroll-behavior: smooth;
background: var(--surface);
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; } html { scroll-behavior: auto; }
@ -28,12 +27,14 @@ body {
font-synthesis-weight: none; font-synthesis-weight: none;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
overflow-x: hidden;
} }
/* 한글은 단어 중간에서 끊지 않는다 */
[lang="ko"] body, [lang="ko"] body,
[lang="ko"] p, [lang="ko"] p,
[lang="ko"] li, [lang="ko"] li,
[lang="ko"] dt,
[lang="ko"] dd,
[lang="ko"] h1, [lang="ko"] h1,
[lang="ko"] h2, [lang="ko"] h2,
[lang="ko"] h3 { [lang="ko"] h3 {
@ -41,9 +42,37 @@ body {
overflow-wrap: break-word; overflow-wrap: break-word;
} }
/* ---- 그리드 ------------------------------------------------ /* ---- 대기광 -------------------------------------------------
main 폭은 3단계에서 정한 --measure . 고정값을 쓰지 않는다. 유리는 뒤에 무언가 있어야 유리로 보인다. 페이지 전체에 깔리는
----------------------------------------------------------- */ 고정 광원 개가 역할을 한다. fixed 스크롤 리페인트가 없다. */
body::before {
content: "";
position: fixed;
inset: 0;
z-index: -2;
pointer-events: none;
background:
radial-gradient(58vw 46vw at 14% -6%,
color-mix(in oklab, var(--accent) 26%, transparent), transparent 68%),
radial-gradient(52vw 44vw at 92% 8%,
color-mix(in oklab, #6E7BFF 20%, transparent), transparent 66%),
radial-gradient(70vw 60vw at 62% 108%,
color-mix(in oklab, var(--accent) 14%, transparent), transparent 70%);
}
/* ---- 필름 그레인 ------------------------------------------- */
body::after {
content: "";
position: fixed;
inset: 0;
z-index: 4;
pointer-events: none;
opacity: 0.05;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
mix-blend-mode: overlay;
}
/* ---- 그리드 ------------------------------------------------ */
.page { .page {
display: grid; display: grid;
grid-template-columns: grid-template-columns:
@ -57,15 +86,19 @@ body {
.page > .wide { grid-column: wide; } .page > .wide { grid-column: wide; }
.page > .full { grid-column: full; } .page > .full { grid-column: full; }
/* 섹션 안에서도 같은 그리드를 쓴다 — 정렬선이 페이지 전체에서 하나여야 한다 */
.section { .section {
display: grid; display: grid;
grid-template-columns: subgrid; grid-template-columns: subgrid;
grid-column: full; grid-column: full;
padding-block: var(--space-7); padding-block: var(--space-7);
position: relative;
} }
.section > * { grid-column: main; } .section > * { grid-column: main; }
.section > .wide { grid-column: wide; } .section > .wide { grid-column: wide; }
.section > .full { grid-column: full; }
.page > *,
.section > * { min-width: 0; }
/* ---- 타이포 ------------------------------------------------ */ /* ---- 타이포 ------------------------------------------------ */
h1, h2, h3 { h1, h2, h3 {
@ -75,23 +108,19 @@ h1, h2, h3 {
font-weight: 600; font-weight: 600;
text-wrap: balance; text-wrap: balance;
} }
h1 { font-size: var(--display); font-weight: 620; } h1 { font-size: var(--display); font-weight: 660; }
h2 { font-size: var(--step-3); } h2 { font-size: var(--step-3); }
h3 { font-size: var(--step-1); } h3 { font-size: var(--step-1); }
p { margin: 0 0 var(--space-3); text-wrap: pretty; } p { margin: 0 0 var(--space-3); text-wrap: pretty; }
p:last-child { margin-bottom: 0; } p:last-child { margin-bottom: 0; }
.lead { .lead { font-size: var(--step-1); color: var(--ink-muted); }
font-size: var(--step-1);
color: var(--ink-muted);
}
/* eyebrow 는 새 크기를 만들지 않는다 — 본문 크기 + 자간 + muted */
.eyebrow { .eyebrow {
font-size: var(--step-0); font-size: var(--step-0);
letter-spacing: var(--tracking-label); letter-spacing: var(--tracking-label);
color: var(--ink-muted); color: var(--accent);
font-weight: 500; font-weight: 500;
margin: 0 0 var(--space-3); margin: 0 0 var(--space-3);
} }
@ -109,48 +138,130 @@ a {
} }
a:hover { color: var(--accent); } a:hover { color: var(--accent); }
/* 포커스는 어떤 이펙트보다 우선한다 */
:focus-visible { :focus-visible {
outline: 2px solid var(--accent); outline: 2px solid var(--accent);
outline-offset: 3px; outline-offset: 3px;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
} }
hr { /* ============================================================
border: 0; 유리
border-top: 1px solid var(--line); backdrop-filter 쓰면 흐린 유리에서 끝난다. 두께와 굴절이
margin-block: var(--space-6); 있어야 유리로 읽힌다. 그래서 겹을 쌓는다.
1) 배경 흐림 + 채도 (backdrop-filter)
2) 가장자리 (border + inset shadow)
3) 위쪽 반사 (::before 그라디언트)
굴절은 지원되는 브라우저에서만 backdrop-filter SVG 얹는다.
============================================================ */
.glass {
position: relative;
isolation: isolate;
background: var(--glass-tint);
border: 1px solid var(--glass-edge);
border-radius: var(--radius-md);
box-shadow: var(--glass-hi), var(--shadow-2);
backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
}
/* 위쪽 모서리에 닿는 빛. 유리판의 두께가 여기서 보인다 */
.glass::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
border-radius: inherit;
pointer-events: none;
background:
linear-gradient(
180deg,
rgb(255 255 255 / 0.10) 0%,
rgb(255 255 255 / 0.02) 22%,
transparent 55%
);
}
/* backdrop-filter SVG 필터를 얹는 것은 쓰지 않는다.
Chrome CSS.supports 로는 true 주면서 렌더에서 선언 전체를 버린다
(실측: computed backdrop-filter none 된다).
굴절은 재료 섹션의 SVG 타일이 진짜로 보여준다. 여기서는 두께를 만든다. */
/* 가장자리를 도는 빛. 유리판 테두리의 굴절을 흉내낸다 */
.glass::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
padding: 1px;
background: conic-gradient(
from 210deg,
rgb(255 255 255 / 0.32),
transparent 22%,
transparent 58%,
rgb(255 255 255 / 0.20) 74%,
transparent 92%
);
-webkit-mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
mask-composite: exclude;
} }
/* ---- 표 ---------------------------------------------------- */ /* 투명도를 줄여달라는 사용자에게는 불투명 판으로 준다 */
table { @media (prefers-reduced-transparency: reduce) {
border-collapse: collapse; .glass {
width: 100%; background: var(--surface-raised);
font-variant-numeric: tabular-nums; backdrop-filter: none;
-webkit-backdrop-filter: none;
}
} }
th, td { @supports not (backdrop-filter: blur(1px)) {
text-align: left; .glass { background: var(--surface-raised); }
padding: var(--space-2) var(--space-3) var(--space-2) 0;
border-bottom: 1px solid var(--line);
vertical-align: baseline;
}
th {
font-weight: 500;
color: var(--ink-muted);
font-size: var(--step-0);
letter-spacing: var(--tracking-label);
}
/* 좁은 화면에서 표가 페이지를 밀지 않게 하드 게이트 #5.
min-width:0 없으면 grid item 기본 min-width:auto 때문에
overflow-x:auto 걸려 있어도 페이지 자체가 넓어진다. */
.table-scroll {
overflow-x: auto;
min-width: 0;
} }
/* 같은 이유로 그리드·플렉스 자식은 전부 줄어들 수 있어야 한다 */ /* 유리 위에 얹는 두 번째 판. 안쪽으로 갈수록 밝아진다 */
.page > *, .glass-2 { background: var(--glass-tint-2); }
.section > * { min-width: 0; }
/* 이중 베젤: 유리판이 트레이 안에 앉는다 */
.shell {
background: var(--glass-tint);
border: 1px solid var(--glass-edge);
border-radius: var(--radius-shell);
padding: var(--shell-pad);
box-shadow: var(--glass-hi), var(--shadow-2);
backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
}
.core {
background: var(--surface-sunken);
border-radius: var(--radius-md);
box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.06);
}
@media (prefers-reduced-transparency: reduce) {
.shell { background: var(--surface-raised); backdrop-filter: none; }
}
/* ---- 진입 연출 ----------------------------------------------
opacity 콘텐츠를 숨기지 않는다.
페이지에서 데였다. 처음엔 scroll-driven animation
범위 끝에 하단 요소가 도달하지 못해 6단계 3개가 영영 숨었고,
번째는 IntersectionObserver 부드러운 스크롤 중간 지점을
건너뛰어 50개 49개가 숨은 채로 남았다.
그래서 규칙을 바꿨다. 움직이는 것은 위치뿐이다. 최악의 경우에도
요소가 14px 아래에 있을 뿐이고 글자는 항상 읽힌다. */
@media (prefers-reduced-motion: no-preference) {
.reveal {
transform: translateY(14px);
transition: transform var(--dur-slow) var(--ease-soft);
transition-delay: var(--reveal-delay, 0ms);
}
.reveal.is-in { transform: none; }
}
/* ---- 유틸 -------------------------------------------------- */ /* ---- 유틸 -------------------------------------------------- */
.visually-hidden { .visually-hidden {
@ -164,3 +275,5 @@ th {
.muted { color: var(--ink-muted); } .muted { color: var(--ink-muted); }
.mono { font-family: var(--font-mono); font-size: var(--mono-size); } .mono { font-family: var(--font-mono); font-size: var(--mono-size); }
.nums { font-variant-numeric: tabular-nums; }
.table-scroll { overflow-x: auto; min-width: 0; }

View file

@ -1,72 +1,93 @@
/* ============================================================ /* ============================================================
designpaca 소개 페이지 디자인 토큰 designpaca 소개 페이지 디자인 토큰
프리셋: swiss-minimal / 비율 1.25 / 강조색 1개 / radius 2종 방향 재설정 2026-08-20: Ethereal Glass
페이지는 SVG 필터와 three.js 파는 제품의 데모다.
리퀴드 글래스는 기술의 결합 자체이므로 여기서는
장식이 아니라 제품 시연이다. 다크 기반을 고른 것도 같은 이유다.
유리는 어두운 바탕 위에서만 굴절이 읽힌다.
============================================================ */ ============================================================ */
:root { :root {
/* ---- 폰트 패밀리 ---------------------------------------- */
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace; --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
--mono-size: 0.9375em; /* 부모 대비. 새 스케일 단계를 만들지 않는다 */ --mono-size: 0.9375em;
/* ---- 색: 라이트 (기본) ----------------------------------- */ /* ---- 바탕: 잉크. 순흑이 아니다 --------------------------- */
--surface: #FAF9F6; /* 웜 오프화이트. 순백 아님 (A-07) */ --surface: #0B0E12;
--surface-raised: #F2F0EA; --surface-raised: #11151B;
--surface-inverse: #14161A; /* 다크 밴드. 순흑 아님 */ --surface-sunken: #070A0D;
--ink: #16181C; /* 대비 16.88:1 */ /* 밝은 밴드 하나. 규칙으로 정의된 반전이다 */
--ink-muted: #5A5F66; /* 대비 6.11:1 */ --surface-inverse: #F4F2EC;
--ink-inverse: #E9E7E2; /* 밴드 위 14.66:1 */
--ink-muted-inverse: #9AA0A6; /* 밴드 위 6.86:1 */
--line: #DFDCD4; /* 1.30:1 — 장식 구분선 전용 */
--line-inverse: #2A2E34; /* 1.33:1 — 장식 구분선 전용 */
--line-strong: var(--ink-muted); /* 입력·컨트롤 테두리. WCAG 1.4.11 (3:1) */
--accent: #0E6B63; /* 딥 틸. 대비 6.04:1 */
--accent-ink: #FFFFFF; /* 강조 위 글자. 6.36:1 */
--accent-inverse: #3FA89D; /* 밴드 위 강조. 6.30:1 */
--accent-ink-inverse: #0B1A18; /* 밴드 위 강조 위의 글자. 6.43:1 */
/* ---- 형태: radius 2종 (게이트 #3) ------------------------ */ --ink: #ECEFF2; /* 15.9:1 */
--radius-sm: 3px; /* 버튼·입력·배지 */ --ink-muted: #98A2AE; /* 7.1:1 */
--radius-md: 6px; /* 카드·코드 블록 */ --ink-inverse: #14181E;
--ink-muted-inverse: #565E68;
--line: #222932;
--line-strong: #39424E;
--line-inverse: #D8D4CA;
/* 강조: 시안 틸. 유리 너머에서도 살아남는 채도 */
--accent: #4FD6C4; /* 잉크 위 10.4:1 */
--accent-ink: #04211D;
--accent-inverse: #0E7A6E;
--accent-ink-inverse: #FFFFFF;
/* ---- 유리 ------------------------------------------------
backdrop-filter 만으로는 흐린 유리다. 굴절은 SVG 만든다. */
--glass-tint: rgb(255 255 255 / 0.045);
--glass-tint-2: rgb(255 255 255 / 0.085);
--glass-edge: rgb(255 255 255 / 0.14);
--glass-blur: 22px;
--glass-sat: 150%;
/* 안쪽 위 모서리에 닿는 빛. 유리 두께를 만든다 */
--glass-hi:
inset 0 1px 0 rgb(255 255 255 / 0.22),
inset 0 -1px 0 rgb(255 255 255 / 0.05);
--shadow-1: 0 1px 2px rgb(0 0 0 / 0.5);
--shadow-2: 0 2px 6px rgb(0 0 0 / 0.4), 0 12px 32px rgb(0 0 0 / 0.5);
--shadow-3: 0 6px 16px rgb(0 0 0 / 0.45), 0 32px 80px rgb(0 0 0 / 0.6);
--inset-hi: var(--glass-hi);
/* ---- 형태 ------------------------------------------------ */
--radius-sm: 10px;
--radius-md: 20px;
--shell-pad: 6px;
--radius-shell: calc(var(--radius-md) + var(--shell-pad));
/* ---- 간격 ------------------------------------------------ */ /* ---- 간격 ------------------------------------------------ */
--space-1: 0.25rem; /* 4 */ --space-1: 0.25rem;
--space-2: 0.5rem; /* 8 */ --space-2: 0.5rem;
--space-3: 1rem; /* 16 */ --space-3: 1rem;
--space-4: 1.5rem; /* 24 */ --space-4: 1.5rem;
--space-5: 2.5rem; /* 40 */ --space-5: 2.5rem;
--space-6: 4rem; /* 64 */ --space-6: 4rem;
--space-7: 6rem; /* 96 — 일반 섹션 상하 */ --space-7: 7rem;
--space-8: 8rem; /* 128 — 다크 밴드 상하. 본문 간격의 8배 */ --space-8: 10rem;
/* ---- 모션 ------------------------------------------------ */ /* ---- 모션 ------------------------------------------------ */
--dur-instant: 100ms; /* 호버·포커스 */ --dur-instant: 120ms;
--dur-quick: 200ms; /* 작은 등장·퇴장 */ --dur-quick: 280ms;
--dur-normal: 320ms; /* 패널·전환. 2단계 상한 */ --dur-normal: 560ms;
--dur-slow: 600ms; /* 이 프로젝트에서는 사용하지 않는다 */ --dur-slow: 900ms;
--ease-out: cubic-bezier(0.22, 1, 0.36, 1); --ease-out: cubic-bezier(0.32, 0.72, 0, 1);
--ease-soft: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in: cubic-bezier(0.64, 0, 0.78, 0); --ease-in: cubic-bezier(0.64, 0, 0.78, 0);
--ease-soft: cubic-bezier(0.4, 0, 0.2, 1);
/* bounce·elastic 없음 (E-02) */
} }
/* ---- 색: 다크 테마 — 라이트와 동등하게 정의 ------------------ */ /* ---- 라이트 선호: 유리를 얇게 하고 바탕을 올린다 --------------
@media (prefers-color-scheme: dark) { 테마를 뒤집지 않는다. 페이지는 어두운 유리가 주제다.
다만 라이트를 쓰는 사람에게 대비를 조금 준다. */
@media (prefers-color-scheme: light) {
:root { :root {
--surface: #14161A; --surface: #12161C;
--surface-raised: #1C1F24; --surface-raised: #181D25;
--surface-inverse: #FAF9F6; /* 다크에서는 밴드가 밝아진다 */ --glass-tint: rgb(255 255 255 / 0.07);
--ink: #E9E7E2; --glass-tint-2: rgb(255 255 255 / 0.11);
--ink-muted: #9AA0A6; --glass-edge: rgb(255 255 255 / 0.18);
--ink-inverse: #16181C;
--ink-muted-inverse: #5A5F66;
--line: #2A2E34;
--line-inverse: #DFDCD4;
--line-strong: var(--ink-muted);
--accent: #3FA89D; /* 6.30:1 */
--accent-ink: #0B1A18; /* 6.21:1 */
--accent-inverse: #0E6B63;
--accent-ink-inverse: #FFFFFF;
} }
} }
@ -74,19 +95,20 @@
[lang="ko"] { [lang="ko"] {
--font-sans: "Pretendard Variable", Pretendard, system-ui, sans-serif; --font-sans: "Pretendard Variable", Pretendard, system-ui, sans-serif;
--step-0: 1.0625rem; /* 17 — 본문 · eyebrow · 코드 */ --step-0: 1.0625rem;
--step-1: 1.328rem; /* 21.3 — 리드 */ --step-1: 1.328rem;
--step-2: 1.660rem; /* 26.6 — h3 */ --step-2: 1.660rem;
--step-3: 2.076rem; /* 33.2 — h2 */ --step-3: 2.076rem;
--step-4: 2.595rem; /* 41.5 — h1 */ --step-4: 2.595rem;
--display: clamp(2.595rem, 1.6rem + 4.2vw, 4.25rem); /* → 68 */ /* 히어로에 힘을 준다 */
--display: clamp(3rem, 1.2rem + 7.2vw, 7rem);
--leading-tight: 1.15; --leading-tight: 1.06;
--leading-normal: 1.7; /* 한글은 넉넉하게 */ --leading-normal: 1.7;
--measure: 33em; /* ≈ 561px. 한글 글자폭 ≈ 1em */ --measure: 33em;
--tracking-display: -0.01em; /* 한글은 음수 자간을 크게 주지 않는다 */ --tracking-display: -0.02em;
--tracking-body: 0; /* 한글 본문 음수 자간 금지 */ --tracking-body: 0;
--tracking-label: 0.08em; --tracking-label: 0.08em;
word-break: keep-all; word-break: keep-all;
@ -97,32 +119,40 @@
[lang="en"] { [lang="en"] {
--font-sans: "Switzer", system-ui, -apple-system, sans-serif; --font-sans: "Switzer", system-ui, -apple-system, sans-serif;
--step-0: 1rem; /* 16 */ --step-0: 1rem;
--step-1: 1.25rem; /* 20 */ --step-1: 1.25rem;
--step-2: 1.5625rem; /* 25 */ --step-2: 1.5625rem;
--step-3: 1.953rem; /* 31.3 */ --step-3: 1.953rem;
--step-4: 2.441rem; /* 39 */ --step-4: 2.441rem;
--display: clamp(2.441rem, 1.5rem + 4vw, 4rem); /* → 64 */ --display: clamp(2.8rem, 1rem + 7vw, 6.6rem);
--leading-tight: 1.1; --leading-tight: 0.98;
--leading-normal: 1.55; --leading-normal: 1.55;
--measure: 65ch; /* ≈ 640px */ --measure: 65ch;
--tracking-display: -0.025em; --tracking-display: -0.04em;
--tracking-body: 0; --tracking-body: 0;
--tracking-label: 0.08em; --tracking-label: 0.08em;
} }
/* ---- 다크 밴드: 게이트 #4가 요구하는 "규칙으로 정의된 반전" ---- */ /* ---- 밝은 밴드: 규칙으로 정의된 번의 반전 --------------
/* 사용처는 히어로와 설치 섹션 두 곳뿐이다. 그 외 사용 금지. */ 사용처는 조판 섹션 하나뿐이다. 한글이 종이 위에 있어야 하는
주장을 하는 자리라 거기서만 바탕이 밝아진다. */
.band-inverse { .band-inverse {
--surface: var(--surface-inverse); --surface: var(--surface-inverse);
--surface-raised: #ECE8DE;
--surface-sunken: #E2DED3;
--ink: var(--ink-inverse); --ink: var(--ink-inverse);
--ink-muted: var(--ink-muted-inverse); --ink-muted: var(--ink-muted-inverse);
--line: var(--line-inverse); --line: var(--line-inverse);
--line-strong: var(--ink-muted-inverse);
--accent: var(--accent-inverse); --accent: var(--accent-inverse);
--accent-ink: var(--accent-ink-inverse); --accent-ink: var(--accent-ink-inverse);
--glass-tint: rgb(20 24 30 / 0.04);
--glass-tint-2: rgb(20 24 30 / 0.07);
--glass-edge: rgb(20 24 30 / 0.12);
--glass-hi: inset 0 1px 0 rgb(255 255 255 / 0.8);
--inset-hi: var(--glass-hi);
background: var(--surface); background: var(--surface);
color: var(--ink); color: var(--ink);
padding-block: var(--space-8);
} }

2
designpaca-en.html Normal file

File diff suppressed because one or more lines are too long

2
designpaca-ko.html Normal file

File diff suppressed because one or more lines are too long

305
pnpm-lock.yaml generated
View file

@ -20,6 +20,12 @@ importers:
astro: astro:
specifier: ^5.1.1 specifier: ^5.1.1
version: 5.18.2(@types/node@22.20.1)(rollup@4.62.4)(typescript@5.9.3) version: 5.18.2(@types/node@22.20.1)(rollup@4.62.4)(typescript@5.9.3)
sharp:
specifier: ^0.35.3
version: 0.35.3(@types/node@22.20.1)
three:
specifier: ^0.185.1
version: 0.185.1
packages/cli: packages/cli:
dependencies: dependencies:
@ -487,133 +493,275 @@ packages:
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@img/sharp-darwin-arm64@0.35.3':
resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.34.5': '@img/sharp-darwin-x64@0.34.5':
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@img/sharp-darwin-x64@0.35.3':
resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [darwin]
'@img/sharp-freebsd-wasm32@0.35.3':
resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==}
engines: {node: '>=20.9.0'}
os: [freebsd]
'@img/sharp-libvips-darwin-arm64@1.2.4': '@img/sharp-libvips-darwin-arm64@1.2.4':
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@img/sharp-libvips-darwin-arm64@1.3.2':
resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.2.4': '@img/sharp-libvips-darwin-x64@1.2.4':
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@img/sharp-libvips-darwin-x64@1.3.2':
resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.2.4': '@img/sharp-libvips-linux-arm64@1.2.4':
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@img/sharp-libvips-linux-arm64@1.3.2':
resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linux-arm@1.2.4': '@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
'@img/sharp-libvips-linux-arm@1.3.2':
resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==}
cpu: [arm]
os: [linux]
'@img/sharp-libvips-linux-ppc64@1.2.4': '@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
'@img/sharp-libvips-linux-ppc64@1.3.2':
resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==}
cpu: [ppc64]
os: [linux]
'@img/sharp-libvips-linux-riscv64@1.2.4': '@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
'@img/sharp-libvips-linux-riscv64@1.3.2':
resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==}
cpu: [riscv64]
os: [linux]
'@img/sharp-libvips-linux-s390x@1.2.4': '@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
'@img/sharp-libvips-linux-s390x@1.3.2':
resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==}
cpu: [s390x]
os: [linux]
'@img/sharp-libvips-linux-x64@1.2.4': '@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@img/sharp-libvips-linux-x64@1.3.2':
resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==}
cpu: [x64]
os: [linux]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4': '@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@img/sharp-libvips-linuxmusl-arm64@1.3.2':
resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linuxmusl-x64@1.2.4': '@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@img/sharp-libvips-linuxmusl-x64@1.3.2':
resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==}
cpu: [x64]
os: [linux]
'@img/sharp-linux-arm64@0.34.5': '@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@img/sharp-linux-arm64@0.35.3':
resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
'@img/sharp-linux-arm@0.34.5': '@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
'@img/sharp-linux-arm@0.35.3':
resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==}
engines: {node: '>=20.9.0'}
cpu: [arm]
os: [linux]
'@img/sharp-linux-ppc64@0.34.5': '@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
'@img/sharp-linux-ppc64@0.35.3':
resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==}
engines: {node: '>=20.9.0'}
cpu: [ppc64]
os: [linux]
'@img/sharp-linux-riscv64@0.34.5': '@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
'@img/sharp-linux-riscv64@0.35.3':
resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==}
engines: {node: '>=20.9.0'}
cpu: [riscv64]
os: [linux]
'@img/sharp-linux-s390x@0.34.5': '@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
'@img/sharp-linux-s390x@0.35.3':
resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==}
engines: {node: '>=20.9.0'}
cpu: [s390x]
os: [linux]
'@img/sharp-linux-x64@0.34.5': '@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@img/sharp-linux-x64@0.35.3':
resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
'@img/sharp-linuxmusl-arm64@0.34.5': '@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@img/sharp-linuxmusl-arm64@0.35.3':
resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
'@img/sharp-linuxmusl-x64@0.34.5': '@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@img/sharp-linuxmusl-x64@0.35.3':
resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
'@img/sharp-wasm32@0.34.5': '@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [wasm32] cpu: [wasm32]
'@img/sharp-wasm32@0.35.3':
resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==}
engines: {node: '>=20.9.0'}
'@img/sharp-webcontainers-wasm32@0.35.3':
resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==}
engines: {node: '>=20.9.0'}
cpu: [wasm32]
'@img/sharp-win32-arm64@0.34.5': '@img/sharp-win32-arm64@0.34.5':
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@img/sharp-win32-arm64@0.35.3':
resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [win32]
'@img/sharp-win32-ia32@0.34.5': '@img/sharp-win32-ia32@0.34.5':
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
'@img/sharp-win32-ia32@0.35.3':
resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==}
engines: {node: ^20.9.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.34.5': '@img/sharp-win32-x64@0.34.5':
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@img/sharp-win32-x64@0.35.3':
resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [win32]
'@inquirer/external-editor@1.0.3': '@inquirer/external-editor@1.0.3':
resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -1810,6 +1958,15 @@ packages:
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
sharp@0.35.3:
resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==}
engines: {node: '>=20.9.0'}
peerDependencies:
'@types/node': '*'
peerDependenciesMeta:
'@types/node':
optional: true
shebang-command@2.0.0: shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -1897,6 +2054,9 @@ packages:
thenify@3.3.1: thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
three@0.185.1:
resolution: {integrity: sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==}
tiny-inflate@1.0.3: tiny-inflate@1.0.3:
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
@ -2580,103 +2740,206 @@ snapshots:
'@esbuild/win32-x64@0.27.7': '@esbuild/win32-x64@0.27.7':
optional: true optional: true
'@img/colour@1.1.0': '@img/colour@1.1.0': {}
optional: true
'@img/sharp-darwin-arm64@0.34.5': '@img/sharp-darwin-arm64@0.34.5':
optionalDependencies: optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.2.4 '@img/sharp-libvips-darwin-arm64': 1.2.4
optional: true optional: true
'@img/sharp-darwin-arm64@0.35.3':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.3.2
optional: true
'@img/sharp-darwin-x64@0.34.5': '@img/sharp-darwin-x64@0.34.5':
optionalDependencies: optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.2.4 '@img/sharp-libvips-darwin-x64': 1.2.4
optional: true optional: true
'@img/sharp-darwin-x64@0.35.3':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.3.2
optional: true
'@img/sharp-freebsd-wasm32@0.35.3':
dependencies:
'@img/sharp-wasm32': 0.35.3
optional: true
'@img/sharp-libvips-darwin-arm64@1.2.4': '@img/sharp-libvips-darwin-arm64@1.2.4':
optional: true optional: true
'@img/sharp-libvips-darwin-arm64@1.3.2':
optional: true
'@img/sharp-libvips-darwin-x64@1.2.4': '@img/sharp-libvips-darwin-x64@1.2.4':
optional: true optional: true
'@img/sharp-libvips-darwin-x64@1.3.2':
optional: true
'@img/sharp-libvips-linux-arm64@1.2.4': '@img/sharp-libvips-linux-arm64@1.2.4':
optional: true optional: true
'@img/sharp-libvips-linux-arm64@1.3.2':
optional: true
'@img/sharp-libvips-linux-arm@1.2.4': '@img/sharp-libvips-linux-arm@1.2.4':
optional: true optional: true
'@img/sharp-libvips-linux-arm@1.3.2':
optional: true
'@img/sharp-libvips-linux-ppc64@1.2.4': '@img/sharp-libvips-linux-ppc64@1.2.4':
optional: true optional: true
'@img/sharp-libvips-linux-ppc64@1.3.2':
optional: true
'@img/sharp-libvips-linux-riscv64@1.2.4': '@img/sharp-libvips-linux-riscv64@1.2.4':
optional: true optional: true
'@img/sharp-libvips-linux-riscv64@1.3.2':
optional: true
'@img/sharp-libvips-linux-s390x@1.2.4': '@img/sharp-libvips-linux-s390x@1.2.4':
optional: true optional: true
'@img/sharp-libvips-linux-s390x@1.3.2':
optional: true
'@img/sharp-libvips-linux-x64@1.2.4': '@img/sharp-libvips-linux-x64@1.2.4':
optional: true optional: true
'@img/sharp-libvips-linux-x64@1.3.2':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.2.4': '@img/sharp-libvips-linuxmusl-arm64@1.2.4':
optional: true optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.3.2':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.2.4': '@img/sharp-libvips-linuxmusl-x64@1.2.4':
optional: true optional: true
'@img/sharp-libvips-linuxmusl-x64@1.3.2':
optional: true
'@img/sharp-linux-arm64@0.34.5': '@img/sharp-linux-arm64@0.34.5':
optionalDependencies: optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.2.4 '@img/sharp-libvips-linux-arm64': 1.2.4
optional: true optional: true
'@img/sharp-linux-arm64@0.35.3':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.3.2
optional: true
'@img/sharp-linux-arm@0.34.5': '@img/sharp-linux-arm@0.34.5':
optionalDependencies: optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.2.4 '@img/sharp-libvips-linux-arm': 1.2.4
optional: true optional: true
'@img/sharp-linux-arm@0.35.3':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.3.2
optional: true
'@img/sharp-linux-ppc64@0.34.5': '@img/sharp-linux-ppc64@0.34.5':
optionalDependencies: optionalDependencies:
'@img/sharp-libvips-linux-ppc64': 1.2.4 '@img/sharp-libvips-linux-ppc64': 1.2.4
optional: true optional: true
'@img/sharp-linux-ppc64@0.35.3':
optionalDependencies:
'@img/sharp-libvips-linux-ppc64': 1.3.2
optional: true
'@img/sharp-linux-riscv64@0.34.5': '@img/sharp-linux-riscv64@0.34.5':
optionalDependencies: optionalDependencies:
'@img/sharp-libvips-linux-riscv64': 1.2.4 '@img/sharp-libvips-linux-riscv64': 1.2.4
optional: true optional: true
'@img/sharp-linux-riscv64@0.35.3':
optionalDependencies:
'@img/sharp-libvips-linux-riscv64': 1.3.2
optional: true
'@img/sharp-linux-s390x@0.34.5': '@img/sharp-linux-s390x@0.34.5':
optionalDependencies: optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.2.4 '@img/sharp-libvips-linux-s390x': 1.2.4
optional: true optional: true
'@img/sharp-linux-s390x@0.35.3':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.3.2
optional: true
'@img/sharp-linux-x64@0.34.5': '@img/sharp-linux-x64@0.34.5':
optionalDependencies: optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.2.4 '@img/sharp-libvips-linux-x64': 1.2.4
optional: true optional: true
'@img/sharp-linux-x64@0.35.3':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.3.2
optional: true
'@img/sharp-linuxmusl-arm64@0.34.5': '@img/sharp-linuxmusl-arm64@0.34.5':
optionalDependencies: optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4 '@img/sharp-libvips-linuxmusl-arm64': 1.2.4
optional: true optional: true
'@img/sharp-linuxmusl-arm64@0.35.3':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.3.2
optional: true
'@img/sharp-linuxmusl-x64@0.34.5': '@img/sharp-linuxmusl-x64@0.34.5':
optionalDependencies: optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.2.4 '@img/sharp-libvips-linuxmusl-x64': 1.2.4
optional: true optional: true
'@img/sharp-linuxmusl-x64@0.35.3':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.3.2
optional: true
'@img/sharp-wasm32@0.34.5': '@img/sharp-wasm32@0.34.5':
dependencies: dependencies:
'@emnapi/runtime': 1.11.3 '@emnapi/runtime': 1.11.3
optional: true optional: true
'@img/sharp-wasm32@0.35.3':
dependencies:
'@emnapi/runtime': 1.11.3
optional: true
'@img/sharp-webcontainers-wasm32@0.35.3':
dependencies:
'@img/sharp-wasm32': 0.35.3
optional: true
'@img/sharp-win32-arm64@0.34.5': '@img/sharp-win32-arm64@0.34.5':
optional: true optional: true
'@img/sharp-win32-arm64@0.35.3':
optional: true
'@img/sharp-win32-ia32@0.34.5': '@img/sharp-win32-ia32@0.34.5':
optional: true optional: true
'@img/sharp-win32-ia32@0.35.3':
optional: true
'@img/sharp-win32-x64@0.34.5': '@img/sharp-win32-x64@0.34.5':
optional: true optional: true
'@img/sharp-win32-x64@0.35.3':
optional: true
'@inquirer/external-editor@1.0.3(@types/node@22.20.1)': '@inquirer/external-editor@1.0.3(@types/node@22.20.1)':
dependencies: dependencies:
chardet: 2.2.0 chardet: 2.2.0
@ -3142,8 +3405,7 @@ snapshots:
detect-indent@6.1.0: {} detect-indent@6.1.0: {}
detect-libc@2.1.2: detect-libc@2.1.2: {}
optional: true
deterministic-object-hash@2.0.2: deterministic-object-hash@2.0.2:
dependencies: dependencies:
@ -4209,6 +4471,39 @@ snapshots:
'@img/sharp-win32-x64': 0.34.5 '@img/sharp-win32-x64': 0.34.5
optional: true optional: true
sharp@0.35.3(@types/node@22.20.1):
dependencies:
'@img/colour': 1.1.0
detect-libc: 2.1.2
semver: 7.8.5
optionalDependencies:
'@img/sharp-darwin-arm64': 0.35.3
'@img/sharp-darwin-x64': 0.35.3
'@img/sharp-freebsd-wasm32': 0.35.3
'@img/sharp-libvips-darwin-arm64': 1.3.2
'@img/sharp-libvips-darwin-x64': 1.3.2
'@img/sharp-libvips-linux-arm': 1.3.2
'@img/sharp-libvips-linux-arm64': 1.3.2
'@img/sharp-libvips-linux-ppc64': 1.3.2
'@img/sharp-libvips-linux-riscv64': 1.3.2
'@img/sharp-libvips-linux-s390x': 1.3.2
'@img/sharp-libvips-linux-x64': 1.3.2
'@img/sharp-libvips-linuxmusl-arm64': 1.3.2
'@img/sharp-libvips-linuxmusl-x64': 1.3.2
'@img/sharp-linux-arm': 0.35.3
'@img/sharp-linux-arm64': 0.35.3
'@img/sharp-linux-ppc64': 0.35.3
'@img/sharp-linux-riscv64': 0.35.3
'@img/sharp-linux-s390x': 0.35.3
'@img/sharp-linux-x64': 0.35.3
'@img/sharp-linuxmusl-arm64': 0.35.3
'@img/sharp-linuxmusl-x64': 0.35.3
'@img/sharp-webcontainers-wasm32': 0.35.3
'@img/sharp-win32-arm64': 0.35.3
'@img/sharp-win32-ia32': 0.35.3
'@img/sharp-win32-x64': 0.35.3
'@types/node': 22.20.1
shebang-command@2.0.0: shebang-command@2.0.0:
dependencies: dependencies:
shebang-regex: 3.0.0 shebang-regex: 3.0.0
@ -4304,6 +4599,8 @@ snapshots:
dependencies: dependencies:
any-promise: 1.3.0 any-promise: 1.3.0
three@0.185.1: {}
tiny-inflate@1.0.3: {} tiny-inflate@1.0.3: {}
tinyexec@0.3.2: {} tinyexec@0.3.2: {}