@xfilecom/front-core 0.2.26 → 0.2.28

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/README.md CHANGED
@@ -1,29 +1,62 @@
1
1
  # @xfilecom/front-core
2
2
 
3
- Design tokens (`tokens.css`), atomic layout/CSS (`base.css`), and browser-only React atoms + overlays. **Nest / 서버 의존 없음.**
3
+ Design tokens (`tokens.css`), atomic layout (`base.css`), browser-only React components. **Nest / 서버 의존 없음.**
4
4
 
5
- ## CSS import order
5
+ ## 문서 (상세)
6
+
7
+ | 문서 | 내용 |
8
+ |------|------|
9
+ | [docs/DESIGN_SYSTEM.md](./docs/DESIGN_SYSTEM.md) | MUI Paper / react-native-paper 등과 **개념 매핑**, elevation·역할 색 |
10
+ | [docs/COMPONENTS.md](./docs/COMPONENTS.md) | 컴포넌트별 **소스 경로**, props 요약, **복붙 예제 코드** |
11
+
12
+ npm 패키지에 `docs/`가 포함되므로, 설치 후 `node_modules/@xfilecom/front-core/docs/`에서도 동일 파일을 볼 수 있다.
13
+
14
+ ## CSS 로드 순서
6
15
 
7
16
  1. `@xfilecom/front-core/tokens.css`
8
17
  2. `@xfilecom/front-core/base.css`
9
- 3. (optional) 앱/공유 테마 — 예: `xfc-theme.css`에서 `--xfc-*` 덮어쓰기
18
+ 3. (선택) `xfc-theme.css`에서 `--xfc-*` 덮어쓰기
19
+ 4. (선택) 레이아웃 `app.css`
20
+
21
+ **다크:** `tokens.css` — `html.dark` / `data-theme="dark"`.
22
+
23
+ **배경:** `body` 기본은 옅은 액센트 그라데이션. 단색만 쓰려면 `body { background-image: none; }`.
24
+
25
+ ## React import
26
+
27
+ ```tsx
28
+ import {
29
+ Paper,
30
+ Card,
31
+ Button,
32
+ Text,
33
+ Dialog,
34
+ } from '@xfilecom/front-core';
35
+ ```
36
+
37
+ 서브패스: `@xfilecom/front-core/atoms`, `@xfilecom/front-core/overlays`.
10
38
 
11
- **Dark mode:** `tokens.css` 끝에 `html.dark` / `data-theme="dark"` 프리셋이 있다. 루트에 클래스 또는 속성만 켜면 동일 변수명으로 전환된다.
39
+ ## Paper (표면)
12
40
 
13
- ## React
41
+ MUI `Paper` / RN `Surface`에 대응하는 얇은 래퍼:
14
42
 
15
- - 루트 export 또는 `@xfilecom/front-core/atoms`, `@xfilecom/front-core/overlays`.
16
- - **기본 UI 문자열은 영어** (토스트 닫기 `Dismiss`, ConfirmDialog `OK` / `Cancel`). 한국어 등은 props로 넘긴다.
43
+ ```tsx
44
+ <Paper elevation={2} style={{ padding: 'var(--xfc-space-lg)' }}>
45
+
46
+ </Paper>
47
+ <Paper variant="outlined" style={{ padding: 16 }}>…</Paper>
48
+ ```
17
49
 
18
- ## Accessibility
50
+ ## 기본 문자열·a11y
19
51
 
20
- - **Dialog / BottomSheet / ConfirmDialog:** body 스크롤 잠금, Escape, 초기 포커스·복귀, **Tab 포커스 트랩** (`useFocusTrap`).
21
- - **Toast:** `error`는 `role="alert"`, `role="status"`. `ToastList`는 `ToastEntry.dismissible`로 행마다 닫기 버튼 on/off.
52
+ - 기본 UI 문구는 **영어** (토스트 Dismiss, Confirm OK/Cancel). 한국어는 props로 덮어쓴다.
53
+ - Dialog / BottomSheet: 스크롤 잠금, Escape, 포커스 트랩 (`useFocusTrap`).
54
+ - Toast: `error` → `role="alert"`, 그 외 `role="status"`.
22
55
 
23
56
  ## Version
24
57
 
25
- `XFRAME_FRONT_CORE_VERSION`은 `npm run build` 시 `package.json`의 `version`으로 `src/generatedVersion.ts`가 갱신된다.
58
+ `npm run build` 시 `package.json`의 `version`이 `src/generatedVersion.ts`에 반영된다.
26
59
 
27
- ## Form atoms
60
+ ##
28
61
 
29
- `Input`, `Textarea`, `Select`, `Checkbox`, `Field` — `Field`는 단일 자식 컨트롤에 `aria-describedby` / `aria-invalid` / `id`를 병합한다. 라벨 `htmlFor`와 컨트롤 `id`를 맞출 것.
62
+ `Input`, `Textarea`, `Select`, `Checkbox`, `Field` — 자세한 예는 [COMPONENTS.md](./docs/COMPONENTS.md).
package/dist/base.css CHANGED
@@ -23,8 +23,11 @@ body {
23
23
  font-size: var(--xfc-text-body);
24
24
  line-height: var(--xfc-leading-normal);
25
25
  color: var(--xfc-fg);
26
- background: var(--xfc-bg);
26
+ background-color: var(--xfc-bg);
27
+ background-image: radial-gradient(ellipse 120% 80% at 50% -20%, var(--xfc-accent-soft), transparent 55%);
28
+ background-attachment: fixed;
27
29
  -webkit-font-smoothing: antialiased;
30
+ text-rendering: optimizeLegibility;
28
31
  }
29
32
 
30
33
  a {
@@ -46,9 +49,10 @@ a:hover {
46
49
 
47
50
  .xfc-text--title {
48
51
  font-size: var(--xfc-text-display);
49
- font-weight: 700;
52
+ font-weight: 800;
50
53
  line-height: var(--xfc-leading-tight);
51
- letter-spacing: -0.02em;
54
+ letter-spacing: -0.035em;
55
+ font-feature-settings: 'ss01' on, 'cv01' on;
52
56
  }
53
57
 
54
58
  .xfc-text--appbar {
@@ -75,6 +79,15 @@ a:hover {
75
79
  font-size: var(--xfc-text-body);
76
80
  }
77
81
 
82
+ /** 본문보다 한 단계 큰 리드 문단 (히어로·카피) */
83
+ .xfc-text--lead {
84
+ font-size: 1.125rem;
85
+ font-weight: 500;
86
+ line-height: 1.6;
87
+ color: var(--xfc-fg-label);
88
+ letter-spacing: -0.01em;
89
+ }
90
+
78
91
  .xfc-text--small {
79
92
  font-size: var(--xfc-text-small);
80
93
  }
@@ -175,51 +188,114 @@ a:hover {
175
188
  .xfc-card {
176
189
  background: var(--xfc-bg-elevated);
177
190
  color: var(--xfc-fg);
178
- border: 1px solid var(--xfc-border);
179
- border-radius: var(--xfc-radius-md);
191
+ border: 1px solid color-mix(in srgb, var(--xfc-border) 88%, var(--xfc-fg) 6%);
192
+ border-radius: var(--xfc-radius-lg);
180
193
  box-shadow: var(--xfc-card-shadow);
181
194
  }
182
195
 
196
+ /** 클릭·링크 카드만 — 정적 패널에는 붙이지 말 것 */
197
+ .xfc-card--interactive {
198
+ cursor: pointer;
199
+ transition:
200
+ box-shadow 0.2s ease,
201
+ border-color 0.2s ease,
202
+ transform 0.2s ease;
203
+ }
204
+
205
+ @media (hover: hover) {
206
+ .xfc-card--interactive:hover {
207
+ border-color: color-mix(in srgb, var(--xfc-border-strong) 75%, var(--xfc-accent) 10%);
208
+ box-shadow: var(--xfc-shadow-md);
209
+ transform: translateY(-1px);
210
+ }
211
+
212
+ .xfc-card--interactive:active {
213
+ transform: translateY(0);
214
+ }
215
+ }
216
+
183
217
  .xfc-card__header {
184
- padding: var(--xfc-space-lg) var(--xfc-space-lg) var(--xfc-space-md);
218
+ padding: var(--xfc-space-xl) var(--xfc-space-xl) var(--xfc-space-md);
185
219
  border-bottom: 1px solid var(--xfc-border);
186
220
  font-size: var(--xfc-text-section);
187
221
  font-weight: 700;
222
+ letter-spacing: -0.02em;
188
223
  color: var(--xfc-fg-strong);
189
224
  }
190
225
 
191
226
  .xfc-card__body {
192
227
  min-width: 0;
193
- padding: var(--xfc-space-lg);
228
+ padding: var(--xfc-space-xl);
194
229
  }
195
230
 
196
231
  .xfc-card__footer {
197
- padding: 0 var(--xfc-space-lg) var(--xfc-space-lg);
232
+ padding: 0 var(--xfc-space-xl) var(--xfc-space-xl);
198
233
  border-top: 1px solid var(--xfc-border);
199
234
  padding-top: var(--xfc-space-md);
200
235
  }
201
236
 
202
- /* —— Buttons (피그마: 80×34 primary, radius 5) —— */
237
+ /* —— Paper / Surface (MUI Paper · RN Paper Surface 스타일 elevation) —— */
238
+
239
+ .xfc-paper {
240
+ box-sizing: border-box;
241
+ background: var(--xfc-bg-elevated);
242
+ color: var(--xfc-fg);
243
+ border-radius: var(--xfc-radius-md);
244
+ border: 1px solid transparent;
245
+ transition:
246
+ box-shadow 0.2s ease,
247
+ border-color 0.2s ease;
248
+ }
249
+
250
+ .xfc-paper--elevation-0 {
251
+ box-shadow: none;
252
+ border-color: var(--xfc-border);
253
+ }
254
+
255
+ .xfc-paper--elevation-1 {
256
+ box-shadow: var(--xfc-shadow-xs);
257
+ }
258
+
259
+ .xfc-paper--elevation-2 {
260
+ box-shadow: var(--xfc-shadow-sm);
261
+ }
262
+
263
+ .xfc-paper--elevation-3 {
264
+ box-shadow: var(--xfc-shadow-md);
265
+ }
266
+
267
+ .xfc-paper--elevation-4 {
268
+ box-shadow: var(--xfc-shadow-lg);
269
+ }
270
+
271
+ .xfc-paper--outlined {
272
+ box-shadow: none;
273
+ border-color: var(--xfc-border-strong);
274
+ }
275
+
276
+ /* —— Buttons —— */
203
277
 
204
278
  .xfc-btn {
205
279
  display: inline-flex;
206
280
  align-items: center;
207
281
  justify-content: center;
208
- gap: var(--xfc-space-xs);
209
- min-height: 34px;
210
- padding: 0 var(--xfc-space-lg);
282
+ gap: var(--xfc-space-sm);
283
+ min-height: 40px;
284
+ padding: 0 calc(var(--xfc-space-lg) + 2px);
211
285
  font: inherit;
212
286
  font-size: var(--xfc-text-body);
213
- font-weight: 700;
287
+ font-weight: 600;
288
+ letter-spacing: -0.01em;
214
289
  line-height: 1;
215
- border-radius: var(--xfc-radius-xs);
290
+ border-radius: var(--xfc-radius-sm);
216
291
  border: 1px solid transparent;
217
292
  cursor: pointer;
218
293
  transition:
219
- background 0.15s ease,
220
- color 0.15s ease,
221
- border-color 0.15s ease,
222
- box-shadow 0.15s ease;
294
+ background 0.18s ease,
295
+ color 0.18s ease,
296
+ border-color 0.18s ease,
297
+ box-shadow 0.18s ease,
298
+ transform 0.18s ease;
223
299
  }
224
300
 
225
301
  .xfc-btn:focus-visible {
@@ -228,8 +304,10 @@ a:hover {
228
304
  }
229
305
 
230
306
  .xfc-btn:disabled {
231
- opacity: 0.5;
307
+ opacity: 0.48;
232
308
  cursor: not-allowed;
309
+ transform: none;
310
+ box-shadow: none;
233
311
  }
234
312
 
235
313
  .xfc-btn--loading {
@@ -250,52 +328,75 @@ a:hover {
250
328
  .xfc-btn--primary {
251
329
  background: var(--xfc-accent);
252
330
  color: var(--xfc-accent-fg);
331
+ box-shadow:
332
+ var(--xfc-shadow-xs),
333
+ 0 2px 12px color-mix(in srgb, var(--xfc-accent) 32%, transparent);
253
334
  }
254
335
 
255
336
  .xfc-btn--primary:hover:not(:disabled) {
256
337
  background: var(--xfc-accent-hover);
338
+ box-shadow:
339
+ var(--xfc-shadow-sm),
340
+ 0 4px 20px color-mix(in srgb, var(--xfc-accent) 38%, transparent);
341
+ }
342
+
343
+ @media (hover: hover) {
344
+ .xfc-btn--primary:hover:not(:disabled) {
345
+ transform: translateY(-1px);
346
+ }
347
+
348
+ .xfc-btn--primary:active:not(:disabled) {
349
+ transform: translateY(0);
350
+ }
257
351
  }
258
352
 
259
353
  .xfc-btn--secondary {
260
354
  background: var(--xfc-bg-elevated);
261
355
  color: var(--xfc-fg);
262
356
  border-color: var(--xfc-border-strong);
357
+ box-shadow: var(--xfc-shadow-xs);
263
358
  }
264
359
 
265
360
  .xfc-btn--secondary:hover:not(:disabled) {
266
- border-color: var(--xfc-fg-muted);
361
+ border-color: color-mix(in srgb, var(--xfc-accent) 35%, var(--xfc-border-strong));
362
+ background: color-mix(in srgb, var(--xfc-bg-muted) 40%, var(--xfc-bg-elevated));
267
363
  }
268
364
 
269
365
  .xfc-btn--outline {
270
- background: var(--xfc-bg-elevated);
366
+ background: transparent;
271
367
  color: var(--xfc-accent);
272
- border-color: var(--xfc-accent);
368
+ border-color: color-mix(in srgb, var(--xfc-accent) 45%, var(--xfc-border-strong));
369
+ box-shadow: none;
273
370
  }
274
371
 
275
372
  .xfc-btn--outline:hover:not(:disabled) {
276
- background: color-mix(in srgb, var(--xfc-accent) 6%, transparent);
373
+ background: var(--xfc-accent-soft);
374
+ border-color: var(--xfc-accent);
277
375
  }
278
376
 
279
377
  .xfc-btn--muted {
280
- background: var(--xfc-bg-disabled);
378
+ background: color-mix(in srgb, var(--xfc-bg-muted) 65%, var(--xfc-bg-elevated));
281
379
  color: var(--xfc-fg-muted);
282
- border-color: transparent;
380
+ border-color: var(--xfc-border);
381
+ box-shadow: none;
283
382
  }
284
383
 
285
384
  .xfc-btn--muted:hover:not(:disabled) {
286
- filter: brightness(0.97);
385
+ background: var(--xfc-bg-muted);
386
+ color: var(--xfc-fg-label);
287
387
  }
288
388
 
289
389
  .xfc-btn--ghost {
290
390
  background: transparent;
291
391
  color: var(--xfc-accent);
392
+ box-shadow: none;
292
393
  }
293
394
 
294
395
  .xfc-btn--ghost:hover:not(:disabled) {
295
- background: color-mix(in srgb, var(--xfc-accent) 8%, transparent);
396
+ background: var(--xfc-accent-soft);
296
397
  }
297
398
 
298
- /* —— Inputs (로그인 필드 48px, border #D4D4D4) —— */
399
+ /* —— Inputs —— */
299
400
 
300
401
  .xfc-input {
301
402
  display: block;
@@ -307,26 +408,32 @@ a:hover {
307
408
  color: var(--xfc-fg);
308
409
  background: var(--xfc-bg-elevated);
309
410
  border: 1px solid var(--xfc-border-strong);
310
- border-radius: var(--xfc-radius-xs);
411
+ border-radius: var(--xfc-radius-sm);
311
412
  box-shadow: var(--xfc-input-shadow);
312
413
  transition:
313
- border-color 0.15s ease,
314
- box-shadow 0.15s ease;
414
+ border-color 0.18s ease,
415
+ box-shadow 0.18s ease,
416
+ background-color 0.18s ease;
315
417
  }
316
418
 
317
419
  .xfc-input::placeholder {
318
420
  color: var(--xfc-fg-placeholder);
319
421
  }
320
422
 
423
+ .xfc-input:hover:not(:disabled):not(:focus) {
424
+ border-color: color-mix(in srgb, var(--xfc-border-strong) 70%, var(--xfc-fg-muted) 30%);
425
+ }
426
+
321
427
  .xfc-input:focus {
322
428
  outline: none;
323
429
  border-color: var(--xfc-accent);
324
- box-shadow: var(--xfc-input-shadow), var(--xfc-focus-ring);
430
+ box-shadow: var(--xfc-shadow-sm), var(--xfc-focus-ring);
325
431
  }
326
432
 
327
433
  .xfc-input:disabled {
328
- opacity: 0.6;
434
+ opacity: 0.55;
329
435
  cursor: not-allowed;
436
+ background: var(--xfc-bg-muted);
330
437
  }
331
438
 
332
439
  .xfc-input--invalid {
@@ -335,7 +442,7 @@ a:hover {
335
442
 
336
443
  .xfc-input--invalid:focus {
337
444
  border-color: var(--xfc-danger);
338
- box-shadow: var(--xfc-input-shadow), 0 0 0 2px color-mix(in srgb, var(--xfc-danger) 25%, transparent);
445
+ box-shadow: var(--xfc-shadow-xs), 0 0 0 3px color-mix(in srgb, var(--xfc-danger) 22%, transparent);
339
446
  }
340
447
 
341
448
  .xfc-input-description {
@@ -410,31 +517,44 @@ a:hover {
410
517
  .xfc-badge {
411
518
  display: inline-flex;
412
519
  align-items: center;
413
- padding: 2px var(--xfc-space-sm);
520
+ padding: 4px 10px;
414
521
  font-size: var(--xfc-text-small);
415
522
  font-weight: 600;
416
- line-height: 1.3;
417
- border-radius: var(--xfc-radius-xs);
523
+ line-height: 1.25;
524
+ letter-spacing: 0.01em;
525
+ border-radius: var(--xfc-radius-full);
526
+ border: 1px solid transparent;
527
+ box-shadow: var(--xfc-shadow-xs);
418
528
  }
419
529
 
420
530
  .xfc-badge--neutral {
421
- background: var(--xfc-border);
422
- color: var(--xfc-fg);
531
+ background: color-mix(in srgb, var(--xfc-bg-muted) 55%, var(--xfc-bg-elevated));
532
+ color: var(--xfc-fg-label);
533
+ border-color: var(--xfc-border);
423
534
  }
424
535
 
425
536
  .xfc-badge--accent {
426
- background: color-mix(in srgb, var(--xfc-accent) 18%, transparent);
427
- color: var(--xfc-accent-hover);
537
+ background: color-mix(in srgb, var(--xfc-accent) 16%, var(--xfc-bg-elevated));
538
+ color: var(--xfc-accent);
539
+ border-color: color-mix(in srgb, var(--xfc-accent) 28%, transparent);
428
540
  }
429
541
 
430
542
  .xfc-badge--success {
431
- background: var(--xfc-success-bg);
543
+ background: color-mix(in srgb, var(--xfc-success) 12%, var(--xfc-success-bg));
432
544
  color: var(--xfc-success);
545
+ border-color: color-mix(in srgb, var(--xfc-success) 22%, transparent);
546
+ }
547
+
548
+ .xfc-badge--warn {
549
+ background: color-mix(in srgb, var(--xfc-warning) 14%, var(--xfc-warning-bg));
550
+ color: color-mix(in srgb, var(--xfc-warning) 85%, #000);
551
+ border-color: color-mix(in srgb, var(--xfc-warning) 25%, transparent);
433
552
  }
434
553
 
435
554
  .xfc-badge--danger {
436
- background: color-mix(in srgb, var(--xfc-danger) 15%, transparent);
555
+ background: color-mix(in srgb, var(--xfc-danger) 12%, var(--xfc-bg-elevated));
437
556
  color: var(--xfc-danger);
557
+ border-color: color-mix(in srgb, var(--xfc-danger) 22%, transparent);
438
558
  }
439
559
 
440
560
  span.xfc-badge[aria-disabled='true'] {
@@ -479,17 +599,18 @@ button.xfc-badge[aria-disabled='true'] {
479
599
  }
480
600
 
481
601
  .xfc-loading-overlay-spinner {
482
- width: 48px;
483
- height: 48px;
484
- border: 4px solid color-mix(in srgb, var(--xfc-accent) 22%, transparent);
602
+ width: 52px;
603
+ height: 52px;
604
+ border: 3px solid color-mix(in srgb, var(--xfc-accent) 18%, transparent);
485
605
  border-top-color: var(--xfc-accent);
606
+ border-right-color: color-mix(in srgb, var(--xfc-accent) 45%, transparent);
486
607
  border-radius: 50%;
487
- animation: xfc-loading-overlay-spin 0.68s linear infinite;
608
+ animation: xfc-loading-overlay-spin 0.72s linear infinite;
488
609
  flex-shrink: 0;
489
610
  box-shadow:
490
- 0 0 0 2px rgba(255, 255, 255, 0.95),
491
- 0 6px 24px rgba(0, 0, 0, 0.14);
492
- filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.08));
611
+ 0 0 0 1px rgba(255, 255, 255, 0.5),
612
+ 0 8px 32px color-mix(in srgb, var(--xfc-accent) 22%, transparent),
613
+ var(--xfc-shadow-md);
493
614
  }
494
615
 
495
616
  .xfc-loading-overlay-title {
@@ -564,32 +685,38 @@ button.xfc-badge[aria-disabled='true'] {
564
685
  box-sizing: border-box;
565
686
  align-items: flex-start;
566
687
  justify-content: flex-start;
567
- gap: var(--xfc-space-lg);
568
- padding: var(--xfc-space-lg);
688
+ gap: var(--xfc-space-md);
689
+ padding: var(--xfc-space-lg) var(--xfc-space-xl);
569
690
  border-radius: var(--xfc-toast-radius);
570
691
  color: #fff;
571
692
  font-family: var(--xfc-font-sans);
572
693
  font-size: var(--xfc-text-body);
573
- font-weight: 400;
574
- line-height: 1.25;
694
+ font-weight: 500;
695
+ line-height: 1.35;
696
+ letter-spacing: -0.01em;
697
+ border: var(--xfc-toast-border);
575
698
  box-shadow: var(--xfc-toast-shadow);
576
- transition: opacity 0.2s ease;
699
+ backdrop-filter: blur(8px);
700
+ -webkit-backdrop-filter: blur(8px);
701
+ transition:
702
+ opacity 0.2s ease,
703
+ transform 0.2s ease;
577
704
  }
578
705
 
579
706
  .xfc-toast--info {
580
- background: var(--xfc-toast-info-bg);
707
+ background: linear-gradient(155deg, rgba(255, 255, 255, 0.12) 0%, transparent 42%), var(--xfc-toast-info-bg);
581
708
  }
582
709
 
583
710
  .xfc-toast--success {
584
- background: var(--xfc-toast-success-bg);
711
+ background: linear-gradient(155deg, rgba(255, 255, 255, 0.14) 0%, transparent 40%), var(--xfc-toast-success-bg);
585
712
  }
586
713
 
587
714
  .xfc-toast--warn {
588
- background: var(--xfc-toast-warn-bg);
715
+ background: linear-gradient(155deg, rgba(255, 255, 255, 0.12) 0%, transparent 40%), var(--xfc-toast-warn-bg);
589
716
  }
590
717
 
591
718
  .xfc-toast--error {
592
- background: var(--xfc-toast-error-bg);
719
+ background: linear-gradient(155deg, rgba(255, 255, 255, 0.12) 0%, transparent 40%), var(--xfc-toast-error-bg);
593
720
  }
594
721
 
595
722
  .xfc-toast__icon {
@@ -662,11 +789,13 @@ button.xfc-badge[aria-disabled='true'] {
662
789
  align-items: center;
663
790
  justify-content: space-between;
664
791
  gap: var(--xfc-space-sm);
665
- padding: var(--xfc-space-md) var(--xfc-space-lg);
666
- background: var(--xfc-danger);
792
+ padding: var(--xfc-space-md) var(--xfc-space-xl);
793
+ background: linear-gradient(135deg, color-mix(in srgb, var(--xfc-danger) 92%, #fff) 0%, var(--xfc-danger) 100%);
667
794
  color: var(--xfc-accent-fg);
668
- border-radius: var(--xfc-radius-md);
795
+ border-radius: var(--xfc-radius-lg);
669
796
  font-size: var(--xfc-text-small);
797
+ font-weight: 600;
798
+ border: 1px solid color-mix(in srgb, var(--xfc-danger) 55%, #fff);
670
799
  box-shadow: var(--xfc-toast-shadow);
671
800
  }
672
801
 
@@ -744,8 +873,10 @@ button.xfc-badge[aria-disabled='true'] {
744
873
  justify-content: center;
745
874
  padding: var(--xfc-space-lg);
746
875
  box-sizing: border-box;
747
- background: color-mix(in srgb, var(--xfc-fg) 42%, transparent);
748
- animation: xfc-dialog-backdrop-in 0.18s ease-out;
876
+ background: color-mix(in srgb, var(--xfc-fg-strong) 48%, transparent);
877
+ backdrop-filter: blur(10px) saturate(1.05);
878
+ -webkit-backdrop-filter: blur(10px) saturate(1.05);
879
+ animation: xfc-dialog-backdrop-in 0.2s ease-out;
749
880
  }
750
881
 
751
882
  .xfc-dialog-backdrop--bottom {
@@ -762,15 +893,13 @@ button.xfc-badge[aria-disabled='true'] {
762
893
  overflow: auto;
763
894
  box-sizing: border-box;
764
895
  padding: var(--xfc-space-xl);
765
- border-radius: var(--xfc-radius-md);
896
+ border-radius: var(--xfc-radius-lg);
766
897
  background: var(--xfc-bg-elevated);
767
898
  color: var(--xfc-fg);
768
- border: 1px solid var(--xfc-border);
769
- box-shadow:
770
- 0 24px 48px color-mix(in srgb, var(--xfc-fg) 12%, transparent),
771
- var(--xfc-card-shadow);
899
+ border: 1px solid color-mix(in srgb, var(--xfc-border) 88%, var(--xfc-accent) 10%);
900
+ box-shadow: var(--xfc-shadow-lg);
772
901
  font-family: var(--xfc-font-sans);
773
- animation: xfc-dialog-panel-in 0.22s ease-out;
902
+ animation: xfc-dialog-panel-in 0.24s cubic-bezier(0.22, 1, 0.36, 1);
774
903
  }
775
904
 
776
905
  .xfc-dialog-panel__title-row {
@@ -916,4 +1045,10 @@ button.xfc-btn.xfc-btn--primary.xfc-confirm-dialog__confirm--danger:hover:not(:d
916
1045
  .xfc-bottom-sheet-panel {
917
1046
  animation: none;
918
1047
  }
1048
+
1049
+ .xfc-btn--primary:hover:not(:disabled),
1050
+ .xfc-card--interactive:hover,
1051
+ .xfc-card--interactive:active {
1052
+ transform: none;
1053
+ }
919
1054
  }
@@ -1,6 +1,6 @@
1
1
  import type { HTMLAttributes, MouseEventHandler, ReactNode } from 'react';
2
2
  export type BadgeProps = Omit<HTMLAttributes<HTMLSpanElement>, 'onClick'> & {
3
- tone?: 'neutral' | 'accent' | 'success' | 'danger';
3
+ tone?: 'neutral' | 'accent' | 'success' | 'warn' | 'danger';
4
4
  children?: ReactNode;
5
5
  /** 앞쪽 아이콘·이모지 등 */
6
6
  icon?: ReactNode;
@@ -5,5 +5,7 @@ export type CardProps = HTMLAttributes<HTMLDivElement> & {
5
5
  title?: ReactNode;
6
6
  /** 하단 푸터 슬롯 */
7
7
  footer?: ReactNode;
8
+ /** true면 호버 시 살짝 떠오르는 인터랙션 (클릭 카드·타일용) */
9
+ interactive?: boolean;
8
10
  };
9
- export declare function Card({ className, children, title, footer, ...rest }: CardProps): import("react/jsx-runtime").JSX.Element;
11
+ export declare function Card({ className, children, title, footer, interactive, ...rest }: CardProps): import("react/jsx-runtime").JSX.Element;
@@ -2,8 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Card = Card;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
- function Card({ className = '', children, title, footer, ...rest }) {
6
- const cls = ['xfc-card', className].filter(Boolean).join(' ');
5
+ function Card({ className = '', children, title, footer, interactive, ...rest }) {
6
+ const cls = ['xfc-card', interactive ? 'xfc-card--interactive' : '', className].filter(Boolean).join(' ');
7
7
  const structured = title != null || footer != null;
8
8
  return ((0, jsx_runtime_1.jsxs)("div", { className: cls, ...rest, children: [title != null ? (0, jsx_runtime_1.jsx)("div", { className: "xfc-card__header", children: title }) : null, structured ? (0, jsx_runtime_1.jsx)("div", { className: "xfc-card__body", children: children }) : children, footer != null ? (0, jsx_runtime_1.jsx)("div", { className: "xfc-card__footer", children: footer }) : null] }));
9
9
  }
@@ -0,0 +1,16 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+ export type PaperProps = HTMLAttributes<HTMLDivElement> & {
3
+ children?: ReactNode;
4
+ /**
5
+ * Material-style elevation: 0은 평면+보더, 1…4는 섀도 강도 증가.
6
+ * `variant="outlined"`일 때는 무시되고 보더만 씀.
7
+ */
8
+ elevation?: 0 | 1 | 2 | 3 | 4;
9
+ /** filled = elevation 섀도, outlined = 보더만 */
10
+ variant?: 'filled' | 'outlined';
11
+ };
12
+ /**
13
+ * 콘텐츠를 올려놓는 표면 컴포넌트 (MUI `Paper` / RN Paper `Surface`에 가깝게).
14
+ * 스타일은 `base.css`의 `.xfc-paper*` — 토큰 `--xfc-shadow-*`와 연동.
15
+ */
16
+ export declare function Paper({ elevation, variant, className, children, ...rest }: PaperProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Paper = Paper;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ /**
6
+ * 콘텐츠를 올려놓는 표면 컴포넌트 (MUI `Paper` / RN Paper `Surface`에 가깝게).
7
+ * 스타일은 `base.css`의 `.xfc-paper*` — 토큰 `--xfc-shadow-*`와 연동.
8
+ */
9
+ function Paper({ elevation = 1, variant = 'filled', className = '', children, ...rest }) {
10
+ const elevClass = variant === 'outlined' ? 'xfc-paper--outlined' : `xfc-paper--elevation-${elevation}`;
11
+ const cls = ['xfc-paper', elevClass, className].filter(Boolean).join(' ');
12
+ return ((0, jsx_runtime_1.jsx)("div", { className: cls, ...rest, children: children }));
13
+ }
@@ -4,6 +4,7 @@ declare const variantClass: {
4
4
  readonly appbar: "xfc-text xfc-text--appbar";
5
5
  readonly section: "xfc-text xfc-text--section";
6
6
  readonly subtitle: "xfc-text xfc-text--subtitle";
7
+ readonly lead: "xfc-text xfc-text--lead";
7
8
  readonly body: "xfc-text xfc-text--body";
8
9
  readonly muted: "xfc-text xfc-text--body xfc-text--muted";
9
10
  readonly small: "xfc-text xfc-text--small";
@@ -7,6 +7,7 @@ const variantClass = {
7
7
  appbar: 'xfc-text xfc-text--appbar',
8
8
  section: 'xfc-text xfc-text--section',
9
9
  subtitle: 'xfc-text xfc-text--subtitle',
10
+ lead: 'xfc-text xfc-text--lead',
10
11
  body: 'xfc-text xfc-text--body',
11
12
  muted: 'xfc-text xfc-text--body xfc-text--muted',
12
13
  small: 'xfc-text xfc-text--small',
@@ -13,6 +13,7 @@ export { Select, type SelectProps } from './Select';
13
13
  export { Textarea, type TextareaProps } from './Textarea';
14
14
  export { InlineErrorList, type InlineErrorEntry, type InlineErrorListProps, } from './InlineErrorList';
15
15
  export { LoadingOverlay, type LoadingOverlayProps } from './LoadingOverlay';
16
+ export { Paper, type PaperProps } from './Paper';
16
17
  export { Stack, type StackProps } from './Stack';
17
18
  export { Toast, ToastList, ToastSeverityIcon, type ToastEntry, type ToastListProps, type ToastProps, type ToastSeverity, type ToastSeverityIconProps, } from './Toast';
18
19
  export { Text, type TextProps, type TextVariant } from './Text';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Text = exports.ToastSeverityIcon = exports.ToastList = exports.Toast = exports.Stack = exports.LoadingOverlay = exports.InlineErrorList = exports.Textarea = exports.Select = exports.Input = exports.Field = exports.Checkbox = exports.Card = exports.Button = exports.Box = exports.Badge = void 0;
3
+ exports.Text = exports.ToastSeverityIcon = exports.ToastList = exports.Toast = exports.Stack = exports.Paper = exports.LoadingOverlay = exports.InlineErrorList = exports.Textarea = exports.Select = exports.Input = exports.Field = exports.Checkbox = exports.Card = exports.Button = exports.Box = exports.Badge = void 0;
4
4
  /**
5
5
  * Atomic UI — design tokens + base.css 의 `.xfc-*` 와 1:1.
6
6
  * 패키지 루트는 `components/index.ts` → 여기서 동일 심볼을 재export.
@@ -27,6 +27,8 @@ var InlineErrorList_1 = require("./InlineErrorList");
27
27
  Object.defineProperty(exports, "InlineErrorList", { enumerable: true, get: function () { return InlineErrorList_1.InlineErrorList; } });
28
28
  var LoadingOverlay_1 = require("./LoadingOverlay");
29
29
  Object.defineProperty(exports, "LoadingOverlay", { enumerable: true, get: function () { return LoadingOverlay_1.LoadingOverlay; } });
30
+ var Paper_1 = require("./Paper");
31
+ Object.defineProperty(exports, "Paper", { enumerable: true, get: function () { return Paper_1.Paper; } });
30
32
  var Stack_1 = require("./Stack");
31
33
  Object.defineProperty(exports, "Stack", { enumerable: true, get: function () { return Stack_1.Stack; } });
32
34
  var Toast_1 = require("./Toast");
@@ -1,2 +1,2 @@
1
1
  /** Auto-generated by scripts/write-version.js — do not edit by hand */
2
- export declare const XFRAME_FRONT_CORE_VERSION: "0.2.26";
2
+ export declare const XFRAME_FRONT_CORE_VERSION: "0.2.28";
@@ -2,4 +2,4 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.XFRAME_FRONT_CORE_VERSION = void 0;
4
4
  /** Auto-generated by scripts/write-version.js — do not edit by hand */
5
- exports.XFRAME_FRONT_CORE_VERSION = "0.2.26";
5
+ exports.XFRAME_FRONT_CORE_VERSION = "0.2.28";
package/dist/index.d.ts CHANGED
@@ -24,7 +24,12 @@ export declare const tokenVars: {
24
24
  readonly colorBorderStrong: "--xfc-border-strong";
25
25
  readonly colorAccent: "--xfc-accent";
26
26
  readonly colorAccentHover: "--xfc-accent-hover";
27
+ readonly colorAccentSoft: "--xfc-accent-soft";
27
28
  readonly colorDanger: "--xfc-danger";
29
+ readonly shadowXs: "--xfc-shadow-xs";
30
+ readonly shadowSm: "--xfc-shadow-sm";
31
+ readonly shadowMd: "--xfc-shadow-md";
32
+ readonly shadowLg: "--xfc-shadow-lg";
28
33
  readonly radiusXs: "--xfc-radius-xs";
29
34
  readonly radiusSm: "--xfc-radius-sm";
30
35
  readonly radiusMd: "--xfc-radius-md";
@@ -46,6 +51,6 @@ export declare const tokenVars: {
46
51
  readonly toastWarnBg: "--xfc-toast-warn-bg";
47
52
  readonly toastErrorBg: "--xfc-toast-error-bg";
48
53
  };
49
- export { Badge, BottomSheet, Box, Button, Card, Checkbox, ConfirmDialog, Dialog, Field, InlineErrorList, Input, LoadingOverlay, Select, Stack, Text, Textarea, Toast, ToastList, ToastSeverityIcon, type BadgeProps, type BottomSheetProps, type BoxProps, type ButtonProps, type CardProps, type CheckboxProps, type ConfirmDialogProps, type DialogProps, type FieldProps, type InlineErrorEntry, type InlineErrorListProps, type InputProps, type LoadingOverlayProps, type SelectProps, type StackProps, type TextProps, type TextVariant, type TextareaProps, type ToastEntry, type ToastListProps, type ToastProps, type ToastSeverity, type ToastSeverityIconProps, } from './components';
54
+ export { Badge, BottomSheet, Box, Button, Card, Checkbox, ConfirmDialog, Dialog, Field, InlineErrorList, Input, LoadingOverlay, Paper, Select, Stack, Text, Textarea, Toast, ToastList, ToastSeverityIcon, type BadgeProps, type BottomSheetProps, type BoxProps, type ButtonProps, type CardProps, type CheckboxProps, type ConfirmDialogProps, type DialogProps, type FieldProps, type InlineErrorEntry, type InlineErrorListProps, type InputProps, type LoadingOverlayProps, type PaperProps, type SelectProps, type StackProps, type TextProps, type TextVariant, type TextareaProps, type ToastEntry, type ToastListProps, type ToastProps, type ToastSeverity, type ToastSeverityIconProps, } from './components';
50
55
  /** 커스텀 오버레이용 — `Dialog`/`BottomSheet`와 동일한 훅 */
51
56
  export { useBodyScrollLock, useEscapeKey, useFocusTrap } from './components';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useFocusTrap = exports.useEscapeKey = exports.useBodyScrollLock = exports.ToastSeverityIcon = exports.ToastList = exports.Toast = exports.Textarea = exports.Text = exports.Stack = exports.Select = exports.LoadingOverlay = exports.Input = exports.InlineErrorList = exports.Field = exports.Dialog = exports.ConfirmDialog = exports.Checkbox = exports.Card = exports.Button = exports.Box = exports.BottomSheet = exports.Badge = exports.tokenVars = exports.XFRAME_FRONT_CORE_VERSION = void 0;
3
+ exports.useFocusTrap = exports.useEscapeKey = exports.useBodyScrollLock = exports.ToastSeverityIcon = exports.ToastList = exports.Toast = exports.Textarea = exports.Text = exports.Stack = exports.Select = exports.Paper = exports.LoadingOverlay = exports.Input = exports.InlineErrorList = exports.Field = exports.Dialog = exports.ConfirmDialog = exports.Checkbox = exports.Card = exports.Button = exports.Box = exports.BottomSheet = exports.Badge = exports.tokenVars = exports.XFRAME_FRONT_CORE_VERSION = void 0;
4
4
  /**
5
5
  * @xfilecom/front-core — design tokens + atomic React components (browser-only).
6
6
  *
@@ -28,7 +28,12 @@ exports.tokenVars = {
28
28
  colorBorderStrong: '--xfc-border-strong',
29
29
  colorAccent: '--xfc-accent',
30
30
  colorAccentHover: '--xfc-accent-hover',
31
+ colorAccentSoft: '--xfc-accent-soft',
31
32
  colorDanger: '--xfc-danger',
33
+ shadowXs: '--xfc-shadow-xs',
34
+ shadowSm: '--xfc-shadow-sm',
35
+ shadowMd: '--xfc-shadow-md',
36
+ shadowLg: '--xfc-shadow-lg',
32
37
  radiusXs: '--xfc-radius-xs',
33
38
  radiusSm: '--xfc-radius-sm',
34
39
  radiusMd: '--xfc-radius-md',
@@ -63,6 +68,7 @@ Object.defineProperty(exports, "Field", { enumerable: true, get: function () { r
63
68
  Object.defineProperty(exports, "InlineErrorList", { enumerable: true, get: function () { return components_1.InlineErrorList; } });
64
69
  Object.defineProperty(exports, "Input", { enumerable: true, get: function () { return components_1.Input; } });
65
70
  Object.defineProperty(exports, "LoadingOverlay", { enumerable: true, get: function () { return components_1.LoadingOverlay; } });
71
+ Object.defineProperty(exports, "Paper", { enumerable: true, get: function () { return components_1.Paper; } });
66
72
  Object.defineProperty(exports, "Select", { enumerable: true, get: function () { return components_1.Select; } });
67
73
  Object.defineProperty(exports, "Stack", { enumerable: true, get: function () { return components_1.Stack; } });
68
74
  Object.defineProperty(exports, "Text", { enumerable: true, get: function () { return components_1.Text; } });
package/dist/tokens.css CHANGED
@@ -1,56 +1,70 @@
1
1
  /**
2
- * Design tokens — `xframe/html/html` (business-promotion Figma export) 기준.
3
- * 참고: main.html, navi_default.html, login_01.html 인라인 스타일에서 추출.
2
+ * Design tokens — 기본 테마 (라이트).
3
+ * 슬레이트 뉴트럴 + 인디고 액센트, 계층형 섀도·라디우스.
4
+ *
5
+ * Material 3 / MUI Paper / react-native-paper 와의 대응(개념):
6
+ * - 페이지 바탕: `--xfc-bg` ≈ surface / screen background
7
+ * - 카드·시트: `--xfc-bg-elevated` + `--xfc-shadow-*` ≈ elevated surface
8
+ * - `<Paper elevation={n}>` → `.xfc-paper--elevation-n` → `--xfc-shadow-xs|sm|md|lg`
9
+ * 자세한 비교: 패키지 `docs/DESIGN_SYSTEM.md`
4
10
  */
5
11
  :root {
6
12
  /* Surfaces */
7
- --xfc-bg: #ffffff;
13
+ --xfc-bg: #f4f5f8;
8
14
  --xfc-bg-elevated: #ffffff;
9
- --xfc-bg-muted: #f8f8f8;
10
- --xfc-bg-disabled: #eaeaea;
15
+ --xfc-bg-muted: #e8ebf2;
16
+ --xfc-bg-disabled: #dce0e8;
11
17
 
12
18
  /* Text */
13
- --xfc-fg: #1c252f;
14
- --xfc-fg-strong: #000000;
15
- --xfc-fg-label: #1d2125;
16
- --xfc-fg-muted: #777777;
17
- --xfc-fg-placeholder: #c7c7c7;
18
- --xfc-fg-icon: #464c50;
19
+ --xfc-fg: #1e293b;
20
+ --xfc-fg-strong: #0f172a;
21
+ --xfc-fg-label: #334155;
22
+ --xfc-fg-muted: #64748b;
23
+ --xfc-fg-placeholder: #94a3b8;
24
+ --xfc-fg-icon: #475569;
19
25
 
20
26
  /* Lines */
21
- --xfc-border: #ededed;
22
- --xfc-border-strong: #d4d4d4;
23
- --xfc-border-header: #e9e8e6;
27
+ --xfc-border: #e2e8f0;
28
+ --xfc-border-strong: #cbd5e1;
29
+ --xfc-border-header: #e2e8f0;
24
30
 
25
31
  /* Brand */
26
- --xfc-accent: #2a5bff;
27
- --xfc-accent-hover: #2248e6;
32
+ --xfc-accent: #4f46e5;
33
+ --xfc-accent-hover: #4338ca;
28
34
  --xfc-accent-fg: #ffffff;
35
+ /** 포커스·글로우용 연한 틴트 */
36
+ --xfc-accent-soft: color-mix(in srgb, var(--xfc-accent) 14%, transparent);
29
37
 
30
38
  /* Status */
31
- --xfc-danger: #f43b3c;
39
+ --xfc-danger: #e11d48;
32
40
  --xfc-success: #059669;
33
41
  --xfc-success-bg: #d1fae5;
34
42
  --xfc-warning: #d97706;
35
43
  --xfc-warning-bg: #fef3c7;
36
44
 
37
- /* Toasts (business-promotion client index.css 동일 톤) */
38
- --xfc-toast-info-bg: #1c252f;
39
- --xfc-toast-success-bg: #19af66;
40
- --xfc-toast-warn-bg: #e8a317;
41
- --xfc-toast-error-bg: #f43b3c;
42
- --xfc-toast-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
43
- --xfc-toast-radius: 6px;
44
-
45
- /* Effects */
46
- --xfc-input-shadow: 0 7px 64px rgba(0, 0, 0, 0.07);
47
- --xfc-card-shadow: 0 1px 2px rgba(28, 37, 47, 0.06);
48
-
49
- /* Radius (px 그대로 디자인 5 / 10 / pill) */
50
- --xfc-radius-xs: 5px;
51
- --xfc-radius-sm: 5px;
52
- --xfc-radius-md: 10px;
53
- --xfc-radius-lg: 10px;
45
+ /* Toasts (단색 그라데이션·글로우는 base.css 에서 보강) */
46
+ --xfc-toast-info-bg: #1e293b;
47
+ --xfc-toast-success-bg: #059669;
48
+ --xfc-toast-warn-bg: #d97706;
49
+ --xfc-toast-error-bg: #e11d48;
50
+ --xfc-toast-shadow: 0 10px 40px rgba(15, 23, 42, 0.18), 0 2px 8px rgba(15, 23, 42, 0.08);
51
+ --xfc-toast-radius: 12px;
52
+ --xfc-toast-border: 1px solid rgba(255, 255, 255, 0.14);
53
+
54
+ /* Layered shadows */
55
+ --xfc-shadow-xs: 0 1px 2px rgba(15, 23, 42, 0.05);
56
+ --xfc-shadow-sm: 0 2px 8px rgba(15, 23, 42, 0.06), 0 1px 2px rgba(15, 23, 42, 0.04);
57
+ --xfc-shadow-md: 0 8px 24px rgba(15, 23, 42, 0.08), 0 2px 6px rgba(15, 23, 42, 0.04);
58
+ --xfc-shadow-lg: 0 24px 48px rgba(15, 23, 42, 0.12), 0 8px 16px rgba(15, 23, 42, 0.06);
59
+
60
+ --xfc-input-shadow: var(--xfc-shadow-xs);
61
+ --xfc-card-shadow: var(--xfc-shadow-sm);
62
+
63
+ /* Radius */
64
+ --xfc-radius-xs: 8px;
65
+ --xfc-radius-sm: 10px;
66
+ --xfc-radius-md: 14px;
67
+ --xfc-radius-lg: 18px;
54
68
  --xfc-radius-full: 9999px;
55
69
 
56
70
  /* Space */
@@ -61,7 +75,7 @@
61
75
  --xfc-space-xl: 24px;
62
76
  --xfc-space-2xl: 32px;
63
77
 
64
- /* Typography — Pretendard Variable (+ 로고용 Poppins 는 앱에서 별도) */
78
+ /* Typography */
65
79
  --xfc-font-sans: 'Pretendard Variable', Pretendard, -apple-system, BlinkMacSystemFont, system-ui,
66
80
  Roboto, 'Noto Sans KR', 'Apple SD Gothic Neo', sans-serif;
67
81
  --xfc-font-mono: ui-monospace, 'Cascadia Code', 'SF Mono', Menlo, monospace;
@@ -74,44 +88,44 @@
74
88
  --xfc-text-input: 0.9375rem;
75
89
 
76
90
  --xfc-leading-tight: 1.25;
77
- --xfc-leading-normal: 1.5;
91
+ --xfc-leading-normal: 1.55;
78
92
  --xfc-leading-label: 22px;
79
93
  --xfc-leading-menu: 50px;
80
94
 
81
95
  /* Focus */
82
- --xfc-focus-ring: 0 0 0 3px color-mix(in srgb, var(--xfc-accent) 35%, transparent);
96
+ --xfc-focus-ring: 0 0 0 3px color-mix(in srgb, var(--xfc-accent) 32%, transparent);
83
97
  }
84
98
 
85
99
  /**
86
- * Dark theme (opt-in): `<html class="dark">` or `data-theme="dark"` on `:root` / `html`.
87
- * 동일한 변수 이름만 덮어쓴다 — 앱은 토큰 참조를 바꿀 필요 없음.
100
+ * Dark theme (opt-in): `<html class="dark">` or `data-theme="dark"`.
88
101
  */
89
102
  html.dark,
90
103
  :root[data-theme='dark'],
91
104
  html[data-theme='dark'] {
92
105
  color-scheme: dark;
93
106
 
94
- --xfc-bg: #0f1419;
95
- --xfc-bg-elevated: #1a222c;
96
- --xfc-bg-muted: #141b22;
97
- --xfc-bg-disabled: #2a3440;
107
+ --xfc-bg: #0c0f14;
108
+ --xfc-bg-elevated: #151b24;
109
+ --xfc-bg-muted: #1a222d;
110
+ --xfc-bg-disabled: #2d3a4a;
98
111
 
99
- --xfc-fg: #e8eef4;
100
- --xfc-fg-strong: #ffffff;
101
- --xfc-fg-label: #dce4ec;
102
- --xfc-fg-muted: #8b9aab;
103
- --xfc-fg-placeholder: #5c6b7a;
112
+ --xfc-fg: #e2e8f0;
113
+ --xfc-fg-strong: #f8fafc;
114
+ --xfc-fg-label: #cbd5e1;
115
+ --xfc-fg-muted: #94a3b8;
116
+ --xfc-fg-placeholder: #64748b;
104
117
  --xfc-fg-icon: #a8b8c8;
105
118
 
106
- --xfc-border: #2a3440;
107
- --xfc-border-strong: #3d4a5c;
108
- --xfc-border-header: #2f3d4d;
119
+ --xfc-border: #2d3a4a;
120
+ --xfc-border-strong: #3d4f66;
121
+ --xfc-border-header: #334155;
109
122
 
110
- --xfc-accent: #5b8aff;
111
- --xfc-accent-hover: #7aa3ff;
112
- --xfc-accent-fg: #0f1419;
123
+ --xfc-accent: #818cf8;
124
+ --xfc-accent-hover: #a5b4fc;
125
+ --xfc-accent-fg: #0f172a;
126
+ --xfc-accent-soft: color-mix(in srgb, var(--xfc-accent) 18%, transparent);
113
127
 
114
- --xfc-danger: #ff6b6b;
128
+ --xfc-danger: #fb7185;
115
129
  --xfc-success: #34d399;
116
130
  --xfc-success-bg: #064e3b;
117
131
  --xfc-warning: #fbbf24;
@@ -120,11 +134,17 @@ html[data-theme='dark'] {
120
134
  --xfc-toast-info-bg: #1e293b;
121
135
  --xfc-toast-success-bg: #047857;
122
136
  --xfc-toast-warn-bg: #b45309;
123
- --xfc-toast-error-bg: #dc2626;
124
- --xfc-toast-shadow: 0 2px 12px rgba(0, 0, 0, 0.45);
137
+ --xfc-toast-error-bg: #be123c;
138
+ --xfc-toast-shadow: 0 12px 40px rgba(0, 0, 0, 0.45), 0 4px 12px rgba(0, 0, 0, 0.3);
139
+ --xfc-toast-border: 1px solid rgba(255, 255, 255, 0.08);
140
+
141
+ --xfc-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.35);
142
+ --xfc-shadow-sm: 0 4px 16px rgba(0, 0, 0, 0.25), 0 1px 3px rgba(0, 0, 0, 0.2);
143
+ --xfc-shadow-md: 0 12px 32px rgba(0, 0, 0, 0.35), 0 4px 12px rgba(0, 0, 0, 0.2);
144
+ --xfc-shadow-lg: 0 28px 56px rgba(0, 0, 0, 0.45), 0 12px 24px rgba(0, 0, 0, 0.25);
125
145
 
126
- --xfc-input-shadow: 0 4px 24px rgba(0, 0, 0, 0.25);
127
- --xfc-card-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
146
+ --xfc-input-shadow: var(--xfc-shadow-xs);
147
+ --xfc-card-shadow: var(--xfc-shadow-sm);
128
148
 
129
- --xfc-focus-ring: 0 0 0 3px color-mix(in srgb, var(--xfc-accent) 45%, transparent);
149
+ --xfc-focus-ring: 0 0 0 3px color-mix(in srgb, var(--xfc-accent) 40%, transparent);
130
150
  }
@@ -0,0 +1,111 @@
1
+ # Components reference
2
+
3
+ 소스는 모두 `packages/front-core/src/` 기준. 빌드 산출물은 `dist/`에 동일 구조로 복사된다.
4
+
5
+ ## 레이아웃·표면
6
+
7
+ | 컴포넌트 | 소스 | 요약 |
8
+ |----------|------|------|
9
+ | `Paper` | [atoms/Paper.tsx](../src/components/atoms/Paper.tsx) | `elevation` 0~4, `variant` filled \| outlined |
10
+ | `Card` | [atoms/Card.tsx](../src/components/atoms/Card.tsx) | `title`, `footer`, `interactive` |
11
+ | `Box` | [atoms/Box.tsx](../src/components/atoms/Box.tsx) | `as`, `padding` |
12
+ | `Stack` | [atoms/Stack.tsx](../src/components/atoms/Stack.tsx) | flex gap·align·justify |
13
+
14
+ ### Paper
15
+
16
+ ```tsx
17
+ import { Paper, Stack, Text } from '@xfilecom/front-core';
18
+
19
+ <Paper elevation={2} style={{ padding: 'var(--xfc-space-lg)' }}>
20
+ <Text variant="body">Elevation 2 surface</Text>
21
+ </Paper>
22
+
23
+ <Paper variant="outlined" style={{ padding: 'var(--xfc-space-md)' }}>
24
+ Border only
25
+ </Paper>
26
+ ```
27
+
28
+ ### Card
29
+
30
+ ```tsx
31
+ import { Card, Button } from '@xfilecom/front-core';
32
+
33
+ <Card
34
+ title="제목"
35
+ footer={<Button variant="primary">확인</Button>}
36
+ >
37
+ 본문
38
+ </Card>
39
+ ```
40
+
41
+ ## 타이포
42
+
43
+ | 컴포넌트 | 소스 |
44
+ |----------|------|
45
+ | `Text` | [atoms/Text.tsx](../src/components/atoms/Text.tsx) |
46
+
47
+ `variant`: `title` | `appbar` | `section` | `subtitle` | `lead` | `body` | `muted` | `small` | `label` | `labelBlock` | `accent`
48
+
49
+ ```tsx
50
+ <Text variant="section">섹션 제목</Text>
51
+ <Text variant="lead">리드 문단</Text>
52
+ ```
53
+
54
+ ## 액션
55
+
56
+ | 컴포넌트 | 소스 |
57
+ |----------|------|
58
+ | `Button` | [atoms/Button.tsx](../src/components/atoms/Button.tsx) |
59
+ | `Badge` | [atoms/Badge.tsx](../src/components/atoms/Badge.tsx) |
60
+
61
+ `Button` variants: `primary` | `secondary` | `outline` | `muted` | `ghost`
62
+ `Badge` tone: `neutral` | `accent` | `success` | `warn` | `danger`
63
+
64
+ ## 폼
65
+
66
+ | 컴포넌트 | 소스 |
67
+ |----------|------|
68
+ | `Input` | [atoms/Input.tsx](../src/components/atoms/Input.tsx) |
69
+ | `Textarea` | [atoms/Textarea.tsx](../src/components/atoms/Textarea.tsx) |
70
+ | `Select` | [atoms/Select.tsx](../src/components/atoms/Select.tsx) |
71
+ | `Checkbox` | [atoms/Checkbox.tsx](../src/components/atoms/Checkbox.tsx) |
72
+ | `Field` | [atoms/Field.tsx](../src/components/atoms/Field.tsx) |
73
+
74
+ `Field`는 **단일** 자식에 `id` / `aria-describedby` / `aria-invalid`를 병합한다. `htmlFor`와 자식 `id`를 일치시킨다.
75
+
76
+ ## 피드백
77
+
78
+ | 컴포넌트 | 소스 |
79
+ |----------|------|
80
+ | `Toast` / `ToastList` | [atoms/Toast.tsx](../src/components/atoms/Toast.tsx) |
81
+ | `ToastSeverityIcon` | 동일 파일 |
82
+ | `InlineErrorList` | [atoms/InlineErrorList.tsx](../src/components/atoms/InlineErrorList.tsx) |
83
+ | `LoadingOverlay` | [atoms/LoadingOverlay.tsx](../src/components/atoms/LoadingOverlay.tsx) |
84
+
85
+ `ToastList`의 `ToastEntry`에 `dismissible?: boolean`으로 행마다 닫기 버튼을 제어한다.
86
+
87
+ ## 오버레이
88
+
89
+ | 컴포넌트 | 소스 |
90
+ |----------|------|
91
+ | `Dialog` | [overlays/Dialog.tsx](../src/components/overlays/Dialog.tsx) |
92
+ | `ConfirmDialog` | [overlays/ConfirmDialog.tsx](../src/components/overlays/ConfirmDialog.tsx) |
93
+ | `BottomSheet` | [overlays/BottomSheet.tsx](../src/components/overlays/BottomSheet.tsx) |
94
+
95
+ 커스텀 모달에 동일한 스크롤 잠금·포커스 트랩을 쓰려면 [overlayHooks.ts](../src/components/overlays/overlayHooks.ts)의 `useFocusTrap`, `useBodyScrollLock`, `useEscapeKey`를 루트에서 import 한다.
96
+
97
+ ## CSS
98
+
99
+ | 파일 | 용도 |
100
+ |------|------|
101
+ | [tokens.css](../src/tokens.css) | `:root` 변수, 다크 프리셋 |
102
+ | [base.css](../src/base.css) | `.xfc-*` atoms 스타일 |
103
+
104
+ ## TypeScript
105
+
106
+ `tokenVars`로 CSS 변수 이름을 문자열 상수로 참조할 수 있다. ([index.ts](../src/index.ts))
107
+
108
+ ```ts
109
+ import { tokenVars } from '@xfilecom/front-core';
110
+ document.body.style.setProperty(tokenVars.colorAccent, '#143c38');
111
+ ```
@@ -0,0 +1,32 @@
1
+ # xframe design system vs Paper-style UI kits
2
+
3
+ `@xfilecom/front-core`는 **브라우저 전용**이며 React Native(`react-native-paper`)나 MUI(`@mui/material`)와 동일한 패키지는 아니다. 대신 **같은 UX 패턴**(표면·높이·토큰)을 웹 CSS 변수 + 얇은 React atoms로 옮긴다.
4
+
5
+ ## 한눈에 비교
6
+
7
+ | 개념 | MUI (`Paper`) | react-native-paper (`Surface`) | front-core |
8
+ |------|---------------|---------------------------------|------------|
9
+ | 표면/카드 | `Paper` + `elevation` | `Surface` + `elevation` | [`Paper`](../src/components/atoms/Paper.tsx) `elevation={0…4}` |
10
+ | 구조형 카드 | `Card` | `Card` | [`Card`](../src/components/atoms/Card.tsx) (title/footer 슬롯) |
11
+ | 버튼 | `Button` | `Button` | [`Button`](../src/components/atoms/Button.tsx) variants |
12
+ | 다이얼로그 | `Dialog` | `Portal`+`Dialog` | [`Dialog`](../src/components/overlays/Dialog.tsx) |
13
+ | 스낵바/토스트 | `Snackbar` | `Snackbar` | [`Toast`](../src/components/atoms/Toast.tsx) / [`ToastList`](../src/components/atoms/Toast.tsx) |
14
+ | 테마 | `ThemeProvider` | `PaperProvider` | `:root` / `html.dark` **CSS 변수** + 선택적 `xfc-theme.css` |
15
+
16
+ ## Elevation (높이)
17
+
18
+ - **MUI**: 그림자 깊이가 elevation 숫자에 대응.
19
+ - **front-core**: `Paper`의 `elevation` 0~4가 `--xfc-shadow-xs` … `--xfc-shadow-lg`에 매핑된다. `variant="outlined"`는 그림자 없이 테두리만 (`outlined` variant).
20
+
21
+ ## 색 (Color roles)
22
+
23
+ Material 3의 *primary / on-primary / surface* 같은 **역할 이름**은 npm 타입으로 강제하지 않고, `--xfc-accent`, `--xfc-bg-elevated`, `--xfc-fg` 등 **토큰 이름**으로 맞춘다. 다크 모드는 `html.dark` 또는 `data-theme="dark"` 한 번에 같은 변수를 덮어쓴다.
24
+
25
+ ## 언제 MUI / RN Paper를 쓰고 언제 front-core를 쓰나
26
+
27
+ - **이미 MUI 생태계**를 쓰는 앱 → MUI 유지.
28
+ - **xframe 스캐폴드**, 번들·의존성을 가볍게, Nest와 무관한 **순수 웹 atoms** → `front-core`.
29
+
30
+ ## 다음 문서
31
+
32
+ - [COMPONENTS.md](./COMPONENTS.md) — 컴포넌트별 props·소스 경로·예제 코드
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfilecom/front-core",
3
- "version": "0.2.26",
3
+ "version": "0.2.28",
4
4
  "description": "Shared design tokens, base CSS, and atomic React components (browser-only; no Nest dependency)",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -21,7 +21,9 @@
21
21
  }
22
22
  },
23
23
  "files": [
24
- "dist"
24
+ "dist",
25
+ "docs",
26
+ "README.md"
25
27
  ],
26
28
  "scripts": {
27
29
  "prepublishOnly": "npm run build",