@conscience-technology/ctds 0.1.13 → 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 (72) hide show
  1. package/assets/components/AppFrame/AppFrame.module.css +6 -0
  2. package/assets/components/AttachmentItem/AttachmentItem.module.css +112 -0
  3. package/assets/components/AttachmentItem/AttachmentItem.tsx +164 -0
  4. package/assets/components/AttachmentItem/index.ts +6 -0
  5. package/assets/components/Button/Button.module.css +11 -2
  6. package/assets/components/Button/Button.tsx +5 -5
  7. package/assets/components/ChatInput/ChatInput.tsx +2 -2
  8. package/assets/components/DatePicker/Calendar.tsx +19 -1
  9. package/assets/components/Drawer/Drawer.tsx +120 -39
  10. package/assets/components/Drawer/index.ts +9 -1
  11. package/assets/components/GeneratedImage/GeneratedImage.module.css +90 -0
  12. package/assets/components/GeneratedImage/GeneratedImage.tsx +70 -0
  13. package/assets/components/GeneratedImage/index.ts +5 -0
  14. package/assets/components/Icon/Icon.tsx +4 -1
  15. package/assets/components/Input/Input.module.css +3 -0
  16. package/assets/components/MessageState/MessageState.module.css +12 -6
  17. package/assets/components/MessageState/MessageState.tsx +23 -5
  18. package/assets/components/Modal/Modal.tsx +114 -33
  19. package/assets/components/Modal/index.ts +12 -2
  20. package/assets/components/Spinner/Spinner.module.css +1 -1
  21. package/assets/components/Spinner/Spinner.tsx +2 -2
  22. package/assets/components/Switch/Switch.tsx +6 -0
  23. package/assets/components/TableHeader/TableHeader.module.css +29 -20
  24. package/assets/components/TableRow/TableRow.module.css +7 -5
  25. package/assets/components/TableRowEdit/TableRowEdit.module.css +10 -3
  26. package/assets/components/TableRowEdit/TableRowEdit.tsx +7 -2
  27. package/assets/components/Toast/Toast.tsx +19 -0
  28. package/assets/components/_data.ts +39 -14
  29. package/assets/components/app-shell.css +17 -0
  30. package/assets/components/index.ts +77 -0
  31. package/assets/components/reset.css +63 -0
  32. package/assets/components/themeBootScript.ts +40 -0
  33. package/assets/components/tokens.css +70 -60
  34. package/assets/prompts/GUIDE.md +1 -1
  35. package/assets/prompts/PRINCIPLES.md +9 -0
  36. package/assets/prompts/axes/cascade.md +49 -0
  37. package/assets/prompts/axes/mode.md +3 -1
  38. package/assets/prompts/components/_variants.md +84 -0
  39. package/assets/prompts/components/attachment-item.md +80 -0
  40. package/assets/prompts/components/attachment-thumb.md +2 -2
  41. package/assets/prompts/components/avatar-upload.md +53 -0
  42. package/assets/prompts/components/bubble.md +2 -2
  43. package/assets/prompts/components/chat-input.md +7 -8
  44. package/assets/prompts/components/code-block.md +2 -2
  45. package/assets/prompts/components/disclosure.md +47 -0
  46. package/assets/prompts/components/file-icon.md +3 -3
  47. package/assets/prompts/components/generated-image.md +38 -0
  48. package/assets/prompts/components/message-quote.md +2 -2
  49. package/assets/prompts/components/message-sources.md +5 -4
  50. package/assets/prompts/components/message-state.md +15 -12
  51. package/assets/prompts/components/message-table.md +9 -6
  52. package/assets/prompts/components/onboarding.md +43 -0
  53. package/assets/prompts/components/quotation-bar.md +1 -1
  54. package/assets/prompts/components/sidebar-split.md +70 -0
  55. package/assets/prompts/components/sidebar.md +2 -0
  56. package/assets/prompts/components/spinner.md +4 -4
  57. package/assets/prompts/components/time-picker.md +69 -0
  58. package/assets/prompts/components/toolbar.md +69 -0
  59. package/assets/prompts/components/user-message.md +2 -2
  60. package/package.json +80 -3
  61. package/src/commands/init.js +53 -23
  62. package/src/utils/deps.js +23 -17
  63. package/src/utils/patch.js +155 -0
  64. package/assets/components/AttachmentChip/AttachmentChip.module.css +0 -73
  65. package/assets/components/AttachmentChip/AttachmentChip.tsx +0 -74
  66. package/assets/components/AttachmentChip/index.ts +0 -2
  67. package/assets/components/Calendar/Calendar.module.css +0 -163
  68. package/assets/components/Calendar/Calendar.tsx +0 -323
  69. package/assets/components/Calendar/index.ts +0 -2
  70. package/assets/prompts/components/attachment-chip.md +0 -46
  71. package/assets/prompts/components/calendar.md +0 -116
  72. package/assets/prompts/components/form-field.md +0 -74
@@ -146,18 +146,20 @@ a.root:not(.disabled):focus-visible,
146
146
  }
147
147
 
148
148
  /* pinned 경계 — scroll shadow 대신 상시 hairline으로 "여기까지 고정 영역"을 분명히.
149
- 좌측 pinned → 우측 경계에 세로 hairline. 우측 pinned → 좌측 경계. */
149
+ 좌측 pinned → 우측 경계, 우측 pinned → 좌측 경계.
150
+ border 대신 inset box-shadow로 그어 sticky 레이어 subpixel 겹침(두 줄처럼
151
+ 보이는 Chrome 렌더 현상) 회피 + 셀 폭에 영향 안 줌. */
150
152
  .cellSlot[data-pinned="left"] {
151
- border-right: var(--hairline) solid var(--border);
153
+ box-shadow: inset calc(var(--hairline) * -1) 0 0 var(--border);
152
154
  }
153
155
  .cellSlot[data-pinned="right"] {
154
- border-left: var(--hairline) solid var(--border);
156
+ box-shadow: inset var(--hairline) 0 0 var(--border);
155
157
  }
156
158
 
157
159
  /* selectable + pinned 조합일 때 selectCell이 "실질적 좌측 pinned의 선두" 역할이라
158
- selectCell에도 우측 hairline을 그어 경계 톤 일치. */
160
+ selectCell에도 우측 경계를 그어 톤 일치. */
159
161
  .selectCell {
160
- border-right: var(--hairline) solid var(--border);
162
+ box-shadow: inset calc(var(--hairline) * -1) 0 0 var(--border);
161
163
  }
162
164
 
163
165
  /* ── Cell ───────────────────────────────────────────────
@@ -63,6 +63,7 @@
63
63
  pointer-events: none;
64
64
  }
65
65
 
66
+
66
67
  /* ── Edit cell ────────────────────────────────────────────
67
68
  클릭/포커스 타겟. 활성 상태엔 input/select가 셀을 꽉 채움. */
68
69
  .editCell {
@@ -102,11 +103,17 @@
102
103
  left: 0;
103
104
  right: 0;
104
105
  /* 1줄 상태엔 셀 높이에 딱 맞도록 min-height: 100%. textarea autosize가
105
- 여러 줄로 늘어나면 host는 content 높이를 따라감(아래로 확장). */
106
+ 여러 줄로 늘어나면 host 자체가 scroll 컨테이너가 되고(max-height 도달 시),
107
+ textarea 전체(자기 padding 포함)가 host 안에서 위로 올라감 → 상·하 padding이
108
+ scroll content의 일부라 host border에서 자연스레 clip, 안쪽 여백 항상 보존.
109
+ scroll.md "textarea 특수 패턴" 규약. padding은 textarea가 책임. */
106
110
  min-height: 100%;
111
+ /* 3.5줄 × line-height + 상·하 padding(space-2 각각) — 그 이상은 host가 scroll. */
112
+ max-height: calc(var(--lh-sm) * 3.5 + var(--space-2) * 2);
107
113
  display: flex;
108
- align-items: center;
109
- padding-inline: var(--space-2);
114
+ align-items: stretch;
115
+ padding: 0;
116
+ overflow-y: auto;
110
117
  background: var(--bg);
111
118
  border: var(--hairline) solid var(--border-strong);
112
119
  border-radius: var(--radius-sm);
@@ -179,8 +179,13 @@ function TableRowEditInner<T>(
179
179
  /* 세로 구분선은 editable 여부와 무관하게 모든 셀 사이에 균일하게.
180
180
  엑셀 톤 "셀 그리드" 신호는 일관성이 핵심 — read-only 셀만 선이 빠지면
181
181
  리듬이 깨져 보임. 첫 데이터 셀은 왼쪽에 선 없음(테이블 외곽).
182
- selectable이면 체크박스 ↔ 첫 데이터 셀 사이에도 선. */
183
- const showLeftDivider = idx > 0 || selectable;
182
+ selectable이면 체크박스 ↔ 첫 데이터 셀 사이에도 선.
183
+ pinned 경계는 cellSlot box-shadow가 담당 — pinned-right 셀 자신과
184
+ pinned-left 바로 다음 셀의 왼쪽 divider는 그으면 두 줄로 겹쳐 보임. */
185
+ const prevCol = idx > 0 ? columnMap.get(orderedKeys[idx - 1]) : undefined;
186
+ const skipDivider =
187
+ col.pinned === "right" || prevCol?.pinned === "left";
188
+ const showLeftDivider = (idx > 0 || selectable) && !skipDivider;
184
189
  return (
185
190
  <div
186
191
  key={col.key}
@@ -27,7 +27,12 @@ export interface ToastOptions {
27
27
  tone?: ToastTone;
28
28
  surface?: ToastSurface;
29
29
  icon?: IconName;
30
+ /** 액션 요소(Button 등). `actionAltText`도 함께 넘겨야 렌더링된다 — 스크린리더용 라벨이 없으면 Radix가 action을 숨긴다. */
30
31
  action?: ReactNode;
32
+ /**
33
+ * action을 쓸 때 **필수** — 스크린리더가 action 버튼을 어떻게 안내할지. Toast 사라져도
34
+ * 사용자가 수행할 수 있는 대안 설명(예: "실행 취소", "설정 열기"). 생략 시 action 미렌더.
35
+ */
31
36
  actionAltText?: string;
32
37
  dismissible?: boolean;
33
38
  dismissLabel?: string;
@@ -227,6 +232,9 @@ export const Toast = forwardRef<HTMLLIElement, ToastProps>(function Toast(
227
232
  {action}
228
233
  </RadixToast.Action>
229
234
  )}
235
+ {action && !actionAltText && typeof window !== "undefined" && process.env.NODE_ENV !== "production" && (
236
+ <ToastActionWarning />
237
+ )}
230
238
  {dismissible && (
231
239
  <RadixToast.Close asChild>
232
240
  <IconButton
@@ -241,3 +249,14 @@ export const Toast = forwardRef<HTMLLIElement, ToastProps>(function Toast(
241
249
  </RadixToast.Root>
242
250
  );
243
251
  });
252
+
253
+ /** dev-only. action은 주고 actionAltText를 빠뜨리면 Radix가 조용히 숨기므로 console로 경고. */
254
+ function ToastActionWarning() {
255
+ if (typeof window !== "undefined") {
256
+ // eslint-disable-next-line no-console
257
+ console.warn(
258
+ "[CTDS Toast] `action` prop을 넘겼는데 `actionAltText`가 비어있어요. 스크린리더용 라벨이 없으면 action이 렌더되지 않습니다.",
259
+ );
260
+ }
261
+ return null;
262
+ }
@@ -585,7 +585,6 @@ export const componentSlugs: Record<string, string> = {
585
585
  "Table Row": "data/table-row",
586
586
  "Table Row Select": "data/table-row-select",
587
587
  "Table Row Edit": "data/table-row-edit",
588
- Calendar: "data/calendar",
589
588
  "Event Chip": "data/event-chip",
590
589
  "List Item": "data/list-item",
591
590
  "Metric Card": "data/metric-card",
@@ -595,7 +594,7 @@ export const componentSlugs: Record<string, string> = {
595
594
  Spinner: "feedback/spinner",
596
595
  "File Icon": "chat/file-icon",
597
596
  "Attachment Thumb": "chat/attachment-thumb",
598
- "Attachment Chip": "chat/attachment-chip",
597
+ "Attachment Item": "data/attachment-item",
599
598
  "Quotation Bar": "chat/quotation-bar",
600
599
  "Chat Input": "chat/chat-input",
601
600
  "Bubble": "chat/bubble",
@@ -607,13 +606,13 @@ export const componentSlugs: Record<string, string> = {
607
606
  "Message Sources": "chat/message-sources",
608
607
  "Message Table": "chat/message-table",
609
608
  "Message State": "chat/message-state",
609
+ "Generated Image": "chat/generated-image",
610
610
  };
611
611
 
612
- /** 작업중 — 사이드바에서 접근은 가능하지만 "작업중" 뱃지로 표시해 사용 자제 유도. */
613
- export const componentsInProgress: Set<string> = new Set([
614
- "Drawer",
615
- "Calendar",
616
- ]);
612
+ /** 작업중 — 사이드바에서 "작업중" 뱃지로 표시되고,
613
+ * 외부에서 import하면 pre-commit 훅(scripts/lint-wip.mjs)이 차단한다.
614
+ * 문서 라우트(web/app/components/**)와 컴포넌트 자신의 폴더는 허용. */
615
+ export const componentsInProgress: Set<string> = new Set([]);
617
616
 
618
617
  export const components: { id: string; title: string; items: string[] }[] = [
619
618
  {
@@ -622,6 +621,7 @@ export const components: { id: string; title: string; items: string[] }[] = [
622
621
  items: [
623
622
  "Button", "Split Button", "Icon Button", "Menu Item", "Toolbar", "Badge", "Avatar",
624
623
  "Tooltip", "Shortcut", "Filter Button", "Chip", "Separator", "Skeleton", "Aspect Ratio",
624
+ "Code Block",
625
625
  ],
626
626
  },
627
627
  {
@@ -631,7 +631,7 @@ export const components: { id: string; title: string; items: string[] }[] = [
631
631
  "Input", "Input OTP", "Textarea", "Select", "Combobox", "Multi-Select",
632
632
  "Checkbox", "Radio Group", "Selectable Card", "Choice Card", "Switch", "Slider",
633
633
  "Date Picker", "Time Picker",
634
- "File Upload", "Form",
634
+ "File Upload",
635
635
  ],
636
636
  },
637
637
  {
@@ -639,7 +639,7 @@ export const components: { id: string; title: string; items: string[] }[] = [
639
639
  title: "Data Display",
640
640
  items: [
641
641
  "Table Header", "Table Row", "Table Row Select", "Table Row Edit",
642
- "List Item", "Metric Card", "Gallery Item", "Calendar", "Event Chip",
642
+ "List Item", "Metric Card", "Gallery Item", "Attachment Item", "Event Chip",
643
643
  "Empty State", "Description List", "Timeline",
644
644
  ],
645
645
  },
@@ -668,12 +668,37 @@ export const components: { id: string; title: string; items: string[] }[] = [
668
668
  ],
669
669
  },
670
670
  {
671
- id: "chat",
672
- title: "AI Chat",
671
+ id: "chat-input",
672
+ title: "AI Chat / 입력",
673
+ items: [
674
+ "Chat Input", "Quotation Bar",
675
+ ],
676
+ },
677
+ {
678
+ id: "chat-attachment",
679
+ title: "AI Chat / 첨부",
680
+ items: [
681
+ "File Icon", "Attachment Thumb", "Generated Image",
682
+ ],
683
+ },
684
+ {
685
+ id: "chat-message",
686
+ title: "AI Chat / 메시지",
687
+ items: [
688
+ "Bubble", "User Message", "AI Message", "Message State", "Message Actions",
689
+ ],
690
+ },
691
+ {
692
+ id: "chat-content",
693
+ title: "AI Chat / 메시지 콘텐츠",
694
+ items: [
695
+ "Message Quote", "Message Sources", "Message Table",
696
+ ],
697
+ },
698
+ {
699
+ id: "chat-shell",
700
+ title: "AI Chat / 쉘",
673
701
  items: [
674
- "File Icon", "Attachment Thumb", "Attachment Chip", "Quotation Bar", "Chat Input",
675
- "Bubble", "Message Actions", "User Message", "AI Message",
676
- "Code Block", "Message Quote", "Message Sources", "Message Table", "Message State",
677
702
  "Chat Sidebar",
678
703
  ],
679
704
  },
@@ -0,0 +1,17 @@
1
+ /* ============================================================
2
+ CTDS app-shell — opt-in. AppFrame(100dvh overflow:hidden)을 쓰는
3
+ "앱 셸" 레이아웃 전제 스타일. 소비자 프로젝트가 일반 body-scroll
4
+ 페이지라면 이 파일을 import하지 않는다. 데모 앱(web/app/globals.css)
5
+ 과 ctds-cli init이 복사하는 앱 템플릿만 사용.
6
+
7
+ app-scroll 패러다임: AppFrame이 뷰포트 100dvh를 꽉 채우고 자기 overflow:hidden,
8
+ 스크롤은 AppFrame 안의 .contentSlot만 담당 — html/body는 스크롤할 필요가 없다.
9
+ overflow:hidden으로 바깥 러버밴드/스크롤 체이닝 자체를 차단.
10
+ ============================================================ */
11
+
12
+ html,
13
+ body {
14
+ height: 100%;
15
+ overflow: hidden;
16
+ overscroll-behavior: none;
17
+ }
@@ -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
+ }
@@ -80,12 +80,11 @@
80
80
  --icon-size-md: calc(20px * var(--space-scale));
81
81
  --icon-size-lg: calc(24px * var(--space-scale));
82
82
 
83
- /* Spinner · 로더 표준 크기 — IconButton 체계와 1:1 매칭 (xs=16, sm=20, md=24, lg=32).
84
- 로더는 캔버스를 다 채우지 않는 특성상 아이콘보다 한 단계 큰 bounds가 체감에 맞음. */
85
- --loader-size-xs: calc(16px * var(--space-scale));
83
+ /* Spinner · 로더 표준 크기 — IconButton 박스와 1:1 매칭 (sm=20, md=24, lg=32, xl=40). */
86
84
  --loader-size-sm: calc(20px * var(--space-scale));
87
85
  --loader-size-md: calc(24px * var(--space-scale));
88
86
  --loader-size-lg: calc(32px * var(--space-scale));
87
+ --loader-size-xl: calc(40px * var(--space-scale));
89
88
 
90
89
  /* Loader 성격 — Motion 축이 스왑 ([data-motion] 블록 참고).
91
90
  기본값은 standard와 같은 ring. */
@@ -243,9 +242,11 @@
243
242
  --bg-muted: oklch(0.955 var(--tone-chroma) var(--tone-hue));
244
243
  --fg-strong: oklch(0.12 var(--tone-chroma) var(--tone-hue));
245
244
  --fg: oklch(0.22 var(--tone-chroma) var(--tone-hue));
246
- --fg-muted: oklch(0.50 var(--tone-chroma) var(--tone-hue));
247
- --fg-subtle: oklch(0.68 var(--tone-chroma) var(--tone-hue));
248
- --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);
249
250
  /* border는 배경 대비 알파 기반 hairline — 겹치면 자연스럽게 짙어짐.
250
251
  라이트 모드: 어두운 잉크 톤(≈rgb(18,24,38))을 낮은 알파로 얹음. */
251
252
  --border: oklch(0.18 var(--tone-chroma) var(--tone-hue) / 0.08);
@@ -578,6 +579,62 @@ html[data-focus-ring="off"] {
578
579
  }
579
580
  }
580
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
+
581
638
  /* ── Font 축 — Language ─────────────────────────────────────
582
639
  슬롯(--font-normal/code)의 스택을 언어별로 스왑.
583
640
  프로젝트마다 여기서 바꾸면 됨. */
@@ -629,9 +686,10 @@ html[data-focus-ring="off"] {
629
686
  --bg-muted: oklch(0.31 var(--tone-chroma) var(--tone-hue));
630
687
  --fg-strong: oklch(1.0 var(--tone-chroma) var(--tone-hue));
631
688
  --fg: oklch(0.95 var(--tone-chroma) var(--tone-hue));
632
- --fg-muted: oklch(0.65 var(--tone-chroma) var(--tone-hue));
633
- --fg-subtle: oklch(0.48 var(--tone-chroma) var(--tone-hue));
634
- --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);
635
693
  /* 다크 모드: fg 톤(밝은 잉크)을 낮은 알파로 얹음. */
636
694
  --border: oklch(0.95 var(--tone-chroma) var(--tone-hue) / 0.08);
637
695
  --border-strong: oklch(0.95 var(--tone-chroma) var(--tone-hue) / 0.16);
@@ -706,57 +764,9 @@ html[data-focus-ring="off"] {
706
764
  [data-mode="dark"][data-palette="calm"] { --status-chroma: 0.17; --status-lightness: 0.70; }
707
765
  :root[data-mode="dark"][data-palette="pastel"],
708
766
  [data-mode="dark"][data-palette="pastel"] { --status-chroma: 0.16; --status-lightness: 0.72; }
709
-
710
- *, *::before, *::after { box-sizing: border-box; }
711
-
712
- html {
713
- scroll-behavior: smooth;
714
- scroll-padding-top: calc(var(--header-h) + 16px);
715
- }
716
-
717
- body {
718
- margin: 0;
719
- background: var(--bg);
720
- color: var(--fg);
721
- font-family: var(--font-sans);
722
- font-size: var(--text-base);
723
- line-height: var(--lh-base);
724
- /* 전역 자간 default — tight(-0.02em). letter-spacing은 상속 property라
725
- 모든 하위 요소가 기본으로 받고, 필요한 자리만 override. "텍스트 쓰면
726
- 자간이 자동으로 따라오는" 구조. */
727
- letter-spacing: var(--tracking-tight);
728
- -webkit-font-smoothing: antialiased;
729
- text-rendering: optimizeLegibility;
730
- transition: background var(--duration-base) var(--ease-out),
731
- color var(--duration-base) var(--ease-out);
732
- }
733
-
734
- a { color: inherit; text-decoration: none; }
735
- h1, h2, h3, h4 {
736
- margin: 0;
737
- font-weight: 600;
738
- }
739
- p { margin: 0; }
740
- code {
741
- font-family: var(--font-mono);
742
- font-size: 0.9em;
743
- background: var(--bg-muted);
744
- padding: 2px 6px;
745
- border-radius: var(--radius-xs);
746
- color: var(--fg);
747
- }
748
767
  /* === AXES END === */
749
768
 
750
- /* ============================================================
751
- 컴포넌트 전역 side-effect
752
- CSS Modules의 :global 로 둘 수 없는, 라이브러리 컴포넌트가 document.body나
753
- document.documentElement에 토글하는 클래스/속성에 대한 스타일.
754
- 토큰 whitelist 아님. 각 항목은 어느 컴포넌트가 설정하는지 명시.
755
- ============================================================ */
756
-
757
- /* Textarea.tsx — drag resize 중 전역 커서 강제. body.textarea-resizing 클래스 토글. */
758
- body.textarea-resizing {
759
- cursor: ns-resize !important;
760
- user-select: none;
761
- }
769
+ /* 전역 element reset, 폰트, 컴포넌트 side-effect 스타일은 ./reset.css로 분리.
770
+ tokens.css는 CSS 변수(토큰·축)만 노출한다. 소비자 프로젝트에서 reset을
771
+ 원하면 reset.css를 별도로 import. 데모 앱은 app/globals.css가 import한다. */
762
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. 접근성이 축보다 우선한다