- 핵심 규칙 6: 완료의 정의는 게이트 통과. 사용자를 QA로 쓰지 않는다 - 5단계 기계 검사 4번째: design-gate 실행 의무화 - references/audit-gate.md: 사고-검사 매핑, SEO/meta 체크리스트, OS/브라우저 특성, 하니스 규칙 - tools/design-gate.mjs: 범용 게이트(메타/SEO·대비·수축·리듬·트랙·스케일×폭 + 옵션 L0/L3/L4, checks 깊은 병합) - 리듬·트랙 불변식: 숫자 라벨 등폭·빈 셀 트랙 균일·등간격 — 시간표 자동배치 결함 재현 픽스처 검출, 앱 15뷰 통과(오탐 0) feat(site): 관리 앱 2종 프로덕션 콘솔(v6→v18) - 가온 학적부 9뷰·두레 수강신청 6뷰: shadcn 문법, Pretendard/Noto Serif/IBM Plex 폰트 전략, 볼드 금지(400/500/600), WCAG AA 대비 전면 교정, 한글 keep-all 조판 - LMS 필수 요소(알림 센터·공지·진도·평가 유형·출결 사유·학점 경고), 12명 기준 데이터 정합, SEO 구조(h1 유일·OG/twitter·og.png) - 시간표 자동배치 결함 수리(명시적 격자 좌표) - QA 게이트: L0 stylelint/html-validate · L1 단위 30 · L2 감사 61+불변식 · L3 시각회귀 30화면 · L4 WebKit · L5 키보드 탐색 — npm run verify 실패 시 배포 금지 체인
This commit is contained in:
parent
78ddc8b61a
commit
72337b7ee0
226 changed files with 18212 additions and 380 deletions
|
|
@ -5,114 +5,161 @@ import BgPhoto from "../components/BgPhoto.astro";
|
|||
import { WORK } from "../data/work.ts";
|
||||
|
||||
/*
|
||||
쇼케이스. 소개 페이지 안의 섹션이 아니라 **별도 페이지**다.
|
||||
쇼케이스. 카드는 간단한 정보만 싣고(1′ 레퍼런스 tailwindcss.com/showcase:
|
||||
이미지 + 이름 + 한 줄 종류 + 태그), 상세는 네이티브 <dialog> 모달로.
|
||||
showModal() 이 Esc·포커스 트랩·aria를 브라우저가 처리한다 —
|
||||
<details> 메뉴와 같은 결정이다. 사이트 자체는 모달의 큰 CTA로 새 탭.
|
||||
|
||||
홈에 있던 예전 섹션은 스크린샷 두 장을 카드에 넣고 각도를 살짝 준 것이었고,
|
||||
들어갈 곳이 없었다 — 그림만 보여주고 끝나는 자리는 증거가 아니라 장식이다.
|
||||
여기서는 타일을 누르면 **실제로 도는 사이트**로 들어간다.
|
||||
|
||||
타일 크기를 섞는다. 균일한 격자는 이 스킬이 금지하는 형태이고,
|
||||
사례마다 중요도가 다른데 같은 크기로 두면 그 차이가 사라진다.
|
||||
그리드는 균일하지 않다: 첫 사례(목요일의 화원)만 2열 폭으로 features,
|
||||
나머지는 3열. 중요도가 다른데 같은 크기로 두면 그 차이가 사라진다.
|
||||
*/
|
||||
const title = "사례 · designpaca";
|
||||
const description =
|
||||
"designpaca 파이프라인으로 만든 사이트들. 각 사례의 컨셉과 감수한 리스크, 실측값을 함께 적었습니다. 타일을 누르면 실제 사이트가 열립니다.";
|
||||
"designpaca 파이프라인으로 만든 사이트들. 카드를 누르면 컨셉과 리스크, 실측값이 담긴 상세가 열립니다.";
|
||||
const ariaClose = "닫기";
|
||||
const ariaDetail = "자세히 보기";
|
||||
const siteLabel = "사이트 보기";
|
||||
---
|
||||
|
||||
<Base lang="ko" title={title} description={description}>
|
||||
<section class="section intro has-bg" aria-labelledby="sc-h">
|
||||
<BgPhoto variant="align" mode="fixed" opacity={0.85} />
|
||||
<p class="eyebrow reveal">사례</p>
|
||||
<p class="eyebrow reveal">사례 아카이브</p>
|
||||
<h1 id="sc-h" class="reveal">파이프라인을 돌려서 나온 것들</h1>
|
||||
<p class="lead intro-lead reveal">
|
||||
모두 0단계부터 6단계까지 그대로 돌려서 나온 결과물입니다. 각 사례마다
|
||||
<strong>감수한 리스크</strong>를 함께 적었습니다 — 그것이 없으면 안전하지만 잊히는 디자인이 됩니다.
|
||||
</p>
|
||||
<p class="muted intro-note reveal">
|
||||
타일을 누르면 실제 사이트가 열립니다. 목업이 아니라 동작하는 페이지입니다.
|
||||
카드를 누르면 상세 화면이 열리고, 사이트는 새 탭에서 열립니다. 목업이 아니라
|
||||
동작하는 페이지입니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="section grid-section" aria-label="사례 목록">
|
||||
<ul class="full mosaic">
|
||||
<ul class="full cards">
|
||||
{
|
||||
WORK.map((w, i) => (
|
||||
<li class:list={["tile", `tile-${w.size}`, "reveal"]} style={`--reveal-delay:${i * 80}ms`}>
|
||||
<a class="tile-link" href={w.href}>
|
||||
<span class="tile-shot">
|
||||
<li class:list={["card", { featured: w.size === "lg" }, "reveal"]} style={`--reveal-delay:${i * 70}ms`}>
|
||||
<article>
|
||||
<figure class="card-shot">
|
||||
<Image
|
||||
src={w.shot}
|
||||
alt={w.alt}
|
||||
widths={[520, 900, 1440]}
|
||||
sizes="(max-width: 820px) 92vw, 46vw"
|
||||
widths={[520, 900, 1200]}
|
||||
sizes={w.size === "lg" ? "(max-width: 900px) 92vw, 62vw" : "(max-width: 900px) 92vw, 30vw"}
|
||||
loading={i === 0 ? "eager" : "lazy"}
|
||||
/>
|
||||
{w.shotAlt && (
|
||||
<Image
|
||||
class="tile-shot-alt"
|
||||
class="card-shot-alt"
|
||||
src={w.shotAlt}
|
||||
alt=""
|
||||
widths={[520, 900, 1440]}
|
||||
sizes="(max-width: 820px) 92vw, 46vw"
|
||||
widths={[520, 900, 1200]}
|
||||
sizes={w.size === "lg" ? "(max-width: 900px) 92vw, 62vw" : "(max-width: 900px) 92vw, 30vw"}
|
||||
loading="lazy"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
<span class="card-badge mono">{w.preset}</span>
|
||||
</figure>
|
||||
|
||||
<span class="tile-body">
|
||||
<span class="tile-head">
|
||||
<span class="tile-name">{w.name}</span>
|
||||
<span class="tile-kind muted">{w.kind}</span>
|
||||
<div class="card-body">
|
||||
<h2 class="card-name">{w.name}</h2>
|
||||
<p class="card-kind">{w.kind}</p>
|
||||
<p class="card-concept">{w.concept}</p>
|
||||
<span class="card-more" aria-hidden="true">
|
||||
{ariaDetail}
|
||||
<svg viewBox="0 0 12 12" width="10" height="10">
|
||||
<path d="M2.5 9.5 9.5 2.5M4 2.5h5.5V8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" fill="none" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="tile-concept">{w.concept}</span>
|
||||
<span class="tile-risk">
|
||||
<span class="risk-label">감수한 리스크</span>
|
||||
{w.risk}
|
||||
</span>
|
||||
<span class="tile-metrics mono">
|
||||
{w.metrics.map((m) => (
|
||||
<span class="metric">{m}</span>
|
||||
))}
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button type="button" class="card-open" data-dialog={w.slug} aria-label={`${w.name} ${ariaDetail}`} />
|
||||
</article>
|
||||
|
||||
{/* 상세 모달. 네이티브 <dialog>: Esc·포커스·inert 를 브라우저가 처리 */}
|
||||
<dialog id={`wd-${w.slug}`} class="work-dialog" aria-labelledby={`wd-${w.slug}-t`}>
|
||||
<div class="dialog-inner">
|
||||
<button type="button" class="dialog-close" data-close aria-label={ariaClose}>
|
||||
<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
|
||||
<path d="M3.5 3.5l9 9M12.5 3.5l-9 9" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<figure class="dialog-shot">
|
||||
<Image
|
||||
src={w.shot}
|
||||
alt={w.alt}
|
||||
widths={[680, 1080, 1440]}
|
||||
sizes="min(92vw, 720px)"
|
||||
loading="lazy"
|
||||
/>
|
||||
</figure>
|
||||
|
||||
<div class="dialog-body">
|
||||
<p class="dialog-meta mono">{w.kind} · {w.preset}</p>
|
||||
<h2 class="dialog-name" id={`wd-${w.slug}-t`}>{w.name}</h2>
|
||||
<p class="dialog-concept">{w.concept}</p>
|
||||
|
||||
<div class="dialog-risk">
|
||||
<span class="risk-label">감수한 리스크</span>
|
||||
<p>{w.risk}</p>
|
||||
</div>
|
||||
|
||||
<ul class="dialog-metrics mono">
|
||||
{w.metrics.map((m) => (
|
||||
<li>{m}</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<a class="btn btn-solid dialog-site" href={w.href} target="_blank" rel="noopener">
|
||||
{siteLabel}
|
||||
<span class="site-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 12 12" width="11" height="11">
|
||||
<path d="M2.5 9.5 9.5 2.5M4 2.5h5.5V8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none" />
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
|
||||
</section>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
/* .page 그리드는 base.css 가 준다. 여기서 다시 정의하면
|
||||
main 칼럼이 본문 폭(--measure)에서 벗어나 전 페이지가 어긋난다. */
|
||||
.intro {
|
||||
padding-block: calc(var(--header-h) + var(--space-7)) var(--space-7);
|
||||
}
|
||||
/* 목록 페이지의 제목이라 홈 히어로만큼 클 이유가 없다.
|
||||
h1 기본값(--display)은 여기서 세 줄로 접힌다. */
|
||||
#sc-h {
|
||||
font-size: clamp(2rem, 1.3rem + 2.4vw, 3.4rem);
|
||||
max-width: 18ch;
|
||||
text-wrap: balance;
|
||||
word-break: keep-all;
|
||||
}
|
||||
.intro-lead {
|
||||
margin-bottom: var(--space-4);
|
||||
max-width: var(--measure);
|
||||
text-wrap: pretty;
|
||||
word-break: keep-all;
|
||||
}
|
||||
.intro-note {
|
||||
max-width: var(--measure);
|
||||
text-wrap: pretty;
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
.grid-section {
|
||||
padding-block: 0 var(--space-8);
|
||||
}
|
||||
|
||||
/* ── 모자이크 ────────────────────────────────────────
|
||||
타일 크기가 섞인다. 사례마다 무게가 다른데 같은 크기로 두면
|
||||
그 차이가 사라지고, 그때부터는 격자가 내용을 지운다. */
|
||||
.mosaic {
|
||||
/* ── 카드 그리드: 첫 사례만 2열, 나머지 3열 ── */
|
||||
.cards {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
max-width: 1180px;
|
||||
|
|
@ -120,37 +167,33 @@ const description =
|
|||
padding: 0 var(--gutter);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: var(--space-6) var(--space-5);
|
||||
gap: var(--space-7) var(--space-5);
|
||||
}
|
||||
.tile {
|
||||
.card {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.tile-lg {
|
||||
grid-column: span 6;
|
||||
}
|
||||
.tile-md {
|
||||
grid-column: span 3;
|
||||
}
|
||||
.tile-sm {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.tile-link {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
.featured {
|
||||
grid-column: span 6;
|
||||
}
|
||||
/* 큰 타일은 그림과 글을 나란히 둔다. 세로로 쌓으면 화면을 통째로 먹는다 */
|
||||
.tile-lg .tile-link {
|
||||
grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
|
||||
|
||||
.card article {
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
height: 100%;
|
||||
}
|
||||
.featured article {
|
||||
grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
|
||||
gap: var(--space-6);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tile-shot {
|
||||
/* 액자: 유리 한 장이 아니라 사진이 든 판. 겉 테두리(쉘) + 속 이미지(코어) */
|
||||
.card-shot {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-md);
|
||||
|
|
@ -158,99 +201,336 @@ const description =
|
|||
background: var(--surface-2);
|
||||
aspect-ratio: 16 / 10;
|
||||
}
|
||||
.tile-shot :global(img) {
|
||||
.card-shot :global(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: top center;
|
||||
display: block;
|
||||
}
|
||||
/* 두 번째 컷은 위에 겹쳐 두고 호버에서 바꾼다.
|
||||
스크롤이나 자동 전환으로 바꾸지 않는다 — 읽는 사람의 속도를 뺏지 않는다 */
|
||||
.tile-shot-alt {
|
||||
.card-shot-alt {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tile-body {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
align-content: start;
|
||||
}
|
||||
.tile-head {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
.tile-name {
|
||||
font-size: var(--step-1);
|
||||
font-weight: var(--weight-medium);
|
||||
}
|
||||
.tile-kind {
|
||||
font-size: var(--step-0);
|
||||
}
|
||||
.tile-concept {
|
||||
color: var(--ink);
|
||||
max-width: 34em;
|
||||
}
|
||||
.tile-risk {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
padding-top: var(--space-3);
|
||||
border-top: 1px solid var(--line);
|
||||
color: var(--ink-muted);
|
||||
max-width: 38em;
|
||||
}
|
||||
.risk-label {
|
||||
font-size: var(--step-0);
|
||||
letter-spacing: var(--tracking-label);
|
||||
.card-badge {
|
||||
position: absolute;
|
||||
top: var(--space-3);
|
||||
right: var(--space-3);
|
||||
background: rgba(11, 14, 18, 0.85);
|
||||
backdrop-filter: blur(4px);
|
||||
border: 1px solid var(--line);
|
||||
color: var(--accent);
|
||||
font-size: var(--mono-size);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
letter-spacing: var(--tracking-label);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.tile-metrics {
|
||||
.card-body {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
align-content: start;
|
||||
}
|
||||
.card-name {
|
||||
margin: 0;
|
||||
font-size: var(--step-1);
|
||||
font-weight: var(--weight-medium);
|
||||
text-wrap: balance;
|
||||
word-break: keep-all;
|
||||
}
|
||||
.card-kind {
|
||||
margin: 0;
|
||||
color: var(--ink-muted);
|
||||
font-size: var(--step-0);
|
||||
}
|
||||
.card-concept {
|
||||
margin: var(--space-2) 0 0;
|
||||
color: var(--ink-muted);
|
||||
font-size: var(--step-0);
|
||||
line-height: var(--leading-normal);
|
||||
text-wrap: pretty;
|
||||
word-break: keep-all;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
.featured .card-concept {
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
}
|
||||
.card-more {
|
||||
margin-top: var(--space-2);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
font-size: var(--step--1);
|
||||
letter-spacing: var(--tracking-label);
|
||||
color: var(--ink-faint);
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
/* 카드 전체가 하나의 버튼이다 — 시각 요소는 비활성 텍스트로 남고
|
||||
실제 포커스 대상은 이 하나 (키보드 탭 순서 = 카드 순서) */
|
||||
.card-open {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
}
|
||||
.card-open:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
/* ── 호버: 테두리 각성 + 스크린샷 교체 + 자세히 화살 이동 ── */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.card-shot {
|
||||
transition: border-color var(--dur-quick) var(--ease-out),
|
||||
box-shadow var(--dur-quick) var(--ease-out);
|
||||
}
|
||||
.card-shot :global(img) {
|
||||
transition: opacity var(--dur-slow) var(--ease-soft);
|
||||
}
|
||||
.card-more svg {
|
||||
transition: transform var(--dur-quick) var(--ease-out);
|
||||
}
|
||||
article:has(.card-open:hover) .card-shot,
|
||||
article:has(.card-open:focus-visible) .card-shot {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 4px 24px rgba(79, 214, 196, 0.08);
|
||||
}
|
||||
article:has(.card-open:hover) .card-shot-alt,
|
||||
article:has(.card-open:focus-visible) .card-shot-alt {
|
||||
opacity: 1;
|
||||
}
|
||||
article:has(.card-open:hover) .card-more {
|
||||
color: var(--ink);
|
||||
}
|
||||
article:has(.card-open:hover) .card-more svg {
|
||||
transform: translate(2px, -2px);
|
||||
}
|
||||
article:has(.card-open:active) {
|
||||
scale: 0.99;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 상세 모달 ── */
|
||||
.work-dialog {
|
||||
width: min(92vw, 720px);
|
||||
max-height: min(88dvh, 900px);
|
||||
padding: 0;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--radius-frame);
|
||||
/* 모달은 유리가 아니라 판이다 — 메뉴 시트와 같은 교훈: 크롬은 불투명 */
|
||||
background: var(--surface);
|
||||
color: var(--ink);
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.work-dialog::backdrop {
|
||||
background: rgba(4, 6, 9, 0.62);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
}
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.work-dialog[open] {
|
||||
animation: dialog-in 220ms var(--ease-soft) both;
|
||||
}
|
||||
@keyframes dialog-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(14px) scale(0.985);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-inner {
|
||||
position: relative;
|
||||
display: grid;
|
||||
}
|
||||
.dialog-close {
|
||||
position: absolute;
|
||||
top: var(--space-3);
|
||||
right: var(--space-3);
|
||||
z-index: 4;
|
||||
width: var(--control-h);
|
||||
height: var(--control-h);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
/* 밝은 스크린샷 위에 얹힌다 — 반투명은 가독성이 아니라 배경과의 타협이다 */
|
||||
background: var(--surface);
|
||||
color: var(--ink-muted);
|
||||
cursor: pointer;
|
||||
transition: color var(--dur-quick) var(--ease-out),
|
||||
border-color var(--dur-quick) var(--ease-out);
|
||||
}
|
||||
.dialog-close:hover {
|
||||
color: var(--ink);
|
||||
border-color: var(--ink-faint);
|
||||
}
|
||||
.dialog-close:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.dialog-shot {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: var(--surface-2);
|
||||
}
|
||||
.dialog-shot :global(img) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-5) var(--space-5) var(--space-6);
|
||||
}
|
||||
.dialog-meta {
|
||||
margin: 0;
|
||||
color: var(--ink-faint);
|
||||
font-size: var(--mono-size);
|
||||
letter-spacing: var(--tracking-label);
|
||||
}
|
||||
.dialog-name {
|
||||
margin: 0;
|
||||
font-size: var(--step-2);
|
||||
line-height: var(--leading-tight);
|
||||
text-wrap: balance;
|
||||
word-break: keep-all;
|
||||
}
|
||||
.dialog-concept {
|
||||
margin: 0;
|
||||
color: var(--ink-muted);
|
||||
line-height: var(--leading-normal);
|
||||
max-width: 38em;
|
||||
text-wrap: pretty;
|
||||
word-break: keep-all;
|
||||
}
|
||||
.dialog-risk {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
margin-top: var(--space-2);
|
||||
padding-top: var(--space-3);
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.risk-label {
|
||||
font-size: var(--step--1);
|
||||
letter-spacing: var(--tracking-label);
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
.dialog-risk p {
|
||||
margin: 0;
|
||||
color: var(--ink-muted);
|
||||
font-size: var(--step-0);
|
||||
line-height: var(--leading-normal);
|
||||
max-width: 40em;
|
||||
text-wrap: pretty;
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
.dialog-metrics {
|
||||
margin: var(--space-1) 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2) var(--space-4);
|
||||
gap: var(--space-2);
|
||||
font-size: var(--mono-size);
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
.metric + .metric::before {
|
||||
content: "·";
|
||||
margin-right: var(--space-4);
|
||||
color: var(--line-strong);
|
||||
.dialog-metrics li {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 3px 10px;
|
||||
}
|
||||
|
||||
|
||||
/* ── 호버: 그림이 바뀌고 테두리가 선다 ─────────────── */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.tile-shot :global(img) {
|
||||
transition: opacity var(--dur-slow) var(--ease-soft);
|
||||
}
|
||||
.tile-shot {
|
||||
transition: border-color var(--dur-quick) var(--ease-out);
|
||||
}
|
||||
.tile-link:hover .tile-shot,
|
||||
.tile-link:focus-visible .tile-shot {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.tile-link:hover .tile-shot-alt,
|
||||
.tile-link:focus-visible .tile-shot-alt {
|
||||
opacity: 1;
|
||||
}
|
||||
/* 큰 CTA — 버튼 안의 버튼(둥근 화살 칸)은 cmd-copy 아이콘과 같은 어휘 */
|
||||
.dialog-site {
|
||||
margin-top: var(--space-4);
|
||||
justify-self: start;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding-inline: var(--space-4) var(--space-3);
|
||||
font-size: var(--step-1);
|
||||
font-weight: var(--weight-medium);
|
||||
}
|
||||
.site-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: color-mix(in oklab, var(--accent-ink) 18%, transparent);
|
||||
transition: transform var(--dur-quick) var(--ease-out);
|
||||
}
|
||||
.dialog-site:hover .site-icon {
|
||||
transform: translate(2px, -2px);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.mosaic {
|
||||
grid-template-columns: 1fr;
|
||||
.cards {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--space-6) var(--space-4);
|
||||
}
|
||||
.tile-lg,
|
||||
.tile-md,
|
||||
.tile-sm {
|
||||
.card {
|
||||
grid-column: span 1;
|
||||
}
|
||||
.tile-lg .tile-link {
|
||||
.featured {
|
||||
grid-column: span 2;
|
||||
}
|
||||
.featured article {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.featured {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// 모달 열기/닫기. showModal 이 포커스 트랩·Esc·inert 를 처리하므로
|
||||
// 여기서는 여는 것과 배경 탭 닫기만.
|
||||
const openers = document.querySelectorAll<HTMLButtonElement>(".card-open");
|
||||
openers.forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const dialog = document.getElementById(`wd-${btn.dataset.dialog}`);
|
||||
if (dialog instanceof HTMLDialogElement) dialog.showModal();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll<HTMLDialogElement>(".work-dialog").forEach((dialog) => {
|
||||
// 배경(=dialog 자신의 영역)을 누르면 닫는다. 내부 클릭은 target이 다르다
|
||||
dialog.addEventListener("click", (e) => {
|
||||
if (e.target === dialog) dialog.close();
|
||||
});
|
||||
const closeBtn = dialog.querySelector("[data-close]");
|
||||
closeBtn?.addEventListener("click", () => dialog.close());
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue