@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,90 @@
1
+ /* GeneratedImage — AI가 생성한 이미지 큰 표시. loading / placeholder / image 세 상태. */
2
+
3
+ .root {
4
+ position: relative;
5
+ width: 100%;
6
+ max-width: calc(var(--input-w-lg) * 1.1);
7
+ border-radius: var(--radius-md);
8
+ overflow: hidden;
9
+ background: var(--bg-subtle);
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ color: var(--fg-muted);
14
+ }
15
+
16
+ /* aspect ratios */
17
+ .aspectSquare {
18
+ aspect-ratio: 1 / 1;
19
+ }
20
+ .aspectPortrait {
21
+ aspect-ratio: 3 / 4;
22
+ }
23
+ .aspectLandscape {
24
+ aspect-ratio: 4 / 3;
25
+ }
26
+
27
+ /* 이미지 */
28
+ .hasImage {
29
+ background: var(--bg-muted);
30
+ }
31
+ .img {
32
+ width: 100%;
33
+ height: 100%;
34
+ object-fit: cover;
35
+ display: block;
36
+ }
37
+
38
+ /* Placeholder / Loading — bg 이미지 + Spinner lottie-dots */
39
+ .placeholder,
40
+ .loading {
41
+ background: url("/illustrations/image-gen-placeholder.png") center / cover no-repeat,
42
+ var(--bg-muted);
43
+ }
44
+
45
+ .spinner {
46
+ color: var(--bg);
47
+ /* image-gen 로띠는 공간이 넉넉해야 보기 좋음 — loader-size override. */
48
+ --loader-size: var(--space-16);
49
+ }
50
+
51
+ /* Download 버튼 — hover 시만 노출, 우상단 */
52
+ .downloadBtn {
53
+ position: absolute;
54
+ top: var(--space-2);
55
+ right: var(--space-2);
56
+ display: inline-flex;
57
+ align-items: center;
58
+ justify-content: center;
59
+ padding: 0;
60
+ border: 0;
61
+ background: var(--bg);
62
+ color: var(--fg);
63
+ border-radius: var(--radius-sm);
64
+ cursor: pointer;
65
+ width: var(--space-8);
66
+ height: var(--space-8);
67
+ box-shadow: var(--shadow-sm);
68
+ opacity: 0;
69
+ transform: scale(0.9);
70
+ transition:
71
+ opacity var(--duration-fast) var(--ease-out),
72
+ transform var(--duration-fast) var(--ease-out),
73
+ var(--transition-colors);
74
+ }
75
+ .root:hover .downloadBtn,
76
+ .downloadBtn:focus-visible {
77
+ opacity: 1;
78
+ transform: scale(1);
79
+ }
80
+ .downloadBtn:hover {
81
+ background: var(--bg-subtle);
82
+ }
83
+ .downloadBtn:focus-visible {
84
+ outline: none;
85
+ box-shadow: var(--shadow-sm), var(--ring);
86
+ }
87
+ .downloadIcon {
88
+ width: 55%;
89
+ height: 55%;
90
+ }
@@ -0,0 +1,70 @@
1
+ "use client";
2
+
3
+ import type { HTMLAttributes, MouseEvent } from "react";
4
+ import { DownloadSimple as PhDownload } from "@phosphor-icons/react";
5
+ import { Spinner } from "@/components/ctds/Spinner";
6
+ import { Tooltip } from "@/components/ctds/Tooltip";
7
+ import styles from "./GeneratedImage.module.css";
8
+
9
+ export type GeneratedImageAspect = "square" | "portrait" | "landscape";
10
+
11
+ export interface GeneratedImageProps
12
+ extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
13
+ /** 생성된 이미지 URL. 없거나 loading이면 placeholder. */
14
+ src?: string;
15
+ alt?: string;
16
+ /** 가로세로 비율. portrait=3/4(기본, 레퍼런스), square=1/1, landscape=4/3. */
17
+ aspect?: GeneratedImageAspect;
18
+ /** 생성 중 — placeholder + sparkle breathe 애니. */
19
+ loading?: boolean;
20
+ /** 다운로드 버튼 클릭. 미지정 시 버튼 숨김. */
21
+ onDownload?: (e: MouseEvent<HTMLButtonElement>) => void;
22
+ }
23
+
24
+ const aspectClass: Record<GeneratedImageAspect, string> = {
25
+ square: styles.aspectSquare,
26
+ portrait: styles.aspectPortrait,
27
+ landscape: styles.aspectLandscape,
28
+ };
29
+
30
+ export function GeneratedImage({
31
+ src,
32
+ alt = "",
33
+ aspect = "portrait",
34
+ loading = false,
35
+ onDownload,
36
+ className,
37
+ ...rest
38
+ }: GeneratedImageProps) {
39
+ const cls = [
40
+ styles.root,
41
+ aspectClass[aspect],
42
+ loading ? styles.loading : src ? styles.hasImage : styles.placeholder,
43
+ className,
44
+ ]
45
+ .filter(Boolean)
46
+ .join(" ");
47
+
48
+ return (
49
+ <div className={cls} {...rest}>
50
+ {loading || !src ? (
51
+ <Spinner variant="lottie-image-gen" size="lg" className={styles.spinner} />
52
+ ) : (
53
+ <img src={src} alt={alt} className={styles.img} />
54
+ )}
55
+
56
+ {onDownload && src && !loading ? (
57
+ <Tooltip content="다운로드">
58
+ <button
59
+ type="button"
60
+ className={styles.downloadBtn}
61
+ onClick={onDownload}
62
+ aria-label="이미지 다운로드"
63
+ >
64
+ <PhDownload className={styles.downloadIcon} weight="regular" />
65
+ </button>
66
+ </Tooltip>
67
+ ) : null}
68
+ </div>
69
+ );
70
+ }
@@ -0,0 +1,5 @@
1
+ export { GeneratedImage } from "./GeneratedImage";
2
+ export type {
3
+ GeneratedImageProps,
4
+ GeneratedImageAspect,
5
+ } from "./GeneratedImage";
@@ -128,6 +128,7 @@ import {
128
128
  Scissors as PhScissors,
129
129
  ClipboardText as PhClipboardText,
130
130
  Camera as PhCamera,
131
+ Globe as PhGlobe,
131
132
  } from "@phosphor-icons/react";
132
133
 
133
134
  // Solar (Linear = regular, Bold = fill) ───────────────────────────
@@ -308,7 +309,8 @@ export type IconName =
308
309
  | "copy-simple"
309
310
  | "scissors"
310
311
  | "clipboard-text"
311
- | "camera";
312
+ | "camera"
313
+ | "globe";
312
314
 
313
315
  type AnyComp = React.ComponentType<any>;
314
316
  // Solar는 weight별 두 컴포넌트가 필요. 다른 family는 단일 컴포넌트.
@@ -422,6 +424,7 @@ const phosphorSet: Partial<Record<IconName, AnyComp>> = {
422
424
  scissors: PhScissors,
423
425
  "clipboard-text": PhClipboardText,
424
426
  camera: PhCamera,
427
+ globe: PhGlobe,
425
428
  };
426
429
 
427
430
  /* solar 세트 — 라이브러리에 자연스러운 1:1 대응이 있는 것만.
@@ -132,6 +132,9 @@
132
132
  font-family: inherit;
133
133
  font-size: inherit;
134
134
  line-height: var(--lh-sm);
135
+ /* 전역 tight 자간(-0.02em)을 상속하면 password dot(●)이 너무 벌어져 보이고
136
+ placeholder도 느슨해 보임. 폼 필드는 자간 조이지 않는다. */
137
+ letter-spacing: var(--tracking-normal);
135
138
  outline: none;
136
139
  /* type="button" 등 non-text input의 기본 center 정렬 무력화. */
137
140
  text-align: start;
@@ -15,12 +15,13 @@
15
15
  font-weight: var(--font-weight-regular);
16
16
  }
17
17
 
18
+ /* 리더 폭 — Spinner size=sm(20px)과 통일. 아이콘/스피너 전환해도 라벨 위치 안 흔들림. */
18
19
  .leadIcon {
19
20
  display: inline-flex;
20
21
  align-items: center;
21
22
  justify-content: center;
22
- width: var(--space-4);
23
- height: var(--space-4);
23
+ width: var(--loader-size-sm);
24
+ height: var(--loader-size-sm);
24
25
  color: var(--primary);
25
26
  flex: 0 0 auto;
26
27
  }
@@ -31,27 +32,32 @@
31
32
  .leadSpinner {
32
33
  color: var(--primary);
33
34
  flex: 0 0 auto;
35
+ /* Spinner sm = 20px = loader-size-sm. 폭 명시해 동일 width 보장. */
36
+ width: var(--loader-size-sm);
37
+ height: var(--loader-size-sm);
34
38
  }
35
39
 
36
- /* 통통거리는 라벨 (AIMessage의 generating 패턴과 동일 — 중복 정의) */
40
+ /* 통통거리는 라벨 — 글자 수에 비례한 cycle, 각 글자 index 지연으로 wave. */
37
41
  .label {
38
42
  display: inline-flex;
39
43
  align-items: center;
40
44
  }
41
45
  .bounceChar {
42
46
  display: inline-block;
43
- animation: msgstate-char-bounce calc(var(--duration-slower) * 5) var(--ease-in-out) infinite;
47
+ animation: msgstate-char-bounce calc(var(--duration-slower) * 10) var(--ease-in-out) infinite;
44
48
  }
45
49
  .bounceSpace {
46
50
  display: inline-block;
47
51
  white-space: pre;
48
52
  }
53
+ /* bounce는 cycle 앞 35%까지만 (올라감 17.5%→피크→내려옴 35%), 나머지 65%는 쉼.
54
+ cycle을 2배로 늘렸으니 bounce 자체 속도는 그대로, 다음 wave까지 간격만 길어짐. */
49
55
  @keyframes msgstate-char-bounce {
50
- 0%, 70%, 100% {
56
+ 0%, 35%, 100% {
51
57
  transform: translateY(0);
52
58
  opacity: 0.6;
53
59
  }
54
- 35% {
60
+ 17.5% {
55
61
  transform: translateY(calc(-1.5px * var(--motion-distance-scale))); /* lint-hardcoded: ignore */
56
62
  opacity: 1;
57
63
  }
@@ -23,9 +23,11 @@ export type MessageStateKind =
23
23
 
24
24
  export interface MessageStateProps extends HTMLAttributes<HTMLDivElement> {
25
25
  kind?: MessageStateKind;
26
- /** 라벨 override. kind별 기본 라벨이 있지만 커스터마이즈 가능. */
26
+ /** 활성 상태 라벨 override. 미지정 시 kind별 기본("…ing…"). */
27
27
  message?: ReactNode;
28
- /** 완료 상태 — true면 spinner 자리에 ✓, 라벨 통통거림 멈춤. */
28
+ /** 완료 상태 라벨 override. 미지정 시 kind별 done 기본("…ed"). */
29
+ doneMessage?: ReactNode;
30
+ /** 완료 상태 — true면 spinner 자리에 ✓, 라벨 통통거림 멈춤, doneMessage로 전환. */
29
31
  done?: boolean;
30
32
  /** 커스텀 아이콘 — kind="custom"이거나 override 하고 싶을 때. */
31
33
  icon?: ReactNode;
@@ -34,13 +36,24 @@ export interface MessageStateProps extends HTMLAttributes<HTMLDivElement> {
34
36
  const DEFAULT_MESSAGE: Record<MessageStateKind, string> = {
35
37
  generating: "응답 생성 중…",
36
38
  searching: "Searching the web…",
37
- memory: "Updated saved memory",
39
+ memory: "Updating memory…",
38
40
  image: "Looking at the image…",
39
41
  video: "Reviewing the video…",
40
42
  file: "Processing file…",
41
43
  custom: "",
42
44
  };
43
45
 
46
+ /** 완료 상태 라벨 — done=true일 때 message prop이 없으면 이걸 사용. */
47
+ const DEFAULT_DONE_MESSAGE: Record<MessageStateKind, string> = {
48
+ generating: "응답 완료",
49
+ searching: "Searched the web",
50
+ memory: "Updated saved memory",
51
+ image: "Looked at the image",
52
+ video: "Reviewed the video",
53
+ file: "Processed file",
54
+ custom: "",
55
+ };
56
+
44
57
  const KIND_ICON: Record<MessageStateKind, React.ComponentType<any> | null> = {
45
58
  generating: null, // Spinner만
46
59
  searching: PhSearch,
@@ -76,12 +89,15 @@ function renderBouncingText(text: string) {
76
89
  export function MessageState({
77
90
  kind = "generating",
78
91
  message,
92
+ doneMessage,
79
93
  done = false,
80
94
  icon,
81
95
  className,
82
96
  ...rest
83
97
  }: MessageStateProps) {
84
- const label = message ?? DEFAULT_MESSAGE[kind];
98
+ const label = done
99
+ ? doneMessage ?? DEFAULT_DONE_MESSAGE[kind]
100
+ : message ?? DEFAULT_MESSAGE[kind];
85
101
  const KindIcon = KIND_ICON[kind];
86
102
 
87
103
  const cls = [styles.root, className].filter(Boolean).join(" ");
@@ -96,11 +112,13 @@ export function MessageState({
96
112
  <Spinner variant="lottie-dots" size="sm" className={styles.leadSpinner} />
97
113
  );
98
114
 
115
+ const labelStr = typeof label === "string" ? label : null;
116
+
99
117
  return (
100
118
  <div className={cls} data-done={done || undefined} {...rest}>
101
119
  {leadIcon}
102
120
  <span className={styles.label}>
103
- {done || typeof label !== "string" ? label : renderBouncingText(label)}
121
+ {done || !labelStr ? label : renderBouncingText(labelStr)}
104
122
  </span>
105
123
  </div>
106
124
  );
@@ -29,7 +29,16 @@ export interface ModalProps {
29
29
  disableEscape?: boolean;
30
30
  }
31
31
 
32
- export const Modal = forwardRef<HTMLDivElement, ModalProps>(function Modal(
32
+ /**
33
+ * 간편 모드 — 가장 흔한 "title + body + footer" 레이아웃을 flat prop으로 넘긴다.
34
+ * 커스터마이즈가 필요하면 compound 모드:
35
+ * <Modal.Root open onOpenChange={set}>
36
+ * <Modal.Header title="…" />
37
+ * <Modal.Body>…</Modal.Body>
38
+ * <Modal.Footer>…</Modal.Footer>
39
+ * </Modal.Root>
40
+ */
41
+ const ModalComponent = forwardRef<HTMLDivElement, ModalProps>(function Modal(
33
42
  {
34
43
  open,
35
44
  onOpenChange,
@@ -43,6 +52,38 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(function Modal(
43
52
  disableEscape,
44
53
  },
45
54
  ref,
55
+ ) {
56
+ return (
57
+ <ModalRoot
58
+ open={open}
59
+ onOpenChange={onOpenChange}
60
+ disableEscape={disableEscape}
61
+ ref={ref}
62
+ >
63
+ <ModalHeader
64
+ title={title}
65
+ description={description}
66
+ titleSrOnly={titleSrOnly}
67
+ dismissible={dismissible}
68
+ dismissLabel={dismissLabel}
69
+ />
70
+ {children !== undefined && children !== null && <ModalBody>{children}</ModalBody>}
71
+ {footer && <ModalFooter>{footer}</ModalFooter>}
72
+ </ModalRoot>
73
+ );
74
+ });
75
+
76
+ export interface ModalRootProps {
77
+ open: boolean;
78
+ onOpenChange: (open: boolean) => void;
79
+ children: ReactNode;
80
+ /** ESC 키 닫힘 차단. 중요한 확인/저장 중엔 true. */
81
+ disableEscape?: boolean;
82
+ }
83
+
84
+ export const ModalRoot = forwardRef<HTMLDivElement, ModalRootProps>(function ModalRoot(
85
+ { open, onOpenChange, children, disableEscape },
86
+ ref,
46
87
  ) {
47
88
  return (
48
89
  <RadixDialog.Root open={open} onOpenChange={onOpenChange}>
@@ -59,40 +100,80 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(function Modal(
59
100
  e.preventDefault();
60
101
  }}
61
102
  >
62
- <div className={styles.header}>
63
- <div className={styles.headerText}>
64
- <RadixDialog.Title
65
- className={[styles.title, titleSrOnly && "visually-hidden"]
66
- .filter(Boolean)
67
- .join(" ")}
68
- >
69
- {title}
70
- </RadixDialog.Title>
71
- {description && (
72
- <RadixDialog.Description className={styles.description}>
73
- {description}
74
- </RadixDialog.Description>
75
- )}
76
- </div>
77
- {dismissible && (
78
- <RadixDialog.Close asChild>
79
- <IconButton
80
- className={styles.close}
81
- variant="ghost"
82
- icon={<Icon name="x" />}
83
- aria-label={dismissLabel}
84
- />
85
- </RadixDialog.Close>
86
- )}
87
- </div>
88
- {children !== undefined && children !== null && (
89
- <div className={styles.body}>
90
- <div className={styles.bodyInner}>{children}</div>
91
- </div>
92
- )}
93
- {footer && <div className={styles.footer}>{footer}</div>}
103
+ {children}
94
104
  </RadixDialog.Content>
95
105
  </RadixDialog.Portal>
96
106
  </RadixDialog.Root>
97
107
  );
98
108
  });
109
+
110
+ export interface ModalHeaderProps {
111
+ title: ReactNode;
112
+ description?: ReactNode;
113
+ titleSrOnly?: boolean;
114
+ dismissible?: boolean;
115
+ dismissLabel?: string;
116
+ }
117
+
118
+ export function ModalHeader({
119
+ title,
120
+ description,
121
+ titleSrOnly,
122
+ dismissible = true,
123
+ dismissLabel,
124
+ }: ModalHeaderProps) {
125
+ return (
126
+ <div className={styles.header}>
127
+ <div className={styles.headerText}>
128
+ <RadixDialog.Title
129
+ className={[styles.title, titleSrOnly && "visually-hidden"]
130
+ .filter(Boolean)
131
+ .join(" ")}
132
+ >
133
+ {title}
134
+ </RadixDialog.Title>
135
+ {description && (
136
+ <RadixDialog.Description className={styles.description}>
137
+ {description}
138
+ </RadixDialog.Description>
139
+ )}
140
+ </div>
141
+ {dismissible && (
142
+ <RadixDialog.Close asChild>
143
+ <IconButton
144
+ className={styles.close}
145
+ variant="ghost"
146
+ icon={<Icon name="x" />}
147
+ aria-label={dismissLabel}
148
+ />
149
+ </RadixDialog.Close>
150
+ )}
151
+ </div>
152
+ );
153
+ }
154
+
155
+ export function ModalBody({ children }: { children: ReactNode }) {
156
+ return (
157
+ <div className={styles.body}>
158
+ <div className={styles.bodyInner}>{children}</div>
159
+ </div>
160
+ );
161
+ }
162
+
163
+ export function ModalFooter({ children }: { children: ReactNode }) {
164
+ return <div className={styles.footer}>{children}</div>;
165
+ }
166
+
167
+ type ModalCompound = typeof ModalComponent & {
168
+ Root: typeof ModalRoot;
169
+ Header: typeof ModalHeader;
170
+ Body: typeof ModalBody;
171
+ Footer: typeof ModalFooter;
172
+ };
173
+
174
+ export const Modal = Object.assign(ModalComponent, {
175
+ Root: ModalRoot,
176
+ Header: ModalHeader,
177
+ Body: ModalBody,
178
+ Footer: ModalFooter,
179
+ }) as ModalCompound;
@@ -1,2 +1,12 @@
1
- export { Modal } from "./Modal";
2
- export type { ModalProps } from "./Modal";
1
+ export {
2
+ Modal,
3
+ ModalRoot,
4
+ ModalHeader,
5
+ ModalBody,
6
+ ModalFooter,
7
+ } from "./Modal";
8
+ export type {
9
+ ModalProps,
10
+ ModalRootProps,
11
+ ModalHeaderProps,
12
+ } from "./Modal";
@@ -14,10 +14,10 @@
14
14
  flex-shrink: 0;
15
15
  }
16
16
 
17
- .xs { --loader-size: var(--loader-size-xs); }
18
17
  .sm { --loader-size: var(--loader-size-sm); }
19
18
  .md { --loader-size: var(--loader-size-md); }
20
19
  .lg { --loader-size: var(--loader-size-lg); }
20
+ .xl { --loader-size: var(--loader-size-xl); }
21
21
 
22
22
  /* 자식 전부 기본 숨김 — 활성 variant만 켬. */
23
23
  .ring,
@@ -19,7 +19,7 @@ import styles from "./Spinner.module.css";
19
19
  import type { LottieName } from "./lottieManifest";
20
20
  import { LottiePlayer } from "./LottiePlayer";
21
21
 
22
- export type SpinnerSize = "xs" | "sm" | "md" | "lg";
22
+ export type SpinnerSize = "sm" | "md" | "lg" | "xl";
23
23
  export type SpinnerBaseVariant = "ring" | "dots-bounce" | "dots-fade" | "pulse";
24
24
  export type SpinnerLottieVariant = `lottie-${LottieName}`;
25
25
  export type SpinnerVariant = SpinnerBaseVariant | SpinnerLottieVariant;
@@ -33,10 +33,10 @@ export type SpinnerProps = HTMLAttributes<HTMLSpanElement> & {
33
33
  };
34
34
 
35
35
  const sizeClass: Record<SpinnerSize, string> = {
36
- xs: styles.xs,
37
36
  sm: styles.sm,
38
37
  md: styles.md,
39
38
  lg: styles.lg,
39
+ xl: styles.xl,
40
40
  };
41
41
 
42
42
  const baseVariantClass: Record<SpinnerBaseVariant, string> = {
@@ -16,6 +16,12 @@ export type SwitchProps = Omit<
16
16
  "type" | "size"
17
17
  > & {
18
18
  size?: SwitchSize;
19
+ /**
20
+ * 토글 변경 콜백. 공식 API — 대부분의 경우 이걸 쓴다.
21
+ * 네이티브 `onChange`도 받아낼 수 있는데, 둘 다 쓰면 호출 순서는:
22
+ * 1) onChange(e) — 호출자가 e.preventDefault() 할 수 있는 escape hatch.
23
+ * 2) onChange가 preventDefault 하지 않으면 onCheckedChange(checked).
24
+ */
19
25
  onCheckedChange?: (checked: boolean) => void;
20
26
  };
21
27
 
@@ -92,16 +92,18 @@
92
92
  background: var(--bg);
93
93
  }
94
94
 
95
- /* pinned 경계 — hairline으로 "고정 영역" 분리. TableRow와 짝. */
95
+ /* pinned 경계 — hairline으로 "고정 영역" 분리. TableRow와 짝.
96
+ border 대신 inset box-shadow로 그어 sticky 레이어 subpixel 겹침(두 줄처럼
97
+ 보이는 Chrome 렌더 현상) 회피 + 셀 폭에 영향 안 줌. */
96
98
  .cellSlot[data-pinned="left"] {
97
- border-right: var(--hairline) solid var(--border);
99
+ box-shadow: inset calc(var(--hairline) * -1) 0 0 var(--border);
98
100
  }
99
101
  .cellSlot[data-pinned="right"] {
100
- border-left: var(--hairline) solid var(--border);
102
+ box-shadow: inset var(--hairline) 0 0 var(--border);
101
103
  }
102
104
 
103
105
  .selectCell {
104
- border-right: var(--hairline) solid var(--border);
106
+ box-shadow: inset calc(var(--hairline) * -1) 0 0 var(--border);
105
107
  }
106
108
 
107
109
  /* Cell (columnheader) ─────────────────────────────────────── */
@@ -210,6 +212,15 @@
210
212
  display: none;
211
213
  }
212
214
 
215
+ /* pinned 경계 hairline과 tick(cellSlot 좌측 세로선) 겹침 방지.
216
+ - pinned-left 셀의 바로 다음 cellSlot: pinned의 border-right와 다음 셀의
217
+ tick이 바로 붙어 두 줄처럼 보이는 현상 제거.
218
+ - pinned-right 셀 자신: border-left와 자기 tick이 같은 지점에 겹침. */
219
+ .cellSlot[data-pinned="left"] + .cellSlot .tick,
220
+ .cellSlot[data-pinned="right"] .tick {
221
+ display: none;
222
+ }
223
+
213
224
  /* Resize handle ──────────────────────────────────────────────
214
225
  히트 영역은 셀 우측 경계에 걸쳐 있는 투명 버튼. 시각 세로선은 안쪽 .resizeLine이
215
226
  담당하고, 라인은 handle 폭 안에서 "우측 끝"이 아니라 "중앙"에 위치 →
@@ -217,11 +228,10 @@
217
228
  .resizeHandle {
218
229
  position: absolute;
219
230
  top: 0;
220
- /* handle 중심을 다음 세로 구분선 위치에 정확히 맞춤.
221
- - 데이터 row의 세로선: 다음 cellSlot의 좌측 경계(= 현재 셀 우측 + cellSlot gap).
222
- - handle 중심이 그 지점에 오도록: right = -(gap + handleWidth/2).
223
- cellSlot gap은 TableRow의 column-gap(--space-1). */
224
- right: calc((var(--space-1) * -1) - (var(--space-3) * 0.5));
231
+ /* handle 중심을 셀 우측 경계(= 데이터 row의 세로 구분선)에 정확히 맞춤.
232
+ TableRow는 cellSlot 사이 column-gap을 쓰지 않으므로 추가 보정 없음.
233
+ right = -(handleWidth/2). */
234
+ right: calc(var(--space-3) * -0.5);
225
235
  bottom: 0;
226
236
  width: var(--space-3);
227
237
  padding: 0;
@@ -241,36 +251,35 @@
241
251
  border-radius: var(--radius-xs);
242
252
  }
243
253
 
244
- /* 시각 세로선 — plate에서만 hover 시 표시. line은 자체 tick으로 대체하므로 숨김.
245
- 색은 border-strong(살짝 밝은 hairline 톤) — fg-subtle은 너무 진해서 데이터 색과 경쟁함. */
246
254
  /* 둥근 pill 핸들 — 선이 아니라 떠있는 그립 느낌.
247
- 폭은 좁고 세로로 길쭉한 캡슐, 연한 중성 톤에 그림자로 "집어들 수 있는" 어포던스. */
255
+ 흰(bg) 캡슐에 연한 hairline + 연한 그림자로 "집어들 수 있는" 어포던스.
256
+ hover 전엔 숨겨두고, 셀 hover 때 opacity로 올려 보여줌. */
248
257
  .resizeLine {
249
258
  display: block;
250
- width: var(--space-1);
251
- height: 60%;
252
- background: var(--border-strong);
259
+ width: calc(var(--space-1) * 1.25);
260
+ height: 48%;
261
+ background: var(--bg);
262
+ border: var(--hairline) solid var(--border);
253
263
  border-radius: var(--radius-full);
254
264
  opacity: 0;
255
265
  box-shadow: var(--shadow-xs);
256
266
  transition:
257
267
  opacity var(--duration-fast) var(--ease-out),
258
- background var(--duration-fast) var(--ease-out),
259
268
  height var(--duration-fast) var(--ease-out),
260
269
  box-shadow var(--duration-fast) var(--ease-out);
261
270
  }
262
271
 
263
272
  /* 셀 전체 hover: pill 연하게 노출. */
264
273
  .cell:hover .resizeLine {
265
- opacity: 0.6;
274
+ opacity: 0.7;
266
275
  }
267
276
 
268
- /* handle hover/active: 더 또렷하게 + 길어지고 shadow 깊어짐 — "집은 상태". */
277
+ /* handle hover/active: 또렷하게 + 길어지고 그림자 깊어짐 — "집은 상태".
278
+ 색/스트로크는 유지, 변하는 건 opacity·height·shadow 뿐. */
269
279
  .resizeHandle:hover .resizeLine,
270
280
  .resizeHandle:active .resizeLine {
271
281
  opacity: 1;
272
- background: var(--fg-muted);
273
- height: 80%;
282
+ height: 64%;
274
283
  box-shadow: var(--shadow-sm);
275
284
  }
276
285