소개 페이지 전면 재디자인 — 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:
parent
6e05e69ca4
commit
be343cdab5
22 changed files with 1695 additions and 439 deletions
|
|
@ -10,6 +10,8 @@
|
|||
"typecheck": "echo \"(사이트는 build 로 검증한다)\""
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^5.1.1"
|
||||
"astro": "^5.1.1",
|
||||
"sharp": "^0.35.3",
|
||||
"three": "^0.185.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
apps/site/src/assets/shots/shot-coffee.png
Normal file
BIN
apps/site/src/assets/shots/shot-coffee.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 387 KiB |
BIN
apps/site/src/assets/shots/shot-saas.png
Normal file
BIN
apps/site/src/assets/shots/shot-saas.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
|
|
@ -8,51 +8,80 @@ interface Props {
|
|||
const { command, copyLabel, copiedLabel, note } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="cmd">
|
||||
<code class="cmd-text">{command}</code>
|
||||
<button type="button" class="cmd-copy" data-copy={command} data-copied={copiedLabel}>
|
||||
{copyLabel}
|
||||
</button>
|
||||
{/* 셸 안에 코어. 명령어는 기계의 부품처럼 앉는다 */}
|
||||
<div class="cmd-shell shell">
|
||||
<div class="cmd core">
|
||||
<code class="cmd-text">{command}</code>
|
||||
<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>
|
||||
{note && <p class="cmd-note muted">{note}</p>}
|
||||
|
||||
<style>
|
||||
.cmd-shell { max-width: 30em; min-width: 0; }
|
||||
.cmd {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
max-width: 34em;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-2) var(--space-2) var(--space-3);
|
||||
}
|
||||
.cmd-text {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
padding: var(--space-3);
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
color: var(--ink);
|
||||
}
|
||||
.cmd-text::before {
|
||||
content: "$ ";
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
/* 버튼 안에 아이콘이 자기 원을 갖는다 */
|
||||
.cmd-copy {
|
||||
flex: 0 0 auto;
|
||||
/* 독립 컨트롤이므로 44px 이상 */
|
||||
min-width: 5.5em;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
min-height: 44px;
|
||||
white-space: nowrap;
|
||||
padding-inline: var(--space-3);
|
||||
padding: 0 var(--space-2) 0 var(--space-3);
|
||||
border: 0;
|
||||
border-left: 1px solid var(--line-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--accent);
|
||||
color: var(--accent-ink);
|
||||
font: inherit;
|
||||
font-size: var(--step-0);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
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 {
|
||||
opacity: 0.88;
|
||||
.cmd-copy:hover { filter: brightness(1.08); }
|
||||
.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 {
|
||||
margin-top: var(--space-2);
|
||||
font-size: var(--step-0);
|
||||
|
|
@ -62,17 +91,19 @@ const { command, copyLabel, copiedLabel, note } = Astro.props;
|
|||
<script>
|
||||
// 이 페이지의 유일한 상호작용이다. 라이브러리를 쓰지 않는다.
|
||||
document.querySelectorAll<HTMLButtonElement>(".cmd-copy").forEach((btn) => {
|
||||
const label = btn.querySelector<HTMLSpanElement>(".cmd-copy-label");
|
||||
if (!label) return;
|
||||
btn.addEventListener("click", async () => {
|
||||
const text = btn.dataset["copy"] ?? "";
|
||||
const done = btn.dataset["copied"] ?? "OK";
|
||||
const original = btn.textContent;
|
||||
const original = label.textContent;
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch {
|
||||
// 클립보드 권한이 없으면 선택 상태로 만들어 사용자가 직접 복사하게 둔다
|
||||
const range = document.createRange();
|
||||
const code = btn.previousElementSibling;
|
||||
const code = btn.parentElement?.querySelector(".cmd-text");
|
||||
if (code) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(code);
|
||||
const sel = getSelection();
|
||||
sel?.removeAllRanges();
|
||||
|
|
@ -80,11 +111,9 @@ const { command, copyLabel, copiedLabel, note } = Astro.props;
|
|||
}
|
||||
return;
|
||||
}
|
||||
btn.textContent = done;
|
||||
btn.setAttribute("aria-live", "polite");
|
||||
setTimeout(() => {
|
||||
btn.textContent = original;
|
||||
}, 1600);
|
||||
label.textContent = done;
|
||||
label.setAttribute("aria-live", "polite");
|
||||
setTimeout(() => { label.textContent = original; }, 1600);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
---
|
||||
import { Image } from "astro:assets";
|
||||
import CopyCommand from "./CopyCommand.astro";
|
||||
import shotCoffee from "../assets/shots/shot-coffee.png";
|
||||
import type { Content } from "../i18n/content";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -9,35 +11,115 @@ interface Props {
|
|||
const { c, ui } = Astro.props;
|
||||
---
|
||||
|
||||
{/* 다크 밴드 두 곳 중 하나. 3단계 토큰이 규칙으로 정의했다(게이트 #4) */}
|
||||
<section class="full band-inverse hero">
|
||||
<div class="hero-inner">
|
||||
<h1>{c.h1}</h1>
|
||||
<p class="lead hero-lead">{c.lead}</p>
|
||||
<CopyCommand
|
||||
command={c.install}
|
||||
copyLabel={ui.copy}
|
||||
copiedLabel={ui.copied}
|
||||
note={c.installNote}
|
||||
/>
|
||||
<p class="hero-links">
|
||||
{c.links.map((l) => (<a href={l.href}>{l.label}</a>))}
|
||||
</p>
|
||||
<section class="full hero">
|
||||
{/* 배경. WebGL 이 없으면 아래 그라디언트가 그대로 결과물이다 */}
|
||||
<div class="hero-bg" aria-hidden="true">
|
||||
<div class="hero-fallback"></div>
|
||||
<canvas id="hero-canvas" class="hero-canvas"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="hero-grid">
|
||||
<div class="hero-copy">
|
||||
<h1 class="hero-title">{c.h1}</h1>
|
||||
<p class="lead hero-lead">{c.lead}</p>
|
||||
|
||||
<div class="hero-cta">
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: grid;
|
||||
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 {
|
||||
max-width: 30em;
|
||||
max-width: 26em;
|
||||
margin-block: var(--space-4) var(--space-5);
|
||||
color: var(--ink);
|
||||
opacity: 0.82;
|
||||
}
|
||||
.hero-cta { max-width: 32em; }
|
||||
.hero-links {
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
|
|
@ -45,4 +127,157 @@ const { c, ui } = Astro.props;
|
|||
font-size: var(--step-0);
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -2,58 +2,80 @@
|
|||
import CopyCommand from "./CopyCommand.astro";
|
||||
import type { Content } from "../i18n/content";
|
||||
|
||||
interface Props {
|
||||
c: Content["install"];
|
||||
ui: Content["ui"];
|
||||
}
|
||||
interface Props { c: Content["install"]; ui: Content["ui"]; }
|
||||
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} />
|
||||
|
||||
<div class="table-scroll targets">
|
||||
<table>
|
||||
<tbody>
|
||||
{
|
||||
c.targets.map((t) => (
|
||||
<tr>
|
||||
<th scope="row">{t.name}</th>
|
||||
<td class="mono">{t.path}</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<section class="full install" aria-labelledby="install-h">
|
||||
<div class="install-grid">
|
||||
<div class="install-head">
|
||||
<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} />
|
||||
</div>
|
||||
|
||||
<dl class="cmds">
|
||||
{
|
||||
c.commands.map((x) => (
|
||||
<div class="cmd-row">
|
||||
<dt class="mono">{x.cmd}</dt>
|
||||
<dd class="muted">{x.what}</dd>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</dl>
|
||||
<div class="install-body">
|
||||
<ul class="targets">
|
||||
{
|
||||
c.targets.map((t) => (
|
||||
<li>
|
||||
<span class="t-name">{t.name}</span>
|
||||
<span class="t-path mono muted">{t.path}</span>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</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>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.install { display: grid; grid-template-columns: subgrid; }
|
||||
.install-inner { grid-column: main; }
|
||||
.install-lead { margin-block: var(--space-3) var(--space-5); }
|
||||
.targets { margin-top: var(--space-6); }
|
||||
.targets th[scope="row"] { font-weight: 400; color: var(--ink); letter-spacing: 0; }
|
||||
.cmds { margin: var(--space-5) 0 0; }
|
||||
.cmd-row { padding-block: var(--space-3); border-top: 1px solid var(--line); }
|
||||
.cmd-row:last-child { border-bottom: 1px solid var(--line); }
|
||||
dt { margin-bottom: var(--space-1); }
|
||||
.install { padding-block: var(--space-8); }
|
||||
.install-grid {
|
||||
max-width: 1180px;
|
||||
margin-inline: auto;
|
||||
padding-inline: var(--space-4);
|
||||
display: grid;
|
||||
gap: var(--space-6);
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
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); }
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.install-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
158
apps/site/src/components/Materials.astro
Normal file
158
apps/site/src/components/Materials.astro
Normal 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>
|
||||
|
|
@ -1,40 +1,79 @@
|
|||
---
|
||||
import type { Content } from "../i18n/content";
|
||||
|
||||
interface Props {
|
||||
c: Content["metrics"];
|
||||
}
|
||||
interface Props { c: Content["metrics"]; }
|
||||
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>
|
||||
<h2 id="metrics-h">{c.h2}</h2>
|
||||
<p class="lead metrics-lead">{c.lead}</p>
|
||||
{/* 레이아웃 패밀리: 벤토. 항목 수만큼 셀이 있고 빈 칸이 없다 */}
|
||||
<section class="section metrics" aria-labelledby="metrics-h">
|
||||
<p class="eyebrow reveal">{c.eyebrow}</p>
|
||||
<h2 id="metrics-h" class="reveal">{c.h2}</h2>
|
||||
<p class="lead metrics-lead reveal">{c.lead}</p>
|
||||
|
||||
<div class="table-scroll">
|
||||
<table>
|
||||
<tbody>
|
||||
{
|
||||
c.items.map((m) => (
|
||||
<tr>
|
||||
<th scope="row">{m.label}</th>
|
||||
<td class="metric-value mono">{m.value}</td>
|
||||
<td class="metric-note muted">{m.note}</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="full bento">
|
||||
{
|
||||
feature.map((m, i) => (
|
||||
<div class="cell cell-lg glass glass-2 reveal" style={`--reveal-delay:${i * 70}ms`}>
|
||||
<span class="cell-value nums">{m.value}</span>
|
||||
<span class="cell-label">{m.label}</span>
|
||||
<span class="cell-note muted">{m.note}</span>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{
|
||||
rest.map((m, i) => (
|
||||
<div class="cell glass reveal" style={`--reveal-delay:${(i + 2) * 70}ms`}>
|
||||
<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>
|
||||
|
||||
<p class="caveat muted">{c.caveat}</p>
|
||||
<p class="caveat muted reveal">{c.caveat}</p>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.metrics-lead { margin-bottom: var(--space-5); }
|
||||
.metric-value { white-space: nowrap; }
|
||||
.metric-note { font-size: var(--step-0); }
|
||||
.caveat { margin-top: var(--space-4); font-size: var(--step-0); }
|
||||
th[scope="row"] { font-weight: 400; color: var(--ink); letter-spacing: 0; }
|
||||
.metrics-lead { margin-block: var(--space-4) var(--space-6); }
|
||||
|
||||
.bento {
|
||||
display: grid;
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -15,19 +15,19 @@ const { c } = Astro.props;
|
|||
role="list" 는 list-style:none 일 때 Safari/VoiceOver 가 목록 의미를 버리는 것을 되돌린다
|
||||
*/}
|
||||
<section class="section" aria-labelledby="pipeline-h">
|
||||
<p class="eyebrow">{c.eyebrow}</p>
|
||||
<h2 id="pipeline-h">{c.h2}</h2>
|
||||
<p class="lead pipeline-lead">{c.lead}</p>
|
||||
<p class="eyebrow reveal">{c.eyebrow}</p>
|
||||
<h2 id="pipeline-h" class="reveal">{c.h2}</h2>
|
||||
<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>
|
||||
<p class="brief-text">{c.brief}</p>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<ol role="list" class="spine">
|
||||
{
|
||||
c.stages.map((s) => (
|
||||
<li class="stop">
|
||||
<li class="stop reveal">
|
||||
<h3 class="stop-name">{s.name}</h3>
|
||||
<p class="stop-does">{s.does}</p>
|
||||
<p class:list={["stop-becomes", { mono: s.mono }]}>{s.becomes}</p>
|
||||
|
|
@ -36,7 +36,7 @@ const { c } = Astro.props;
|
|||
}
|
||||
</ol>
|
||||
|
||||
<p class="after muted">{c.after}</p>
|
||||
<p class="after muted reveal">{c.after}</p>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
|
|
@ -44,12 +44,8 @@ const { c } = Astro.props;
|
|||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.brief {
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-4);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.brief { margin-bottom: var(--space-2); }
|
||||
.brief-core { padding: var(--space-4); }
|
||||
.brief-label {
|
||||
display: block;
|
||||
font-size: var(--step-0);
|
||||
|
|
|
|||
|
|
@ -1,39 +1,70 @@
|
|||
---
|
||||
import type { Content } from "../i18n/content";
|
||||
|
||||
interface Props {
|
||||
c: Content["preflight"];
|
||||
}
|
||||
interface Props { c: Content["preflight"]; }
|
||||
const { c } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="section" aria-labelledby="preflight-h">
|
||||
<h2 id="preflight-h">{c.h2}</h2>
|
||||
<p class="lead pf-lead">{c.lead}</p>
|
||||
{/* 레이아웃 패밀리: 2열 대비. 왼쪽이 걸린 것, 오른쪽이 고친 것 */}
|
||||
<section class="section preflight" aria-labelledby="pf-h">
|
||||
<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) => (
|
||||
<li class="entry">
|
||||
<p class="caught">{i.caught}</p>
|
||||
c.items.map((i, idx) => (
|
||||
<article class="entry glass reveal" style={`--reveal-delay:${idx * 60}ms`}>
|
||||
<p class="caught">
|
||||
<span class="tick" aria-hidden="true" />
|
||||
{i.caught}
|
||||
</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>
|
||||
|
||||
<style>
|
||||
.pf-lead { margin-bottom: var(--space-5); }
|
||||
.log { margin: 0; padding: 0; list-style: none; }
|
||||
.entry {
|
||||
padding-block: var(--space-3);
|
||||
border-top: 1px solid var(--line);
|
||||
.pf-lead { margin-block: var(--space-4) var(--space-6); }
|
||||
|
||||
.log {
|
||||
max-width: 1180px;
|
||||
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); }
|
||||
.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>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,21 @@
|
|||
---
|
||||
import type { Content } from "../i18n/content";
|
||||
|
||||
interface Props {
|
||||
c: Content["rules"];
|
||||
}
|
||||
interface Props { c: Content["rules"]; }
|
||||
const { c } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="section" aria-labelledby="rules-h">
|
||||
<h2 id="rules-h">{c.h2}</h2>
|
||||
<p class="lead rules-lead">{c.lead}</p>
|
||||
{/* 레이아웃 패밀리: 정의 목록. 규칙이 크고 근거가 옆에 붙는다 */}
|
||||
<section class="section rules" aria-labelledby="rules-h">
|
||||
<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) => (
|
||||
<div class="rule">
|
||||
c.items.map((r, i) => (
|
||||
<div class="rule glass reveal" style={`--reveal-delay:${i * 55}ms`}>
|
||||
<dt>{r.rule}</dt>
|
||||
<dd>{r.why}</dd>
|
||||
<dd class="muted">{r.why}</dd>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
|
@ -24,19 +23,30 @@ const { c } = Astro.props;
|
|||
</section>
|
||||
|
||||
<style>
|
||||
.rules-lead { margin-bottom: var(--space-5); }
|
||||
.rules { margin: 0; }
|
||||
.rules-lead { margin-block: var(--space-4) var(--space-6); }
|
||||
|
||||
.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 {
|
||||
padding-block: var(--space-4);
|
||||
border-top: 1px solid var(--line);
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
.rule:last-child { border-bottom: 1px solid var(--line); }
|
||||
dt {
|
||||
font-size: var(--step-2);
|
||||
font-weight: 560;
|
||||
margin-bottom: var(--space-1);
|
||||
letter-spacing: var(--tracking-display);
|
||||
line-height: var(--leading-tight);
|
||||
}
|
||||
dd {
|
||||
margin: 0;
|
||||
color: var(--ink-muted);
|
||||
dd { margin: 0; font-size: var(--step-0); }
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.rule-list { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
77
apps/site/src/components/Showcase.astro
Normal file
77
apps/site/src/components/Showcase.astro
Normal 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>
|
||||
|
|
@ -7,25 +7,25 @@ interface Props {
|
|||
const { c } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="section" aria-labelledby="type-h">
|
||||
<p class="eyebrow">{c.eyebrow}</p>
|
||||
<h2 id="type-h">{c.h2}</h2>
|
||||
<p class="lead type-lead">{c.lead}</p>
|
||||
<p class="type-body">{c.body}</p>
|
||||
<section class="section band-inverse" aria-labelledby="type-h">
|
||||
<p class="eyebrow reveal">{c.eyebrow}</p>
|
||||
<h2 id="type-h" class="reveal">{c.h2}</h2>
|
||||
<p class="lead type-lead reveal">{c.lead}</p>
|
||||
<p class="type-body reveal">{c.body}</p>
|
||||
|
||||
{/* 주장을 글로 하지 않고 실물로 보여준다. 두 문단의 차이는 word-break 한 줄뿐이다 */}
|
||||
<div class="demo">
|
||||
<div class="demo-col">
|
||||
<div class="demo reveal">
|
||||
<div class="demo-col glass"><div class="demo-inner">
|
||||
<p class="demo-label muted mono">{c.demoLabels[0]}</p>
|
||||
<p lang="ko" class="demo-text demo-default">{c.demoText}</p>
|
||||
</div>
|
||||
<div class="demo-col">
|
||||
</div></div>
|
||||
<div class="demo-col glass"><div class="demo-inner">
|
||||
<p class="demo-label muted mono">{c.demoLabels[1]}</p>
|
||||
<p lang="ko" class="demo-text demo-keepall">{c.demoText}</p>
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<ul class="type-rules">
|
||||
<ul class="type-rules reveal">
|
||||
{c.rules.map((r) => (<li>{r}</li>))}
|
||||
</ul>
|
||||
</section>
|
||||
|
|
@ -40,11 +40,7 @@ const { c } = Astro.props;
|
|||
grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
.demo-col {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-3);
|
||||
}
|
||||
.demo-inner { padding: var(--space-4); }
|
||||
.demo-label {
|
||||
font-size: var(--mono-size);
|
||||
letter-spacing: var(--tracking-label);
|
||||
|
|
|
|||
|
|
@ -1,19 +1,16 @@
|
|||
/**
|
||||
* 두 언어판은 번역이 아니라 각각 따로 쓴 것이다.
|
||||
* 특히 §5(조판)는 한국어판에서 주장이고 영어판에서 사례다 — 구조 자체가 다르다.
|
||||
* 특히 조판 섹션은 한국어판에서 주장이고 영어판에서 사례다.
|
||||
*
|
||||
* 규칙: 이 파일에 em-dash 와 en-dash 를 쓰지 않는다.
|
||||
* 하이픈, 마침표, 콜론, 괄호로 대신한다.
|
||||
*/
|
||||
|
||||
export interface Stage {
|
||||
/** 단계 이름 */
|
||||
name: string;
|
||||
/** 그 단계가 하는 일 한 줄 */
|
||||
does: string;
|
||||
/**
|
||||
* 같은 브리프 하나가 이 단계를 지나며 무엇으로 변하는가.
|
||||
* 번호 대신 이 변화가 순서를 만든다 (IKEA 조립 설명서 방식).
|
||||
*/
|
||||
/** 같은 브리프가 이 단계를 지나며 무엇으로 변하는가. 번호 대신 이 변화가 순서를 만든다 */
|
||||
becomes: string;
|
||||
/** becomes 를 코드로 보여줄 때 */
|
||||
mono?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -21,6 +18,8 @@ export interface Metric {
|
|||
label: string;
|
||||
value: string;
|
||||
note: string;
|
||||
/** 벤토에서 크게 잡을 항목 */
|
||||
feature?: boolean;
|
||||
}
|
||||
|
||||
export interface Rule {
|
||||
|
|
@ -36,6 +35,8 @@ export interface Content {
|
|||
install: string;
|
||||
installNote: string;
|
||||
links: { label: string; href: string }[];
|
||||
proofAlt: string;
|
||||
proofCaption: string;
|
||||
};
|
||||
pipeline: {
|
||||
eyebrow: string;
|
||||
|
|
@ -46,30 +47,55 @@ export interface Content {
|
|||
stages: Stage[];
|
||||
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 };
|
||||
preflight: {
|
||||
eyebrow: string;
|
||||
h2: string;
|
||||
lead: string;
|
||||
items: { caught: string; fixed: string }[];
|
||||
note: string;
|
||||
};
|
||||
typography: { eyebrow: string; h2: string; lead: string; body: string; demoLabels: [string, string]; demoText: string; rules: string[] };
|
||||
rules: { eyebrow: string; h2: string; lead: string; items: Rule[] };
|
||||
install: { eyebrow: string; h2: string; lead: string; command: string; targets: { name: string; path: string }[]; commands: { cmd: string; what: string }[] };
|
||||
typography: {
|
||||
eyebrow: 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 };
|
||||
ui: { copy: string; copied: string; skipToContent: string };
|
||||
ui: { copy: string; copied: string };
|
||||
}
|
||||
|
||||
const STAGES_KO: Stage[] = [
|
||||
{
|
||||
name: "브리프 게이트",
|
||||
does: "무엇을·누구에게·제약을 세 줄로 압축한다. 규모를 판정해 경로를 고른다.",
|
||||
does: "무엇을, 누구에게, 어떤 제약인지 세 줄로 압축한다. 규모를 판정해 경로를 고른다.",
|
||||
becomes: "누구에게: 홈카페 3년차 이상. 산미와 로스팅 프로파일을 구분한다",
|
||||
},
|
||||
{
|
||||
name: "레퍼런스 조사",
|
||||
does: "실제로 존재하는 사이트 3개를 서로 다른 층위에서 고른다. 구조·톤·디테일.",
|
||||
does: "실제로 존재하는 사이트 3개를 서로 다른 층위에서 고른다. 구조, 톤, 디테일.",
|
||||
becomes: "R2 = 독립 문학 출판사. 커피 업계 밖에서 톤을 가져온다",
|
||||
},
|
||||
{
|
||||
|
|
@ -85,14 +111,14 @@ const STAGES_KO: Stage[] = [
|
|||
},
|
||||
{
|
||||
name: "구현",
|
||||
does: "레이아웃 → 재질 → 입체 → 모션 순서로. 이펙트를 다 꺼도 완성돼 있어야 한다.",
|
||||
becomes: '<h1>산미 8.5 · 바디 6.0 · 로스팅 라이트</h1>',
|
||||
does: "레이아웃, 재질, 입체, 모션 순서로. 이펙트를 다 꺼도 완성돼 있어야 한다.",
|
||||
becomes: "<h1>산미 8.5 · 바디 6.0 · 로스팅 라이트</h1>",
|
||||
mono: true,
|
||||
},
|
||||
{
|
||||
name: "프리플라이트",
|
||||
does: "자기 결과물을 남의 것처럼 본다. 게이트 12개는 전부 '아니오'여야 한다.",
|
||||
becomes: "게이트 12/12 통과 · 슬롭 지문 0건 · 320px 오버플로 31px → 수정",
|
||||
becomes: "게이트 12/12 통과 · 슬롭 지문 0건 · 320px 오버플로 31px 수정",
|
||||
mono: true,
|
||||
},
|
||||
];
|
||||
|
|
@ -105,7 +131,7 @@ const STAGES_EN: Stage[] = [
|
|||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
|
|
@ -121,119 +147,171 @@ const STAGES_EN: Stage[] = [
|
|||
},
|
||||
{
|
||||
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>",
|
||||
mono: true,
|
||||
},
|
||||
{
|
||||
name: "Preflight",
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
export const ko: Content = {
|
||||
meta: {
|
||||
title: "designpaca — 웹 디자인 파이프라인 스킬",
|
||||
title: "designpaca · 웹 디자인 파이프라인 스킬",
|
||||
description:
|
||||
"AI 코딩 에이전트에게 웹 디자인을 순서대로 하게 만드는 스킬. 레퍼런스 조사부터 셀프 감사까지 여섯 단계. Claude Code · Codex · Cursor · Windsurf 에 한 줄로 설치한다.",
|
||||
"AI 코딩 에이전트에게 웹 디자인을 순서대로 하게 만드는 스킬. 레퍼런스 조사부터 셀프 감사까지 여섯 단계. Claude Code, Codex, Cursor, Windsurf 에 한 줄로 설치한다.",
|
||||
},
|
||||
hero: {
|
||||
h1: "designpaca",
|
||||
lead:
|
||||
"에이전트에게 “예쁘게 만들어줘”라고 하면 자기가 아는 기본값을 쏟아낸다. 못 만들어서가 아니라 결정을 안 해서다. 이 스킬은 그 결정을 순서대로 강제한다.",
|
||||
h1: "결정하지 않으면 기본값이 나온다",
|
||||
lead: "AI가 만든 티는 못 만들어서가 아니라 결정을 안 해서 난다. 그 결정을 여섯 단계로 강제하는 스킬.",
|
||||
install: "npx designpaca",
|
||||
installNote: "Node 20.11+ · 설치 대상을 감지해 골라준다",
|
||||
links: [
|
||||
{ label: "저장소", href: "https://git.chanpaca.net/yunchan/designpaca" },
|
||||
{ label: "npm", href: "https://www.npmjs.com/package/designpaca" },
|
||||
],
|
||||
proofAlt:
|
||||
"designpaca 로 만든 커피 구독 랜딩. 헤드라인은 취향은 형용사가 아니라 좌표입니다 이고, 옆에 원두 3종의 수치표가 있다.",
|
||||
proofCaption: "이 스킬로 만든 페이지. 브리프 한 줄에서 시작했다.",
|
||||
},
|
||||
pipeline: {
|
||||
eyebrow: "여섯 단계",
|
||||
h2: "브리프 한 줄이 검증된 페이지가 될 때까지",
|
||||
lead:
|
||||
"각 단계에는 통과 조건이 있다. 통과하지 못하면 다음으로 가지 않는다. 아래는 실제로 이 스킬을 돌려 만든 커피 로스터리 랜딩에서 같은 브리프가 단계마다 무엇으로 변했는지다.",
|
||||
"각 단계에는 통과 조건이 있다. 통과하지 못하면 다음으로 가지 않는다. 아래는 같은 브리프가 단계마다 무엇으로 변했는지다.",
|
||||
briefLabel: "들어온 브리프",
|
||||
brief: "수제 커피 로스터리의 원두 정기구독 랜딩 만들어줘",
|
||||
stages: STAGES_KO,
|
||||
after:
|
||||
"여섯 단계 끝에 남는 것은 페이지 하나와 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: {
|
||||
eyebrow: "이 페이지의 수치",
|
||||
h2: "카피만 고쳐서는 슬롭이 없어지지 않는다",
|
||||
lead:
|
||||
"문장을 다듬는 것으로 해결되는 것은 문장뿐이다. radius 어휘 수, 반투명 패널 개수, 타입 단계 수는 그대로 남는다. 그래서 이 페이지 자신의 형태 수치를 공개한다. 결과가 나쁘게 나와도 그대로 싣는다.",
|
||||
items: [
|
||||
{ label: "radius 어휘 수", value: "2종", note: "3px · 6px. 그 외 값을 쓰지 않는다" },
|
||||
{ label: "반투명 패널(backdrop-filter)", value: "0개", note: "실기기 FPS를 15~30% 깎는다" },
|
||||
{ label: "타입 스케일 단계", value: "5단계", note: "디스플레이 1개는 스케일 밖에 따로 정의" },
|
||||
{ label: "폰트 패밀리", value: "2개", note: "본문 1 + 코드용 모노 1. 언어판마다 자기 것만 받는다" },
|
||||
{ label: "강조색", value: "1개", note: "딥 틸. 라이트·다크는 같은 색의 대응값이다" },
|
||||
{ label: "히어로까지 JS", value: "314B", note: "gzip. 예산 50KB의 0.6%. 복사 버튼 하나뿐이다" },
|
||||
{ label: "슬롭 지문 grep", value: "0건", note: "9개 패턴 검사" },
|
||||
{ label: "하드 게이트", value: "12/12", note: "12개 전부 검사. 4개는 320px 실렌더로 측정했다" },
|
||||
{ label: "첫 인터랙션", value: "—", note: "미측정. 프로덕션 배포 후 Lighthouse 로 잰다" },
|
||||
{ label: "히어로까지 JS", value: "0.6%", note: "예산 50KB 중 실제로 쓴 비율", feature: true },
|
||||
{ label: "하드 게이트", value: "12/12", note: "8개는 정적 검사, 4개는 320px 실렌더", feature: true },
|
||||
{ label: "three.js 추가분", value: "185KB", note: "재료 섹션이 가까워질 때만 받는다. 초기 번들에는 없다" },
|
||||
{ label: "슬롭 지문", value: "0건", note: "9개 패턴 grep" },
|
||||
{ label: "radius 어휘", value: "2종", note: "10px, 20px. 셸은 파생값" },
|
||||
{ label: "반투명 패널", value: "29개", note: "유리가 이 페이지의 주제다. 대신 스크롤 콘텐츠에 필터를 걸지 않고 고정 광원 위에 얹었다" },
|
||||
{ label: "타입 단계", value: "5단계", note: "디스플레이는 스케일 밖" },
|
||||
{ label: "폰트 패밀리", value: "2개", note: "언어판마다 자기 것만 받는다" },
|
||||
{ label: "강조색", value: "1개", note: "라이트와 다크는 같은 색의 대응값" },
|
||||
{ label: "첫 인터랙션", value: "미측정", note: "배포 후 Lighthouse 로 잰다" },
|
||||
],
|
||||
caveat: "값은 프로덕션 빌드에서 측정한다. 아직 측정하지 않은 항목은 비워둔다 — 지어낸 숫자보다 구멍이 정직하다.",
|
||||
caveat:
|
||||
"값은 프로덕션 빌드에서 측정한다. 아직 측정하지 않은 항목은 비워둔다. 지어낸 숫자보다 구멍이 정직하다. 이 페이지는 제품 데모이므로 three.js 를 얹으며 JS 예산을 올렸다. 올렸다는 사실을 여기 적는 것이 규칙이다.",
|
||||
},
|
||||
preflight: {
|
||||
eyebrow: "프리플라이트 로그",
|
||||
h2: "이 페이지를 만들며 실제로 걸린 것",
|
||||
lead: "남을 검사한 결과가 아니라 자신을 검사한 결과다.",
|
||||
items: [
|
||||
{
|
||||
caught: "eyebrow 라벨이 6개 — 카운트 규칙 상한은 ceil(섹션 7 / 3) = 3개",
|
||||
fixed: "세 섹션만 남기고 나머지는 헤드라인만으로 구분했다. 라벨이 없어도 섹션은 읽힌다",
|
||||
caught: "유리에 SVG 굴절을 얹었더니 흐림까지 통째로 사라졌다",
|
||||
fixed:
|
||||
"backdrop-filter 에 url() 을 붙이면 Chrome 은 CSS.supports 로 참을 주면서 렌더에서 선언 전체를 버린다. computed 값이 none 이 된다. 굴절은 재료 섹션의 SVG 타일이 진짜로 보여주고, 유리 패널은 흐림과 가장자리 빛으로 두께를 만든다.",
|
||||
},
|
||||
{
|
||||
caught: "스크롤 리빌에 clip-path 를 쓰려 했다 — 하드 게이트 8은 transform·opacity 만 허용한다",
|
||||
fixed: "덮개를 translateX 로 미는 방식으로 바꿨다. 보이는 결과는 같고, clip-path 보간 제약도 함께 사라졌다",
|
||||
caught: "가로로 444px 넘쳤다. 레이아웃 전체가 어긋나 있었다",
|
||||
fixed:
|
||||
"main 에서 그리드 클래스가 빠져 있었다. subgrid 는 부모가 그리드일 때만 작동하므로 모든 섹션이 정렬선을 잃은 상태였다. 눈으로는 어색한 정도였고 폭을 재고서야 드러났다.",
|
||||
},
|
||||
{
|
||||
caught: "구분선 색의 대비가 1.30:1 — 장식선으로는 괜찮지만 입력 테두리로 쓰면 WCAG 1.4.11(3:1) 위반",
|
||||
fixed: "6.11:1 짜리 별칭을 하나 더 두고 용도를 갈랐다. 새 색을 만든 게 아니라 팔레트 수는 그대로다",
|
||||
caught: "스크롤을 끝까지 내려도 파이프라인 6단계 중 뒤 3개가 안 보였다",
|
||||
fixed:
|
||||
"scroll-driven animation 의 범위 끝에 페이지 하단 요소는 도달하지 못한다. 이펙트를 끄면 보이고 켜면 사라지는 것은 폴백이 있는 것과 다르다. 한 번 보이면 유지되는 방식으로 바꿨다.",
|
||||
},
|
||||
{
|
||||
caught: "본문 컬럼이 960px — 우리가 정한 한 줄 길이(561px)를 넘는다",
|
||||
fixed: "그리드의 본문 폭을 --measure 로 바꿨다. 스킬 문서의 예시 코드도 같이 고쳤다",
|
||||
caught: "다크 밴드 안 버튼의 대비가 2.88:1. 기준은 4.5:1",
|
||||
fixed:
|
||||
"밴드가 강조색만 반전시키고 그 위 글자색은 흰색 그대로였다. 반전 대응 토큰을 하나 더 두고 함께 바꿨다.",
|
||||
},
|
||||
{
|
||||
caught: "320px 에서 가로로 13px 넘쳤다 — 표를 overflow 컨테이너로 감쌌는데도",
|
||||
fixed: "grid item 의 기본 min-width 는 auto 라 콘텐츠가 부모를 밀어낸다. min-width:0 을 걸어야 overflow-x 가 실제로 작동한다",
|
||||
caught: "320px 에서 가로로 13px 넘쳤다. 표를 스크롤 컨테이너로 감쌌는데도",
|
||||
fixed:
|
||||
"grid item 의 기본 min-width 는 auto 라 콘텐츠가 부모를 밀어낸다. min-width:0 을 걸어야 overflow 가 실제로 작동한다.",
|
||||
},
|
||||
{
|
||||
caught: "다크 밴드 안 버튼의 대비가 2.88:1 — 기준은 4.5:1",
|
||||
fixed: "밴드가 강조색만 반전시키고 그 위 글자색은 흰색 그대로였다. 반전 대응 토큰을 하나 더 두고 함께 바꿨다",
|
||||
caught: "eyebrow 라벨이 6개. 카운트 규칙 상한은 3개",
|
||||
fixed: "세 섹션만 남기고 나머지는 헤드라인만으로 구분했다. 라벨이 없어도 섹션은 읽힌다.",
|
||||
},
|
||||
{
|
||||
caught: "스크롤을 끝까지 내려도 파이프라인 6단계 중 뒤 3개가 opacity 0 으로 남았다 — 이 페이지의 본체가 절반 사라졌다",
|
||||
fixed: "animation-range 의 cover 22% 에 페이지 하단 요소는 영영 도달하지 못한다. 이펙트를 끄면 보이지만 켜면 사라지는 것은 폴백이 있는 것과 다르다. 연출을 뺐다",
|
||||
caught: "본문 컬럼이 960px. 우리가 정한 한 줄 길이는 561px",
|
||||
fixed: "그리드의 본문 폭을 토큰에 묶었다. 스킬 문서의 예시 코드도 같이 고쳤다.",
|
||||
},
|
||||
],
|
||||
note: "일곱 건 모두 만들면서 걸린 것이고, 걸린 뒤에 고쳤다. 마지막 셋은 정적 검사로는 안 잡히고 320px 실렌더에서만 나왔다.",
|
||||
note: "일곱 건 모두 만들면서 걸린 것이고, 걸린 뒤에 고쳤다. 정적 검사로 잡힌 것은 하나도 없다. 전부 실제로 렌더해서 재고 나서야 나왔다.",
|
||||
},
|
||||
typography: {
|
||||
eyebrow: "한글 조판",
|
||||
h2: "라틴 기준으로 만든 도구는 한글에서 멈춘다",
|
||||
lead:
|
||||
"영어권 디자인 도구는 한글 조판 규칙을 갖고 있지 않다. 9~11px 텍스트는 라틴에서 라벨이지만 한글에서는 읽을 수 없다. line-height 1.5는 라틴에서 표준이지만 한글에서는 답답하다.",
|
||||
"영어권 디자인 도구는 한글 조판 규칙을 갖고 있지 않다. 9에서 11px 텍스트는 라틴에서 라벨이지만 한글에서는 읽을 수 없다. line-height 1.5는 라틴에서 표준이지만 한글에서는 답답하다.",
|
||||
body:
|
||||
"가장 자주 깨지는 것은 줄바꿈이다. 기본값은 단어 중간에서 줄을 끊는다. 아래 두 문단은 같은 글자, 같은 폭, 같은 폰트다. 차이는 word-break 한 줄뿐이다.",
|
||||
demoLabels: ["기본값", "keep-all"],
|
||||
demoText: "원두의 산미와 바디를 구분하는 사람에게 필요한 것은 형용사가 아니라 수치다.",
|
||||
rules: [
|
||||
"word-break: keep-all — 없으면 단어가 아무 데서나 잘린다",
|
||||
"line-height 1.6~1.8 — 라틴보다 넉넉하게",
|
||||
"음수 자간 금지 — 한글은 이미 고정폭에 가까워 즉시 뭉개진다",
|
||||
"한 줄 25~40자 — 라틴 기준 65~75자를 그대로 쓰면 너무 길다",
|
||||
"word-break: keep-all 없으면 단어가 아무 데서나 잘린다",
|
||||
"line-height 1.6에서 1.8 사이. 라틴보다 넉넉하게",
|
||||
"음수 자간 금지. 한글은 이미 고정폭에 가까워 즉시 뭉개진다",
|
||||
"한 줄 25자에서 40자. 라틴 기준 65자를 그대로 쓰면 너무 길다",
|
||||
"한글 폰트를 지정하지 않는 것 자체가 완성도 미달 신호다",
|
||||
],
|
||||
},
|
||||
rules: {
|
||||
eyebrow: "규칙",
|
||||
h2: "규칙마다 왜 그런지 적혀 있다",
|
||||
lead: "근거 없는 금지는 지켜지지 않는다. 사용자가 명시적으로 요구하면 규칙보다 그쪽이 이긴다.",
|
||||
items: [
|
||||
|
|
@ -246,7 +324,6 @@ export const ko: Content = {
|
|||
],
|
||||
},
|
||||
install: {
|
||||
eyebrow: "설치",
|
||||
h2: "한 줄이면 된다",
|
||||
lead: "설치 대상을 감지해 기본 선택해준다. 스킬 파일을 직접 고쳤다면 업데이트가 그 파일을 건드리지 않는다.",
|
||||
command: "npx designpaca",
|
||||
|
|
@ -255,7 +332,7 @@ export const ko: Content = {
|
|||
{ name: "Codex CLI", path: "~/.codex/skills/designpaca/" },
|
||||
{ name: "Cursor", path: ".cursor/rules/designpaca.mdc" },
|
||||
{ name: "Windsurf", path: ".windsurf/rules/designpaca.md" },
|
||||
{ name: "AGENTS.md", path: "블록 주입 — 에이전트 무관" },
|
||||
{ name: "AGENTS.md", path: "블록 주입. 에이전트 무관" },
|
||||
],
|
||||
commands: [
|
||||
{ cmd: "npx designpaca update", what: "최신 스킬로 갱신. 직접 고친 파일은 건너뛴다" },
|
||||
|
|
@ -268,110 +345,163 @@ export const ko: Content = {
|
|||
repo: "git.chanpaca.net/yunchan/designpaca",
|
||||
license: "MIT",
|
||||
},
|
||||
ui: { copy: "복사", copied: "복사됨", skipToContent: "본문으로 건너뛰기" },
|
||||
ui: { copy: "복사", copied: "복사됨" },
|
||||
};
|
||||
|
||||
export const en: Content = {
|
||||
meta: {
|
||||
title: "designpaca — a web design pipeline skill",
|
||||
title: "designpaca · a web design pipeline skill",
|
||||
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: {
|
||||
h1: "designpaca",
|
||||
lead:
|
||||
"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.",
|
||||
h1: "Undecided design ships as defaults",
|
||||
lead: "The AI look is not a skill problem. It is a decision problem. This skill forces the decisions, in order.",
|
||||
install: "npx designpaca",
|
||||
installNote: "Node 20.11+ · detects and preselects your install targets",
|
||||
links: [
|
||||
{ label: "Repository", href: "https://git.chanpaca.net/yunchan/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: {
|
||||
eyebrow: "Six stages",
|
||||
h2: "From one line of brief to a page that passed its own audit",
|
||||
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",
|
||||
brief: "Build a subscription landing page for a small-batch coffee roastery",
|
||||
stages: STAGES_EN,
|
||||
after:
|
||||
"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: {
|
||||
eyebrow: "This page, measured",
|
||||
h2: "Fixing the copy does not remove the slop",
|
||||
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.",
|
||||
items: [
|
||||
{ label: "radius vocabulary", value: "2", note: "3px and 6px. Nothing else" },
|
||||
{ label: "translucent panels (backdrop-filter)", value: "0", note: "costs 15–30% FPS on real devices" },
|
||||
{ label: "type scale steps", value: "5", note: "the display size is defined outside the scale" },
|
||||
{ label: "font families", value: "2", note: "one text + one mono. Each language loads only its own" },
|
||||
{ label: "accent colours", value: "1", note: "deep teal. Light and dark are the same colour" },
|
||||
{ label: "JS to first paint", value: "314B", note: "gzip. 0.6% of the 50KB budget. One copy button" },
|
||||
{ label: "slop fingerprints", value: "0", note: "9 patterns checked" },
|
||||
{ label: "hard gates", value: "12/12", note: "all twelve checked. Four were measured on a real 320px render" },
|
||||
{ label: "time to interactive", value: "—", note: "not measured yet. Lighthouse, after deploy" },
|
||||
{ label: "JS to first paint", value: "0.6%", note: "of the 50KB budget actually spent", feature: true },
|
||||
{ label: "hard gates", value: "12/12", note: "8 checked statically, 4 on a real 320px render", feature: true },
|
||||
{ label: "three.js payload", value: "185KB", note: "fetched only as the materials section approaches. Never in the initial bundle" },
|
||||
{ label: "slop fingerprints", value: "0", note: "9 patterns grepped" },
|
||||
{ label: "radius vocabulary", value: "2", note: "10px and 20px. The shell is derived" },
|
||||
{ label: "translucent panels", value: "29", note: "glass is the subject here. No filter sits on scrolling content, only on top of fixed light" },
|
||||
{ label: "type scale steps", value: "5", note: "the display size sits outside the scale" },
|
||||
{ label: "font families", value: "2", note: "each language loads only its own" },
|
||||
{ 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: {
|
||||
eyebrow: "Preflight log",
|
||||
h2: "What this page actually got caught on",
|
||||
lead: "Not the result of auditing someone else. The result of auditing itself.",
|
||||
items: [
|
||||
{
|
||||
caught: "6 eyebrow labels — the count rule caps them at ceil(7 sections / 3) = 3",
|
||||
fixed: "Kept three, dropped the rest. Sections read fine without a label above the heading",
|
||||
caught: "Adding SVG refraction to the glass removed the blur along with it",
|
||||
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",
|
||||
fixed: "Switched to a cover pushed with translateX. Same result, and clip-path's interpolation limits went away with it",
|
||||
caught: "444px of horizontal overflow. The entire layout was off its rails",
|
||||
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",
|
||||
fixed: "Added a 6.11:1 alias and split the two uses. No new colour, so the palette count is unchanged",
|
||||
caught: "Scrolling to the very bottom still left the last 3 of 6 pipeline stages invisible",
|
||||
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)",
|
||||
fixed: "Bound the grid's main column to --measure. The skill's own example code was wrong too, so that got fixed as well",
|
||||
caught: "The button inside the dark band sat at 2.88:1. The threshold is 4.5:1",
|
||||
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",
|
||||
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",
|
||||
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 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",
|
||||
fixed: "The band inverted the accent colour but left the text on it white. Added an inverted counterpart token and switched both together",
|
||||
caught: "6 eyebrow labels. The count rule caps them at 3",
|
||||
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",
|
||||
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",
|
||||
caught: "The text column was 960px. The measure we had just set was 561px",
|
||||
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: {
|
||||
eyebrow: "Your language is probably not Latin",
|
||||
h2: "Tools built on Latin assumptions stop at the script boundary",
|
||||
lead:
|
||||
"Design tooling written in English carries no rules for other scripts. 9–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.",
|
||||
"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:
|
||||
"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"],
|
||||
demoText: "원두의 산미와 바디를 구분하는 사람에게 필요한 것은 형용사가 아니라 수치다.",
|
||||
rules: [
|
||||
"word-break: keep-all — without it, words break at arbitrary points",
|
||||
"line-height 1.6–1.8 — more generous than Latin needs",
|
||||
"no negative letter-spacing — Korean glyphs are near-monospaced and collapse immediately",
|
||||
"25–40 characters per line — Latin's 65–75 is far too long",
|
||||
"word-break: keep-all, or words break at arbitrary points",
|
||||
"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",
|
||||
"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",
|
||||
],
|
||||
},
|
||||
rules: {
|
||||
eyebrow: "Rules",
|
||||
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.",
|
||||
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: "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: "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: {
|
||||
eyebrow: "Install",
|
||||
h2: "One line",
|
||||
lead: "Targets are detected and preselected. If you edit a skill file yourself, updates leave it alone.",
|
||||
command: "npx designpaca",
|
||||
|
|
@ -393,7 +525,7 @@ export const en: Content = {
|
|||
{ name: "Codex CLI", path: "~/.codex/skills/designpaca/" },
|
||||
{ name: "Cursor", path: ".cursor/rules/designpaca.mdc" },
|
||||
{ name: "Windsurf", path: ".windsurf/rules/designpaca.md" },
|
||||
{ name: "AGENTS.md", path: "block injection — agent agnostic" },
|
||||
{ name: "AGENTS.md", path: "block injection, agent agnostic" },
|
||||
],
|
||||
commands: [
|
||||
{ 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",
|
||||
license: "MIT",
|
||||
},
|
||||
ui: { copy: "Copy", copied: "Copied", skipToContent: "Skip to content" },
|
||||
ui: { copy: "Copy", copied: "Copied" },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,23 +16,41 @@ const FONT_CSS = {
|
|||
} as const;
|
||||
|
||||
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>
|
||||
<html lang={lang}>
|
||||
<html lang={lang} class="no-js">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<link rel="icon" href={FAVICON} />
|
||||
|
||||
<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="en" href={new URL("/en/", Astro.site)} />
|
||||
<link rel="alternate" hreflang="x-default" href={new URL("/", Astro.site)} />
|
||||
|
||||
{/* 본문 폰트만 preconnect·preload 한다. 모노는 코드 블록에만 쓰인다 */}
|
||||
<link rel="preconnect" href={lang === "ko" ? "https://cdn.jsdelivr.net" : "https://api.fontshare.com"} crossorigin />
|
||||
<link
|
||||
rel="preconnect"
|
||||
href={lang === "ko" ? "https://cdn.jsdelivr.net" : "https://api.fontshare.com"}
|
||||
crossorigin
|
||||
/>
|
||||
<link rel="stylesheet" href={FONT_CSS[lang]} />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
|
|
@ -44,20 +62,36 @@ const alt = lang === "ko" ? "/en/" : "/";
|
|||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content={lang === "ko" ? "ko_KR" : "en_US"} />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
|
||||
{/* JS 가 살아 있으면 no-js 를 떼고 진입 연출을 켠다 */}
|
||||
<script is:inline>
|
||||
document.documentElement.classList.remove("no-js");
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<a class="skip" href="#main">{lang === "ko" ? "본문으로 건너뛰기" : "Skip to content"}</a>
|
||||
|
||||
<header class="page site-header">
|
||||
<a class="wordmark" href={lang === "ko" ? "/" : "/en/"}>designpaca</a>
|
||||
<nav aria-label={lang === "ko" ? "언어" : "Language"}>
|
||||
{/* 언어는 국가가 아니다 — 국기 아이콘을 쓰지 않는다 */}
|
||||
<a href={alt} hreflang={lang === "ko" ? "en" : "ko"} lang={lang === "ko" ? "en" : "ko"}>
|
||||
{lang === "ko" ? "English" : "한국어"}
|
||||
<header class="site-header">
|
||||
<div class="header-inner glass">
|
||||
<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">
|
||||
<path d="M11 7v18" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" />
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<main id="main" class="page">
|
||||
|
|
@ -67,6 +101,29 @@ const alt = lang === "ko" ? "/en/" : "/";
|
|||
<footer class="page site-footer">
|
||||
<slot name="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>
|
||||
</html>
|
||||
|
||||
|
|
@ -82,31 +139,51 @@ const alt = lang === "ko" ? "/en/" : "/";
|
|||
z-index: 10;
|
||||
transition: transform var(--dur-quick) var(--ease-out);
|
||||
}
|
||||
.skip:focus {
|
||||
transform: translateY(calc(var(--space-3) * 5));
|
||||
}
|
||||
.skip:focus { transform: translateY(calc(var(--space-3) * 5)); }
|
||||
|
||||
/* 상단에 붙은 풀블리드 바 대신 떠 있는 헤더 */
|
||||
/* 상단에 붙은 바가 아니라 떠 있는 유리 알약 */
|
||||
.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 > * {
|
||||
grid-column: main;
|
||||
}
|
||||
.site-header {
|
||||
align-items: baseline;
|
||||
.header-inner {
|
||||
max-width: 1180px;
|
||||
margin-inline: auto;
|
||||
display: flex;
|
||||
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 {
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
font-weight: 560;
|
||||
letter-spacing: -0.01em;
|
||||
text-decoration: none;
|
||||
}
|
||||
.site-header nav {
|
||||
justify-self: end;
|
||||
grid-row: 1;
|
||||
grid-column: main;
|
||||
.mark { color: var(--accent); flex: none; }
|
||||
.site-header nav a {
|
||||
display: inline-flex;
|
||||
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);
|
||||
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 {
|
||||
padding-block: var(--space-6);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
import Base from "../../layouts/Base.astro";
|
||||
import Hero from "../../components/Hero.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 Preflight from "../../components/Preflight.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}>
|
||||
<Hero c={c.hero} ui={c.ui} />
|
||||
<Pipeline c={c.pipeline} />
|
||||
<Showcase c={c.showcase} />
|
||||
<Materials c={c.materials} />
|
||||
<Metrics c={c.metrics} />
|
||||
<Preflight c={c.preflight} />
|
||||
<Typography c={c.typography} />
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
import Base from "../layouts/Base.astro";
|
||||
import Hero from "../components/Hero.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 Preflight from "../components/Preflight.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}>
|
||||
<Hero c={c.hero} ui={c.ui} />
|
||||
<Pipeline c={c.pipeline} />
|
||||
<Showcase c={c.showcase} />
|
||||
<Materials c={c.materials} />
|
||||
<Metrics c={c.metrics} />
|
||||
<Preflight c={c.preflight} />
|
||||
<Typography c={c.typography} />
|
||||
|
|
|
|||
|
|
@ -5,13 +5,12 @@
|
|||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
*::after { box-sizing: border-box; }
|
||||
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
scroll-behavior: smooth;
|
||||
background: var(--surface);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html { scroll-behavior: auto; }
|
||||
|
|
@ -28,12 +27,14 @@ body {
|
|||
font-synthesis-weight: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 한글은 단어 중간에서 끊지 않는다 */
|
||||
[lang="ko"] body,
|
||||
[lang="ko"] p,
|
||||
[lang="ko"] li,
|
||||
[lang="ko"] dt,
|
||||
[lang="ko"] dd,
|
||||
[lang="ko"] h1,
|
||||
[lang="ko"] h2,
|
||||
[lang="ko"] h3 {
|
||||
|
|
@ -41,9 +42,37 @@ body {
|
|||
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 {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
|
|
@ -57,15 +86,19 @@ body {
|
|||
.page > .wide { grid-column: wide; }
|
||||
.page > .full { grid-column: full; }
|
||||
|
||||
/* 섹션 안에서도 같은 그리드를 쓴다 — 정렬선이 페이지 전체에서 하나여야 한다 */
|
||||
.section {
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
grid-column: full;
|
||||
padding-block: var(--space-7);
|
||||
position: relative;
|
||||
}
|
||||
.section > * { grid-column: main; }
|
||||
.section > .wide { grid-column: wide; }
|
||||
.section > .full { grid-column: full; }
|
||||
|
||||
.page > *,
|
||||
.section > * { min-width: 0; }
|
||||
|
||||
/* ---- 타이포 ------------------------------------------------ */
|
||||
h1, h2, h3 {
|
||||
|
|
@ -75,23 +108,19 @@ h1, h2, h3 {
|
|||
font-weight: 600;
|
||||
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); }
|
||||
h3 { font-size: var(--step-1); }
|
||||
|
||||
p { margin: 0 0 var(--space-3); text-wrap: pretty; }
|
||||
p:last-child { margin-bottom: 0; }
|
||||
|
||||
.lead {
|
||||
font-size: var(--step-1);
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
.lead { font-size: var(--step-1); color: var(--ink-muted); }
|
||||
|
||||
/* eyebrow 는 새 크기를 만들지 않는다 — 본문 크기 + 자간 + muted */
|
||||
.eyebrow {
|
||||
font-size: var(--step-0);
|
||||
letter-spacing: var(--tracking-label);
|
||||
color: var(--ink-muted);
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
margin: 0 0 var(--space-3);
|
||||
}
|
||||
|
|
@ -109,48 +138,130 @@ a {
|
|||
}
|
||||
a:hover { color: var(--accent); }
|
||||
|
||||
/* 포커스는 어떤 이펙트보다 우선한다 */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 3px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--line);
|
||||
margin-block: var(--space-6);
|
||||
/* ============================================================
|
||||
유리
|
||||
backdrop-filter 만 쓰면 흐린 유리에서 끝난다. 두께와 굴절이
|
||||
있어야 유리로 읽힌다. 그래서 세 겹을 쌓는다.
|
||||
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 {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
font-variant-numeric: tabular-nums;
|
||||
/* 투명도를 줄여달라는 사용자에게는 불투명 판으로 준다 */
|
||||
@media (prefers-reduced-transparency: reduce) {
|
||||
.glass {
|
||||
background: var(--surface-raised);
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
}
|
||||
th, td {
|
||||
text-align: left;
|
||||
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;
|
||||
@supports not (backdrop-filter: blur(1px)) {
|
||||
.glass { background: var(--surface-raised); }
|
||||
}
|
||||
|
||||
/* 같은 이유로 그리드·플렉스 자식은 전부 줄어들 수 있어야 한다 */
|
||||
.page > *,
|
||||
.section > * { min-width: 0; }
|
||||
/* 유리 위에 얹는 두 번째 판. 안쪽으로 갈수록 밝아진다 */
|
||||
.glass-2 { background: var(--glass-tint-2); }
|
||||
|
||||
/* 이중 베젤: 유리판이 트레이 안에 앉는다 */
|
||||
.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 {
|
||||
|
|
@ -164,3 +275,5 @@ th {
|
|||
|
||||
.muted { color: var(--ink-muted); }
|
||||
.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; }
|
||||
|
|
|
|||
|
|
@ -1,72 +1,93 @@
|
|||
/* ============================================================
|
||||
designpaca 소개 페이지 — 디자인 토큰
|
||||
프리셋: swiss-minimal / 비율 1.25 / 강조색 1개 / radius 2종
|
||||
방향 재설정 2026-08-20: Ethereal Glass
|
||||
|
||||
이 페이지는 SVG 필터와 three.js 를 파는 제품의 데모다.
|
||||
리퀴드 글래스는 그 두 기술의 결합 그 자체이므로 여기서는
|
||||
장식이 아니라 제품 시연이다. 다크 기반을 고른 것도 같은 이유다.
|
||||
유리는 어두운 바탕 위에서만 굴절이 읽힌다.
|
||||
============================================================ */
|
||||
|
||||
:root {
|
||||
/* ---- 폰트 패밀리 ---------------------------------------- */
|
||||
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
--mono-size: 0.9375em; /* 부모 대비. 새 스케일 단계를 만들지 않는다 */
|
||||
--mono-size: 0.9375em;
|
||||
|
||||
/* ---- 색: 라이트 (기본) ----------------------------------- */
|
||||
--surface: #FAF9F6; /* 웜 오프화이트. 순백 아님 (A-07) */
|
||||
--surface-raised: #F2F0EA;
|
||||
--surface-inverse: #14161A; /* 다크 밴드. 순흑 아님 */
|
||||
--ink: #16181C; /* 대비 16.88:1 */
|
||||
--ink-muted: #5A5F66; /* 대비 6.11:1 */
|
||||
--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 */
|
||||
/* ---- 바탕: 잉크. 순흑이 아니다 --------------------------- */
|
||||
--surface: #0B0E12;
|
||||
--surface-raised: #11151B;
|
||||
--surface-sunken: #070A0D;
|
||||
/* 밝은 밴드 하나. 규칙으로 정의된 반전이다 */
|
||||
--surface-inverse: #F4F2EC;
|
||||
|
||||
/* ---- 형태: radius 2종 (게이트 #3) ------------------------ */
|
||||
--radius-sm: 3px; /* 버튼·입력·배지 */
|
||||
--radius-md: 6px; /* 카드·코드 블록 */
|
||||
--ink: #ECEFF2; /* 15.9:1 */
|
||||
--ink-muted: #98A2AE; /* 7.1:1 */
|
||||
--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-2: 0.5rem; /* 8 */
|
||||
--space-3: 1rem; /* 16 */
|
||||
--space-4: 1.5rem; /* 24 */
|
||||
--space-5: 2.5rem; /* 40 */
|
||||
--space-6: 4rem; /* 64 */
|
||||
--space-7: 6rem; /* 96 — 일반 섹션 상하 */
|
||||
--space-8: 8rem; /* 128 — 다크 밴드 상하. 본문 간격의 8배 */
|
||||
--space-1: 0.25rem;
|
||||
--space-2: 0.5rem;
|
||||
--space-3: 1rem;
|
||||
--space-4: 1.5rem;
|
||||
--space-5: 2.5rem;
|
||||
--space-6: 4rem;
|
||||
--space-7: 7rem;
|
||||
--space-8: 10rem;
|
||||
|
||||
/* ---- 모션 ------------------------------------------------ */
|
||||
--dur-instant: 100ms; /* 호버·포커스 */
|
||||
--dur-quick: 200ms; /* 작은 등장·퇴장 */
|
||||
--dur-normal: 320ms; /* 패널·전환. 2단계 상한 */
|
||||
--dur-slow: 600ms; /* 이 프로젝트에서는 사용하지 않는다 */
|
||||
--dur-instant: 120ms;
|
||||
--dur-quick: 280ms;
|
||||
--dur-normal: 560ms;
|
||||
--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-soft: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
/* bounce·elastic 없음 (E-02) */
|
||||
}
|
||||
|
||||
/* ---- 색: 다크 테마 — 라이트와 동등하게 정의 ------------------ */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
/* ---- 라이트 선호: 유리를 얇게 하고 바탕을 올린다 --------------
|
||||
테마를 뒤집지 않는다. 이 페이지는 어두운 유리가 주제다.
|
||||
다만 라이트를 쓰는 사람에게 대비를 조금 더 준다. */
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--surface: #14161A;
|
||||
--surface-raised: #1C1F24;
|
||||
--surface-inverse: #FAF9F6; /* 다크에서는 밴드가 밝아진다 */
|
||||
--ink: #E9E7E2;
|
||||
--ink-muted: #9AA0A6;
|
||||
--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;
|
||||
--surface: #12161C;
|
||||
--surface-raised: #181D25;
|
||||
--glass-tint: rgb(255 255 255 / 0.07);
|
||||
--glass-tint-2: rgb(255 255 255 / 0.11);
|
||||
--glass-edge: rgb(255 255 255 / 0.18);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,19 +95,20 @@
|
|||
[lang="ko"] {
|
||||
--font-sans: "Pretendard Variable", Pretendard, system-ui, sans-serif;
|
||||
|
||||
--step-0: 1.0625rem; /* 17 — 본문 · eyebrow · 코드 */
|
||||
--step-1: 1.328rem; /* 21.3 — 리드 */
|
||||
--step-2: 1.660rem; /* 26.6 — h3 */
|
||||
--step-3: 2.076rem; /* 33.2 — h2 */
|
||||
--step-4: 2.595rem; /* 41.5 — h1 */
|
||||
--display: clamp(2.595rem, 1.6rem + 4.2vw, 4.25rem); /* → 68 */
|
||||
--step-0: 1.0625rem;
|
||||
--step-1: 1.328rem;
|
||||
--step-2: 1.660rem;
|
||||
--step-3: 2.076rem;
|
||||
--step-4: 2.595rem;
|
||||
/* 히어로에 힘을 준다 */
|
||||
--display: clamp(3rem, 1.2rem + 7.2vw, 7rem);
|
||||
|
||||
--leading-tight: 1.15;
|
||||
--leading-normal: 1.7; /* 한글은 넉넉하게 */
|
||||
--measure: 33em; /* ≈ 561px. 한글 글자폭 ≈ 1em */
|
||||
--leading-tight: 1.06;
|
||||
--leading-normal: 1.7;
|
||||
--measure: 33em;
|
||||
|
||||
--tracking-display: -0.01em; /* 한글은 음수 자간을 크게 주지 않는다 */
|
||||
--tracking-body: 0; /* 한글 본문 음수 자간 금지 */
|
||||
--tracking-display: -0.02em;
|
||||
--tracking-body: 0;
|
||||
--tracking-label: 0.08em;
|
||||
|
||||
word-break: keep-all;
|
||||
|
|
@ -97,32 +119,40 @@
|
|||
[lang="en"] {
|
||||
--font-sans: "Switzer", system-ui, -apple-system, sans-serif;
|
||||
|
||||
--step-0: 1rem; /* 16 */
|
||||
--step-1: 1.25rem; /* 20 */
|
||||
--step-2: 1.5625rem; /* 25 */
|
||||
--step-3: 1.953rem; /* 31.3 */
|
||||
--step-4: 2.441rem; /* 39 */
|
||||
--display: clamp(2.441rem, 1.5rem + 4vw, 4rem); /* → 64 */
|
||||
--step-0: 1rem;
|
||||
--step-1: 1.25rem;
|
||||
--step-2: 1.5625rem;
|
||||
--step-3: 1.953rem;
|
||||
--step-4: 2.441rem;
|
||||
--display: clamp(2.8rem, 1rem + 7vw, 6.6rem);
|
||||
|
||||
--leading-tight: 1.1;
|
||||
--leading-tight: 0.98;
|
||||
--leading-normal: 1.55;
|
||||
--measure: 65ch; /* ≈ 640px */
|
||||
--measure: 65ch;
|
||||
|
||||
--tracking-display: -0.025em;
|
||||
--tracking-display: -0.04em;
|
||||
--tracking-body: 0;
|
||||
--tracking-label: 0.08em;
|
||||
}
|
||||
|
||||
/* ---- 다크 밴드: 게이트 #4가 요구하는 "규칙으로 정의된 반전" ---- */
|
||||
/* 사용처는 히어로와 설치 섹션 두 곳뿐이다. 그 외 사용 금지. */
|
||||
/* ---- 밝은 밴드: 규칙으로 정의된 단 한 번의 반전 --------------
|
||||
사용처는 조판 섹션 하나뿐이다. 한글이 종이 위에 있어야 하는
|
||||
주장을 하는 자리라 거기서만 바탕이 밝아진다. */
|
||||
.band-inverse {
|
||||
--surface: var(--surface-inverse);
|
||||
--surface-raised: #ECE8DE;
|
||||
--surface-sunken: #E2DED3;
|
||||
--ink: var(--ink-inverse);
|
||||
--ink-muted: var(--ink-muted-inverse);
|
||||
--line: var(--line-inverse);
|
||||
--line-strong: var(--ink-muted-inverse);
|
||||
--accent: var(--accent-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);
|
||||
color: var(--ink);
|
||||
padding-block: var(--space-8);
|
||||
}
|
||||
|
|
|
|||
2
designpaca-en.html
Normal file
2
designpaca-en.html
Normal file
File diff suppressed because one or more lines are too long
2
designpaca-ko.html
Normal file
2
designpaca-ko.html
Normal file
File diff suppressed because one or more lines are too long
305
pnpm-lock.yaml
generated
305
pnpm-lock.yaml
generated
|
|
@ -20,6 +20,12 @@ importers:
|
|||
astro:
|
||||
specifier: ^5.1.1
|
||||
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:
|
||||
dependencies:
|
||||
|
|
@ -487,133 +493,275 @@ packages:
|
|||
cpu: [arm64]
|
||||
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':
|
||||
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
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':
|
||||
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
|
||||
cpu: [arm64]
|
||||
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':
|
||||
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
|
||||
cpu: [x64]
|
||||
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':
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
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':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
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':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
cpu: [ppc64]
|
||||
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':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
cpu: [riscv64]
|
||||
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':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
cpu: [s390x]
|
||||
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':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
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':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
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':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
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':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
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':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
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':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
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':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [riscv64]
|
||||
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':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
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':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
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':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
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':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
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':
|
||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
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':
|
||||
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
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':
|
||||
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ia32]
|
||||
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':
|
||||
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
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':
|
||||
resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -1810,6 +1958,15 @@ packages:
|
|||
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -1897,6 +2054,9 @@ packages:
|
|||
thenify@3.3.1:
|
||||
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
|
||||
|
||||
three@0.185.1:
|
||||
resolution: {integrity: sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==}
|
||||
|
||||
tiny-inflate@1.0.3:
|
||||
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
|
||||
|
||||
|
|
@ -2580,103 +2740,206 @@ snapshots:
|
|||
'@esbuild/win32-x64@0.27.7':
|
||||
optional: true
|
||||
|
||||
'@img/colour@1.1.0':
|
||||
optional: true
|
||||
'@img/colour@1.1.0': {}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
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':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
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':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.3.2':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.3.2':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.3.2':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.3.2':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.3.2':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.3.2':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.3.2':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.3.2':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.3.2':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.3.2':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
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':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
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':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
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':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
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':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
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':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
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':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
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':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
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':
|
||||
dependencies:
|
||||
'@emnapi/runtime': 1.11.3
|
||||
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':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-arm64@0.35.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-ia32@0.35.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.35.3':
|
||||
optional: true
|
||||
|
||||
'@inquirer/external-editor@1.0.3(@types/node@22.20.1)':
|
||||
dependencies:
|
||||
chardet: 2.2.0
|
||||
|
|
@ -3142,8 +3405,7 @@ snapshots:
|
|||
|
||||
detect-indent@6.1.0: {}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
optional: true
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
deterministic-object-hash@2.0.2:
|
||||
dependencies:
|
||||
|
|
@ -4209,6 +4471,39 @@ snapshots:
|
|||
'@img/sharp-win32-x64': 0.34.5
|
||||
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:
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
|
|
@ -4304,6 +4599,8 @@ snapshots:
|
|||
dependencies:
|
||||
any-promise: 1.3.0
|
||||
|
||||
three@0.185.1: {}
|
||||
|
||||
tiny-inflate@1.0.3: {}
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue