@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
@@ -0,0 +1,155 @@
1
+ /**
2
+ * ctds init이 소비자 플젝의 globals.css · layout.tsx를 자동 패치하는 유틸.
3
+ *
4
+ * 설계:
5
+ * - 각 패치는 멱등(idempotent): 이미 적용돼 있으면 skip 리턴, 재실행해도 안전.
6
+ * - 실패 시 fallback 수동 안내로 넘긴다. 억지로 깨뜨리지 않음.
7
+ * - 변경 전 .bak 백업. 실패하면 되돌릴 수 있게.
8
+ * - --dry 모드에선 쓰지 않고 "하게 될 일" 리포트만.
9
+ *
10
+ * 리턴 형태 (공통):
11
+ * { status: "applied" | "skipped" | "failed", reason: string, before?, after? }
12
+ */
13
+
14
+ import { existsSync, readFileSync, writeFileSync, copyFileSync } from "node:fs";
15
+
16
+ const GLOBALS_IMPORT = `@import "../components/ctds/tokens.css";`;
17
+ const GLOBALS_IMPORT_RE = /@import\s+["']\.\.\/components\/ctds\/tokens\.css["']\s*;?/;
18
+
19
+ /**
20
+ * globals.css 최상단에 CTDS tokens.css import를 추가.
21
+ * 이미 있으면 skip. 다른 import 라인은 건드리지 않음.
22
+ */
23
+ export function patchGlobalsCss(filePath, { dry = false } = {}) {
24
+ if (!existsSync(filePath)) {
25
+ return { status: "failed", reason: `파일 없음: ${filePath}` };
26
+ }
27
+ const before = readFileSync(filePath, "utf8");
28
+ if (GLOBALS_IMPORT_RE.test(before)) {
29
+ return { status: "skipped", reason: "CTDS tokens.css import가 이미 있어요." };
30
+ }
31
+ const after = GLOBALS_IMPORT + "\n" + before;
32
+ if (dry) {
33
+ return { status: "applied", reason: "(dry) 최상단에 @import 추가 예정", before, after };
34
+ }
35
+ copyFileSync(filePath, filePath + ".bak");
36
+ writeFileSync(filePath, after, "utf8");
37
+ return { status: "applied", reason: "최상단에 @import 추가", before, after };
38
+ }
39
+
40
+ const THEME_CONTROLLER_IMPORT_RE =
41
+ /import\s*\{\s*ThemeController\s*\}\s*from\s*["'][^"']+ThemeController["']\s*;?/;
42
+ const THEME_BOOT_IMPORT_RE =
43
+ /import\s*\{\s*themeBootScript\s*\}\s*from\s*["'][^"']+themeBootScript["']\s*;?/;
44
+
45
+ /**
46
+ * layout.tsx 자동 패치:
47
+ * 1) ThemeController import + <body>를 <ThemeController>로 wrap
48
+ * 2) themeBootScript import + <head> 안 blocking <script> 삽입 (FOUC 방지)
49
+ * 3) <html>에 suppressHydrationWarning 추가 (boot script가 attr을 미리 세팅하므로)
50
+ *
51
+ * 각 단계는 멱등 — 이미 적용돼 있으면 그 단계만 skip. 둘 다 skip이면 전체 skip 리턴.
52
+ * <body> 매칭 실패 같은 치명 조건에선 전체 fail — fallback 수동 안내.
53
+ */
54
+ export function patchLayoutTsx(
55
+ filePath,
56
+ {
57
+ dry = false,
58
+ importPath = "../components/ctds/ThemeController",
59
+ bootImportPath = "../components/ctds/themeBootScript",
60
+ } = {},
61
+ ) {
62
+ if (!existsSync(filePath)) {
63
+ return { status: "failed", reason: `파일 없음: ${filePath}` };
64
+ }
65
+ const before = readFileSync(filePath, "utf8");
66
+
67
+ const hasController =
68
+ THEME_CONTROLLER_IMPORT_RE.test(before) || /<ThemeController[\s>]/.test(before);
69
+ const hasBoot =
70
+ THEME_BOOT_IMPORT_RE.test(before) || /themeBootScript\s*\(/.test(before);
71
+
72
+ if (hasController && hasBoot) {
73
+ return { status: "skipped", reason: "ThemeController + boot script가 이미 wiring 돼 있어요." };
74
+ }
75
+
76
+ let next = before;
77
+ const applied = [];
78
+
79
+ // 1) ThemeController wrap
80
+ if (!hasController) {
81
+ const bodyRe = /<body(\s[^>]*)?>([\s\S]*?)<\/body>/;
82
+ const match = next.match(bodyRe);
83
+ if (!match) {
84
+ return { status: "failed", reason: "<body> 태그를 찾지 못했어요 — 수동 설정 필요." };
85
+ }
86
+ const secondBodyIdx = next.indexOf("<body", match.index + 5);
87
+ if (secondBodyIdx !== -1) {
88
+ return { status: "failed", reason: "<body>가 둘 이상이에요 — 수동 설정 필요." };
89
+ }
90
+ const [, attrs = "", inner] = match;
91
+ const wrapped =
92
+ `<body${attrs}>\n` +
93
+ ` <ThemeController>${inner}</ThemeController>\n` +
94
+ ` </body>`;
95
+ next = insertAfterLastImport(next, `import { ThemeController } from "${importPath}";`);
96
+ next = next.replace(bodyRe, wrapped);
97
+ applied.push("ThemeController wrap");
98
+ }
99
+
100
+ // 2) themeBootScript import + <head> 안 <script> 삽입
101
+ if (!hasBoot) {
102
+ next = insertAfterLastImport(next, `import { themeBootScript } from "${bootImportPath}";`);
103
+ const scriptTag = `<script dangerouslySetInnerHTML={{ __html: themeBootScript() }} />`;
104
+ // <head>...</head> 쌍 매칭 우선. 없으면 self-closed <head />를 쌍 형태로 치환.
105
+ const headPairRe = /<head(\s[^>]*)?>([\s\S]*?)<\/head>/;
106
+ const headSelfRe = /<head(\s[^>]*)?\/>/;
107
+ if (headPairRe.test(next)) {
108
+ next = next.replace(headPairRe, (_m, attrs = "", inner) => {
109
+ const a = attrs ? ` ${attrs.trim()}` : "";
110
+ return `<head${a}>${inner}\n ${scriptTag}\n </head>`;
111
+ });
112
+ } else if (headSelfRe.test(next)) {
113
+ next = next.replace(headSelfRe, (_m, attrs = "") => {
114
+ const a = attrs ? ` ${attrs.trim()}` : "";
115
+ return `<head${a}>\n ${scriptTag}\n </head>`;
116
+ });
117
+ } else {
118
+ // <head>가 아예 없으면 <html ...> 바로 다음에 <head>를 신규 삽입.
119
+ const htmlOpenRe = /<html(\s[^>]*)?>/;
120
+ if (!htmlOpenRe.test(next)) {
121
+ return { status: "failed", reason: "<html> 태그를 찾지 못했어요 — 수동 설정 필요." };
122
+ }
123
+ next = next.replace(htmlOpenRe, (m) => `${m}\n <head>\n ${scriptTag}\n </head>`);
124
+ }
125
+ applied.push("boot script 삽입");
126
+ }
127
+
128
+ // 3) <html>에 suppressHydrationWarning 추가 (boot script가 attr을 재세팅하므로 필요)
129
+ if (!/suppressHydrationWarning/.test(next)) {
130
+ next = next.replace(/<html(\s[^>]*)?>/, (m, attrs = "") => {
131
+ return `<html${attrs} suppressHydrationWarning>`;
132
+ });
133
+ applied.push("suppressHydrationWarning");
134
+ }
135
+
136
+ if (next === before) {
137
+ return { status: "skipped", reason: "변경 사항 없음." };
138
+ }
139
+ if (dry) {
140
+ return { status: "applied", reason: `(dry) ${applied.join(" + ")}`, before, after: next };
141
+ }
142
+ copyFileSync(filePath, filePath + ".bak");
143
+ writeFileSync(filePath, next, "utf8");
144
+ return { status: "applied", reason: applied.join(" + "), before, after: next };
145
+ }
146
+
147
+ /** 파일 상단의 마지막 import 바로 아래에 line 삽입. import가 없으면 파일 최상단. */
148
+ function insertAfterLastImport(source, line) {
149
+ const lastImportMatch = [...source.matchAll(/^import\s[^\n]+\n/gm)].pop();
150
+ if (lastImportMatch) {
151
+ const insertAt = lastImportMatch.index + lastImportMatch[0].length;
152
+ return source.slice(0, insertAt) + line + "\n" + source.slice(insertAt);
153
+ }
154
+ return line + "\n\n" + source;
155
+ }
@@ -1,73 +0,0 @@
1
- /* AttachmentChip — [FileIcon] filename [X]
2
- 레퍼런스: ChatInput 안에서 쓰이는 둥근 사각 — radius-md, hairline border. */
3
-
4
- .root {
5
- display: inline-flex;
6
- align-items: center;
7
- gap: var(--space-2);
8
- min-width: 0;
9
- background: var(--bg);
10
- border: var(--hairline) solid var(--border);
11
- border-radius: var(--radius-md);
12
- color: var(--fg);
13
- transition: var(--transition-colors);
14
- }
15
-
16
- /* 패딩만 size 차이. 오른쪽은 X 버튼 자리라 좁게. 폭은 고정 — label이 길면 ellipsis. */
17
- .sizeSm {
18
- padding: var(--space-1) var(--space-1) var(--space-1) var(--space-1);
19
- font-size: var(--text-xs);
20
- width: calc(var(--space-20) * 2);
21
- }
22
- .sizeMd {
23
- padding: var(--space-1_5) var(--space-1_5) var(--space-1_5) var(--space-1_5);
24
- font-size: var(--text-sm);
25
- width: calc(var(--space-24) * 2);
26
- }
27
-
28
- /* 파일명 */
29
- .label {
30
- flex: 1 1 auto;
31
- min-width: 0;
32
- overflow: hidden;
33
- text-overflow: ellipsis;
34
- white-space: nowrap;
35
- color: var(--fg-strong);
36
- line-height: 1.2;
37
- padding-right: var(--space-1);
38
- }
39
-
40
- /* X 버튼 — size에 따라 width/height가 변하도록 토큰 조합 */
41
- .removeBtn {
42
- display: inline-flex;
43
- align-items: center;
44
- justify-content: center;
45
- padding: 0;
46
- border: 0;
47
- background: transparent;
48
- color: var(--fg-muted);
49
- border-radius: var(--radius-sm);
50
- cursor: pointer;
51
- transition: var(--transition-colors);
52
- width: var(--space-5);
53
- height: var(--space-5);
54
- flex: 0 0 auto;
55
- }
56
- .sizeSm .removeBtn {
57
- width: var(--space-4);
58
- height: var(--space-4);
59
- }
60
-
61
- .removeBtn:hover {
62
- background: var(--bg-subtle);
63
- color: var(--fg);
64
- }
65
- .removeBtn:focus-visible {
66
- outline: none;
67
- box-shadow: var(--ring);
68
- }
69
-
70
- .removeIcon {
71
- width: 70%;
72
- height: 70%;
73
- }
@@ -1,74 +0,0 @@
1
- "use client";
2
-
3
- import type { HTMLAttributes, MouseEvent } from "react";
4
- import { X as PhX } from "@phosphor-icons/react";
5
- import { FileIcon, type FileIconSize } from "@/components/ctds/FileIcon";
6
- import styles from "./AttachmentChip.module.css";
7
-
8
- export type AttachmentChipSize = "sm" | "md";
9
-
10
- export interface AttachmentChipProps
11
- extends Omit<HTMLAttributes<HTMLSpanElement>, "children" | "onError"> {
12
- /** 파일명. 확장자가 FileIcon 색/라벨을 결정. */
13
- filename: string;
14
- /** 지정 시 우측에 X 노출, 클릭 시 호출. */
15
- onRemove?: (e: MouseEvent<HTMLButtonElement>) => void;
16
- /** sm=padding·icon 작게 / md=기본. */
17
- size?: AttachmentChipSize;
18
- }
19
-
20
- const sizeClass: Record<AttachmentChipSize, string> = {
21
- sm: styles.sizeSm,
22
- md: styles.sizeMd,
23
- };
24
-
25
- const fileIconSize: Record<AttachmentChipSize, FileIconSize> = {
26
- sm: "sm",
27
- md: "md",
28
- };
29
-
30
- /** 긴 파일명을 중간 생략으로 자르되 확장자는 유지.
31
- * "very-long-report-name.pdf" → "very-long-rep…pdf" */
32
- function middleEllipsis(filename: string, max = 28): string {
33
- if (filename.length <= max) return filename;
34
- const dot = filename.lastIndexOf(".");
35
- if (dot < 0 || dot < filename.length - 6) {
36
- return filename.slice(0, max - 1) + "…";
37
- }
38
- const ext = filename.slice(dot); // ".pdf"
39
- const head = filename.slice(0, Math.max(1, max - ext.length - 1));
40
- return head + "…" + ext;
41
- }
42
-
43
- export function AttachmentChip({
44
- filename,
45
- onRemove,
46
- size = "md",
47
- className,
48
- ...rest
49
- }: AttachmentChipProps) {
50
- const cls = [styles.root, sizeClass[size], className]
51
- .filter(Boolean)
52
- .join(" ");
53
-
54
- const label = middleEllipsis(filename);
55
-
56
- return (
57
- <span className={cls} {...rest}>
58
- <FileIcon ext={filename} size={fileIconSize[size]} />
59
- <span className={styles.label} title={filename}>
60
- {label}
61
- </span>
62
- {onRemove ? (
63
- <button
64
- type="button"
65
- className={styles.removeBtn}
66
- onClick={onRemove}
67
- aria-label="첨부 제거"
68
- >
69
- <PhX className={styles.removeIcon} weight="regular" />
70
- </button>
71
- ) : null}
72
- </span>
73
- );
74
- }
@@ -1,2 +0,0 @@
1
- export { AttachmentChip } from "./AttachmentChip";
2
- export type { AttachmentChipProps, AttachmentChipSize } from "./AttachmentChip";
@@ -1,163 +0,0 @@
1
- /* Calendar ───────────────────────────────────────────────────
2
- 페이지에 인라인으로 들어가는 월간 캘린더. DatePicker와 분리.
3
- 레이아웃은 flex column + 그리드 flex-1로 뷰포트 높이에 반응.
4
- 스크롤 2-레이어 규약: cellEvents는 overflow:hidden 유지 — 넘치면
5
- hiddenCount로 "+N" 뱃지가 대체한다 (내부 스크롤 금지). */
6
- .root {
7
- display: flex;
8
- flex-direction: column;
9
- min-height: 0;
10
- min-width: 0;
11
- background: var(--bg);
12
- color: var(--fg);
13
- }
14
-
15
- /* Toolbar ──────────────────────────────────────────────────── */
16
- .toolbar {
17
- position: relative;
18
- display: flex;
19
- align-items: center;
20
- justify-content: space-between;
21
- padding-block: var(--space-2);
22
- gap: var(--space-3);
23
- }
24
-
25
- .toolbarSide {
26
- display: flex;
27
- align-items: center;
28
- gap: var(--space-1);
29
- min-width: 0;
30
- flex: 1 1 0;
31
- }
32
-
33
- .toolbarSide[data-align="end"] {
34
- justify-content: flex-end;
35
- }
36
-
37
- .monthNav {
38
- position: absolute;
39
- left: 50%;
40
- transform: translateX(-50%);
41
- display: flex;
42
- align-items: center;
43
- gap: var(--space-1);
44
- }
45
-
46
- .monthTitle {
47
- min-width: calc(var(--space-2) * 14);
48
- text-align: center;
49
- font-family: var(--font-normal);
50
- font-size: var(--text-base);
51
- font-weight: var(--font-weight-medium);
52
- color: var(--fg);
53
- }
54
-
55
- /* "오늘"은 현재 달이 아닐 때만 등장. 사라져도 toolbar 높이가 흔들리지 않도록
56
- 자리를 유지하고 visibility만 토글. */
57
- .todaySlot {
58
- visibility: hidden;
59
- }
60
- .todaySlot[data-visible] {
61
- visibility: visible;
62
- }
63
-
64
- /* Weekdays ─────────────────────────────────────────────────── */
65
- .weekdays {
66
- display: grid;
67
- gap: 0;
68
- }
69
-
70
- .weekday {
71
- padding-block: var(--space-2);
72
- text-align: center;
73
- font-family: var(--label-font);
74
- font-size: var(--text-xs);
75
- color: var(--fg-muted);
76
- letter-spacing: var(--label-tracking);
77
- }
78
-
79
- .weekday[data-tone="sat"] { color: var(--info); }
80
- .weekday[data-tone="sun"] { color: var(--destructive); }
81
-
82
- /* Month grid ────────────────────────────────────────────────
83
- 보더는 그리드가 전체 둘레(top/left)를 그리고, 각 셀이 right/bottom만 그린다.
84
- 이렇게 하면 열 수(5열/7열)가 바뀌어도 왼쪽 세로선이 안정적으로 유지됨. */
85
- .grid {
86
- display: grid;
87
- flex: 1 1 0;
88
- min-height: 0;
89
- border-top: var(--hairline) solid var(--border);
90
- border-left: var(--hairline) solid var(--border);
91
- }
92
-
93
- /* Cell ───────────────────────────────────────────────────── */
94
- .cell {
95
- position: relative;
96
- display: flex;
97
- flex-direction: column;
98
- gap: var(--space-1);
99
- min-width: 0;
100
- min-height: 0;
101
- padding: var(--space-2);
102
- border-right: var(--hairline) solid var(--border);
103
- border-bottom: var(--hairline) solid var(--border);
104
- overflow: hidden;
105
- background: var(--bg);
106
- cursor: pointer;
107
- transition: var(--transition-colors);
108
- }
109
-
110
- .cell:hover:not([data-selected]) {
111
- background: var(--hover);
112
- }
113
-
114
- .cell:focus-visible {
115
- outline: var(--ring-width) solid var(--ring);
116
- outline-offset: calc(-1 * var(--ring-width));
117
- }
118
-
119
- /* selected 셀 — inset ring. active 상태 위 hover가 border/bg를 덮지 않도록
120
- ring만 사용 (feedback_hover_over_active 규약). */
121
- .cell[data-selected] {
122
- z-index: var(--z-raise);
123
- box-shadow: inset 0 0 0 var(--ring-width) var(--primary);
124
- }
125
-
126
- .cellHead {
127
- display: flex;
128
- align-items: center;
129
- gap: var(--space-1);
130
- }
131
-
132
- .dayNum {
133
- font-family: var(--font-normal);
134
- font-size: var(--text-sm);
135
- color: var(--fg);
136
- line-height: 1;
137
- }
138
-
139
- .dayNum[data-tone="muted"] { color: var(--fg-disabled); }
140
- .dayNum[data-tone="sat"] { color: var(--info); }
141
- .dayNum[data-tone="sun"] { color: var(--destructive); }
142
-
143
- .dayNum[data-selected],
144
- .cell[data-today] .dayNum:not([data-tone="muted"]) {
145
- color: var(--primary);
146
- font-weight: var(--font-weight-medium);
147
- }
148
-
149
- .cellBadges {
150
- margin-left: auto;
151
- display: inline-flex;
152
- align-items: center;
153
- gap: var(--space-1);
154
- }
155
-
156
- .cellEvents {
157
- display: flex;
158
- flex-direction: column;
159
- gap: var(--space-1);
160
- flex: 1 1 0;
161
- min-height: 0;
162
- overflow: hidden;
163
- }