kwon-ui 0.0.0-stage → 0.1.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.
- package/CATALOG.md +301 -0
- package/CHANGELOG.md +31 -0
- package/LICENSE +4 -0
- package/README.md +71 -2
- package/bin/kwon-ui.mjs +60 -0
- package/check/index.mjs +233 -0
- package/claude/ADOPTING.md +99 -0
- package/claude/APP-CLAUDE.md +15 -0
- package/claude/hook.mjs +46 -0
- package/dist/classes.json +69 -0
- package/dist/kwon-ui.css +438 -0
- package/dist/react/controls.d.ts +20 -0
- package/dist/react/controls.js +19 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.js +5 -0
- package/dist/react/layout.d.ts +56 -0
- package/dist/react/layout.js +48 -0
- package/dist/react/tone.d.ts +13 -0
- package/dist/react/tone.js +21 -0
- package/dist/react/type.d.ts +27 -0
- package/dist/react/type.js +28 -0
- package/dist/react/util.d.ts +10 -0
- package/dist/react/util.js +4 -0
- package/package.json +62 -4
package/dist/kwon-ui.css
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/* kwon-ui 0.1.0. 이 파일은 빌드 결과다. 앱에서 고치지 않는다. 원본: src/css/ */
|
|
2
|
+
|
|
3
|
+
/* ── tokens.css ── */
|
|
4
|
+
/*
|
|
5
|
+
* 토큰. 색, 글자 크기, 모서리, 폭 같은 값은 이 파일에만 있다.
|
|
6
|
+
* 앱 코드와 이 패키지의 다른 CSS 는 값을 직접 쓰지 않고 이 변수만 쓴다.
|
|
7
|
+
* 값은 kwonandcompany.com 의 밤 테마(운영 화면)에서 그대로 옮겼다.
|
|
8
|
+
*/
|
|
9
|
+
.night {
|
|
10
|
+
/* 바탕과 면 */
|
|
11
|
+
--n-bg: #000000;
|
|
12
|
+
--n-panel: #0d0d0d;
|
|
13
|
+
--n-card: #171717;
|
|
14
|
+
--n-card-2: #202020;
|
|
15
|
+
--n-line: rgba(255, 255, 255, 0.1);
|
|
16
|
+
|
|
17
|
+
/* 글자 네 톤: 진한 글자, 본문, 받쳐 주는 말, 가장 흐린 글 */
|
|
18
|
+
--n-hi: #ffffff;
|
|
19
|
+
--n-soft: rgba(255, 255, 255, 0.78);
|
|
20
|
+
--n-mid: rgba(255, 255, 255, 0.5);
|
|
21
|
+
--n-dim: rgba(255, 255, 255, 0.32);
|
|
22
|
+
|
|
23
|
+
/* 버튼 */
|
|
24
|
+
--n-btn: #ffffff;
|
|
25
|
+
--n-btn-ink: #000000;
|
|
26
|
+
--n-btn-hover: #e9e9e9;
|
|
27
|
+
--n-ghost: rgba(255, 255, 255, 0.08);
|
|
28
|
+
--n-ghost-hover: rgba(255, 255, 255, 0.16);
|
|
29
|
+
--n-focus: rgba(255, 255, 255, 0.55);
|
|
30
|
+
|
|
31
|
+
/* 글자 크기. 읽는 글은 제목, 본문, 작은 글 셋이다. display 는 첫 화면 제목, control 은 버튼과 링크 */
|
|
32
|
+
--n-fs-display: clamp(34px, 3.8vw, 50px);
|
|
33
|
+
--n-fs-title: clamp(26px, 2.4vw, 32px);
|
|
34
|
+
--n-fs-body: 18px;
|
|
35
|
+
--n-fs-small: 14px;
|
|
36
|
+
--n-fs-control: 15px;
|
|
37
|
+
|
|
38
|
+
/* 모서리: 판, 그림, 알약 */
|
|
39
|
+
--n-radius: 32px;
|
|
40
|
+
--n-radius-md: 20px;
|
|
41
|
+
--n-radius-pill: 999px;
|
|
42
|
+
|
|
43
|
+
/* 폭과 바깥 여백 */
|
|
44
|
+
--n-wrap: 1064px;
|
|
45
|
+
--n-gutter: 24px;
|
|
46
|
+
--n-edge: clamp(20px, 3.2vw, 48px);
|
|
47
|
+
|
|
48
|
+
/* 글꼴 이름. 글꼴 파일은 앱이 불러온다(패키지는 글꼴을 싣지 않는다) */
|
|
49
|
+
--n-font: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, system-ui, "Segoe UI", Roboto,
|
|
50
|
+
"Helvetica Neue", Arial, sans-serif;
|
|
51
|
+
}
|
|
52
|
+
@media (min-width: 768px) {
|
|
53
|
+
.night {
|
|
54
|
+
--n-gutter: 40px;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* ── base.css ── */
|
|
59
|
+
/*
|
|
60
|
+
* 바탕. 페이지 맨 바깥 요소에 .night 를 붙이면 그 안이 검은 화면이 된다.
|
|
61
|
+
* 여백(margin)과 글꼴 지정은 넣지 않는다: 앱의 초기화 규칙과 Tailwind 유틸리티가 그대로 먹어야 한다.
|
|
62
|
+
*/
|
|
63
|
+
.night {
|
|
64
|
+
background: var(--n-bg);
|
|
65
|
+
color: var(--n-hi);
|
|
66
|
+
font-weight: 400;
|
|
67
|
+
overflow-x: clip;
|
|
68
|
+
/* 한글은 낱말 단위로만 줄을 바꾼다(글자 중간에서 끊지 않는다) */
|
|
69
|
+
word-break: keep-all;
|
|
70
|
+
}
|
|
71
|
+
/* 스크롤 끝에서 흰 바탕이 비치지 않게 문서 바탕도 검정으로 덮는다 */
|
|
72
|
+
html:has(.night),
|
|
73
|
+
html:has(.night) body {
|
|
74
|
+
background: #000000;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* 화면에는 안 보이고 화면 낭독기만 읽는 글(두 톤 제목 앞의 구간 이름) */
|
|
78
|
+
.night .n-sr {
|
|
79
|
+
position: absolute;
|
|
80
|
+
width: 1px;
|
|
81
|
+
height: 1px;
|
|
82
|
+
padding: 0;
|
|
83
|
+
margin: -1px;
|
|
84
|
+
overflow: hidden;
|
|
85
|
+
clip: rect(0, 0, 0, 0);
|
|
86
|
+
white-space: nowrap;
|
|
87
|
+
border-width: 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* ── type.css ── */
|
|
91
|
+
/*
|
|
92
|
+
* 글자. 읽는 글자 크기는 화면마다 셋이다: 제목(n-title), 본문(n-body), 작은 글(n-small).
|
|
93
|
+
* n-display 는 첫 화면 제목에만 쓴다. 위계는 굵기 대신 두 톤(--n-hi, --n-mid)으로 만든다.
|
|
94
|
+
* 굵기는 제목 500, 본문 400 두 가지다.
|
|
95
|
+
*/
|
|
96
|
+
.night .n-display {
|
|
97
|
+
font-size: var(--n-fs-display);
|
|
98
|
+
line-height: 1.16;
|
|
99
|
+
letter-spacing: -0.035em;
|
|
100
|
+
font-weight: 500;
|
|
101
|
+
color: var(--n-hi);
|
|
102
|
+
}
|
|
103
|
+
.night .n-title {
|
|
104
|
+
font-size: var(--n-fs-title);
|
|
105
|
+
line-height: 1.2;
|
|
106
|
+
letter-spacing: -0.03em;
|
|
107
|
+
font-weight: 500;
|
|
108
|
+
color: var(--n-hi);
|
|
109
|
+
}
|
|
110
|
+
/* 여러 줄 제목은 줄 길이를 고르게 맞춘다 */
|
|
111
|
+
.night :is(.n-display, .n-title) {
|
|
112
|
+
text-wrap: balance;
|
|
113
|
+
}
|
|
114
|
+
/* 두 톤: 제목 안에서 받쳐 주는 말. lo 는 줄을 바꿔서, lo-inline 은 같은 줄에서 회색 */
|
|
115
|
+
.night .n-title .lo,
|
|
116
|
+
.night .n-display .lo {
|
|
117
|
+
display: block;
|
|
118
|
+
color: var(--n-mid);
|
|
119
|
+
}
|
|
120
|
+
.night .n-title .lo-inline,
|
|
121
|
+
.night .n-display .lo-inline {
|
|
122
|
+
color: var(--n-mid);
|
|
123
|
+
}
|
|
124
|
+
.night .n-body {
|
|
125
|
+
font-size: var(--n-fs-body);
|
|
126
|
+
line-height: 1.5;
|
|
127
|
+
letter-spacing: -0.01em;
|
|
128
|
+
color: var(--n-soft);
|
|
129
|
+
/* 마지막 줄에 낱말 하나만 남지 않게 */
|
|
130
|
+
text-wrap: pretty;
|
|
131
|
+
}
|
|
132
|
+
/* 목록이나 카드 안의 작은 제목. 본문과 같은 크기이고 진한 글자 */
|
|
133
|
+
.night .n-card-title {
|
|
134
|
+
font-size: var(--n-fs-body);
|
|
135
|
+
line-height: 1.4;
|
|
136
|
+
font-weight: 500;
|
|
137
|
+
letter-spacing: -0.01em;
|
|
138
|
+
color: var(--n-hi);
|
|
139
|
+
}
|
|
140
|
+
.night .n-small {
|
|
141
|
+
font-size: var(--n-fs-small);
|
|
142
|
+
line-height: 1.45;
|
|
143
|
+
letter-spacing: 0;
|
|
144
|
+
color: var(--n-mid);
|
|
145
|
+
}
|
|
146
|
+
/* 숫자가 세로로 줄이 맞아야 하는 곳(연도, 금액) */
|
|
147
|
+
.night .n-num {
|
|
148
|
+
font-variant-numeric: tabular-nums;
|
|
149
|
+
letter-spacing: 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* ── layout.css ── */
|
|
153
|
+
/*
|
|
154
|
+
* 뼈대. 읽는 글은 한 줄기로 흐른다: 왼쪽에 작은 라벨이나 제목, 오른쪽 한 칸에 본문.
|
|
155
|
+
* 규칙의 순서를 바꾸지 않는다(특이도가 같은 규칙은 뒤에 온 것이 이긴다).
|
|
156
|
+
*/
|
|
157
|
+
|
|
158
|
+
/* 내용 폭: 가운데 1064px */
|
|
159
|
+
.night .n-wrap {
|
|
160
|
+
width: 100%;
|
|
161
|
+
max-width: var(--n-wrap);
|
|
162
|
+
margin-inline: auto;
|
|
163
|
+
padding-inline: var(--n-gutter);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* 하위 페이지의 바깥과 머리. 위 여백은 떠 있는 메뉴 자리다 */
|
|
167
|
+
.night.n-page {
|
|
168
|
+
padding-top: clamp(120px, 12vw, 164px);
|
|
169
|
+
min-height: 100vh;
|
|
170
|
+
}
|
|
171
|
+
.night .n-page-head {
|
|
172
|
+
padding-bottom: 72px;
|
|
173
|
+
}
|
|
174
|
+
@media (min-width: 1024px) {
|
|
175
|
+
.night .n-page-head {
|
|
176
|
+
padding-bottom: 96px;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* 구간 */
|
|
181
|
+
.night .n-sect {
|
|
182
|
+
position: relative;
|
|
183
|
+
padding: 96px 0 72px;
|
|
184
|
+
}
|
|
185
|
+
@media (min-width: 1024px) {
|
|
186
|
+
.night .n-sect {
|
|
187
|
+
padding: 128px 0 96px;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* 가는 선 하나(위쪽) */
|
|
192
|
+
.night .n-rule {
|
|
193
|
+
border-top: 1px solid var(--n-line);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* 구간 머리: 넓은 화면에서 왼쪽 제목, 오른쪽 본문 */
|
|
197
|
+
.night .n-head {
|
|
198
|
+
display: grid;
|
|
199
|
+
gap: 16px;
|
|
200
|
+
margin-bottom: 48px;
|
|
201
|
+
}
|
|
202
|
+
@media (min-width: 1024px) {
|
|
203
|
+
.night .n-head {
|
|
204
|
+
grid-template-columns: 1fr 1fr;
|
|
205
|
+
align-items: center;
|
|
206
|
+
gap: 40px;
|
|
207
|
+
margin-bottom: 64px;
|
|
208
|
+
}
|
|
209
|
+
.night .n-head > :last-child {
|
|
210
|
+
max-width: 30em;
|
|
211
|
+
justify-self: end;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/* 제목만 있으면 한 줄 전체를 쓴다 */
|
|
215
|
+
.night .n-head > :only-child {
|
|
216
|
+
justify-self: start;
|
|
217
|
+
max-width: none;
|
|
218
|
+
grid-column: 1 / -1;
|
|
219
|
+
}
|
|
220
|
+
/* 선으로 시작하는 구간 머리(<SectionHead rule>). 위 여백까지 한 이름으로 묶었다 */
|
|
221
|
+
.night .n-head.ruled {
|
|
222
|
+
border-top: 1px solid var(--n-line);
|
|
223
|
+
padding-top: 48px;
|
|
224
|
+
}
|
|
225
|
+
@media (min-width: 768px) {
|
|
226
|
+
.night .n-head.ruled {
|
|
227
|
+
padding-top: 64px;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/* 페이지 머리 안에서는 아래 여백을 페이지 머리가 정한다 */
|
|
231
|
+
.night .n-page-head > .n-head {
|
|
232
|
+
margin-bottom: 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* 줄 목록: 왼쪽 200px 라벨, 오른쪽 본문 */
|
|
236
|
+
.night .n-rows {
|
|
237
|
+
border-top: 1px solid var(--n-line);
|
|
238
|
+
}
|
|
239
|
+
.night .n-row {
|
|
240
|
+
display: grid;
|
|
241
|
+
gap: 8px 24px;
|
|
242
|
+
padding: 24px 0;
|
|
243
|
+
border-bottom: 1px solid var(--n-line);
|
|
244
|
+
}
|
|
245
|
+
@media (min-width: 768px) {
|
|
246
|
+
.night .n-row {
|
|
247
|
+
grid-template-columns: 200px 1fr;
|
|
248
|
+
padding: 28px 0;
|
|
249
|
+
}
|
|
250
|
+
.night .n-row.wide-label {
|
|
251
|
+
grid-template-columns: 280px 1fr;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
.night .n-row > .n-row-label {
|
|
255
|
+
font-size: var(--n-fs-small);
|
|
256
|
+
line-height: 1.6;
|
|
257
|
+
font-weight: 400;
|
|
258
|
+
letter-spacing: 0;
|
|
259
|
+
color: var(--n-mid);
|
|
260
|
+
}
|
|
261
|
+
.night .n-rows > .n-row:last-child {
|
|
262
|
+
border-bottom: 0;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* 검은 판: 화면 양옆에서 한 칸 들어온 둥근 면 */
|
|
266
|
+
.night .n-panel {
|
|
267
|
+
position: relative;
|
|
268
|
+
z-index: 1;
|
|
269
|
+
margin: 0 var(--n-gutter);
|
|
270
|
+
border-radius: var(--n-radius);
|
|
271
|
+
background: var(--n-panel);
|
|
272
|
+
padding: 88px 0 72px;
|
|
273
|
+
}
|
|
274
|
+
@media (min-width: 1024px) {
|
|
275
|
+
.night .n-panel {
|
|
276
|
+
padding: 120px 0 104px;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
@media (max-width: 767px) {
|
|
280
|
+
.night .n-panel {
|
|
281
|
+
margin: 0 12px;
|
|
282
|
+
padding: 72px 0 56px;
|
|
283
|
+
}
|
|
284
|
+
.night .n-panel .n-wrap {
|
|
285
|
+
padding: 0 16px;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* 그림과 영상의 틀 */
|
|
290
|
+
.night .n-media {
|
|
291
|
+
position: relative;
|
|
292
|
+
aspect-ratio: 4 / 3;
|
|
293
|
+
border-radius: var(--n-radius-md);
|
|
294
|
+
overflow: hidden;
|
|
295
|
+
background: var(--n-card);
|
|
296
|
+
}
|
|
297
|
+
.night .n-media.wide {
|
|
298
|
+
aspect-ratio: 16 / 9;
|
|
299
|
+
}
|
|
300
|
+
.night .n-media video,
|
|
301
|
+
.night .n-media img {
|
|
302
|
+
display: block;
|
|
303
|
+
width: 100%;
|
|
304
|
+
height: 100%;
|
|
305
|
+
object-fit: cover;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* 문의 띠: 왼쪽 문장, 오른쪽 버튼 */
|
|
309
|
+
.night .n-cta-band {
|
|
310
|
+
display: grid;
|
|
311
|
+
gap: 24px;
|
|
312
|
+
padding: 56px 0 8px;
|
|
313
|
+
border-top: 1px solid var(--n-line);
|
|
314
|
+
}
|
|
315
|
+
@media (min-width: 768px) {
|
|
316
|
+
.night .n-cta-band {
|
|
317
|
+
grid-template-columns: 1fr auto;
|
|
318
|
+
align-items: center;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
.night .n-cta-band .n-title + .n-body {
|
|
322
|
+
margin-top: 16px;
|
|
323
|
+
max-width: 34em;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* ── controls.css ── */
|
|
327
|
+
/*
|
|
328
|
+
* 조작부. 첫 화면과 띠에는 버튼 하나(알약)와 글자 링크 하나까지만 둔다.
|
|
329
|
+
*/
|
|
330
|
+
|
|
331
|
+
/* 알약 버튼: 높이 48, 흰 면. ghost 는 어두운 면 */
|
|
332
|
+
.night .n-pill {
|
|
333
|
+
display: inline-flex;
|
|
334
|
+
align-items: center;
|
|
335
|
+
justify-content: center;
|
|
336
|
+
gap: 10px;
|
|
337
|
+
height: 48px;
|
|
338
|
+
padding: 0 28px;
|
|
339
|
+
border-radius: var(--n-radius-pill);
|
|
340
|
+
background: var(--n-btn);
|
|
341
|
+
color: var(--n-btn-ink);
|
|
342
|
+
font-size: var(--n-fs-control);
|
|
343
|
+
line-height: 1;
|
|
344
|
+
font-weight: 500;
|
|
345
|
+
letter-spacing: 0.01em;
|
|
346
|
+
transition:
|
|
347
|
+
background-color 0.2s,
|
|
348
|
+
transform 0.2s;
|
|
349
|
+
}
|
|
350
|
+
.night .n-pill:hover {
|
|
351
|
+
background: var(--n-btn-hover);
|
|
352
|
+
}
|
|
353
|
+
.night .n-pill.ghost {
|
|
354
|
+
background: var(--n-ghost);
|
|
355
|
+
color: var(--n-hi);
|
|
356
|
+
}
|
|
357
|
+
.night .n-pill.ghost:hover {
|
|
358
|
+
background: var(--n-ghost-hover);
|
|
359
|
+
}
|
|
360
|
+
.night .n-pill:focus-visible {
|
|
361
|
+
outline: 2px solid var(--n-focus);
|
|
362
|
+
outline-offset: 3px;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/* 글자 링크: 버튼 옆의 둘째 행동, 본문 아래의 '더 보기' */
|
|
366
|
+
.night .n-link {
|
|
367
|
+
display: inline-flex;
|
|
368
|
+
align-items: center;
|
|
369
|
+
gap: 8px;
|
|
370
|
+
font-size: var(--n-fs-control);
|
|
371
|
+
font-weight: 500;
|
|
372
|
+
color: var(--n-hi);
|
|
373
|
+
transition: opacity 0.2s;
|
|
374
|
+
}
|
|
375
|
+
.night .n-link:hover {
|
|
376
|
+
opacity: 0.7;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/* 버튼과 링크를 나란히 놓는 줄 */
|
|
380
|
+
.night .n-actions {
|
|
381
|
+
display: flex;
|
|
382
|
+
flex-wrap: wrap;
|
|
383
|
+
align-items: center;
|
|
384
|
+
gap: 16px 32px;
|
|
385
|
+
}
|
|
386
|
+
.night .n-page-head > .n-actions {
|
|
387
|
+
margin-top: 48px;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* ── timeline.css ── */
|
|
391
|
+
/*
|
|
392
|
+
* 연도별 이력. 왼쪽에 연도, 오른쪽에 그 해의 줄들. 많으면 앞의 몇 줄만 보이고 '외 N건'으로 접는다.
|
|
393
|
+
*/
|
|
394
|
+
.night .n-timeline {
|
|
395
|
+
border-top: 1px solid var(--n-line);
|
|
396
|
+
}
|
|
397
|
+
.night .n-tl-year {
|
|
398
|
+
display: grid;
|
|
399
|
+
gap: 12px;
|
|
400
|
+
padding: 28px 0;
|
|
401
|
+
border-bottom: 1px solid var(--n-line);
|
|
402
|
+
}
|
|
403
|
+
@media (min-width: 768px) {
|
|
404
|
+
.night .n-tl-year {
|
|
405
|
+
grid-template-columns: 160px 1fr;
|
|
406
|
+
gap: 24px;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
.night .n-tl-year ul {
|
|
410
|
+
display: grid;
|
|
411
|
+
gap: 12px;
|
|
412
|
+
}
|
|
413
|
+
.night .n-tl-more summary {
|
|
414
|
+
display: inline-flex;
|
|
415
|
+
align-items: center;
|
|
416
|
+
gap: 6px;
|
|
417
|
+
width: max-content;
|
|
418
|
+
cursor: pointer;
|
|
419
|
+
list-style: none;
|
|
420
|
+
transition: color 0.2s;
|
|
421
|
+
}
|
|
422
|
+
.night .n-tl-more summary::-webkit-details-marker {
|
|
423
|
+
display: none;
|
|
424
|
+
}
|
|
425
|
+
.night .n-tl-more summary:hover {
|
|
426
|
+
color: var(--n-hi);
|
|
427
|
+
}
|
|
428
|
+
.night .n-tl-more summary .chev {
|
|
429
|
+
display: inline-block;
|
|
430
|
+
transform: rotate(90deg);
|
|
431
|
+
}
|
|
432
|
+
.night .n-tl-more summary:focus-visible {
|
|
433
|
+
outline: 2px solid var(--n-mid);
|
|
434
|
+
outline-offset: 4px;
|
|
435
|
+
}
|
|
436
|
+
.night .n-tl-more details[open] > summary {
|
|
437
|
+
display: none;
|
|
438
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ElementType, ReactNode } from "react";
|
|
2
|
+
import { type PolyProps } from "./util.js";
|
|
3
|
+
type ControlProps = {
|
|
4
|
+
className?: string;
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* 알약 버튼. 한 화면의 주 행동 하나에 쓴다. ghost 는 어두운 면(둘째 행동).
|
|
9
|
+
* 링크면 as 로 프레임워크의 링크 컴포넌트를, 단추면 as="button" 을 넘긴다.
|
|
10
|
+
*/
|
|
11
|
+
export declare function Pill<T extends ElementType = "a">({ as, ghost, className, ...rest }: PolyProps<T, ControlProps & {
|
|
12
|
+
ghost?: boolean;
|
|
13
|
+
}>): import("react").JSX.Element;
|
|
14
|
+
/** 글자 링크. 버튼 옆의 둘째 행동이나 본문 아래의 '더 보기'. arrow 를 주면 끝에 화살표가 붙는다. */
|
|
15
|
+
export declare function TextLink<T extends ElementType = "a">({ as, arrow, className, children, ...rest }: PolyProps<T, ControlProps & {
|
|
16
|
+
arrow?: boolean;
|
|
17
|
+
}>): import("react").JSX.Element;
|
|
18
|
+
/** 버튼과 글자 링크를 나란히 놓는 줄. 버튼 하나와 링크 하나까지만 둔다. */
|
|
19
|
+
export declare function Actions({ className, children }: ControlProps): import("react").JSX.Element;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./util.js";
|
|
3
|
+
/**
|
|
4
|
+
* 알약 버튼. 한 화면의 주 행동 하나에 쓴다. ghost 는 어두운 면(둘째 행동).
|
|
5
|
+
* 링크면 as 로 프레임워크의 링크 컴포넌트를, 단추면 as="button" 을 넘긴다.
|
|
6
|
+
*/
|
|
7
|
+
export function Pill({ as, ghost, className, ...rest }) {
|
|
8
|
+
const Tag = as ?? "a";
|
|
9
|
+
return _jsx(Tag, { className: cx("n-pill", ghost && "ghost", className), ...rest });
|
|
10
|
+
}
|
|
11
|
+
/** 글자 링크. 버튼 옆의 둘째 행동이나 본문 아래의 '더 보기'. arrow 를 주면 끝에 화살표가 붙는다. */
|
|
12
|
+
export function TextLink({ as, arrow, className, children, ...rest }) {
|
|
13
|
+
const Tag = as ?? "a";
|
|
14
|
+
return (_jsxs(Tag, { className: cx("n-link", className), ...rest, children: [children, arrow ? _jsx("span", { "aria-hidden": "true", children: "\u2192" }) : null] }));
|
|
15
|
+
}
|
|
16
|
+
/** 버튼과 글자 링크를 나란히 놓는 줄. 버튼 하나와 링크 하나까지만 둔다. */
|
|
17
|
+
export function Actions({ className, children }) {
|
|
18
|
+
return _jsx("div", { className: cx("n-actions", className), children: children });
|
|
19
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { tone, titleText } from "./tone.js";
|
|
2
|
+
export { cx } from "./util.js";
|
|
3
|
+
export type { PolyProps } from "./util.js";
|
|
4
|
+
export { Display, Title, Body, CardTitle, Small } from "./type.js";
|
|
5
|
+
export { Page, PageHead, Section, SectionHead, Rows, Row, Panel, Media, CtaBand } from "./layout.js";
|
|
6
|
+
export { Pill, TextLink, Actions } from "./controls.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { tone, titleText } from "./tone.js";
|
|
2
|
+
export { cx } from "./util.js";
|
|
3
|
+
export { Display, Title, Body, CardTitle, Small } from "./type.js";
|
|
4
|
+
export { Page, PageHead, Section, SectionHead, Rows, Row, Panel, Media, CtaBand } from "./layout.js";
|
|
5
|
+
export { Pill, TextLink, Actions } from "./controls.js";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ElementType, ReactNode } from "react";
|
|
2
|
+
import { type PolyProps } from "./util.js";
|
|
3
|
+
type BoxProps = {
|
|
4
|
+
className?: string;
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
};
|
|
7
|
+
/** 하위 페이지의 맨 바깥. 검은 바탕과 떠 있는 메뉴 자리(위 여백)를 준다. */
|
|
8
|
+
export declare function Page<T extends ElementType = "div">({ as, className, ...rest }: PolyProps<T, BoxProps>): import("react").JSX.Element;
|
|
9
|
+
/** 페이지 머리: 제목 한 줄, 받쳐 주는 문단 하나, 버튼 줄. */
|
|
10
|
+
export declare function PageHead({ title, label, lead, actions, className, children, }: {
|
|
11
|
+
/** 페이지 제목. "**진한 낱말** 나머지" 꼴이면 두 톤으로 그린다 */
|
|
12
|
+
title: string;
|
|
13
|
+
label?: string;
|
|
14
|
+
lead?: ReactNode;
|
|
15
|
+
/** 버튼 하나와 글자 링크 하나까지 */
|
|
16
|
+
actions?: ReactNode;
|
|
17
|
+
className?: string;
|
|
18
|
+
children?: ReactNode;
|
|
19
|
+
}): import("react").JSX.Element;
|
|
20
|
+
/** 구간. 안쪽은 내용 폭(1064px)으로 감싼다. bare 를 주면 감싸지 않는다(화면 끝까지 쓰는 내용). */
|
|
21
|
+
export declare function Section<T extends ElementType = "section">({ as, bare, className, children, ...rest }: PolyProps<T, BoxProps & {
|
|
22
|
+
bare?: boolean;
|
|
23
|
+
}>): import("react").JSX.Element;
|
|
24
|
+
/** 구간 머리: 왼쪽에 한 줄 제목, 오른쪽에 본문(children). 본문이 없으면 제목이 한 줄 전체를 쓴다. */
|
|
25
|
+
export declare function SectionHead({ title, label, rule, as, className, children, }: {
|
|
26
|
+
title: string;
|
|
27
|
+
label?: string;
|
|
28
|
+
/** 위에 가는 선을 긋고 그 아래에서 시작한다 */
|
|
29
|
+
rule?: boolean;
|
|
30
|
+
as?: "h1" | "h2" | "h3";
|
|
31
|
+
className?: string;
|
|
32
|
+
children?: ReactNode;
|
|
33
|
+
}): import("react").JSX.Element;
|
|
34
|
+
/** 줄 목록의 바깥. 순서가 있으면 as="ol". */
|
|
35
|
+
export declare function Rows<T extends ElementType = "div">({ as, className, ...rest }: PolyProps<T, BoxProps>): import("react").JSX.Element;
|
|
36
|
+
/** 한 줄: 왼쪽 작은 라벨, 오른쪽 본문. 글만 넘기면 본문 문단으로 그린다. */
|
|
37
|
+
export declare function Row<T extends ElementType = "div">({ as, label, labelAs, wide, className, children, ...rest }: PolyProps<T, BoxProps & {
|
|
38
|
+
label: ReactNode;
|
|
39
|
+
labelAs?: "h3" | "h4" | "p" | "dt";
|
|
40
|
+
wide?: boolean;
|
|
41
|
+
}>): import("react").JSX.Element;
|
|
42
|
+
/** 검은 판. 화면 양옆에서 한 칸 들어온 둥근 면으로 구간 하나를 감싼다. */
|
|
43
|
+
export declare function Panel<T extends ElementType = "section">({ as, className, children, ...rest }: PolyProps<T, BoxProps>): import("react").JSX.Element;
|
|
44
|
+
/** 그림과 영상의 틀. 기본 4:3, wide 는 16:9. 안의 img, video 는 틀을 채운다. */
|
|
45
|
+
export declare function Media<T extends ElementType = "div">({ as, wide, className, ...rest }: PolyProps<T, BoxProps & {
|
|
46
|
+
wide?: boolean;
|
|
47
|
+
}>): import("react").JSX.Element;
|
|
48
|
+
/** 문의 띠: 왼쪽 한 줄 문장(과 짧은 문단), 오른쪽 버튼 줄. 페이지 끝에 하나 둔다. */
|
|
49
|
+
export declare function CtaBand({ title, label, body, actions, className, }: {
|
|
50
|
+
title: string;
|
|
51
|
+
label?: string;
|
|
52
|
+
body?: ReactNode;
|
|
53
|
+
actions: ReactNode;
|
|
54
|
+
className?: string;
|
|
55
|
+
}): import("react").JSX.Element;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { titleText } from "./tone.js";
|
|
3
|
+
import { cx } from "./util.js";
|
|
4
|
+
/** 하위 페이지의 맨 바깥. 검은 바탕과 떠 있는 메뉴 자리(위 여백)를 준다. */
|
|
5
|
+
export function Page({ as, className, ...rest }) {
|
|
6
|
+
const Tag = as ?? "div";
|
|
7
|
+
return _jsx(Tag, { className: cx("night n-page", className), ...rest });
|
|
8
|
+
}
|
|
9
|
+
/** 페이지 머리: 제목 한 줄, 받쳐 주는 문단 하나, 버튼 줄. */
|
|
10
|
+
export function PageHead({ title, label, lead, actions, className, children, }) {
|
|
11
|
+
return (_jsxs("header", { className: cx("n-wrap n-page-head", className), children: [_jsxs("div", { className: "n-head", children: [_jsx("h1", { className: "n-title", children: titleText(title, label) }), lead ? _jsx("p", { className: "n-body", children: lead }) : null] }), actions ? _jsx("div", { className: "n-actions", children: actions }) : null, children] }));
|
|
12
|
+
}
|
|
13
|
+
/** 구간. 안쪽은 내용 폭(1064px)으로 감싼다. bare 를 주면 감싸지 않는다(화면 끝까지 쓰는 내용). */
|
|
14
|
+
export function Section({ as, bare, className, children, ...rest }) {
|
|
15
|
+
const Tag = as ?? "section";
|
|
16
|
+
return (_jsx(Tag, { className: cx("n-sect", className), ...rest, children: bare ? children : _jsx("div", { className: "n-wrap", children: children }) }));
|
|
17
|
+
}
|
|
18
|
+
/** 구간 머리: 왼쪽에 한 줄 제목, 오른쪽에 본문(children). 본문이 없으면 제목이 한 줄 전체를 쓴다. */
|
|
19
|
+
export function SectionHead({ title, label, rule, as, className, children, }) {
|
|
20
|
+
const H = as ?? "h2";
|
|
21
|
+
return (_jsxs("div", { className: cx("n-head", rule && "ruled", className), children: [_jsx(H, { className: "n-title", children: titleText(title, label) }), children ? _jsx("div", { children: typeof children === "string" ? _jsx("p", { className: "n-body", children: children }) : children }) : null] }));
|
|
22
|
+
}
|
|
23
|
+
/** 줄 목록의 바깥. 순서가 있으면 as="ol". */
|
|
24
|
+
export function Rows({ as, className, ...rest }) {
|
|
25
|
+
const Tag = as ?? "div";
|
|
26
|
+
return _jsx(Tag, { className: cx("n-rows", className), ...rest });
|
|
27
|
+
}
|
|
28
|
+
/** 한 줄: 왼쪽 작은 라벨, 오른쪽 본문. 글만 넘기면 본문 문단으로 그린다. */
|
|
29
|
+
export function Row({ as, label, labelAs, wide, className, children, ...rest }) {
|
|
30
|
+
const Tag = as ?? "div";
|
|
31
|
+
const L = labelAs ?? "h3";
|
|
32
|
+
return (_jsxs(Tag, { className: cx("n-row", wide && "wide-label", className), ...rest, children: [_jsx(L, { className: "n-row-label", children: label }), typeof children === "string" ? _jsx("p", { className: "n-body", children: children }) : children] }));
|
|
33
|
+
}
|
|
34
|
+
/** 검은 판. 화면 양옆에서 한 칸 들어온 둥근 면으로 구간 하나를 감싼다. */
|
|
35
|
+
export function Panel({ as, className, children, ...rest }) {
|
|
36
|
+
const Tag = as ?? "section";
|
|
37
|
+
return (_jsx(Tag, { className: cx("n-panel", className), ...rest, children: _jsx("div", { className: "n-wrap", children: children }) }));
|
|
38
|
+
}
|
|
39
|
+
/** 그림과 영상의 틀. 기본 4:3, wide 는 16:9. 안의 img, video 는 틀을 채운다. */
|
|
40
|
+
export function Media({ as, wide, className, ...rest }) {
|
|
41
|
+
const Tag = as ?? "div";
|
|
42
|
+
return _jsx(Tag, { className: cx("n-media", wide && "wide", className), ...rest });
|
|
43
|
+
}
|
|
44
|
+
/** 문의 띠: 왼쪽 한 줄 문장(과 짧은 문단), 오른쪽 버튼 줄. 페이지 끝에 하나 둔다. */
|
|
45
|
+
export function CtaBand({ title, label, body, actions, className, }) {
|
|
46
|
+
const heading = _jsx("h2", { className: "n-title", children: titleText(title, label) });
|
|
47
|
+
return (_jsxs("div", { className: cx("n-cta-band", className), children: [body ? (_jsxs("div", { children: [heading, _jsx("p", { className: "n-body", children: body })] })) : (heading), _jsx("div", { className: "n-actions", children: actions })] }));
|
|
48
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* 한 줄 두 톤 제목. 글은 "**진한 낱말** 나머지" 꼴로 쓴다.
|
|
4
|
+
* ** 로 감싼 곳만 진한 글자(제목 기본색)이고, 나머지는 같은 줄의 회색(.lo-inline)이다.
|
|
5
|
+
* 진한 글자는 문장의 핵심 낱말 한두 개에만 준다. 숫자는 숫자가 요점일 때만 진하게 둔다.
|
|
6
|
+
* label 을 주면 화면 낭독기만 읽는 구간 이름을 앞에 붙인다(보이는 문장에 구간 이름이 없을 때).
|
|
7
|
+
*/
|
|
8
|
+
export declare function tone(line: string, label?: string): ReactNode;
|
|
9
|
+
/**
|
|
10
|
+
* 제목 글. ** 표시가 있으면 두 톤으로, 없으면 전부 진한 글자로 그린다.
|
|
11
|
+
* (tone 은 표시가 없는 글을 전부 회색으로 그리므로, 표시 없는 제목은 이 함수를 거친다.)
|
|
12
|
+
*/
|
|
13
|
+
export declare function titleText(line: string, label?: string): ReactNode;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* 한 줄 두 톤 제목. 글은 "**진한 낱말** 나머지" 꼴로 쓴다.
|
|
5
|
+
* ** 로 감싼 곳만 진한 글자(제목 기본색)이고, 나머지는 같은 줄의 회색(.lo-inline)이다.
|
|
6
|
+
* 진한 글자는 문장의 핵심 낱말 한두 개에만 준다. 숫자는 숫자가 요점일 때만 진하게 둔다.
|
|
7
|
+
* label 을 주면 화면 낭독기만 읽는 구간 이름을 앞에 붙인다(보이는 문장에 구간 이름이 없을 때).
|
|
8
|
+
*/
|
|
9
|
+
export function tone(line, label) {
|
|
10
|
+
const parts = line.split(/(\*\*[^*]+\*\*)/).filter(Boolean);
|
|
11
|
+
return (_jsxs(_Fragment, { children: [label ? _jsxs("span", { className: "n-sr", children: [label, ". "] }) : null, parts.map((p, i) => p.startsWith("**") && p.endsWith("**") ? (_jsx(Fragment, { children: p.slice(2, -2) }, i)) : (_jsx("span", { className: "lo-inline", children: p }, i)))] }));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 제목 글. ** 표시가 있으면 두 톤으로, 없으면 전부 진한 글자로 그린다.
|
|
15
|
+
* (tone 은 표시가 없는 글을 전부 회색으로 그리므로, 표시 없는 제목은 이 함수를 거친다.)
|
|
16
|
+
*/
|
|
17
|
+
export function titleText(line, label) {
|
|
18
|
+
if (line.includes("**"))
|
|
19
|
+
return tone(line, label);
|
|
20
|
+
return (_jsxs(_Fragment, { children: [label ? _jsxs("span", { className: "n-sr", children: [label, ". "] }) : null, line] }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ElementType, ReactNode } from "react";
|
|
2
|
+
import { type PolyProps } from "./util.js";
|
|
3
|
+
type LineProps = {
|
|
4
|
+
/** "**진한 낱말** 나머지" 꼴의 한 줄. children 대신 쓴다 */
|
|
5
|
+
line?: string;
|
|
6
|
+
/** 화면 낭독기만 읽는 구간 이름 */
|
|
7
|
+
label?: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
children?: ReactNode;
|
|
10
|
+
};
|
|
11
|
+
/** 첫 화면 제목. 한 페이지에 한 번만 쓴다. */
|
|
12
|
+
export declare function Display<T extends ElementType = "h1">({ as, line, label, className, children, ...rest }: PolyProps<T, LineProps>): import("react").JSX.Element;
|
|
13
|
+
/** 구간 제목. 한 문장 한 줄로 쓰고 진한 글자는 핵심 낱말 한두 개에만 준다. */
|
|
14
|
+
export declare function Title<T extends ElementType = "h2">({ as, line, label, className, children, ...rest }: PolyProps<T, LineProps>): import("react").JSX.Element;
|
|
15
|
+
type TextProps = {
|
|
16
|
+
className?: string;
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
};
|
|
19
|
+
/** 본문. */
|
|
20
|
+
export declare function Body<T extends ElementType = "p">({ as, className, ...rest }: PolyProps<T, TextProps>): import("react").JSX.Element;
|
|
21
|
+
/** 목록이나 카드 안의 작은 제목. 본문과 같은 크기이고 진한 글자다. */
|
|
22
|
+
export declare function CardTitle<T extends ElementType = "h3">({ as, className, ...rest }: PolyProps<T, TextProps>): import("react").JSX.Element;
|
|
23
|
+
/** 작은 글: 캡션, 기준, 라벨. num 을 주면 숫자 폭을 맞춘다. */
|
|
24
|
+
export declare function Small<T extends ElementType = "p">({ as, num, className, ...rest }: PolyProps<T, TextProps & {
|
|
25
|
+
num?: boolean;
|
|
26
|
+
}>): import("react").JSX.Element;
|
|
27
|
+
export {};
|