@conscience-technology/ctds 0.1.7 → 0.1.8

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.
@@ -124,7 +124,7 @@
124
124
  align-items: center;
125
125
  justify-content: space-between;
126
126
  gap: var(--space-1);
127
- padding: 0 var(--space-4) var(--space-2);
127
+ padding: 0 var(--space-2) var(--space-2);
128
128
  }
129
129
 
130
130
  /* SingleLine */
@@ -141,6 +141,8 @@
141
141
  background: transparent;
142
142
  border: 0;
143
143
  outline: none;
144
+ resize: none;
145
+ overflow: hidden;
144
146
  color: var(--fg-strong);
145
147
  font: inherit;
146
148
  font-size: var(--text-sm);
@@ -149,6 +151,9 @@
149
151
  .singleField::placeholder {
150
152
  color: var(--fg-muted);
151
153
  }
154
+ .singleField::placeholder {
155
+ color: var(--fg-muted);
156
+ }
152
157
 
153
158
  /* Attach 버튼 */
154
159
  .attachBtn {
@@ -108,11 +108,10 @@ export const ChatInput = forwardRef<HTMLDivElement, ChatInputProps>(
108
108
  const canSend = !disabled && (value.trim().length > 0 || attachments.length > 0);
109
109
  const showStop = isStreaming && !!onStop;
110
110
 
111
- /* autoResize for multi — textarea는 content에 맞춰 자람(scroll 아님).
112
- 외부 scroll wrapper가 max-height를 책임 → 넘치면 wrapper가 스크롤.
113
- 룰: 스크롤이 wrapper에 생기므로 wrapper padding이 "트랙 역할" (padding 안쪽까지 흘러 border에서 잘림). */
111
+ /* autoResize — multi/single 둘 다. textarea는 content에 맞춰 자람(scroll 아님).
112
+ 외부 scroll wrapper(multi)가 max-height 책임. single은 wrapper 없이 textarea가 직접 자람.
113
+ Shift+Enter로 줄바꿈 → textarea가 즉시 자라고, SingleLine도 자연스레 늘어남. */
114
114
  useLayoutEffect(() => {
115
- if (shape !== "multi") return;
116
115
  const el = taRef.current;
117
116
  if (!el) return;
118
117
  el.style.height = "auto";
@@ -122,8 +121,8 @@ export const ChatInput = forwardRef<HTMLDivElement, ChatInputProps>(
122
121
  const handleKeyDown = useCallback(
123
122
  (e: KeyboardEvent<HTMLTextAreaElement | HTMLInputElement>) => {
124
123
  if (e.key !== "Enter") return;
125
- if (e.shiftKey) return; // multi: 줄바꿈 허용
126
- if (shape === "multi" && e.nativeEvent.isComposing) return;
124
+ if (e.shiftKey) return; // Shift+Enter: multi/single 모두 줄바꿈 허용
125
+ if (e.nativeEvent.isComposing) return; // 한글 조합 중 전송 방지
127
126
  e.preventDefault();
128
127
  if (showStop) {
129
128
  onStop?.();
@@ -131,7 +130,7 @@ export const ChatInput = forwardRef<HTMLDivElement, ChatInputProps>(
131
130
  onSend();
132
131
  }
133
132
  },
134
- [shape, canSend, showStop, onSend, onStop]
133
+ [canSend, showStop, onSend, onStop]
135
134
  );
136
135
 
137
136
  const handleSendClick = useCallback(() => {
@@ -277,14 +276,15 @@ export const ChatInput = forwardRef<HTMLDivElement, ChatInputProps>(
277
276
  {shape === "single" ? (
278
277
  <>
279
278
  {AttachButton}
280
- <input
281
- type="text"
279
+ <textarea
280
+ ref={taRef}
282
281
  className={styles.singleField}
283
282
  value={value}
284
283
  onChange={(e) => onChange(e.target.value)}
285
284
  onKeyDown={handleKeyDown}
286
285
  placeholder={placeholder}
287
286
  disabled={disabled}
287
+ rows={1}
288
288
  aria-label={placeholder}
289
289
  />
290
290
  {ModelAndSend}
@@ -92,6 +92,18 @@
92
92
  background: var(--bg);
93
93
  }
94
94
 
95
+ /* pinned 경계 — hairline으로 "고정 영역" 분리. TableRow와 짝. */
96
+ .cellSlot[data-pinned="left"] {
97
+ border-right: var(--hairline) solid var(--border);
98
+ }
99
+ .cellSlot[data-pinned="right"] {
100
+ border-left: var(--hairline) solid var(--border);
101
+ }
102
+
103
+ .selectCell {
104
+ border-right: var(--hairline) solid var(--border);
105
+ }
106
+
95
107
  /* Cell (columnheader) ─────────────────────────────────────── */
96
108
  /* width:100%로 슬롯 폭을 그대로 채워 라벨/tick/handle 정렬이 슬롯 경계에 맞춰 계산됨.
97
109
  padding-inline은 hover/drag hit 영역이 콘텐츠에 바짝 붙지 않도록 필수.
@@ -394,6 +394,25 @@ const TableHeader = forwardRef<HTMLDivElement, TableHeaderProps>(function TableH
394
394
  .map((k) => columnMap.get(k))
395
395
  .filter((c): c is TableHeaderColumn => !!c);
396
396
 
397
+ /* selectCell이 렌더되면 그 실제 폭을 측정해 좌측 pinned columns의 left offset에
398
+ 더해준다. CSS 토큰(--iconbtn-h-lg / --control-h-sm) + space-scale 반영이 자동으로
399
+ 흡수되므로 하드코딩이나 CSS 변수 파싱 없이 처리 가능. */
400
+ const selectCellRef = useRef<HTMLDivElement>(null);
401
+ const [selectCellWidth, setSelectCellWidth] = useState(0);
402
+ useEffect(() => {
403
+ if (!selectable) {
404
+ setSelectCellWidth(0);
405
+ return;
406
+ }
407
+ const el = selectCellRef.current;
408
+ if (!el) return;
409
+ const update = () => setSelectCellWidth(el.getBoundingClientRect().width);
410
+ update();
411
+ const ro = new ResizeObserver(update);
412
+ ro.observe(el);
413
+ return () => ro.disconnect();
414
+ }, [selectable]);
415
+
397
416
  /* Sort ─────────────────────────────────────────────────── */
398
417
  const toggleSort = useCallback(
399
418
  (key: string) => {
@@ -497,7 +516,7 @@ const TableHeader = forwardRef<HTMLDivElement, TableHeaderProps>(function TableH
497
516
  const cells = (
498
517
  <>
499
518
  {selectable && (
500
- <div className={styles.selectCell} role="columnheader">
519
+ <div ref={selectCellRef} className={styles.selectCell} role="columnheader">
501
520
  <Checkbox
502
521
  checked={selectAllChecked}
503
522
  onChange={(e) => onSelectAllChange?.(e.currentTarget.checked)}
@@ -524,7 +543,7 @@ const TableHeader = forwardRef<HTMLDivElement, TableHeaderProps>(function TableH
524
543
  className={styles.cellSlot}
525
544
  style={{
526
545
  ...cellSlotStyle(col, widths[col.key]),
527
- ...pinnedStyle(col, order, columnMap, widths),
546
+ ...pinnedStyle(col, order, columnMap, widths, selectCellWidth),
528
547
  }}
529
548
  >
530
549
  <Cell
@@ -551,7 +570,7 @@ const TableHeader = forwardRef<HTMLDivElement, TableHeaderProps>(function TableH
551
570
  className={styles.cellSlot}
552
571
  style={{
553
572
  ...cellSlotStyle(col, widths[col.key]),
554
- ...pinnedStyle(col, order, columnMap, widths),
573
+ ...pinnedStyle(col, order, columnMap, widths, selectCellWidth),
555
574
  }}
556
575
  >
557
576
  <Cell
@@ -145,6 +145,21 @@ a.root:not(.disabled):focus-visible,
145
145
  contain: none;
146
146
  }
147
147
 
148
+ /* pinned 경계 — scroll shadow 대신 상시 hairline으로 "여기까지 고정 영역"을 분명히.
149
+ 좌측 pinned → 우측 경계에 세로 hairline. 우측 pinned → 좌측 경계. */
150
+ .cellSlot[data-pinned="left"] {
151
+ border-right: var(--hairline) solid var(--border);
152
+ }
153
+ .cellSlot[data-pinned="right"] {
154
+ border-left: var(--hairline) solid var(--border);
155
+ }
156
+
157
+ /* selectable + pinned 조합일 때 selectCell이 "실질적 좌측 pinned의 선두" 역할이라
158
+ selectCell에도 우측 hairline을 그어 경계 톤 일치. */
159
+ .selectCell {
160
+ border-right: var(--hairline) solid var(--border);
161
+ }
162
+
148
163
  /* ── Cell ───────────────────────────────────────────────
149
164
  Header의 .cell과 1:1 동일. slot 안쪽 width:100%로 가득 채우는 콘텐츠 그릇.
150
165
  display:inline-flex 는 Header 그대로 따라감 — flex vs inline-flex 차이가
@@ -2,6 +2,9 @@
2
2
 
3
3
  import {
4
4
  forwardRef,
5
+ useEffect,
6
+ useRef,
7
+ useState,
5
8
  type AnchorHTMLAttributes,
6
9
  type CSSProperties,
7
10
  type HTMLAttributes,
@@ -143,6 +146,25 @@ function TableRowInner<T>(
143
146
  const orderedKeys = resolveOrder(columns, order);
144
147
  const columnMap = new Map(columns.map((c) => [c.key, c]));
145
148
 
149
+ /* selectCell 폭을 측정해 좌측 pinned column의 left offset에 반영.
150
+ Header와 동일한 로직 — selectCell이 sticky left:0이라 pinned column들이 그 뒤에
151
+ 위치하려면 폭만큼 밀어야 함. */
152
+ const selectCellRef = useRef<HTMLDivElement>(null);
153
+ const [selectCellWidth, setSelectCellWidth] = useState(0);
154
+ useEffect(() => {
155
+ if (!selectable) {
156
+ setSelectCellWidth(0);
157
+ return;
158
+ }
159
+ const el = selectCellRef.current;
160
+ if (!el) return;
161
+ const update = () => setSelectCellWidth(el.getBoundingClientRect().width);
162
+ update();
163
+ const ro = new ResizeObserver(update);
164
+ ro.observe(el);
165
+ return () => ro.disconnect();
166
+ }, [selectable]);
167
+
146
168
  const rootClass = [
147
169
  styles.root,
148
170
  variant === "plate" ? styles.plate : styles.line,
@@ -163,7 +185,7 @@ function TableRowInner<T>(
163
185
  const cells = (
164
186
  <>
165
187
  {selectable && (
166
- <div className={styles.selectCell} onClick={stop}>
188
+ <div ref={selectCellRef} className={styles.selectCell} onClick={stop}>
167
189
  <Checkbox
168
190
  checked={selected}
169
191
  disabled={disabled}
@@ -184,7 +206,7 @@ function TableRowInner<T>(
184
206
  minWidth: toCssSize(col.minWidth) ?? cssWidth,
185
207
  flex: col.flex ? "1 1 0" : undefined,
186
208
  justifyContent: alignToJustify(col.align),
187
- ...pinnedStyle(col, orderedKeys, columnMap, widths ?? {}),
209
+ ...pinnedStyle(col, orderedKeys, columnMap, widths ?? {}, selectCellWidth),
188
210
  };
189
211
  const content = col.render ? col.render(data) : (data as Record<string, ReactNode>)?.[col.key];
190
212
  return (
@@ -598,7 +598,7 @@ export const componentSlugs: Record<string, string> = {
598
598
 
599
599
  /** 작업중 — 사이드바에서 접근은 가능하지만 "작업중" 뱃지로 표시해 사용 자제 유도. */
600
600
  export const componentsInProgress: Set<string> = new Set([
601
- "Table Row",
601
+ "Drawer",
602
602
  "Chat Input",
603
603
  ]);
604
604
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conscience-technology/ctds",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "CTDS — 하이브리드 디자인 시스템 설치 · 온보딩 CLI",
5
5
  "type": "module",
6
6
  "bin": {