@conscience-technology/ctds 0.1.14 → 0.2.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 (64) hide show
  1. package/assets/components/AppFrame/AppFrame.module.css +6 -0
  2. package/assets/components/{AttachmentChip/AttachmentChip.module.css → AttachmentItem/AttachmentItem.module.css} +5 -8
  3. package/assets/components/{AttachmentChip/AttachmentChip.tsx → AttachmentItem/AttachmentItem.tsx} +21 -15
  4. package/assets/components/AttachmentItem/index.ts +6 -0
  5. package/assets/components/Button/Button.module.css +11 -2
  6. package/assets/components/ChatInput/ChatInput.tsx +2 -2
  7. package/assets/components/DatePicker/Calendar.tsx +19 -1
  8. package/assets/components/Drawer/Drawer.tsx +120 -39
  9. package/assets/components/Drawer/index.ts +9 -1
  10. package/assets/components/{MessageAttachment/MessageAttachment.module.css → GeneratedImage/GeneratedImage.module.css} +3 -2
  11. package/assets/components/{MessageAttachment/MessageAttachment.tsx → GeneratedImage/GeneratedImage.tsx} +8 -8
  12. package/assets/components/GeneratedImage/index.ts +5 -0
  13. package/assets/components/Icon/Icon.tsx +4 -1
  14. package/assets/components/Modal/Modal.tsx +114 -33
  15. package/assets/components/Modal/index.ts +12 -2
  16. package/assets/components/Spinner/lottieManifest.ts +8 -8
  17. package/assets/components/Switch/Switch.tsx +6 -0
  18. package/assets/components/TableHeader/TableHeader.module.css +29 -20
  19. package/assets/components/TableRow/TableRow.module.css +7 -5
  20. package/assets/components/TableRowEdit/TableRowEdit.module.css +10 -3
  21. package/assets/components/TableRowEdit/TableRowEdit.tsx +7 -2
  22. package/assets/components/Toast/Toast.tsx +19 -0
  23. package/assets/components/_data.ts +39 -16
  24. package/assets/components/app-shell.css +17 -0
  25. package/assets/components/index.ts +77 -0
  26. package/assets/components/reset.css +63 -0
  27. package/assets/components/themeBootScript.ts +40 -0
  28. package/assets/components/tokens.css +68 -57
  29. package/assets/prompts/GUIDE.md +1 -1
  30. package/assets/prompts/PRINCIPLES.md +9 -0
  31. package/assets/prompts/axes/cascade.md +49 -0
  32. package/assets/prompts/axes/mode.md +3 -1
  33. package/assets/prompts/components/_variants.md +84 -0
  34. package/assets/prompts/components/{attachment-chip.md → attachment-item.md} +12 -10
  35. package/assets/prompts/components/attachment-thumb.md +2 -2
  36. package/assets/prompts/components/avatar-upload.md +53 -0
  37. package/assets/prompts/components/bubble.md +2 -2
  38. package/assets/prompts/components/chat-input.md +7 -8
  39. package/assets/prompts/components/code-block.md +2 -2
  40. package/assets/prompts/components/disclosure.md +47 -0
  41. package/assets/prompts/components/file-icon.md +3 -3
  42. package/assets/prompts/components/generated-image.md +38 -0
  43. package/assets/prompts/components/message-quote.md +2 -2
  44. package/assets/prompts/components/message-sources.md +5 -4
  45. package/assets/prompts/components/message-table.md +9 -6
  46. package/assets/prompts/components/onboarding.md +43 -0
  47. package/assets/prompts/components/quotation-bar.md +1 -1
  48. package/assets/prompts/components/sidebar-split.md +70 -0
  49. package/assets/prompts/components/sidebar.md +2 -0
  50. package/assets/prompts/components/time-picker.md +69 -0
  51. package/assets/prompts/components/toolbar.md +69 -0
  52. package/assets/prompts/components/user-message.md +2 -2
  53. package/package.json +80 -3
  54. package/src/commands/init.js +53 -23
  55. package/src/utils/deps.js +23 -17
  56. package/src/utils/patch.js +155 -0
  57. package/assets/components/AttachmentChip/index.ts +0 -6
  58. package/assets/components/Calendar/Calendar.module.css +0 -163
  59. package/assets/components/Calendar/Calendar.tsx +0 -323
  60. package/assets/components/Calendar/index.ts +0 -2
  61. package/assets/components/MessageAttachment/index.ts +0 -5
  62. package/assets/prompts/components/calendar.md +0 -116
  63. package/assets/prompts/components/form-field.md +0 -74
  64. package/assets/prompts/components/message-attachment.md +0 -40
@@ -0,0 +1,77 @@
1
+ /**
2
+ * CTDS 컴포넌트 루트 배럴.
3
+ * 소비자는 `import { Button, Modal } from "@/components/ctds/ctds"` 한 줄로 접근.
4
+ * 각 하위 폴더가 자체 index.ts로 공개 API를 명시한다 — 이 파일은 그것들의 합.
5
+ *
6
+ * 유지: 신규 컴포넌트 폴더를 추가하면 아래 한 줄 추가. 알파벳순.
7
+ */
8
+
9
+ export * from "./AIMessage";
10
+ export * from "./AppFrame";
11
+ export * from "./AttachmentItem";
12
+ export * from "./AttachmentThumb";
13
+ export * from "./Avatar";
14
+ export * from "./AvatarUpload";
15
+ export * from "./Badge";
16
+ export * from "./Breadcrumb";
17
+ export * from "./Bubble";
18
+ export * from "./Button";
19
+ export * from "./Calendar";
20
+ export * from "./Callout";
21
+ export * from "./ChatInput";
22
+ export * from "./Checkbox";
23
+ export * from "./Chip";
24
+ export * from "./ChoiceCard";
25
+ export * from "./CodeBlock";
26
+ export * from "./ContextMenu";
27
+ export * from "./DatePicker";
28
+ export * from "./Disclosure";
29
+ export * from "./Drawer";
30
+ export * from "./DropdownMenu";
31
+ export * from "./EmptyState";
32
+ export * from "./EventChip";
33
+ export * from "./FileIcon";
34
+ export * from "./FilterButton";
35
+ export * from "./GalleryItem";
36
+ export * from "./GeneratedImage";
37
+ export * from "./Icon";
38
+ export * from "./IconButton";
39
+ export * from "./IdentityButton";
40
+ export * from "./Input";
41
+ export * from "./ListItem";
42
+ export * from "./MenuItem";
43
+ export * from "./MessageActions";
44
+ export * from "./MessageQuote";
45
+ export * from "./MessageSources";
46
+ export * from "./MessageState";
47
+ export * from "./MessageTable";
48
+ export * from "./MetricCard";
49
+ export * from "./Modal";
50
+ export * from "./Onboarding";
51
+ export * from "./Pagination";
52
+ export * from "./ProgressBar";
53
+ export * from "./QuotationBar";
54
+ export * from "./Radio";
55
+ export * from "./Select";
56
+ export * from "./SelectableCard";
57
+ export * from "./Shortcut";
58
+ export * from "./Sidebar";
59
+ export * from "./SidebarSplit";
60
+ export * from "./Slider";
61
+ export * from "./Spinner";
62
+ export * from "./SplitButton";
63
+ export * from "./Stepper";
64
+ export * from "./Switch";
65
+ export * from "./TableCell";
66
+ export * from "./TableHeader";
67
+ export * from "./TableRow";
68
+ export * from "./TableRowEdit";
69
+ export * from "./TableRowSelect";
70
+ export * from "./Tabs";
71
+ export * from "./Textarea";
72
+ export * from "./TimePicker";
73
+ export * from "./Toast";
74
+ export * from "./Toolbar";
75
+ export * from "./Tooltip";
76
+ export * from "./Topbar";
77
+ export * from "./UserMessage";
@@ -0,0 +1,63 @@
1
+ /* ============================================================
2
+ CTDS reset — opt-in global element reset.
3
+ tokens.css는 CSS 변수만 노출한다. 이 파일은 그 토큰을 소비해
4
+ body·heading·anchor 같은 네이티브 element에 CTDS 룩을 입히는
5
+ 전역 reset. 소비자 프로젝트에서 CTDS 외 영역이 있으면 이 파일을
6
+ import하지 말고 섹션 단위로 .ctds-root 유틸 클래스 등을 조립해라.
7
+
8
+ 의존: tokens.css가 선행 import 돼야 한다.
9
+ 토글: 컴포넌트가 document.body/documentElement에 토글하는 side-effect
10
+ 스타일도 여기 둔다. 각 블록에 "어느 컴포넌트가 토글하는지" 주석.
11
+ ============================================================ */
12
+
13
+ *, *::before, *::after { box-sizing: border-box; }
14
+
15
+ html {
16
+ scroll-behavior: smooth;
17
+ scroll-padding-top: calc(var(--header-h) + var(--space-4));
18
+ }
19
+
20
+ body {
21
+ margin: 0;
22
+ background: var(--bg);
23
+ color: var(--fg);
24
+ font-family: var(--font-sans);
25
+ font-size: var(--text-base);
26
+ line-height: var(--lh-base);
27
+ /* 전역 자간 default — tight(-0.02em). letter-spacing은 상속 property라
28
+ 모든 하위 요소가 기본으로 받고, 필요한 자리만 override. "텍스트 쓰면
29
+ 자간이 자동으로 따라오는" 구조. */
30
+ letter-spacing: var(--tracking-tight);
31
+ -webkit-font-smoothing: antialiased;
32
+ text-rendering: optimizeLegibility;
33
+ transition: background var(--duration-base) var(--ease-out),
34
+ color var(--duration-base) var(--ease-out);
35
+ }
36
+
37
+ a { color: inherit; text-decoration: none; }
38
+ h1, h2, h3, h4 {
39
+ margin: 0;
40
+ font-weight: var(--font-weight-medium);
41
+ }
42
+ p { margin: 0; }
43
+ code {
44
+ font-family: var(--font-mono);
45
+ font-size: 0.9em;
46
+ background: var(--bg-muted);
47
+ padding: var(--space-0_5) var(--space-1_5);
48
+ border-radius: var(--radius-xs);
49
+ color: var(--fg);
50
+ }
51
+
52
+ /* ============================================================
53
+ 컴포넌트 전역 side-effect
54
+ CSS Modules의 :global 로 둘 수 없는, 라이브러리 컴포넌트가 document.body나
55
+ document.documentElement에 토글하는 클래스/속성에 대한 스타일.
56
+ 토큰 whitelist 아님. 각 항목은 어느 컴포넌트가 설정하는지 명시.
57
+ ============================================================ */
58
+
59
+ /* Textarea.tsx — drag resize 중 전역 커서 강제. body.textarea-resizing 클래스 토글. */
60
+ body.textarea-resizing {
61
+ cursor: ns-resize !important;
62
+ user-select: none;
63
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * CTDS 테마 blocking boot script.
3
+ *
4
+ * Next.js SSR이 HTML을 보내기 전에 <head> 안에서 **동기적으로** 실행돼야 한다.
5
+ * 하이드레이션 전에 localStorage/prefers-color-scheme을 읽고 <html>에 모든 축
6
+ * data-* 어트리뷰트를 세팅 → 첫 페인트부터 올바른 모드로 뜸(FOUC 방지).
7
+ *
8
+ * 사용:
9
+ * <html suppressHydrationWarning>
10
+ * <head>
11
+ * <script dangerouslySetInnerHTML={{ __html: themeBootScript() }} />
12
+ * </head>
13
+ * <body><ThemeController>{children}</ThemeController></body>
14
+ * </html>
15
+ *
16
+ * 데모 앱(web/app/layout.tsx) + ctds-cli init이 패치하는 소비자 layout.tsx
17
+ * 양쪽이 이 함수 하나를 쓴다. ThemeController와 로직이 drift 나지 않도록 single-source.
18
+ *
19
+ * 옵션:
20
+ * onboardingPath — 온보딩 라우트는 설정 전 단계라 다크 선호도 무시하고 라이트 고정.
21
+ * 기본값 "/onboarding". 소비자가 다른 경로면 넘겨서 교체.
22
+ */
23
+ export function themeBootScript({ onboardingPath = "/onboarding" }: { onboardingPath?: string } = {}): string {
24
+ // IIFE. try/catch로 서버 렌더 환경·시크릿 모드·localStorage 접근 실패 전부 흡수.
25
+ // 축이 늘면 아래 배열에 (storageKey, attrName, default)를 추가.
26
+ return `(function(){try{
27
+ var ONBOARD=${JSON.stringify(onboardingPath)};
28
+ var isOnboarding=location.pathname===ONBOARD||location.pathname.indexOf(ONBOARD+'/')===0;
29
+ var mode=isOnboarding?'light':(localStorage.getItem('ctds-mode')||(matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'));
30
+ document.documentElement.setAttribute('data-mode',mode);
31
+ var axes=[['ctds-palette','data-palette','standard'],['ctds-shape','data-shape','standard'],['ctds-density','data-density','default'],['ctds-motion','data-motion','standard'],['ctds-icon-family','data-icon-family','phosphor'],['ctds-lang','data-lang','ko'],['ctds-focus-ring','data-focus-ring','off'],['ctds-voice','data-voice','neutral']];
32
+ for(var i=0;i<axes.length;i++){var k=axes[i][0],a=axes[i][1],d=axes[i][2];var v=localStorage.getItem(k)||d;document.documentElement.setAttribute(a,v);if(a==='data-lang')document.documentElement.lang=v;}
33
+ try{localStorage.removeItem('ctds-accent');}catch(e){}
34
+ document.documentElement.removeAttribute('data-accent');
35
+ try{localStorage.removeItem('ctds-typography');}catch(e){}
36
+ document.documentElement.removeAttribute('data-typography');
37
+ var hue=localStorage.getItem('ctds-hue');if(hue)document.documentElement.style.setProperty('--hue',hue);
38
+ var ah=localStorage.getItem('ctds-accent-hue');if(ah)document.documentElement.style.setProperty('--secondary-hue',ah);
39
+ }catch(e){}})();`;
40
+ }
@@ -242,9 +242,11 @@
242
242
  --bg-muted: oklch(0.955 var(--tone-chroma) var(--tone-hue));
243
243
  --fg-strong: oklch(0.12 var(--tone-chroma) var(--tone-hue));
244
244
  --fg: oklch(0.22 var(--tone-chroma) var(--tone-hue));
245
- --fg-muted: oklch(0.50 var(--tone-chroma) var(--tone-hue));
246
- --fg-subtle: oklch(0.68 var(--tone-chroma) var(--tone-hue));
247
- --fg-disabled: oklch(0.80 var(--tone-chroma) var(--tone-hue));
245
+ /* muted/subtle/disabled은 fg 톤의 알파 — 색 있는 배경 위에서도 자연스럽게 섞임.
246
+ 라이트: 어두운 잉크(≈0.18)를 낮은 알파로 얹음. */
247
+ --fg-muted: oklch(0.18 var(--tone-chroma) var(--tone-hue) / 0.60);
248
+ --fg-subtle: oklch(0.18 var(--tone-chroma) var(--tone-hue) / 0.42);
249
+ --fg-disabled: oklch(0.18 var(--tone-chroma) var(--tone-hue) / 0.26);
248
250
  /* border는 배경 대비 알파 기반 hairline — 겹치면 자연스럽게 짙어짐.
249
251
  라이트 모드: 어두운 잉크 톤(≈rgb(18,24,38))을 낮은 알파로 얹음. */
250
252
  --border: oklch(0.18 var(--tone-chroma) var(--tone-hue) / 0.08);
@@ -577,6 +579,62 @@ html[data-focus-ring="off"] {
577
579
  }
578
580
  }
579
581
 
582
+ /* 접근성 우선 — OS 하이콘트라스트 모드(macOS Increase Contrast, Windows High Contrast)
583
+ 를 쓰는 사용자에겐 border·text 대비를 상향한다. 축 값은 안 건드리고 토큰만 override —
584
+ 컴포넌트 코드는 변경 없이 자동 적용.
585
+ 라이트: alpha 0.08/0.14 → 0.42/0.6, muted fg 0.50 → 0.32.
586
+ 다크: alpha 0.08/0.16 → 0.50/0.72, muted fg 0.65 → 0.80. */
587
+ @media (prefers-contrast: more) {
588
+ :root {
589
+ --border: oklch(0.18 var(--tone-chroma) var(--tone-hue) / 0.42);
590
+ --border-strong: oklch(0.18 var(--tone-chroma) var(--tone-hue) / 0.60);
591
+ --fg-muted: oklch(0.18 var(--tone-chroma) var(--tone-hue) / 0.82);
592
+ --fg-subtle: oklch(0.18 var(--tone-chroma) var(--tone-hue) / 0.66);
593
+ }
594
+ :root[data-mode="dark"] {
595
+ --border: oklch(0.95 var(--tone-chroma) var(--tone-hue) / 0.50);
596
+ --border-strong: oklch(0.95 var(--tone-chroma) var(--tone-hue) / 0.72);
597
+ --fg-muted: oklch(0.95 var(--tone-chroma) var(--tone-hue) / 0.80);
598
+ --fg-subtle: oklch(0.95 var(--tone-chroma) var(--tone-hue) / 0.60);
599
+ }
600
+ }
601
+
602
+ /* 레거시 브라우저 폴백 — oklch/color-mix 미지원 환경(Safari <16.4, Chrome <119,
603
+ Firefox <113)에서 색상이 검정으로 깨지지 않게 중립 회색 테마로 폴백.
604
+ Palette·Hue 축은 비활성. 다크/라이트는 각각 색만 뒤집음. */
605
+ @supports not (color: oklch(0 0 0)) {
606
+ :root {
607
+ --bg: #ffffff;
608
+ --bg-subtle: #f6f6f6;
609
+ --bg-muted: #ececec;
610
+ --bg-elev-1: #ffffff;
611
+ --bg-elev-2: #ffffff;
612
+ --fg: #1a1a1a;
613
+ --fg-strong: #0a0a0a;
614
+ --fg-muted: #666666;
615
+ --fg-subtle: #999999;
616
+ --fg-disabled: #c0c0c0;
617
+ --border: rgba(0, 0, 0, 0.08);
618
+ --border-strong: rgba(0, 0, 0, 0.14);
619
+ --primary: #1a1a1a;
620
+ }
621
+ :root[data-mode="dark"] {
622
+ --bg: #0f0f0f;
623
+ --bg-subtle: #1a1a1a;
624
+ --bg-muted: #262626;
625
+ --bg-elev-1: #171717;
626
+ --bg-elev-2: #1f1f1f;
627
+ --fg: #f0f0f0;
628
+ --fg-strong: #ffffff;
629
+ --fg-muted: #a0a0a0;
630
+ --fg-subtle: #7a7a7a;
631
+ --fg-disabled: #4a4a4a;
632
+ --border: rgba(255, 255, 255, 0.08);
633
+ --border-strong: rgba(255, 255, 255, 0.16);
634
+ --primary: #f0f0f0;
635
+ }
636
+ }
637
+
580
638
  /* ── Font 축 — Language ─────────────────────────────────────
581
639
  슬롯(--font-normal/code)의 스택을 언어별로 스왑.
582
640
  프로젝트마다 여기서 바꾸면 됨. */
@@ -628,9 +686,10 @@ html[data-focus-ring="off"] {
628
686
  --bg-muted: oklch(0.31 var(--tone-chroma) var(--tone-hue));
629
687
  --fg-strong: oklch(1.0 var(--tone-chroma) var(--tone-hue));
630
688
  --fg: oklch(0.95 var(--tone-chroma) var(--tone-hue));
631
- --fg-muted: oklch(0.65 var(--tone-chroma) var(--tone-hue));
632
- --fg-subtle: oklch(0.48 var(--tone-chroma) var(--tone-hue));
633
- --fg-disabled: oklch(0.35 var(--tone-chroma) var(--tone-hue));
689
+ /* 다크: 밝은 잉크(≈0.95)를 낮은 알파로 얹음. */
690
+ --fg-muted: oklch(0.95 var(--tone-chroma) var(--tone-hue) / 0.60);
691
+ --fg-subtle: oklch(0.95 var(--tone-chroma) var(--tone-hue) / 0.40);
692
+ --fg-disabled: oklch(0.95 var(--tone-chroma) var(--tone-hue) / 0.24);
634
693
  /* 다크 모드: fg 톤(밝은 잉크)을 낮은 알파로 얹음. */
635
694
  --border: oklch(0.95 var(--tone-chroma) var(--tone-hue) / 0.08);
636
695
  --border-strong: oklch(0.95 var(--tone-chroma) var(--tone-hue) / 0.16);
@@ -705,57 +764,9 @@ html[data-focus-ring="off"] {
705
764
  [data-mode="dark"][data-palette="calm"] { --status-chroma: 0.17; --status-lightness: 0.70; }
706
765
  :root[data-mode="dark"][data-palette="pastel"],
707
766
  [data-mode="dark"][data-palette="pastel"] { --status-chroma: 0.16; --status-lightness: 0.72; }
708
-
709
- *, *::before, *::after { box-sizing: border-box; }
710
-
711
- html {
712
- scroll-behavior: smooth;
713
- scroll-padding-top: calc(var(--header-h) + 16px);
714
- }
715
-
716
- body {
717
- margin: 0;
718
- background: var(--bg);
719
- color: var(--fg);
720
- font-family: var(--font-sans);
721
- font-size: var(--text-base);
722
- line-height: var(--lh-base);
723
- /* 전역 자간 default — tight(-0.02em). letter-spacing은 상속 property라
724
- 모든 하위 요소가 기본으로 받고, 필요한 자리만 override. "텍스트 쓰면
725
- 자간이 자동으로 따라오는" 구조. */
726
- letter-spacing: var(--tracking-tight);
727
- -webkit-font-smoothing: antialiased;
728
- text-rendering: optimizeLegibility;
729
- transition: background var(--duration-base) var(--ease-out),
730
- color var(--duration-base) var(--ease-out);
731
- }
732
-
733
- a { color: inherit; text-decoration: none; }
734
- h1, h2, h3, h4 {
735
- margin: 0;
736
- font-weight: 600;
737
- }
738
- p { margin: 0; }
739
- code {
740
- font-family: var(--font-mono);
741
- font-size: 0.9em;
742
- background: var(--bg-muted);
743
- padding: 2px 6px;
744
- border-radius: var(--radius-xs);
745
- color: var(--fg);
746
- }
747
767
  /* === AXES END === */
748
768
 
749
- /* ============================================================
750
- 컴포넌트 전역 side-effect
751
- CSS Modules의 :global 로 둘 수 없는, 라이브러리 컴포넌트가 document.body나
752
- document.documentElement에 토글하는 클래스/속성에 대한 스타일.
753
- 토큰 whitelist 아님. 각 항목은 어느 컴포넌트가 설정하는지 명시.
754
- ============================================================ */
755
-
756
- /* Textarea.tsx — drag resize 중 전역 커서 강제. body.textarea-resizing 클래스 토글. */
757
- body.textarea-resizing {
758
- cursor: ns-resize !important;
759
- user-select: none;
760
- }
769
+ /* 전역 element reset, 폰트, 컴포넌트 side-effect 스타일은 ./reset.css로 분리.
770
+ tokens.css는 CSS 변수(토큰·축)만 노출한다. 소비자 프로젝트에서 reset을
771
+ 원하면 reset.css를 별도로 import. 데모 앱은 app/globals.css가 import한다. */
761
772
 
@@ -44,7 +44,7 @@
44
44
  - <명령형 규칙>
45
45
 
46
46
  ### Don't
47
- - <하드 리젝 사유>
47
+ - <하드 리젝 사유. 강도가 애매하면 bullet 앞에 `[HARD]` 또는 `[SOFT]` 명시 — PRINCIPLES §4a 참고>
48
48
 
49
49
  ### Fallback
50
50
  <규칙이 애매할 때의 기본값. 없으면 "규칙대로 판단 불가한 경우 사용자에게 확인".>
@@ -58,6 +58,15 @@ CTDS는 9개 축(Mode, Palette, Hue, Shape, Icon, Density, Motion, Font, Voice)
58
58
  - 사용자 지시가 접근성이나 PRINCIPLES를 어기려 할 땐 따르지 않고 사용자에게 확인 (§8 참고)
59
59
  - 같은 계층 내 충돌은 §8에 따라 사용자에게 확인
60
60
 
61
+ ### 4a. Don't 강도 레벨 (선택)
62
+
63
+ 컴포넌트 `Don't` 블록 안에서 **강도 레벨**을 선택적으로 명시할 수 있다. 신규 Don't를 쓰거나 기존 Don't를 수정할 때 애매하면 아래 둘 중 하나를 bullet 맨 앞에 붙인다 — 명시가 없으면 "SOFT 수준으로 해석"이 기본 (사용자 지시로 깨도 됨).
64
+
65
+ - `[HARD]` — 어기면 CTDS 보증 밖. 사용자 지시가 요청해도 §8에 따라 **먼저 확인한다**. 예: "primary 2개 금지", "하드코딩 금지", "접근성 라벨 생략 금지".
66
+ - `[SOFT]` — 밀도·톤·관행 유지용. 사용자가 특별한 이유로 요청하면 따를 수 있고, 추후 되돌리기 쉬움. 예: "section 사이 gap은 space-6 선호".
67
+
68
+ 레벨은 Don't에만 쓴다 (Do는 긍정형이라 레벨 불필요). AI는 신규 Don't 작성 시 **HARD인지 SOFT인지 애매하면 사용자에게 확인**하고 결정 — 임의로 HARD 안 붙임.
69
+
61
70
  ---
62
71
 
63
72
  ## 5. 접근성이 축보다 우선한다
@@ -0,0 +1,49 @@
1
+ # Cascade
2
+
3
+ ### 원칙
4
+ 축은 `html` 또는 중간 컨테이너에 `data-*` 어트리뷰트로 선언되고, CSS 변수가
5
+ 서브트리로 상속되는 방식으로 동작한다. 축 하나(예: Mode)만 뒤집으면 다른 축
6
+ (Palette·Shape·Density·Motion·Icon·Lang·Focus·Voice)은 **부모 값을 그대로 상속**한다.
7
+
8
+ ### Do
9
+ - 국소 스코프 바꾸기는 `data-mode="dark"` 처럼 **바꾸는 축 하나만** 자식 컨테이너에 선언한다. 나머지는 자동 상속.
10
+ - 자식 영역의 축 변경이 손주까지 전파되길 원할 때는 **아무 것도 안 한다** — CSS 변수 상속이 공짜로 처리한다.
11
+ - 변경한 축에 영향받는 토큰 셋만 바뀐다는 사실을 명시적으로 활용 (예: 다크 카드 안에서도 Shape는 그대로).
12
+ - 디버깅할 땐 DevTools에서 `html` 또는 중간 컨테이너의 `data-*` 값을 눈으로 확인하는 걸 1순위로.
13
+
14
+ ### Don't
15
+ - 자식 컨테이너에 쓸모없는 전체 축 셋을 다시 선언하지 않는다. 예: `<div data-mode="dark" data-palette="standard" data-shape="standard" ...>` → `<div data-mode="dark">` 로 충분.
16
+ - 자식 컨테이너에서 CSS 변수를 **직접** 재선언하지 않는다 (예: `style={{ "--bg": "..." }}`). 토큰 whitelist 바깥의 값을 섞으면 축 스왑이 깨짐. 축으로 못 표현하는 색은 축에 값을 추가하거나 테마 프리셋 레벨에서 해결.
17
+ - 축 중 하나가 "값이 없을 때 자동 폴백" 되지 않는다고 가정하지 않는다 — 모든 축은 `:root`에 기본값이 선언돼 있고 자식은 명시적으로 override 할 때만 바뀜.
18
+
19
+ ### 선언이 두 번이면 가까운 쪽이 이긴다
20
+ ```tsx
21
+ <html data-mode="light"> {/* L1 */}
22
+ <main data-mode="dark"> {/* L2: light 뒤집음 */}
23
+ <aside data-mode="light"> {/* L3: 다시 light */}
24
+ {/* 여기서 --bg, --fg는 L3 light. --shape는 L1 standard 그대로. */}
25
+ </aside>
26
+ </main>
27
+ </html>
28
+ ```
29
+
30
+ ### 축별 "국소 스코프" 적용 가능성
31
+ | 축 | 국소 override | 대표 쓰임 |
32
+ |---|---|---|
33
+ | Mode | ✅ 흔함 | 이미지 오버레이, 어두운 카드, 다크 툴바 |
34
+ | Palette | ⚠️ 드물게 | 브랜드 섹션에 vivid 강조 (주의: 전체 톤 깨짐) |
35
+ | Shape | ⚠️ 드물게 | 이벤트 배너에만 soft 둥글기 |
36
+ | Density | ✅ 자주 | 대시보드 안 compact 테이블 vs 외곽 default 패널 |
37
+ | Motion | ⚠️ 거의 안 함 | 접근성용 `none` 국소 적용 외엔 금지 |
38
+ | Icon | ⚠️ 거의 안 함 | A/B 테스트용 |
39
+ | Lang | ✅ 자주 | 다국어 혼재 페이지 (한국어 본문 + 영문 코드 블록) |
40
+ | Focus | ✅ 가끔 | 키보드 네비게이션 섹션만 ring on |
41
+ | Voice | ⚠️ 드물게 | 공식·캐주얼 영역이 섞인 UI |
42
+
43
+ ### 디버깅
44
+ - `html` 요소에 전체 축 셋이 다 보이는지 DevTools로 체크. ThemeController가 마운트되지 않으면 축 어트리뷰트가 비어있을 수 있음 — boot script(themeBootScript)가 `<head>`에 inline으로 들어가 있어야 FOUC 없이 뜬다.
45
+ - 자식 영역이 축을 상속받지 못한 것 같으면: (1) 중간에 `data-*` 재선언이 있는지, (2) iframe·portal로 넘어가 조상 체인이 끊긴 건 아닌지 확인.
46
+
47
+ ### 관련
48
+ - 데모 앱 wiring: [web/app/layout.tsx](/web/app/layout.tsx) + [web/components/ThemeController.tsx](/web/components/ThemeController.tsx) + [web/components/themeBootScript.ts](/web/components/themeBootScript.ts)
49
+ - 각 축의 값·기본값·국소 스코프 디테일: [mode.md](mode.md) · [palette.md](palette.md) · [shape.md](shape.md) · [density.md](density.md) · [motion.md](motion.md) · [icon.md](icon.md)
@@ -18,7 +18,9 @@
18
18
  ### 값
19
19
  - `light` — 밝은 배경, 어두운 텍스트. 일반 웹 UI 기본.
20
20
  - `dark` — 어두운 배경, 밝은 텍스트. 야간·장시간 응시·대시보드.
21
- - `high-contrast` — 접근성 강화 대비. KWCAG/WCAG AAA 대응. (미구현, 로드맵)
21
+
22
+ ### 접근성 — 하이콘트라스트
23
+ OS 하이콘트라스트 모드(macOS Increase Contrast · Windows High Contrast)를 켠 사용자는 `@media (prefers-contrast: more)`로 자동 처리 — border alpha + muted fg를 상향해 WCAG AA 선을 넘긴다. 축 값이 아니라 토큰 override라, 컴포넌트는 변경 없이 자동 적용. 테스트: Chrome DevTools "Emulate CSS prefers-contrast: more" 또는 OS 설정 토글.
22
24
 
23
25
  ### 기본값
24
26
  `light`
@@ -0,0 +1,84 @@
1
+ # Variants
2
+
3
+ ### 원칙
4
+ 컴포넌트마다 variant 이름이 역사적으로 조금씩 다르다 (Button `primary` ≈ Badge `solid` ≈ Input `filled`). 이 문서는 **역할 축**과 **컴포넌트 축**을 교차해, 지금 자리에 들어가야 할 variant 이름이 뭔지 매번 확인하지 않고 결정할 수 있게 하는 매핑표다.
5
+
6
+ 이 표는 **기존 이름을 바꾸지 않는다**. 각 컴포넌트의 variant 이름은 그대로 유지된다 (breaking change 금지). 대신 "같은 역할"이 컴포넌트마다 뭐라 불리는지 보여줘, AI 에이전트·사람이 일관되게 매핑하도록 돕는다.
7
+
8
+ ### 역할 축
9
+ - **Primary** — 화면의 주 액션. 한 화면에 1개 원칙.
10
+ - **Secondary** — 보조 액션. 폼의 "취소", 모달의 "닫기".
11
+ - **Tertiary / Quiet** — 조용한 액션. 테이블 행 action, 아이콘 버튼 default.
12
+ - **Destructive** — 되돌릴 수 없는 액션. 삭제·해제.
13
+ - **Inverse** — 반전된 배경(어두운 섹션의 라이트 버튼) 위.
14
+
15
+ 표면(surface) 축은 별도로 둔다 — Badge·Chip·Callout·Input 같이 액션이 아닌 "표시" 컴포넌트에만 쓴다:
16
+ - **Solid** — 꽉 찬 배경.
17
+ - **Soft** — 연한 배경 + 어두운 텍스트.
18
+ - **Outline** — 투명 배경 + 테두리.
19
+ - **Line** — 아래선 하나 (입력류).
20
+ - **Fill** — 꽉 찬 배경 (알림류).
21
+ - **Plain / Ghost** — 배경·테두리 둘 다 없음.
22
+
23
+ ### 액션 컴포넌트 — 역할 × variant 매핑
24
+
25
+ | 컴포넌트 | Primary | Secondary | Tertiary | Destructive | Inverse |
26
+ |---|---|---|---|---|---|
27
+ | [Button](button.md) | `primary` | `outline` *(또는 `secondary`)* | `ghost` | `destructive` + `destructive-ghost` | `inverse` |
28
+ | [IconButton](icon-button.md) | — *(강조는 Button 쪽)* | `outline` | `ghost` | `destructive` | — |
29
+ | [SplitButton](split-button.md) | `primary` | `outline` | `ghost` | `destructive` | — |
30
+ | [IdentityButton](identity-button.md) | — | `muted` | `ghost` | — | — |
31
+
32
+ **선택 규칙 (액션)**
33
+ - 중요 액션=`primary`, 서브 액션=`outline`. 폼·모달·페이지 액션의 기본 쌍. **`secondary`는 레거시, 신규 코드에선 `outline`을 쓴다.** (관련: feedback 메모리 [Button variant defaults](../../memory/feedback_button_variant_defaults.md))
34
+ - 테이블 행·툴바 안 액션은 `ghost`.
35
+ - 삭제·해제는 `destructive`, 삭제가 두 번째 시선이면 `destructive-ghost`.
36
+ - IconButton엔 `primary`가 없다 — "꽉 찬 색의 아이콘 버튼"은 CTDS에서 금지. 강조가 필요하면 label 붙은 Button을 써라.
37
+
38
+ ### 표시 컴포넌트 — 역할 × variant 매핑
39
+
40
+ | 컴포넌트 | Solid | Soft | Outline | Line | 기타 |
41
+ |---|---|---|---|---|---|
42
+ | [Badge](badge.md) | `solid` | `soft` | — | — | `inverse` |
43
+ | [Chip](chip.md) *(필터 선택값 전용)* | — | `soft` | `outline` | — | — |
44
+ | [Callout](callout.md) *(알림 블록)* | — | — | `outline` | — | `fill` |
45
+ | [MetricCard](metric-card.md) | — | `soft` | — | `line` | `bare` |
46
+
47
+ **선택 규칙 (표시)**
48
+ - 상태·카테고리 라벨=`Badge solid`(기본) 또는 `soft`(순한 톤). 선택된 필터 값=`Chip`.
49
+ - Chip은 "선택된 값(X로 제거 가능)" 전용. 카테고리·태그·메타 라벨은 **반드시 Badge** (관련: feedback 메모리 [Chip filter-only](../../memory/feedback_chip_filter_only.md)).
50
+ - 알림·안내 박스는 `Callout fill` *(색 테두리+색 배경 금지 — 메모리 [No outlined Callout](../../memory/feedback_no_outlined_callout.md))*.
51
+
52
+ ### 입력 컴포넌트 — 매핑
53
+
54
+ | 컴포넌트 | Default | 폼 블록 | 밀도 승부처 |
55
+ |---|---|---|---|
56
+ | [Input](input.md) | `line` | `outline` | `filled` |
57
+ | [Textarea](textarea.md) | `line` | `outline` | `filled` |
58
+ | [Select](select.md) | `line` | `outline` | `filled` |
59
+ | [TimePicker](time-picker.md) | `line` | `outline` | `filled` |
60
+
61
+ **선택 규칙 (입력)**
62
+ - `line`을 먼저 고려. 독립 폼 블록이면 `outline`, 테이블 셀처럼 밀도가 극한이면 `filled` (관련: feedback 메모리 [Input line default](../../memory/feedback_input_line_default.md)).
63
+
64
+ ### 레이아웃·구조 컴포넌트
65
+ "역할" 매핑과 맞지 않는 축을 쓰는 것들. 이름 그대로 사용:
66
+ - [AppFrame](app-frame.md): `outlined` · `floating` · `flat` — 외곽 쉘 톤
67
+ - [Toolbar](toolbar.md): `floating` · `attached` — 위치
68
+ - [Tabs](tabs.md): `underline` · `segmented` · `text` — 시각 패턴
69
+ - [TableHeader](table-header.md): `line` · `plate` — 헤더 톤
70
+ - [Pagination](pagination.md): `numbered` · `compact` · `jump` — 밀도
71
+ - [MessageQuote](message-quote.md): `plain` · `blockquote` — 인용 톤
72
+ - [ListItem](list-item.md): `activity` · `notification` · `mention` · `system` — 내용 종류
73
+ - [Spinner](spinner.md): `ring` · `dots-bounce` · `dots-fade` · `pulse` · `lottie-*` — 애니메이션 종류
74
+
75
+ ### Don't
76
+ - 신규 컴포넌트에 "역할 축"의 네이밍을 쓸 수 있으면 **primary/outline/ghost/destructive**를 그대로 재사용한다. "solid"는 표면 축, "primary"는 역할 축이라는 구분을 유지.
77
+ - 기존 컴포넌트의 variant 이름을 바꾸지 않는다 — 소비자 breaking.
78
+ - 매핑표에 없는 역할을 임의로 추가하지 않는다. 신규 역할이 필요하면 이 문서를 먼저 업데이트.
79
+
80
+ ### 신규 컴포넌트 체크리스트
81
+ 1. 액션? → 역할 축 네이밍(`primary`/`outline`/`ghost`/`destructive`)
82
+ 2. 표시? → 표면 축 네이밍(`solid`/`soft`/`outline`/`line`/`fill`)
83
+ 3. 입력? → `line` 기본 + `outline`/`filled` 선택
84
+ 4. 레이아웃? → 자유. 다만 "variant"가 아니라 더 의미 있는 prop 이름(예: `position`, `tone`)을 먼저 검토
@@ -1,7 +1,7 @@
1
- # Attachment Chip
1
+ # Attachment Item
2
2
 
3
3
  ### 원칙
4
- 파일 첨부 1건 — `[FileIcon] filename (meta) [actions]`. 폭 400px 고정, 레이아웃은 `size` 하나로 결정. 파일명에서 확장자를 뽑아 [[components/file-icon]] 색/라벨을 자동 결정. `state="loading"`이면 좌측 뱃지를 Spinner(lottie-dots)로 교체. trailing은 콜백 조합(`onRemove`/`onDownload`/`onMore`)으로 노출. 긴 파일명은 **중간 생략 + 끝 확장자 유지**.
4
+ 파일 첨부 1건 — `[FileIcon] filename (meta) [actions]`. 폭 400px 고정, 레이아웃은 `size` 하나로 결정. 파일명에서 확장자를 뽑아 [[components/file-icon]] 색/라벨을 자동 결정. `state="loading"`이면 좌측 뱃지를 Spinner(lottie-dots)로 교체. trailing은 콜백 조합(`onRemove`/`onDownload`/`onMore`)으로 노출. 긴 파일명은 **중간 생략 + 끝 확장자 유지**. "Chip"이 아님에 주의 — [[components/chip]]은 필터 선택값 전용.
5
5
 
6
6
  ### Do
7
7
  - 파일명 통째로 넘기면 끝 — `filename="report.pdf"` 하나로 아이콘·라벨·식별까지.
@@ -12,7 +12,7 @@
12
12
  - 업로드 중은 `state="loading"` + `meta` 문구 — 좌측이 Spinner(lottie-dots)로 교체. 상태 문구는 소비처 책임.
13
13
  - 즉시 액션(미리보기·업로드 재시도 등)은 `meta`에 Button `variant="ghost"` `size="xs"`로 넣는다 — 전역 Button 체계와 합치.
14
14
  - trailing은 콜백을 지정한 만큼만 노출. 복수 지정 시 download → more → remove 순으로 배치.
15
- - Thumb(이미지)·Chip(파일) 섞여 흐를 자리는 `flex-wrap: wrap` + `gap: space-2`로 조립.
15
+ - Thumb(이미지)·Item(파일) 섞여 흐를 자리는 `flex-wrap: wrap` + `gap: space-2`로 조립.
16
16
 
17
17
  ### Don't
18
18
  - 폭을 임의로 바꾸지 않는다 — 전부 `--input-w-lg` (400px) 고정. 소비처가 폭 걱정 없이 세로/wrap만 결정.
@@ -20,8 +20,9 @@
20
20
  - trailing에 임의 ReactNode 슬롯을 뚫지 않는다 — 접근성·간격·톤 통일이 깨짐. 정의된 콜백 조합만.
21
21
  - trailing을 수제 버튼으로 그리지 않는다 — 반드시 IconButton 재사용 (sm/md → IconButton sm 20, lg → IconButton md 24). IconButton에 없는 치수(28·36 등)는 금지.
22
22
  - loading 좌측에 베이스 Spinner variant(pulse·dots-bounce 등)를 쓰지 않는다 — CTDS 로더 톤 통일 위해 `lottie-dots` 고정. [[components/spinner]].
23
- - 배경을 과한 톤으로 덮지 않는다 — bg는 `var(--bg)`, 테두리는 hairline.
23
+ - 배경을 과한 톤으로 덮지 않는다 — bg는 `var(--bg)`, 테두리는 hairline, drop shadow는 가장 연한 `--shadow-xs`만.
24
24
  - meta 라인에 가운뎃점 "·"는 OK — 하지만 `··•·|` 같은 꾸밈 구분자는 금지. [[feedback-no-bullet-separator]].
25
+ - `AttachmentChip`으로 부르지 않는다 — [[components/chip]]은 필터 선택값(X 제거) 전용. 파일 첨부는 Item.
25
26
 
26
27
  ### Fallback
27
28
  size 애매하면 `md`. 모르는 확장자는 FileIcon이 `others`로 폴백하니 그대로 넘겨도 됨.
@@ -31,13 +32,14 @@ size 애매하면 `md`. 모르는 확장자는 FileIcon이 `others`로 폴백하
31
32
  - [[components/spinner]] — loading 상태 좌측 (lottie-dots variant)
32
33
  - [[components/icon-button]] — trailing 액션 버튼
33
34
  - [[components/button]] — meta 안 즉시 액션 (ghost xs)
34
- - [[components/attachment-thumb]] — 이미지 첨부
35
+ - [[components/attachment-thumb]] — 이미지 첨부 (사촌)
36
+ - [[components/list-item]] — data 그룹 이웃
35
37
  - [[components/chat-input]] — 상위 소비처 (size=sm/md)
36
- - [[components/message-bubble]] — 메시지 내 파일 인용 (size=lg)
38
+ - [[components/user-message]] — 메시지 내 파일 인용 (size=lg)
37
39
 
38
40
  ### Props 시그니처
39
41
  ```tsx
40
- <AttachmentChip
42
+ <AttachmentItem
41
43
  filename={string} // 필수. 확장자 자동 추출
42
44
  size="sm | md | lg" // default: md. lg는 2줄 큰 카드
43
45
  state="idle | loading" // default: idle. loading이면 좌측이 Spinner
@@ -50,7 +52,7 @@ size 애매하면 `md`. 모르는 확장자는 FileIcon이 `others`로 폴백하
50
52
 
51
53
  ### 비율·레이아웃
52
54
  - 폭: 전부 `--input-w-lg` (400px) 고정.
53
- - 외곽: `--hairline` 1px border, `var(--border)` 색.
55
+ - 외곽: `--hairline` 1px border, `var(--border)` 색. drop shadow는 `--shadow-xs` (가장 연함).
54
56
  - radius: sm/md=`--radius-md`, lg=`--radius-lg`.
55
57
  - 내부 gap (leading ↔ text): sm=`--space-1_5`, md=`--space-2`, lg=`--space-2_5`. actions 간은 전부 `--space-1`.
56
58
  - padding:
@@ -58,7 +60,7 @@ size 애매하면 `md`. 모르는 확장자는 FileIcon이 `others`로 폴백하
58
60
  - md: `space-1_5` 상하, `space-1_5` 우, `space-2_5` 좌
59
61
  - lg: `space-2_5` 상하, `space-1_5` 우, `space-2_5` 좌 (세로 숨 쉬게 + 좌우는 아이콘 버튼 자리 고려해 타이트)
60
62
  - leading FileIcon size: sm→sm(16) · md→md(24) · lg→lg(32).
61
- - lg 안쪽 두 줄: label=`--text-sm`(14.5) · meta=`--text-xs`(13). 한 단계 차이. 무게/색은 label=medium/fg-strong, meta=regular/fg-muted. (meta는 모든 size 공통 `--text-xs`.)
63
+ - lg 안쪽 두 줄: label=`--text-sm`(14.5) · meta=`--text-xs`(13). 한 단계 차이. 무게/색은 label=medium/fg-strong, meta=regular/fg-subtle. (meta는 모든 size 공통 `--text-xs` + `--fg-subtle`.)
62
64
  - leading loading slot: FileIcon과 동일 치수(sm=`space-4`, md=`space-6`, lg=`space-8`), 내부 Spinner는 100%.
63
65
  - trailing IconButton: sm/md→IconButton `sm`(20), lg→IconButton `md`(24). variant는 전부 ghost.
64
66
 
@@ -72,7 +74,7 @@ size 애매하면 `md`. 모르는 확장자는 FileIcon이 `others`로 폴백하
72
74
  - CSS `text-overflow: ellipsis`는 안전망으로 유지하되, 글자 수 cap을 400px 폭보다 보수적으로 잡아 CSS가 확장자를 다시 잘라내지 않게 한다.
73
75
 
74
76
  ### Loading 좌측 뱃지
75
- `state="loading"`일 때 FileIcon 자리에 Spinner — `variant="lottie-dots"`. 색은 `--fg-muted` currentColor 바인딩. 크기는 FileIcon의 각 size와 동일 치수를 가지는 정사각 슬롯이 감싸고 내부 Spinner는 100%.
77
+ `state="loading"`일 때 FileIcon 자리에 Spinner — `variant="lottie-dots"`. 색은 `--fg-subtle` currentColor 바인딩. 크기는 FileIcon의 각 size와 동일 치수를 가지는 정사각 슬롯이 감싸고 내부 Spinner는 100%.
76
78
 
77
79
  ### meta 폰트
78
80
  meta는 size와 무관하게 항상 `--text-xs`(13) — 파일명보다 한 단계 작은 보조 톤 유지. label(파일명)만 size별로 변동(sm=`--text-xs`, md=`--text-sm`, lg=`--text-sm`).
@@ -4,7 +4,7 @@
4
4
  Chat Input·Message의 **이미지 첨부 썸네일**. 정사각 radius ~22%(size × 0.22) 고정, 세 가지 상태 — `src` 있음(이미지)/없음(블루톤 image glyph)/`loading`(pulse). 전부 inner hairline으로 edge definition. hover 시 코너에 X 삭제 버튼 노출(`onRemove` 지정 시).
5
5
 
6
6
  ### Do
7
- - 이미지 첨부(png/jpg/gif/webp)엔 썸네일, 그 외 포맷은 [[components/file-icon]] 또는 [[components/attachment-chip]].
7
+ - 이미지 첨부(png/jpg/gif/webp)엔 썸네일, 그 외 포맷은 [[components/file-icon]] 또는 [[components/attachment-item]].
8
8
  - 업로드 처리 중/디코딩 전엔 `loading` → pulse. 끝나면 `src` 세팅.
9
9
  - 메타만 받고 썸네일 URL이 늦게 오면 `src` 비워두면 image glyph placeholder — 로딩이 길어 답답한 자리를 메움.
10
10
  - 삭제 UI가 필요하면 `onRemove`를 넘김 — hover/focus-visible에서 코너 X 활성.
@@ -19,7 +19,7 @@ Chat Input·Message의 **이미지 첨부 썸네일**. 정사각 radius ~22%(siz
19
19
  size 애매하면 `md`(40). `src`·`loading` 둘 다 없으면 자동 placeholder.
20
20
 
21
21
  ### 관련
22
- - [[components/attachment-chip]] — 파일형 첨부
22
+ - [[components/attachment-item]] — 파일형 첨부
23
23
  - [[components/file-icon]] — 브랜드 사각 뱃지
24
24
  - [[components/chat-input]] — 상위 소비처
25
25