@qualisoft/ai-skills 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +234 -0
  3. package/bin/cli.mjs +331 -0
  4. package/package.json +53 -0
  5. package/skills/erd-visual/SKILL.md +294 -0
  6. package/skills/erd-visual/build.mjs +404 -0
  7. package/skills/erd-visual/engine/dbml.mjs +133 -0
  8. package/skills/erd-visual/engine/ingest.mjs +273 -0
  9. package/skills/erd-visual/engine/layout.mjs +165 -0
  10. package/skills/erd-visual/engine/overview.mjs +314 -0
  11. package/skills/erd-visual/engine/render.mjs +144 -0
  12. package/skills/erd-visual/engine/router.mjs +401 -0
  13. package/skills/erd-visual/engine/verify.mjs +138 -0
  14. package/skills/erd-visual/engine/wire.mjs +115 -0
  15. package/skills/erd-visual/fixtures/crm-large.dbml +1337 -0
  16. package/skills/erd-visual/fixtures/edge-cases.dbml +43 -0
  17. package/skills/erd-visual/fixtures/map-dynamics.json +32 -0
  18. package/skills/erd-visual/fixtures/shop-basic.dbml +78 -0
  19. package/skills/erd-visual/fixtures/src-json/api.json +38 -0
  20. package/skills/erd-visual/fixtures/src-prisma/schema.prisma +44 -0
  21. package/skills/erd-visual/fixtures/src-sql/shop.sql +62 -0
  22. package/skills/erd-visual/readers/csv.mjs +81 -0
  23. package/skills/erd-visual/readers/index.mjs +55 -0
  24. package/skills/erd-visual/readers/jsonschema.mjs +86 -0
  25. package/skills/erd-visual/readers/prisma.mjs +102 -0
  26. package/skills/erd-visual/readers/sql.mjs +205 -0
  27. package/skills/erd-visual/readers/xlsx.mjs +220 -0
  28. package/skills/erd-visual/readers/xml.mjs +130 -0
  29. package/skills/erd-visual/readers/zip.mjs +74 -0
  30. package/skills/erd-visual/viewer/index.html +539 -0
  31. package/skills/project-build/SKILL.md +171 -0
  32. package/skills/project-build/templates/build-rules.md +88 -0
  33. package/skills/project-build/templates/change-log.md +29 -0
  34. package/skills/project-build/templates/coverage.md +49 -0
  35. package/skills/project-build/templates/parity.html +375 -0
  36. package/skills/project-build/templates/theme.css +45 -0
  37. package/skills/project-design/MEDIUMS.md +104 -0
  38. package/skills/project-design/QUESTIONS.md +126 -0
  39. package/skills/project-design/SKILL.md +365 -0
  40. package/skills/project-design/templates/audit.html +689 -0
  41. package/skills/project-design/templates/change-log.md +53 -0
  42. package/skills/project-design/templates/design-rules.md +118 -0
  43. package/skills/project-design/templates/mockup-app.html +96 -0
  44. package/skills/project-design/templates/mockup-slides.html +82 -0
  45. package/skills/project-design/templates/mockup-web.html +45 -0
  46. package/skills/project-design/templates/styleguide.html +436 -0
  47. package/skills/project-design/templates/tokens.css +69 -0
  48. package/skills/project-design/templates/tone-options.html +138 -0
  49. package/skills/project-init/GUIDE.md +370 -0
  50. package/skills/project-init/RUNBOOK.md +192 -0
  51. package/skills/project-init/SKILL.md +382 -0
  52. package/skills/project-init/build.mjs +2898 -0
  53. package/skills/project-init/evals/RUBRIC.md +81 -0
  54. package/skills/project-init/evals/cases/conflicting.expect.json +17 -0
  55. package/skills/project-init/evals/cases/conflicting.md +9 -0
  56. package/skills/project-init/evals/cases/vague-idea.expect.json +11 -0
  57. package/skills/project-init/evals/cases/vague-idea.md +7 -0
  58. package/skills/project-init/evals/cases/well-formed.expect.json +18 -0
  59. package/skills/project-init/evals/cases/well-formed.md +28 -0
  60. package/skills/project-init/markdown.mjs +0 -0
  61. package/skills/project-init/modules/a11y.json +46 -0
  62. package/skills/project-init/modules/ai.json +77 -0
  63. package/skills/project-init/modules/audience.json +48 -0
  64. package/skills/project-init/modules/backend.json +129 -0
  65. package/skills/project-init/modules/brand.json +78 -0
  66. package/skills/project-init/modules/core.json +132 -0
  67. package/skills/project-init/modules/design.json +72 -0
  68. package/skills/project-init/modules/engineering.json +86 -0
  69. package/skills/project-init/modules/mobile.json +22 -0
  70. package/skills/project-init/modules/ops.json +122 -0
  71. package/skills/project-init/modules/process.json +104 -0
  72. package/skills/project-init/modules/product.json +37 -0
  73. package/skills/project-init/modules/ux.json +52 -0
  74. package/skills/project-init/modules/web.json +129 -0
  75. package/skills/project-init/presets/ai-product.json +12 -0
  76. package/skills/project-init/presets/internal-system.json +15 -0
  77. package/skills/project-init/presets/mobile-app.json +20 -0
  78. package/skills/project-init/presets/web-corporate.json +116 -0
  79. package/skills/project-init/schema.json +80 -0
  80. package/skills/project-init/templates/log.md +54 -0
  81. package/skills/project-init/templates/readme.md +62 -0
  82. package/skills/project-init/templates/reference.md +34 -0
  83. package/skills/project-init/templates/register.md +63 -0
  84. package/skills/project-init/templates/spec.md +46 -0
  85. package/skills/project-interview/INTERVIEW.md +221 -0
  86. package/skills/project-interview/SKILL.md +156 -0
  87. package/skills/project-interview/fixtures/brief.md +49 -0
  88. package/skills/project-interview/fixtures/decisions.md +13 -0
  89. package/skills/project-interview/fixtures/open-questions.md +9 -0
  90. package/skills/project-interview/templates/brief.md +179 -0
  91. package/skills/project-interview/templates/decisions.md +55 -0
  92. package/skills/project-interview/templates/open-questions.md +40 -0
  93. package/skills/project-interview/validate.mjs +49 -0
@@ -0,0 +1,436 @@
1
+ <!doctype html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>스타일 가이드 — {{프로젝트명}}</title>
7
+ <!--
8
+ 살아있는 스타일 가이드. Docs/_design/styleguide.html
9
+
10
+ 값을 여기에 적지 않는다. tokens.css 를 읽어 **실제로 렌더**한다.
11
+ 그래서 토큰을 고치면 이 페이지가 자동으로 따라오고, 문서와 어긋날 수 없다.
12
+
13
+ **목록을 손으로 관리하지 않는다.** tokens.css 의 :root 에 선언된 커스텀 속성을
14
+ 전부 읽어 값의 생김새로 분류해 렌더한다. 토큰을 추가하면 여기에 저절로 나타난다.
15
+
16
+ 예전에는 이 파일에 이름 목록이 박혀 있었고 "없는 토큰은 조용히 건너뛴다"고
17
+ 적혀 있었다. 조용히 건너뛰면 조용히 낡는다 — 실제로 새로 만든 토큰 몇 개가
18
+ 스타일 가이드에 없는 채로 여러 회차가 지나갔다.
19
+ -->
20
+ <link rel="stylesheet" href="tokens.css">
21
+ <style>
22
+ body{background:var(--bg-subtle); padding:0 0 100px}
23
+ .wrap{max-width:1080px; margin:0 auto; padding:0 28px}
24
+ header.sg{padding:56px 0 32px; border-bottom:1px solid var(--line); margin-bottom:44px}
25
+ header.sg h1{font-size:40px; margin:0 0 8px}
26
+ header.sg p{margin:0; color:var(--ink-muted); font-size:15px}
27
+
28
+ section.sg{margin-bottom:56px}
29
+ section.sg > h2{font-size:22px; margin:0 0 6px; letter-spacing:-.01em}
30
+ section.sg > .hint{margin:0 0 20px; color:var(--ink-muted); font-size:13.5px; line-height:1.6}
31
+
32
+ .grid{display:grid; gap:12px}
33
+ .g-color{grid-template-columns:repeat(auto-fill,minmax(212px,1fr))}
34
+ .card{background:var(--bg); border:1px solid var(--line); border-radius:10px; overflow:hidden}
35
+
36
+ /* 색 */
37
+ .chip{height:78px; border-bottom:1px solid var(--line)}
38
+ .card-b{padding:11px 13px}
39
+ .name{font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px; color:var(--ink)}
40
+ .val{font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px; color:var(--ink-muted);
41
+ margin-top:2px; text-transform:uppercase}
42
+ .ratios{display:flex; gap:6px; margin-top:9px; flex-wrap:wrap}
43
+ .ratio{display:flex; align-items:center; gap:4px; font-size:11px;
44
+ padding:2px 7px; border-radius:4px; background:var(--bg-subtle); color:var(--ink-secondary)}
45
+ .ok{background:#e6f4ea; color:#0f7b6c}
46
+ .warn{background:#fdecea; color:#b02f1c}
47
+
48
+ /* 타이포 */
49
+ .type-row{display:flex; align-items:baseline; gap:20px; padding:16px 0;
50
+ border-bottom:1px solid var(--line)}
51
+ .type-meta{flex:none; width:200px; font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
52
+ font-size:11.5px; color:var(--ink-muted); line-height:1.5}
53
+ .type-meta b{display:block; color:var(--ink); font-size:12px; font-weight:600}
54
+ .type-sample{flex:1; min-width:0; color:var(--ink); overflow:hidden; text-overflow:ellipsis}
55
+
56
+ /* 간격 */
57
+ .space-row{display:flex; align-items:center; gap:16px; padding:9px 0}
58
+ .space-meta{flex:none; width:200px; font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
59
+ font-size:11.5px; color:var(--ink-muted)}
60
+ .space-meta b{color:var(--ink)}
61
+ .bar{height:14px; border-radius:3px; background:var(--accent); opacity:.75}
62
+
63
+ /* 형태 */
64
+ .g-shape{grid-template-columns:repeat(auto-fill,minmax(150px,1fr))}
65
+ .shape-box{height:76px; background:var(--bg); border:1px solid var(--line-strong); margin-bottom:9px}
66
+
67
+ /* 상태 */
68
+ .states{display:flex; gap:10px; flex-wrap:wrap; align-items:center}
69
+ .btn-disabled{opacity:.45; pointer-events:none}
70
+ .btn-focus{outline:2px solid var(--accent); outline-offset:2px}
71
+ .btn-loading::before{content:""; width:13px; height:13px; margin-right:8px; border-radius:50%;
72
+ border:2px solid currentColor; border-top-color:transparent; animation:spin .7s linear infinite}
73
+ @keyframes spin{to{transform:rotate(360deg)}}
74
+ .state-label{font-size:11.5px; color:var(--ink-muted); text-align:center; margin-top:5px}
75
+ .state-cell{display:flex; flex-direction:column; align-items:center}
76
+
77
+ /* 해도 되는 것 / 안 되는 것 */
78
+ .g-do{grid-template-columns:1fr 1fr; gap:16px}
79
+ .do,.dont{padding:18px; border-radius:10px; border:1px solid var(--line); background:var(--bg)}
80
+ .do{border-left:3px solid #0f7b6c}
81
+ .dont{border-left:3px solid #b02f1c}
82
+ .do h4,.dont h4{margin:0 0 10px; font-size:13px; letter-spacing:.02em}
83
+ .do h4{color:#0f7b6c} .dont h4{color:#b02f1c}
84
+ .do ul,.dont ul{margin:0; padding-left:18px; font-size:13.5px; line-height:1.7; color:var(--ink-secondary)}
85
+
86
+ .empty-note{color:var(--ink-muted); font-size:13.5px; padding:14px 0}
87
+
88
+ /* 상태 화면 */
89
+ .g-state{grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:14px}
90
+ .state-card{border:1px solid var(--line); border-radius:12px; background:var(--bg); overflow:hidden}
91
+ .state-card > h4{margin:0; padding:10px 14px; border-bottom:1px solid var(--line);
92
+ font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:11.5px; color:var(--ink-muted);
93
+ letter-spacing:.04em; font-weight:600}
94
+ .state-body{padding:22px 16px; min-height:132px; display:flex; flex-direction:column;
95
+ align-items:center; justify-content:center; text-align:center; gap:9px}
96
+ .state-body p{margin:0; font-size:13px; color:var(--ink-secondary); line-height:1.6; max-width:30ch}
97
+ .state-body strong{font-size:14.5px; letter-spacing:-.01em}
98
+ .state-icon{width:30px; height:30px; border-radius:50%; display:grid; place-items:center;
99
+ border:1px solid var(--line-strong); color:var(--ink-muted); font-size:14px}
100
+ .state-msg{display:flex; align-items:center; gap:8px; width:100%; padding:10px 12px;
101
+ border-radius:9px; font-size:12.5px; text-align:left; line-height:1.5}
102
+ .state-msg.ok{background:#e6f4ea; color:#0f7b6c}
103
+ .state-msg.err{background:#fdecea; color:#b02f1c}
104
+ .spin{width:15px; height:15px; border-radius:50%; border:2px solid var(--line-strong);
105
+ border-top-color:var(--ink); animation:spin .7s linear infinite}
106
+ .field{width:100%; padding:9px 11px; border:1px solid var(--line); border-radius:8px;
107
+ font-size:12.5px; color:var(--ink-muted); text-align:left}
108
+ .field.bad{border-color:#b02f1c}
109
+ @media (max-width:720px){
110
+ .type-meta,.space-meta{width:118px}
111
+ .g-do{grid-template-columns:1fr}
112
+ }
113
+ </style>
114
+ </head>
115
+ <body>
116
+
117
+ <div class="wrap">
118
+ <header class="sg">
119
+ <h1>스타일 가이드</h1>
120
+ <p>{{프로젝트명}} · <code>tokens.css</code> 에 선언된 토큰 <b id="token-count">…</b>개를 전부 읽어 실제로 렌더한 값입니다.
121
+ 목록을 손으로 관리하지 않으므로 <b>빠뜨릴 수 없습니다.</b></p>
122
+ </header>
123
+
124
+ <section class="sg">
125
+ <h2>색</h2>
126
+ <p class="hint">배경색 대비 명도비를 자동 계산합니다. <b>AA</b> 는 본문 4.5:1, <b>AA Large</b> 는 큰 글자 3:1 기준입니다.
127
+ 통과하지 못한 색은 텍스트에 쓰지 않습니다.</p>
128
+ <div class="grid g-color" id="colors"></div>
129
+ </section>
130
+
131
+ <section class="sg">
132
+ <h2>타이포그래피</h2>
133
+ <p class="hint">현재 창 크기에서 실제로 계산된 픽셀 값입니다. <code>clamp()</code> 를 쓰면 창을 줄였다 늘려 보세요.</p>
134
+ <div id="type"></div>
135
+ </section>
136
+
137
+ <section class="sg">
138
+ <h2>간격</h2>
139
+ <p class="hint">막대 길이가 실제 값입니다.</p>
140
+ <div id="space"></div>
141
+ </section>
142
+
143
+ <section class="sg">
144
+ <h2>형태</h2>
145
+ <div class="grid g-shape" id="shape"></div>
146
+ </section>
147
+
148
+ <section class="sg">
149
+ <h2>그림자</h2>
150
+ <p class="hint">쓰기로 한 곳에만 씁니다. 섹션·카드 구분은 선과 배경 차이로 하세요.</p>
151
+ <div class="grid g-shape" id="shadow"></div>
152
+ </section>
153
+
154
+ <section class="sg">
155
+ <h2>서체</h2>
156
+ <div id="font"></div>
157
+ </section>
158
+
159
+ <section class="sg">
160
+ <h2>기타 토큰</h2>
161
+ <p class="hint">위 분류에 들어가지 않은 값입니다. 안 보이면 낡으므로 여기에 전부 나옵니다.</p>
162
+ <div id="other"></div>
163
+ </section>
164
+
165
+ <section class="sg">
166
+ <h2>버튼 상태</h2>
167
+ <p class="hint">인터랙션 문서가 요구하는 8개 상태입니다. 하나라도 빠지면 미완성입니다.</p>
168
+ <div class="states">
169
+ <div class="state-cell"><a class="btn btn-primary" href="#">기본</a><span class="state-label">Default</span></div>
170
+ <div class="state-cell"><a class="btn btn-primary" href="#" style="background:var(--brand-hover)">호버</a><span class="state-label">Hover</span></div>
171
+ <div class="state-cell"><a class="btn btn-primary btn-focus" href="#">포커스</a><span class="state-label">Focus</span></div>
172
+ <div class="state-cell"><a class="btn btn-primary" href="#" style="filter:brightness(.9)">눌림</a><span class="state-label">Active</span></div>
173
+ <div class="state-cell"><a class="btn btn-primary btn-disabled" href="#" data-state="disabled">비활성</a><span class="state-label">Disabled</span></div>
174
+ <div class="state-cell"><a class="btn btn-primary btn-loading" href="#" data-state="loading">처리 중</a><span class="state-label">Loading</span></div>
175
+ <div class="state-cell"><a class="btn btn-primary" href="#" style="background:#0f7b6c" data-state="success">완료</a><span class="state-label">Success</span></div>
176
+ <div class="state-cell"><a class="btn btn-primary" href="#" style="background:#b02f1c" data-state="error">오류</a><span class="state-label">Error</span></div>
177
+ </div>
178
+ <div class="states" style="margin-top:16px">
179
+ <div class="state-cell"><a class="btn btn-secondary" href="#">보조 버튼</a><span class="state-label">Secondary</span></div>
180
+ </div>
181
+ </section>
182
+
183
+ <section class="sg">
184
+ <h2>상태 화면</h2>
185
+ <p class="hint">화면 정의서가 정의한 상태입니다. 버튼 8상태와 별개로 <b>화면 단위</b>로 그려야
186
+ 구현 단계에서 각자 지어내지 않습니다. 각 블록에 <code>data-state="이름"</code>을 붙이면
187
+ <code>build.mjs check</code>가 문서와 대조합니다.</p>
188
+ <div class="grid g-state">
189
+
190
+ <div class="state-card" data-state="empty">
191
+ <h4>EMPTY — 목록이 비었을 때</h4>
192
+ <div class="state-body">
193
+ <span class="state-icon">—</span>
194
+ <strong>{{빈 상태 제목}}</strong>
195
+ <p>{{빈 상태 안내 — 다음에 할 수 있는 행동을 준다}}</p>
196
+ <a class="btn btn-secondary" href="#" style="height:36px;min-height:36px;font-size:.82rem">{{다음 행동}}</a>
197
+ </div>
198
+ </div>
199
+
200
+ <div class="state-card" data-state="idle">
201
+ <h4>IDLE — 문의 폼 기본</h4>
202
+ <div class="state-body" style="align-items:stretch">
203
+ <div class="field">{{입력 항목 1}}</div>
204
+ <div class="field">{{입력 항목 2}}</div>
205
+ <a class="btn btn-primary" href="#" style="height:38px;min-height:38px;font-size:.85rem">보내기</a>
206
+ </div>
207
+ </div>
208
+
209
+ <div class="state-card" data-state="loading">
210
+ <h4>SUBMITTING — 전송 중</h4>
211
+ <div class="state-body">
212
+ <span class="spin"></span>
213
+ <p>보내는 중입니다. 중복 제출은 막힙니다.</p>
214
+ </div>
215
+ </div>
216
+
217
+ <div class="state-card" data-state="success">
218
+ <h4>SUCCESS — 전송 완료</h4>
219
+ <div class="state-body" style="align-items:stretch">
220
+ <div class="state-msg ok">{{성공 메시지}}</div>
221
+ <p style="text-align:left">서버 성공 응답 뒤에만 이 상태로 바꿉니다.</p>
222
+ </div>
223
+ </div>
224
+
225
+ <div class="state-card" data-state="error">
226
+ <h4>ERROR — 전송 실패</h4>
227
+ <div class="state-body" style="align-items:stretch">
228
+ <div class="field bad">{{필드 검증 메시지}}</div>
229
+ <div class="state-msg err">{{전송 실패 메시지 — 입력은 보존한다}}</div>
230
+ <p style="text-align:left">입력을 보존하고 재시도 · 메일 대체 경로를 함께 제공합니다.</p>
231
+ </div>
232
+ </div>
233
+
234
+ <div class="state-card" data-state="disabled">
235
+ <h4>DISABLED — 필수 입력 전</h4>
236
+ <div class="state-body">
237
+ <a class="btn btn-primary btn-disabled" href="#" style="height:38px;min-height:38px;font-size:.85rem">보내기</a>
238
+ <p>왜 눌리지 않는지 폼 안에서 함께 알립니다.</p>
239
+ </div>
240
+ </div>
241
+
242
+ </div>
243
+ </section>
244
+
245
+ <section class="sg">
246
+ <h2>해도 되는 것 / 안 되는 것</h2>
247
+ <p class="hint">2단계 디자인 시스템의 안티패턴을 여기에 옮겨 적습니다. 시안이 이 목록을 어기면 안 됩니다.</p>
248
+ <div class="grid g-do">
249
+ <div class="do">
250
+ <h4>이렇게</h4>
251
+ <ul>
252
+ <li>{{지향 1}}</li>
253
+ <li>{{지향 2}}</li>
254
+ </ul>
255
+ </div>
256
+ <div class="dont">
257
+ <h4>이렇게는 안 됨</h4>
258
+ <ul>
259
+ <li>{{안티패턴 1}}</li>
260
+ <li>{{안티패턴 2}}</li>
261
+ </ul>
262
+ </div>
263
+ </div>
264
+ </section>
265
+ </div>
266
+
267
+ <div class="mockup-flag">스타일 가이드 · {{YYYY-MM-DD}}</div>
268
+
269
+ <script>
270
+ (function(){
271
+ var root = getComputedStyle(document.documentElement);
272
+ var get = function(n){ return root.getPropertyValue(n).trim(); };
273
+
274
+ /* ── 토큰 자동 수집 ─────────────────────────────────────
275
+ 선언된 것을 전부 읽는다. 손으로 관리하는 목록이 없으므로 빠뜨릴 수 없다. */
276
+ function allTokens(){
277
+ var names = [];
278
+ try{
279
+ for(var i = 0; i < root.length; i++){
280
+ var n = root[i];
281
+ if(n && n.indexOf('--') === 0) names.push(n);
282
+ }
283
+ }catch(e){}
284
+ return names;
285
+ }
286
+
287
+ /* 값의 생김새로 분류한다. 이름 규칙에만 기대지 않는다. */
288
+ function classify(name, value){
289
+ if(/^--shadow/.test(name)) return 'shadow';
290
+ if(/^--font/.test(name) || /(sans-serif|serif|monospace)\s*$/.test(value)) return 'font';
291
+ if(/^--ease/.test(name) || /cubic-bezier|steps\(/.test(value)) return 'other';
292
+ if(/gradient\(/.test(value)) return 'other';
293
+ if(toRgb(value)) return 'color';
294
+ if(/^--text/.test(name)) return 'text';
295
+ if(/^--radius/.test(name)) return 'shape';
296
+ if(/^[.\d]|clamp\(|calc\(|min\(|max\(/.test(value)) return 'space';
297
+ return 'other';
298
+ }
299
+
300
+ var TOKENS = { color:[], text:[], space:[], shape:[], shadow:[], font:[], other:[] };
301
+
302
+ // ── 색: 명도비 계산 (WCAG) ───────────────────────────────
303
+ function toRgb(c){
304
+ var d = document.createElement('div');
305
+ // 유효하지 않은 값은 style.color 대입이 무시된다. 그걸 확인하지 않으면
306
+ // clamp()·길이값까지 '상속된 검정'으로 읽혀 전부 색으로 분류된다.
307
+ d.style.color = '';
308
+ d.style.color = c;
309
+ if(!d.style.color) return null;
310
+ document.body.appendChild(d);
311
+ var m = getComputedStyle(d).color.match(/[\d.]+/g);
312
+ d.remove();
313
+ return m ? m.slice(0,3).map(Number) : null;
314
+ }
315
+ function lum(rgb){
316
+ var a = rgb.map(function(v){
317
+ v /= 255;
318
+ return v <= 0.03928 ? v/12.92 : Math.pow((v+0.055)/1.055, 2.4);
319
+ });
320
+ return 0.2126*a[0] + 0.7152*a[1] + 0.0722*a[2];
321
+ }
322
+ function ratio(a, b){
323
+ var l1 = lum(a), l2 = lum(b);
324
+ return (Math.max(l1,l2) + 0.05) / (Math.min(l1,l2) + 0.05);
325
+ }
326
+ function badge(r, label){
327
+ var pass = r >= 4.5, large = r >= 3;
328
+ var cls = pass ? 'ok' : (large ? '' : 'warn');
329
+ var tag = pass ? 'AA' : (large ? 'AA Large' : '미달');
330
+ return '<span class="ratio ' + cls + '">' + label + ' ' + r.toFixed(2) + ':1 · ' + tag + '</span>';
331
+ }
332
+
333
+ allTokens().forEach(function(n){
334
+ var v = get(n);
335
+ if(!v) return;
336
+ var k = classify(n, v);
337
+ (TOKENS[k] || TOKENS.other).push(n);
338
+ });
339
+
340
+ var bgRgb = toRgb(get('--bg') || '#ffffff');
341
+ var inkRgb = toRgb(get('--ink') || '#000000');
342
+ var out = '';
343
+ TOKENS.color.forEach(function(n){
344
+ var v = get(n);
345
+ if(!v) return;
346
+ var rgb = toRgb(v);
347
+ if(!rgb) return;
348
+ out += '<div class="card"><div class="chip" style="background:' + v + '"></div>' +
349
+ '<div class="card-b"><div class="name">' + n + '</div>' +
350
+ '<div class="val">' + v + '</div><div class="ratios">' +
351
+ badge(ratio(rgb, bgRgb), '배경') + badge(ratio(rgb, inkRgb), '본문색') +
352
+ '</div></div></div>';
353
+ });
354
+ document.getElementById('colors').innerHTML =
355
+ out || '<p class="empty-note">색 토큰을 찾지 못했습니다. tokens.css 를 확인하세요.</p>';
356
+
357
+ // ── 타이포: 실제 계산된 px 를 읽는다 ─────────────────────
358
+ var SAMPLE = '다시 봐도 좋은 문장 Aa Bb 0123';
359
+ var t = '';
360
+ TOKENS.text.forEach(function(n){
361
+ var v = get(n);
362
+ if(!v) return;
363
+ t += '<div class="type-row"><div class="type-meta"><b>' + n + '</b>' +
364
+ '<span data-px="' + n + '">…</span><br>' + v + '</div>' +
365
+ '<div class="type-sample" style="font-size:var(' + n + ')">' + SAMPLE + '</div></div>';
366
+ });
367
+ t += '<div class="type-row"><div class="type-meta"><b>본문</b><span>16px</span></div>' +
368
+ '<div class="type-sample" style="font-size:16px">' + SAMPLE + '</div></div>';
369
+ document.getElementById('type').innerHTML =
370
+ t || '<p class="empty-note">타이포 토큰을 찾지 못했습니다.</p>';
371
+
372
+ function refreshPx(){
373
+ document.querySelectorAll('.type-row').forEach(function(row){
374
+ var el = row.querySelector('[data-px]');
375
+ var s = row.querySelector('.type-sample');
376
+ if(el && s) el.textContent = Math.round(parseFloat(getComputedStyle(s).fontSize)) + 'px';
377
+ });
378
+ }
379
+ refreshPx();
380
+ window.addEventListener('resize', refreshPx);
381
+
382
+ // ── 간격 ────────────────────────────────────────────────
383
+ var sp = '';
384
+ TOKENS.space.forEach(function(n){
385
+ var v = get(n);
386
+ if(!v) return;
387
+ sp += '<div class="space-row"><div class="space-meta"><b>' + n + '</b><br>' + v + '</div>' +
388
+ '<div class="bar" style="width:min(var(' + n + '), 100%)"></div></div>';
389
+ });
390
+ document.getElementById('space').innerHTML =
391
+ sp || '<p class="empty-note">간격 토큰을 찾지 못했습니다.</p>';
392
+
393
+ // ── 형태 ────────────────────────────────────────────────
394
+ var sh = '';
395
+ TOKENS.shape.forEach(function(n){
396
+ var v = get(n);
397
+ if(!v) return;
398
+ sh += '<div><div class="shape-box" style="border-radius:var(' + n + ')"></div>' +
399
+ '<div class="name">' + n + '</div><div class="val">' + v + '</div></div>';
400
+ });
401
+ document.getElementById('shape').innerHTML =
402
+ sh || '<p class="empty-note">형태 토큰을 찾지 못했습니다.</p>';
403
+
404
+ function renderInto(id, list, fn){
405
+ var el = document.getElementById(id);
406
+ if(!el) return;
407
+ var section = el.closest('section.sg');
408
+ if(!list.length){ if(section) section.style.display = 'none'; return; }
409
+ el.innerHTML = list.map(function(n){ return fn(n, get(n)); }).join('');
410
+ }
411
+
412
+ // ── 그림자 ──────────────────────────────────────────────
413
+ renderInto('shadow', TOKENS.shadow, function(n, v){
414
+ return '<div><div class="shape-box" style="border:0;border-radius:14px;box-shadow:var(' + n + ')"></div>' +
415
+ '<div class="name">' + n + '</div><div class="val" style="text-transform:none">' + v + '</div></div>';
416
+ });
417
+
418
+ // ── 서체 ────────────────────────────────────────────────
419
+ renderInto('font', TOKENS.font, function(n, v){
420
+ return '<div class="type-row"><div class="type-meta"><b>' + n + '</b><br>' + v.split(',')[0] + '</div>' +
421
+ '<div class="type-sample" style="font-family:var(' + n + ')">' + SAMPLE + '</div></div>';
422
+ });
423
+
424
+ // ── 기타 — 분류되지 않은 토큰도 반드시 보여준다. 안 보이는 토큰이 낡는다.
425
+ renderInto('other', TOKENS.other, function(n, v){
426
+ return '<div class="space-row"><div class="space-meta"><b>' + n + '</b></div>' +
427
+ '<div class="val" style="text-transform:none;flex:1;min-width:0;word-break:break-all">' + v + '</div></div>';
428
+ });
429
+
430
+ var total = Object.keys(TOKENS).reduce(function(a,k){ return a + TOKENS[k].length; }, 0);
431
+ var cnt = document.getElementById('token-count');
432
+ if(cnt) cnt.textContent = total;
433
+ })();
434
+ </script>
435
+ </body>
436
+ </html>
@@ -0,0 +1,69 @@
1
+ /* ──────────────────────────────────────────────────────────────
2
+ 디자인 토큰 — 단일 출처
3
+ Docs/_design/tokens.css
4
+
5
+ 모든 시안이 이 파일을 링크한다. 값을 바꾸면 시안 전체가 함께 바뀐다.
6
+ 시안마다 값을 복사해 두면 반드시 어긋난다.
7
+
8
+ 값은 design-rules.md 3항과 일치해야 한다.
9
+ 바꿀 때는 change-log.md 에 DS- 항목을 남긴다.
10
+ ────────────────────────────────────────────────────────────── */
11
+ :root{
12
+ /* 색 */
13
+ --bg:#ffffff;
14
+ --bg-subtle:#fafbfc;
15
+ --ink:#0e1420;
16
+ --ink-secondary:#45505f;
17
+ --ink-muted:#6b7684;
18
+ --brand:#12447e;
19
+ --brand-hover:#0e3765;
20
+ --accent:#00a3e0;
21
+ --line:#e4e7ec;
22
+
23
+ /* 서체 */
24
+ --font: ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",
25
+ "Pretendard Variable",Pretendard,"Apple SD Gothic Neo","Noto Sans KR",sans-serif;
26
+
27
+ /* 크기 — 웹 기준. 슬라이드·인쇄는 각 시안에서 덮어쓴다 */
28
+ --text-display: clamp(2.5rem, 1.4rem + 4vw, 4rem);
29
+ --text-h2: clamp(1.5rem, 1.1rem + 1.6vw, 2.25rem);
30
+ --text-lead: clamp(1rem, .96rem + .3vw, 1.19rem);
31
+
32
+ /* 간격 · 형태 */
33
+ --container: 82.5rem;
34
+ --section-y: clamp(4rem, 8vw, 7.5rem);
35
+ --radius: 6px;
36
+ --tap: 44px; /* 최소 터치 타깃 */
37
+ }
38
+
39
+ *,*::before,*::after{box-sizing:border-box}
40
+ body{
41
+ margin:0; background:var(--bg); color:var(--ink);
42
+ font-family:var(--font); font-size:16px; line-height:1.7;
43
+ word-break:keep-all; -webkit-font-smoothing:antialiased;
44
+ }
45
+ .container{max-width:var(--container); margin:0 auto; padding:0 24px}
46
+ @media (min-width:768px){ .container{padding:0 40px} }
47
+
48
+ .eyebrow{font-size:.8rem; font-weight:600; letter-spacing:.09em;
49
+ text-transform:uppercase; color:var(--accent); margin:0 0 14px}
50
+ h1{font-size:var(--text-display); line-height:1.1; letter-spacing:-.03em; font-weight:700; margin:0 0 20px}
51
+ h2{font-size:var(--text-h2); line-height:1.25; letter-spacing:-.02em; font-weight:600; margin:0 0 16px}
52
+ .lead{font-size:var(--text-lead); color:var(--ink-secondary); max-width:60ch; margin:0 0 32px}
53
+
54
+ .btn{display:inline-flex; align-items:center; justify-content:center;
55
+ min-height:var(--tap); padding:0 26px; border-radius:var(--radius);
56
+ font-size:.95rem; font-weight:600; text-decoration:none; border:1px solid transparent;
57
+ transition:background .15s, border-color .15s}
58
+ .btn-primary{background:var(--brand); color:#fff}
59
+ .btn-primary:hover{background:var(--brand-hover)}
60
+ .btn-secondary{background:transparent; color:var(--ink); border-color:var(--line)}
61
+ .btn-secondary:hover{border-color:var(--ink-muted)}
62
+ .btn + .btn{margin-left:10px}
63
+
64
+ /* 시안임을 화면에 명시한다 — 실제 서비스로 오인되면 안 된다 */
65
+ .mockup-flag{position:fixed; right:12px; bottom:12px; z-index:99;
66
+ padding:6px 12px; border-radius:999px;
67
+ background:var(--ink); color:#fff; font-size:12px; letter-spacing:.02em}
68
+
69
+ @media (prefers-reduced-motion:reduce){ *{transition-duration:.01ms !important} }
@@ -0,0 +1,138 @@
1
+ <!doctype html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>톤 방향 — 고르는 화면</title>
7
+ <!--
8
+ 톤 관문. Docs/_design/tone-options.html
9
+
10
+ ── 왜 이게 있는가 ──
11
+ 완성 시안을 만든 뒤에 "배경이 좀…" 소리를 들으면 전부 다시 만들어야 한다.
12
+ 실제로 Hero 배경 하나 때문에 다섯 판을 갈아엎은 적이 있다.
13
+ 흰색으로 갔다가 → 회색으로 되돌렸다가 → 검정으로 갔다. 헛걸음이 두 번이다.
14
+
15
+ 그래서 **시안보다 먼저** 이 화면을 만든다.
16
+ 레이아웃·카피·구조는 전부 같고 **톤만 다르다.** 고를 것이 색 하나뿐이라
17
+ 사용자가 5초 만에 답할 수 있다.
18
+
19
+ ── 쓰는 법 ──
20
+ 1. 아래 .opt 블록의 변수만 바꿔 2~4개를 만든다. 마크업은 건드리지 않는다.
21
+ 2. 사용자에게 보여주고 하나를 고르게 한다.
22
+ 3. 고른 값을 tokens.css 로 옮기고 change-log 에 DS 항목으로 남긴다.
23
+ 4. 그 다음에 시안을 만든다.
24
+
25
+ 고르지 못하겠다고 하면 그것도 정보다. "무엇이 걸리는지"를 물어 후보를 좁힌다.
26
+ -->
27
+ <style>
28
+ :root{
29
+ --ui-bg:#f7f7f5; --ui-ink:#111; --ui-muted:#6b6b68; --ui-line:#dededb;
30
+ --font: -apple-system,BlinkMacSystemFont,"Pretendard Variable",Pretendard,"Segoe UI","Apple SD Gothic Neo",sans-serif;
31
+ }
32
+ *,*::before,*::after{box-sizing:border-box}
33
+ body{margin:0;background:var(--ui-bg);color:var(--ui-ink);font:15px/1.6 var(--font);word-break:keep-all}
34
+ .wrap{max-width:1240px;margin:0 auto;padding:36px 24px 100px}
35
+ h1{margin:0 0 6px;font-size:26px;letter-spacing:-.02em}
36
+ .sub{margin:0 0 8px;color:var(--ui-muted);font-size:14px}
37
+ .ask{margin:0 0 30px;padding:14px 16px;border-left:3px solid var(--ui-ink);background:#fff;
38
+ border-radius:0 10px 10px 0;font-size:14px;line-height:1.7}
39
+ .ask b{font-weight:650}
40
+
41
+ .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:20px}
42
+
43
+ /* 후보 카드 — 이 안의 변수만 바꾼다 */
44
+ .opt{border:1px solid var(--ui-line);border-radius:16px;background:#fff;overflow:hidden}
45
+ .opt-head{display:flex;align-items:center;gap:10px;padding:13px 16px;border-bottom:1px solid var(--ui-line)}
46
+ .opt-head b{font-size:14px;letter-spacing:-.01em}
47
+ .opt-head .tag{margin-left:auto;padding:2px 9px;border-radius:999px;background:var(--ui-bg);
48
+ font-size:11px;color:var(--ui-muted)}
49
+ .chips{display:flex;gap:5px;padding:11px 16px;border-bottom:1px solid var(--ui-line);flex-wrap:wrap}
50
+ .chips i{display:flex;align-items:center;gap:5px;font-size:10.5px;color:var(--ui-muted);font-style:normal}
51
+ .chips i::before{content:"";width:13px;height:13px;border-radius:4px;border:1px solid rgba(0,0,0,.12)}
52
+
53
+ /* 미리보기 — 모든 후보가 똑같은 구조를 쓴다. 다른 건 색뿐이다. */
54
+ .pv{padding:26px 22px 22px;background:var(--t-bg);color:var(--t-ink);min-height:270px}
55
+ .pv .eb{display:inline-block;margin:0 0 14px;padding:5px 11px;border:1px solid var(--t-line);
56
+ border-radius:999px;font-size:10.5px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--t-muted)}
57
+ .pv h2{margin:0 0 10px;font-size:26px;line-height:1.15;letter-spacing:-.03em;font-weight:640}
58
+ .pv p{margin:0 0 18px;font-size:13px;line-height:1.6;color:var(--t-muted)}
59
+ .pv .cta{display:inline-flex;align-items:center;height:34px;padding:0 16px;border-radius:999px;
60
+ background:var(--t-cta);color:var(--t-cta-ink);font-size:12.5px;font-weight:600}
61
+ .pv .card{margin-top:20px;padding:13px 14px;border:1px solid var(--t-line);border-radius:12px;background:var(--t-surface)}
62
+ .pv .card b{display:block;font-size:12px;margin-bottom:7px}
63
+ .pv .bar{height:5px;border-radius:3px;background:var(--t-line);margin-bottom:5px}
64
+
65
+ /* ── 후보 1 ── */
66
+ .opt-1 .pv{--t-bg:#ffffff;--t-ink:#111111;--t-muted:#5a5a58;--t-line:#e2e2df;
67
+ --t-surface:#f7f7f5;--t-cta:#111111;--t-cta-ink:#ffffff}
68
+ /* ── 후보 2 ── */
69
+ .opt-2 .pv{--t-bg:#f4f4f1;--t-ink:#161615;--t-muted:#5c5c58;--t-line:#dcdcd7;
70
+ --t-surface:#ffffff;--t-cta:#161615;--t-cta-ink:#ffffff}
71
+ /* ── 후보 3 ── */
72
+ .opt-3 .pv{--t-bg:#111111;--t-ink:#ffffff;--t-muted:#adadaa;--t-line:#2e2e2c;
73
+ --t-surface:#1b1b1a;--t-cta:#ffffff;--t-cta-ink:#111111}
74
+
75
+ .note{margin:30px 0 0;padding:16px 18px;border-left:3px solid var(--ui-line);background:#fff;
76
+ border-radius:0 10px 10px 0;font-size:13px;color:var(--ui-muted);line-height:1.75}
77
+ .note b{color:var(--ui-ink)}
78
+ </style>
79
+ </head>
80
+ <body>
81
+ <div class="wrap">
82
+ <h1>톤 방향</h1>
83
+ <p class="sub">구조·카피·여백은 전부 같습니다. <b>색과 명도만</b> 다릅니다.</p>
84
+ <p class="ask">
85
+ <b>고를 것 하나:</b> 첫 화면이 어느 쪽에 가까웠으면 하나요?<br>
86
+ 고르기 어려우면 "무엇이 걸리는지"만 말해 주세요 — 그게 더 빠릅니다.
87
+ <b>여기서 정하면 시안을 다시 만들 일이 없습니다.</b>
88
+ </p>
89
+
90
+ <div class="grid">
91
+
92
+ <div class="opt opt-1">
93
+ <div class="opt-head"><b>A. 밝게, 흰 바탕</b><span class="tag">가장 중립적</span></div>
94
+ <div class="chips"><i style="--c:#ffffff">배경 #FFFFFF</i><i>본문 #111111</i><i>면 #F7F7F5</i></div>
95
+ <div class="pv">
96
+ <span class="eb">Eyebrow</span>
97
+ <h2>여기에 실제<br>제목 카피가 들어갑니다.</h2>
98
+ <p>설명 한 줄. 실제 카피를 넣으세요 — 글자 길이가 인상을 바꿉니다.</p>
99
+ <span class="cta">주요 행동</span>
100
+ <div class="card"><b>카드 · 제품 UI 자리</b><div class="bar" style="width:78%"></div><div class="bar" style="width:54%"></div></div>
101
+ </div>
102
+ </div>
103
+
104
+ <div class="opt opt-2">
105
+ <div class="opt-head"><b>B. 밝게, 옅은 회색 바탕</b><span class="tag">면이 덜 눈부심</span></div>
106
+ <div class="chips"><i>배경 #F4F4F1</i><i>본문 #161615</i><i>면 #FFFFFF</i></div>
107
+ <div class="pv">
108
+ <span class="eb">Eyebrow</span>
109
+ <h2>여기에 실제<br>제목 카피가 들어갑니다.</h2>
110
+ <p>설명 한 줄. 실제 카피를 넣으세요 — 글자 길이가 인상을 바꿉니다.</p>
111
+ <span class="cta">주요 행동</span>
112
+ <div class="card"><b>카드 · 제품 UI 자리</b><div class="bar" style="width:78%"></div><div class="bar" style="width:54%"></div></div>
113
+ </div>
114
+ </div>
115
+
116
+ <div class="opt opt-3">
117
+ <div class="opt-head"><b>C. 어둡게</b><span class="tag">제품 화면이 도드라짐</span></div>
118
+ <div class="chips"><i>배경 #111111</i><i>본문 #FFFFFF</i><i>면 #1B1B1A</i></div>
119
+ <div class="pv">
120
+ <span class="eb">Eyebrow</span>
121
+ <h2>여기에 실제<br>제목 카피가 들어갑니다.</h2>
122
+ <p>설명 한 줄. 실제 카피를 넣으세요 — 글자 길이가 인상을 바꿉니다.</p>
123
+ <span class="cta">주요 행동</span>
124
+ <div class="card"><b>카드 · 제품 UI 자리</b><div class="bar" style="width:78%"></div><div class="bar" style="width:54%"></div></div>
125
+ </div>
126
+ </div>
127
+
128
+ </div>
129
+
130
+ <p class="note">
131
+ <b>이 화면에서 정하지 않는 것.</b> 서체·간격·컴포넌트 형태는 여기서 묻지 않습니다.
132
+ 한 번에 하나만 물어야 답이 빨리 나옵니다. 나머지는 스타일 가이드에서 정합니다.<br>
133
+ <b>화면마다 톤이 다를 수 있습니다.</b> Hero 같은 지배적인 면이 사이트 전체와 다른 톤을 갖는 경우가 흔합니다.
134
+ 그때는 "Hero는 C, 나머지는 A"처럼 화면 단위로 답을 받으세요.
135
+ </p>
136
+ </div>
137
+ </body>
138
+ </html>