소개 페이지 — 스킬을 스스로 적용해 만든 첫 사례
designpaca 파이프라인 0~6단계를 실제로 돌려 만들었다. 근거는 research/site-brief.md, 결정 기록은 apps/site/design.md 에 있다. 한 문장 컨셉 - 이 페이지는 designpaca 를 거쳐 만들어졌고, 절차와 형태 수치를 한국어 기준으로 전부 공개한다 번호 없이 순서 표현 (C-07 회피) - 파이프라인 6단계를 번호로 매기면 검출률 3위 슬롭 지문에 걸린다 - 시각: 스파인 위 위치가 순서를 만든다 / 내용: 같은 브리프가 단계마다 변한다 - 의미: <ol role="list"> — 화면에 번호 0개, 보조기술에는 "6개 중 n번째" 멀티링구얼 - / = 한국어, /en/ = 영어. 번역이 아니라 각 판을 따로 썼다 - 언어별 레이아웃이 자기 폰트만 로드한다 (판당 2개) - 조판 섹션은 한국어판에서 주장이고 영어판에서 사례다 실측 - JS 314B (gzip, 예산 50KB) / CSS 3.0KB / HTML 4.4~4.8KB - radius 2종 · backdrop-filter 0개 · 타입 5단계 · 강조색 1개 - 슬롭 지문 grep 9패턴 0건, 정적 하드 게이트 8개 전부 통과 감사에서 걸려 고친 것 - eyebrow 6개 → 상한 ceil(7/3)=3 위반 → 3개로 - clip-path 리빌 → 게이트 #8 위반 → transform 으로 교정 - 구분선 대비 1.30:1 → 입력 테두리용 별칭 분리 (WCAG 1.4.11) - 본문 컬럼 960px → --measure 로. layout.md 예시 코드도 같이 수정
This commit is contained in:
parent
8808c672dc
commit
6103700d38
18 changed files with 1497 additions and 20 deletions
88
apps/site/src/components/CopyCommand.astro
Normal file
88
apps/site/src/components/CopyCommand.astro
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
---
|
||||
interface Props {
|
||||
command: string;
|
||||
copyLabel: string;
|
||||
copiedLabel: string;
|
||||
note?: string;
|
||||
}
|
||||
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>
|
||||
{note && <p class="cmd-note muted">{note}</p>}
|
||||
|
||||
<style>
|
||||
.cmd {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
max-width: 34em;
|
||||
}
|
||||
.cmd-text {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
padding: var(--space-3);
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cmd-copy {
|
||||
flex: 0 0 auto;
|
||||
/* 독립 컨트롤이므로 44px 이상 */
|
||||
min-width: 5.5em;
|
||||
min-height: 44px;
|
||||
padding-inline: var(--space-3);
|
||||
border: 0;
|
||||
border-left: 1px solid var(--line-strong);
|
||||
background: var(--accent);
|
||||
color: var(--accent-ink);
|
||||
font: inherit;
|
||||
font-size: var(--step-0);
|
||||
cursor: pointer;
|
||||
transition: opacity var(--dur-instant) var(--ease-out);
|
||||
}
|
||||
.cmd-copy:hover {
|
||||
opacity: 0.88;
|
||||
}
|
||||
.cmd-note {
|
||||
margin-top: var(--space-2);
|
||||
font-size: var(--step-0);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// 이 페이지의 유일한 상호작용이다. 라이브러리를 쓰지 않는다.
|
||||
document.querySelectorAll<HTMLButtonElement>(".cmd-copy").forEach((btn) => {
|
||||
btn.addEventListener("click", async () => {
|
||||
const text = btn.dataset["copy"] ?? "";
|
||||
const done = btn.dataset["copied"] ?? "OK";
|
||||
const original = btn.textContent;
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch {
|
||||
// 클립보드 권한이 없으면 선택 상태로 만들어 사용자가 직접 복사하게 둔다
|
||||
const range = document.createRange();
|
||||
const code = btn.previousElementSibling;
|
||||
if (code) {
|
||||
range.selectNodeContents(code);
|
||||
const sel = getSelection();
|
||||
sel?.removeAllRanges();
|
||||
sel?.addRange(range);
|
||||
}
|
||||
return;
|
||||
}
|
||||
btn.textContent = done;
|
||||
btn.setAttribute("aria-live", "polite");
|
||||
setTimeout(() => {
|
||||
btn.textContent = original;
|
||||
}, 1600);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
48
apps/site/src/components/Hero.astro
Normal file
48
apps/site/src/components/Hero.astro
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
---
|
||||
import CopyCommand from "./CopyCommand.astro";
|
||||
import type { Content } from "../i18n/content";
|
||||
|
||||
interface Props {
|
||||
c: Content["hero"];
|
||||
ui: Content["ui"];
|
||||
}
|
||||
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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
}
|
||||
.hero-inner {
|
||||
grid-column: main;
|
||||
}
|
||||
.hero-lead {
|
||||
max-width: 30em;
|
||||
margin-block: var(--space-4) var(--space-5);
|
||||
}
|
||||
.hero-links {
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
margin-top: var(--space-4);
|
||||
font-size: var(--step-0);
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
</style>
|
||||
59
apps/site/src/components/Install.astro
Normal file
59
apps/site/src/components/Install.astro
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
import CopyCommand from "./CopyCommand.astro";
|
||||
import type { Content } from "../i18n/content";
|
||||
|
||||
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>
|
||||
</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>
|
||||
</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); }
|
||||
dd { margin: 0; font-size: var(--step-0); }
|
||||
</style>
|
||||
40
apps/site/src/components/Metrics.astro
Normal file
40
apps/site/src/components/Metrics.astro
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
import type { Content } from "../i18n/content";
|
||||
|
||||
interface Props {
|
||||
c: Content["metrics"];
|
||||
}
|
||||
const { c } = Astro.props;
|
||||
---
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<p class="caveat muted">{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; }
|
||||
</style>
|
||||
152
apps/site/src/components/Pipeline.astro
Normal file
152
apps/site/src/components/Pipeline.astro
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
---
|
||||
import type { Content } from "../i18n/content";
|
||||
|
||||
interface Props {
|
||||
c: Content["pipeline"];
|
||||
}
|
||||
const { c } = Astro.props;
|
||||
---
|
||||
|
||||
{/*
|
||||
순서를 번호 없이 표현하는 세 층:
|
||||
① 시각 — 스파인 위의 위치가 순서를 만든다 (Beck 튜브맵)
|
||||
② 내용 — 같은 브리프가 단계마다 변해간다 (IKEA 조립 설명서)
|
||||
③ 의미 — <ol role="list"> — 화면에 번호는 없지만 보조기술은 "6개 중 n번째"로 읽는다
|
||||
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>
|
||||
|
||||
<div class="brief">
|
||||
<span class="brief-label">{c.briefLabel}</span>
|
||||
<p class="brief-text">{c.brief}</p>
|
||||
</div>
|
||||
|
||||
<ol role="list" class="spine">
|
||||
{
|
||||
c.stages.map((s) => (
|
||||
<li class="stop">
|
||||
<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>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ol>
|
||||
|
||||
<p class="after muted">{c.after}</p>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.pipeline-lead {
|
||||
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-label {
|
||||
display: block;
|
||||
font-size: var(--step-0);
|
||||
letter-spacing: var(--tracking-label);
|
||||
color: var(--ink-muted);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.brief-text {
|
||||
margin: 0;
|
||||
font-size: var(--step-1);
|
||||
}
|
||||
|
||||
/* ---- 스파인 ---------------------------------------------
|
||||
선은 장식이 아니라 순서 그 자체다. 정거장의 위치가 번호를 대신한다.
|
||||
---------------------------------------------------------- */
|
||||
.spine {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 0 0 var(--space-5);
|
||||
position: relative;
|
||||
}
|
||||
.spine::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: var(--space-4);
|
||||
bottom: var(--space-4);
|
||||
width: 1px;
|
||||
background: var(--line-strong);
|
||||
}
|
||||
|
||||
.stop {
|
||||
position: relative;
|
||||
padding-block: var(--space-4);
|
||||
}
|
||||
.stop + .stop {
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
/* 정거장 표시 — 채워진 원. 숫자가 아니다 */
|
||||
.stop::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: calc(var(--space-5) * -1);
|
||||
top: calc(var(--space-4) + 0.55em);
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface);
|
||||
border: 3px solid var(--accent);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.stop-name {
|
||||
font-size: var(--step-1);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.stop-does {
|
||||
color: var(--ink-muted);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
/* 브리프가 이 단계에서 무엇이 됐는가 — 변화가 순서를 만든다 */
|
||||
.stop-becomes {
|
||||
margin: 0;
|
||||
padding-left: var(--space-3);
|
||||
border-left: 2px solid var(--accent);
|
||||
}
|
||||
.stop-becomes.mono {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--mono-size);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.after {
|
||||
margin-top: var(--space-5);
|
||||
}
|
||||
|
||||
/* 진입 연출은 하나뿐이고, 없어도 전부 읽힌다.
|
||||
scroll-driven animation 이라 scroll 리스너가 없다. */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
@supports (animation-timeline: view()) {
|
||||
.stop {
|
||||
animation: stop-in linear both;
|
||||
animation-timeline: view();
|
||||
animation-range: entry 10% cover 22%;
|
||||
}
|
||||
@keyframes stop-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
39
apps/site/src/components/Preflight.astro
Normal file
39
apps/site/src/components/Preflight.astro
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
import type { Content } from "../i18n/content";
|
||||
|
||||
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>
|
||||
|
||||
<ul class="log">
|
||||
{
|
||||
c.items.map((i) => (
|
||||
<li class="entry">
|
||||
<p class="caught">{i.caught}</p>
|
||||
<p class="fixed muted">{i.fixed}</p>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
|
||||
{c.note && <p class="pf-note muted">{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);
|
||||
}
|
||||
.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); }
|
||||
</style>
|
||||
42
apps/site/src/components/Rules.astro
Normal file
42
apps/site/src/components/Rules.astro
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
import type { Content } from "../i18n/content";
|
||||
|
||||
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>
|
||||
|
||||
<dl class="rules">
|
||||
{
|
||||
c.items.map((r) => (
|
||||
<div class="rule">
|
||||
<dt>{r.rule}</dt>
|
||||
<dd>{r.why}</dd>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.rules-lead { margin-bottom: var(--space-5); }
|
||||
.rules { margin: 0; }
|
||||
.rule {
|
||||
padding-block: var(--space-4);
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.rule:last-child { border-bottom: 1px solid var(--line); }
|
||||
dt {
|
||||
font-weight: 560;
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
dd {
|
||||
margin: 0;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
</style>
|
||||
75
apps/site/src/components/Typography.astro
Normal file
75
apps/site/src/components/Typography.astro
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
---
|
||||
import type { Content } from "../i18n/content";
|
||||
|
||||
interface Props {
|
||||
c: Content["typography"];
|
||||
}
|
||||
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>
|
||||
|
||||
{/* 주장을 글로 하지 않고 실물로 보여준다. 두 문단의 차이는 word-break 한 줄뿐이다 */}
|
||||
<div class="demo">
|
||||
<div class="demo-col">
|
||||
<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">
|
||||
<p class="demo-label muted mono">{c.demoLabels[1]}</p>
|
||||
<p lang="ko" class="demo-text demo-keepall">{c.demoText}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="type-rules">
|
||||
{c.rules.map((r) => (<li>{r}</li>))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.type-lead { margin-bottom: var(--space-4); }
|
||||
.type-body { margin-bottom: var(--space-5); }
|
||||
|
||||
.demo {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
.demo-col {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-3);
|
||||
}
|
||||
.demo-label {
|
||||
font-size: var(--mono-size);
|
||||
letter-spacing: var(--tracking-label);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.demo-text {
|
||||
margin: 0;
|
||||
/* 줄바꿈 차이가 드러나도록 폭을 좁게 고정한다 */
|
||||
max-width: 13em;
|
||||
line-height: 1.7;
|
||||
}
|
||||
/* 기본값 — 단어 중간에서 끊긴다 */
|
||||
.demo-default { word-break: break-all; }
|
||||
/* 우리 규칙 */
|
||||
.demo-keepall { word-break: keep-all; overflow-wrap: break-word; }
|
||||
|
||||
.type-rules {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.type-rules li {
|
||||
padding-block: var(--space-2);
|
||||
border-top: 1px solid var(--line);
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
.type-rules li:last-child { border-bottom: 1px solid var(--line); }
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue