@iyulab/data-components 0.1.8 → 0.3.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.
@@ -151,6 +151,15 @@ export declare class USimpleSheet extends UElement {
151
151
  minCol: number;
152
152
  maxCol: number;
153
153
  } | null;
154
+ /** 선택 영역을 프로그래밍 방식으로 설정 */
155
+ setSelection(range: {
156
+ minRow: number;
157
+ maxRow: number;
158
+ minCol: number;
159
+ maxCol: number;
160
+ }): void;
161
+ /** 전체 셀 선택 */
162
+ selectAll(): void;
154
163
  /** Undo 가능 여부 */
155
164
  get canUndo(): boolean;
156
165
  /** Redo 가능 여부 */
@@ -967,6 +967,26 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
967
967
  if (!this._sel) return null;
968
968
  return normalizeRange(this._sel);
969
969
  }
970
+ /** 선택 영역을 프로그래밍 방식으로 설정 */
971
+ setSelection(range) {
972
+ const minRow = Math.max(0, Math.min(range.minRow, this._rowCount - 1));
973
+ const maxRow = Math.max(0, Math.min(range.maxRow, this._rowCount - 1));
974
+ const minCol = Math.max(0, Math.min(range.minCol, this._colCount - 1));
975
+ const maxCol = Math.max(0, Math.min(range.maxCol, this._colCount - 1));
976
+ this._sel = {
977
+ anchor: { row: minRow, col: minCol },
978
+ focus: { row: maxRow, col: maxCol }
979
+ };
980
+ this.requestUpdate();
981
+ }
982
+ /** 전체 셀 선택 */
983
+ selectAll() {
984
+ this._sel = {
985
+ anchor: { row: 0, col: 0 },
986
+ focus: { row: this._rowCount - 1, col: this._colCount - 1 }
987
+ };
988
+ this.requestUpdate();
989
+ }
970
990
  /** Undo 가능 여부 */
971
991
  get canUndo() {
972
992
  return this._historyIndex > 0;
@@ -0,0 +1,70 @@
1
+ import { LitElement, TemplateResult } from 'lit';
2
+ import { ColumnDef } from './types.js';
3
+ export declare class URichTable extends LitElement {
4
+ static styles: import('lit').CSSResult;
5
+ columns: ColumnDef[];
6
+ data: Record<string, unknown>[];
7
+ totalCount: number;
8
+ pageSize: number;
9
+ currentPage: number;
10
+ loading: boolean;
11
+ emptyMessage: string;
12
+ selectable: boolean;
13
+ editable: boolean;
14
+ addable: boolean;
15
+ filterable: boolean;
16
+ expandable: boolean;
17
+ detailRenderer?: (row: Record<string, unknown>) => TemplateResult;
18
+ private selectedIds;
19
+ private focusedCell;
20
+ private editingCell;
21
+ private editValue;
22
+ private expandedIds;
23
+ private sort;
24
+ private filters;
25
+ private validationErrors;
26
+ private rowErrors;
27
+ revertRow(_rowId: string): void;
28
+ setRowError(rowId: string, message: string): void;
29
+ clearRowError(rowId: string): void;
30
+ getSelectedRows(): Record<string, unknown>[];
31
+ render(): TemplateResult;
32
+ private _renderToolbar;
33
+ private _renderHeader;
34
+ private _renderFilterRow;
35
+ private _renderBody;
36
+ private _renderCell;
37
+ private _renderCellContent;
38
+ private _renderNewRow;
39
+ private _renderPagination;
40
+ private _onSelectAll;
41
+ private _onRowSelect;
42
+ private _lastSelectedIndex;
43
+ private _onShiftSelect;
44
+ private _onSortClick;
45
+ private _onFilterChange;
46
+ private _onCellClick;
47
+ private _onCellDblClick;
48
+ private _onEditKeyDown;
49
+ private _onCellEditConfirm;
50
+ private _onExpandToggle;
51
+ private _onAddRowClick;
52
+ private _onNewRowFocus;
53
+ private _onNewRowKeyDown;
54
+ private _onRowMenu;
55
+ private _onPageChange;
56
+ private _onPageSizeChange;
57
+ private _colSpan;
58
+ private _getOptionLabel;
59
+ private _getPageNumbers;
60
+ private _moveToNextEditableCell;
61
+ private _fireSelectionChange;
62
+ connectedCallback(): void;
63
+ disconnectedCallback(): void;
64
+ private _onGlobalKeyDown;
65
+ private _handleCopy;
66
+ private _handlePaste;
67
+ private _moveFocus;
68
+ private _selectAll;
69
+ static define(tagName?: string): void;
70
+ }
@@ -0,0 +1,672 @@
1
+ import { LitElement, html } from 'lit';
2
+ import { property, state } from 'lit/decorators.js';
3
+ import { richTableStyles } from './styles.js';
4
+ import { toTSV, parseTSV } from './utils/clipboard.js';
5
+
6
+ var __defProp = Object.defineProperty;
7
+ var __decorateClass = (decorators, target, key, kind) => {
8
+ var result = void 0 ;
9
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
10
+ if (decorator = decorators[i])
11
+ result = (decorator(target, key, result) ) || result;
12
+ if (result) __defProp(target, key, result);
13
+ return result;
14
+ };
15
+ class URichTable extends LitElement {
16
+ constructor() {
17
+ super(...arguments);
18
+ this.columns = [];
19
+ this.data = [];
20
+ this.totalCount = 0;
21
+ this.pageSize = 25;
22
+ this.currentPage = 1;
23
+ this.loading = false;
24
+ this.emptyMessage = "데이터가 없습니다";
25
+ this.selectable = false;
26
+ this.editable = false;
27
+ this.addable = false;
28
+ this.filterable = false;
29
+ this.expandable = false;
30
+ this.selectedIds = /* @__PURE__ */ new Set();
31
+ this.focusedCell = null;
32
+ this.editingCell = null;
33
+ this.editValue = "";
34
+ this.expandedIds = /* @__PURE__ */ new Set();
35
+ this.sort = null;
36
+ this.filters = {};
37
+ this.validationErrors = /* @__PURE__ */ new Map();
38
+ this.rowErrors = /* @__PURE__ */ new Map();
39
+ this._lastSelectedIndex = -1;
40
+ // --- Clipboard & Keyboard ---
41
+ this._onGlobalKeyDown = (e) => {
42
+ if (e.ctrlKey || e.metaKey) {
43
+ if (e.key === "c") this._handleCopy();
44
+ else if (e.key === "v") this._handlePaste();
45
+ else if (e.key === "a" && !this.editingCell) {
46
+ e.preventDefault();
47
+ this._selectAll();
48
+ }
49
+ }
50
+ if (!this.editingCell && this.focusedCell) {
51
+ if (e.key === "ArrowUp") {
52
+ e.preventDefault();
53
+ this._moveFocus(0, -1);
54
+ }
55
+ if (e.key === "ArrowDown") {
56
+ e.preventDefault();
57
+ this._moveFocus(0, 1);
58
+ }
59
+ if (e.key === "ArrowLeft") {
60
+ e.preventDefault();
61
+ this._moveFocus(-1, 0);
62
+ }
63
+ if (e.key === "ArrowRight") {
64
+ e.preventDefault();
65
+ this._moveFocus(1, 0);
66
+ }
67
+ if (e.key === "Enter") {
68
+ const col = this.columns[this.focusedCell.colIndex];
69
+ if (col?.editable) {
70
+ const value = this.data[this.focusedCell.rowIndex]?.[col.key];
71
+ this._onCellDblClick(this.focusedCell.rowIndex, this.focusedCell.colIndex, value);
72
+ }
73
+ }
74
+ if (e.key === " " && this.selectable) {
75
+ e.preventDefault();
76
+ const rowId = this.data[this.focusedCell.rowIndex]?._id;
77
+ if (rowId) this._onRowSelect(rowId);
78
+ }
79
+ if (e.key === "Delete" && this.selectedIds.size > 0) {
80
+ for (const row of this.getSelectedRows()) {
81
+ this.dispatchEvent(new CustomEvent("row-delete", { detail: { row }, bubbles: true, composed: true }));
82
+ }
83
+ }
84
+ }
85
+ };
86
+ }
87
+ static {
88
+ this.styles = richTableStyles;
89
+ }
90
+ // --- Public API ---
91
+ revertRow(_rowId) {
92
+ }
93
+ setRowError(rowId, message) {
94
+ this.rowErrors = new Map(this.rowErrors).set(rowId, message);
95
+ }
96
+ clearRowError(rowId) {
97
+ const next = new Map(this.rowErrors);
98
+ next.delete(rowId);
99
+ this.rowErrors = next;
100
+ }
101
+ getSelectedRows() {
102
+ return this.data.filter((row) => this.selectedIds.has(row._id));
103
+ }
104
+ // --- Rendering ---
105
+ render() {
106
+ return html`
107
+ ${this._renderToolbar()}
108
+ <table>
109
+ ${this._renderHeader()}
110
+ <tbody>
111
+ ${this.filterable ? this._renderFilterRow() : ""}
112
+ ${this._renderBody()}
113
+ ${this.addable ? this._renderNewRow() : ""}
114
+ </tbody>
115
+ </table>
116
+ ${this._renderPagination()}
117
+ `;
118
+ }
119
+ _renderToolbar() {
120
+ const selectedCount = this.selectedIds.size;
121
+ return html`
122
+ <div class="toolbar">
123
+ ${this.selectable ? html`
124
+ <div class="selection-info">
125
+ <input type="checkbox"
126
+ .checked=${selectedCount > 0 && selectedCount === this.data.length}
127
+ .indeterminate=${selectedCount > 0 && selectedCount < this.data.length}
128
+ @change=${this._onSelectAll} />
129
+ ${selectedCount > 0 ? html`<span>${selectedCount}건 선택됨</span>` : ""}
130
+ </div>
131
+ ${selectedCount > 0 ? html`<slot name="bulk-actions"></slot>` : ""}
132
+ ` : ""}
133
+ <div style="flex:1"></div>
134
+ <slot name="toolbar-end"></slot>
135
+ ${this.addable ? html`
136
+ <button class="btn btn-success" @click=${this._onAddRowClick}>+ 새 행</button>
137
+ ` : ""}
138
+ </div>
139
+ `;
140
+ }
141
+ _renderHeader() {
142
+ return html`
143
+ <thead>
144
+ <tr>
145
+ ${this.selectable ? html`<th class="checkbox-cell"></th>` : ""}
146
+ ${this.expandable ? html`<th class="expand-cell"></th>` : ""}
147
+ ${this.columns.map((col) => html`
148
+ <th
149
+ class=${col.sortable ? "sortable" : ""}
150
+ style=${col.width ? `width: ${col.width}` : ""}
151
+ @click=${() => col.sortable && this._onSortClick(col.key)}>
152
+ ${col.label}
153
+ ${this.sort?.field === col.key ? html`
154
+ <span class="sort-indicator">${this.sort.direction === "asc" ? "▲" : "▼"}</span>
155
+ ` : ""}
156
+ </th>
157
+ `)}
158
+ <th class="actions-cell"></th>
159
+ </tr>
160
+ </thead>
161
+ `;
162
+ }
163
+ _renderFilterRow() {
164
+ return html`
165
+ <tr class="filter-row">
166
+ ${this.selectable ? html`<td></td>` : ""}
167
+ ${this.expandable ? html`<td></td>` : ""}
168
+ ${this.columns.map((col) => html`
169
+ <td>
170
+ ${col.filterable !== false ? col.filterType === "select" && col.options ? html`<select @change=${(e) => this._onFilterChange(col.key, e.target.value)}>
171
+ <option value="">전체</option>
172
+ ${col.options.map((o) => html`<option value=${o.value}>${o.label}</option>`)}
173
+ </select>` : html`<input
174
+ placeholder="필터..."
175
+ @input=${(e) => this._onFilterChange(col.key, e.target.value)} />` : ""}
176
+ </td>
177
+ `)}
178
+ <td></td>
179
+ </tr>
180
+ `;
181
+ }
182
+ _renderBody() {
183
+ if (this.loading) {
184
+ return html`<tr><td colspan=${this._colSpan()}><div class="loading-overlay">로딩 중...</div></td></tr>`;
185
+ }
186
+ if (this.data.length === 0) {
187
+ return html`<tr><td colspan=${this._colSpan()}><div class="empty-message">${this.emptyMessage}</div></td></tr>`;
188
+ }
189
+ return this.data.map((row, rowIdx) => {
190
+ const rowId = row._id;
191
+ const isSelected = this.selectedIds.has(rowId);
192
+ const isExpanded = this.expandedIds.has(rowId);
193
+ const hasError = this.rowErrors.has(rowId);
194
+ return html`
195
+ <tr class="${isSelected ? "selected" : ""} ${hasError ? "error" : ""} ${this.editingCell?.rowIndex === rowIdx ? "editing" : ""}">
196
+ ${this.selectable ? html`
197
+ <td class="checkbox-cell">
198
+ <input type="checkbox" .checked=${isSelected}
199
+ @change=${() => this._onRowSelect(rowId)}
200
+ @click=${(e) => e.shiftKey && this._onShiftSelect(rowIdx)} />
201
+ </td>
202
+ ` : ""}
203
+ ${this.expandable ? html`
204
+ <td class="expand-cell" @click=${() => this._onExpandToggle(rowId)}>
205
+ ${isExpanded ? "▼" : "▶"}
206
+ </td>
207
+ ` : ""}
208
+ ${this.columns.map((col, colIdx) => this._renderCell(row, rowIdx, col, colIdx))}
209
+ <td class="actions-cell">
210
+ <span style="cursor:pointer;color:#94a3b8" @click=${() => this._onRowMenu(row)}>⋯</span>
211
+ </td>
212
+ </tr>
213
+ ${isExpanded && this.detailRenderer ? html`
214
+ <tr class="detail-row">
215
+ <td colspan=${this._colSpan()}>${this.detailRenderer(row)}</td>
216
+ </tr>
217
+ ` : ""}
218
+ ${hasError ? html`
219
+ <tr><td colspan=${this._colSpan()} style="padding:2px 8px;background:#fef2f2;color:#ef4444;font-size:11px;">
220
+ ${this.rowErrors.get(rowId)}
221
+ </td></tr>
222
+ ` : ""}
223
+ `;
224
+ });
225
+ }
226
+ _renderCell(row, rowIdx, col, colIdx) {
227
+ const isEditing = this.editingCell?.rowIndex === rowIdx && this.editingCell?.colIndex === colIdx;
228
+ const isFocused = this.focusedCell?.rowIndex === rowIdx && this.focusedCell?.colIndex === colIdx;
229
+ const value = row[col.key];
230
+ if (isEditing && col.editable) {
231
+ const validationError = this.validationErrors.get(`${rowIdx}-${colIdx}`);
232
+ if (col.type === "select" && col.options) {
233
+ return html`
234
+ <td>
235
+ <select class="cell-edit-input" @change=${this._onCellEditConfirm} @keydown=${this._onEditKeyDown}>
236
+ ${col.options.map((o) => html`<option value=${o.value} ?selected=${o.value === String(value)}>${o.label}</option>`)}
237
+ </select>
238
+ </td>
239
+ `;
240
+ }
241
+ return html`
242
+ <td>
243
+ <input class="cell-edit-input ${validationError ? "invalid" : ""}"
244
+ type=${col.type === "number" ? "number" : col.type === "date" ? "date" : "text"}
245
+ .value=${this.editValue}
246
+ @input=${(e) => this.editValue = e.target.value}
247
+ @keydown=${this._onEditKeyDown}
248
+ @blur=${this._onCellEditConfirm} />
249
+ ${validationError ? html`<div class="validation-error">${validationError}</div>` : ""}
250
+ </td>
251
+ `;
252
+ }
253
+ return html`
254
+ <td class=${isFocused ? "focused-cell" : ""}
255
+ style=${col.align ? `text-align: ${col.align}` : ""}
256
+ @click=${() => this._onCellClick(rowIdx, colIdx)}
257
+ @dblclick=${() => col.editable && this._onCellDblClick(rowIdx, colIdx, value)}>
258
+ ${this._renderCellContent(col, value, row)}
259
+ </td>
260
+ `;
261
+ }
262
+ _renderCellContent(col, value, row) {
263
+ if (col.render) {
264
+ const result = col.render(value, row);
265
+ if (typeof result === "string") return result;
266
+ if (result instanceof HTMLElement) {
267
+ const container = document.createElement("span");
268
+ container.appendChild(result);
269
+ return html`${container}`;
270
+ }
271
+ return String(value ?? "");
272
+ }
273
+ if (col.type === "badge" && col.badgeColors) {
274
+ const color = col.badgeColors[String(value)] ?? "#f3f4f6";
275
+ return html`<span class="badge" style="background:${color}">${this._getOptionLabel(col, value)}</span>`;
276
+ }
277
+ if (col.type === "select" && col.options) {
278
+ return this._getOptionLabel(col, value);
279
+ }
280
+ if (col.type === "number" && value != null) {
281
+ return Number(value).toLocaleString();
282
+ }
283
+ return String(value ?? "");
284
+ }
285
+ _renderNewRow() {
286
+ return html`
287
+ <tr class="new-row">
288
+ ${this.selectable ? html`<td class="checkbox-cell"><span style="color:#86efac">+</span></td>` : ""}
289
+ ${this.expandable ? html`<td></td>` : ""}
290
+ ${this.columns.map((col, colIdx) => html`
291
+ <td>
292
+ ${col.editable !== false ? html`
293
+ <input placeholder=${col.label}
294
+ data-new-col=${colIdx}
295
+ @keydown=${this._onNewRowKeyDown}
296
+ @focus=${this._onNewRowFocus} />
297
+ ` : html`<span></span>`}
298
+ </td>
299
+ `)}
300
+ <td></td>
301
+ </tr>
302
+ `;
303
+ }
304
+ _renderPagination() {
305
+ if (this.totalCount <= 0) return html``;
306
+ const totalPages = Math.ceil(this.totalCount / this.pageSize);
307
+ const start = (this.currentPage - 1) * this.pageSize + 1;
308
+ const end = Math.min(this.currentPage * this.pageSize, this.totalCount);
309
+ return html`
310
+ <div class="pagination">
311
+ <span>전체 ${this.totalCount.toLocaleString()}건 중 ${start}-${end} 표시</span>
312
+ <div class="page-buttons">
313
+ <button ?disabled=${this.currentPage <= 1} @click=${() => this._onPageChange(this.currentPage - 1)}>◀</button>
314
+ ${this._getPageNumbers(totalPages).map((p) => html`
315
+ <button class=${p === this.currentPage ? "active" : ""} @click=${() => this._onPageChange(p)}>${p}</button>
316
+ `)}
317
+ <button ?disabled=${this.currentPage >= totalPages} @click=${() => this._onPageChange(this.currentPage + 1)}>▶</button>
318
+ <select @change=${(e) => this._onPageSizeChange(Number(e.target.value))}>
319
+ ${[25, 50, 100].map((s) => html`<option value=${s} ?selected=${s === this.pageSize}>${s}행</option>`)}
320
+ </select>
321
+ </div>
322
+ </div>
323
+ `;
324
+ }
325
+ // --- Event Handlers ---
326
+ _onSelectAll(e) {
327
+ const checked = e.target.checked;
328
+ if (checked) {
329
+ this.selectedIds = new Set(this.data.map((r) => r._id));
330
+ } else {
331
+ this.selectedIds = /* @__PURE__ */ new Set();
332
+ }
333
+ this._fireSelectionChange();
334
+ }
335
+ _onRowSelect(rowId) {
336
+ const next = new Set(this.selectedIds);
337
+ if (next.has(rowId)) next.delete(rowId);
338
+ else next.add(rowId);
339
+ this.selectedIds = next;
340
+ this._fireSelectionChange();
341
+ }
342
+ _onShiftSelect(rowIdx) {
343
+ if (this._lastSelectedIndex < 0) return;
344
+ const start = Math.min(this._lastSelectedIndex, rowIdx);
345
+ const end = Math.max(this._lastSelectedIndex, rowIdx);
346
+ const next = new Set(this.selectedIds);
347
+ for (let i = start; i <= end; i++) {
348
+ next.add(this.data[i]._id);
349
+ }
350
+ this.selectedIds = next;
351
+ this._fireSelectionChange();
352
+ }
353
+ _onSortClick(field) {
354
+ if (this.sort?.field === field) {
355
+ if (this.sort.direction === "asc") {
356
+ this.sort = { field, direction: "desc" };
357
+ } else {
358
+ this.sort = null;
359
+ }
360
+ } else {
361
+ this.sort = { field, direction: "asc" };
362
+ }
363
+ this.dispatchEvent(new CustomEvent("sort-change", {
364
+ detail: this.sort ? { field: this.sort.field, direction: this.sort.direction } : { field, direction: null },
365
+ bubbles: true,
366
+ composed: true
367
+ }));
368
+ }
369
+ _onFilterChange(field, value) {
370
+ if (value) {
371
+ this.filters = { ...this.filters, [field]: value };
372
+ } else {
373
+ const { [field]: _, ...rest } = this.filters;
374
+ this.filters = rest;
375
+ }
376
+ this.dispatchEvent(new CustomEvent("filter-change", {
377
+ detail: { filters: this.filters },
378
+ bubbles: true,
379
+ composed: true
380
+ }));
381
+ }
382
+ _onCellClick(rowIdx, colIdx) {
383
+ this.focusedCell = { rowIndex: rowIdx, colIndex: colIdx };
384
+ this._lastSelectedIndex = rowIdx;
385
+ }
386
+ _onCellDblClick(rowIdx, colIdx, value) {
387
+ this.editingCell = { rowIndex: rowIdx, colIndex: colIdx };
388
+ this.editValue = String(value ?? "");
389
+ this.requestUpdate();
390
+ requestAnimationFrame(() => {
391
+ const input = this.shadowRoot?.querySelector(".cell-edit-input");
392
+ input?.focus();
393
+ input?.select();
394
+ });
395
+ }
396
+ _onEditKeyDown(e) {
397
+ if (e.key === "Enter") {
398
+ e.preventDefault();
399
+ this._onCellEditConfirm();
400
+ if (this.editingCell && this.editingCell.rowIndex < this.data.length - 1) {
401
+ const nextRow = this.editingCell.rowIndex + 1;
402
+ const col = this.editingCell.colIndex;
403
+ const nextValue = this.data[nextRow][this.columns[col].key];
404
+ this._onCellDblClick(nextRow, col, nextValue);
405
+ }
406
+ } else if (e.key === "Escape") {
407
+ this.editingCell = null;
408
+ this.editValue = "";
409
+ } else if (e.key === "Tab") {
410
+ e.preventDefault();
411
+ this._onCellEditConfirm();
412
+ this._moveToNextEditableCell(e.shiftKey);
413
+ }
414
+ }
415
+ _onCellEditConfirm() {
416
+ if (!this.editingCell) return;
417
+ const { rowIndex, colIndex } = this.editingCell;
418
+ const col = this.columns[colIndex];
419
+ const row = this.data[rowIndex];
420
+ const oldValue = row[col.key];
421
+ let newValue = this.editValue;
422
+ if (col.type === "number") newValue = Number(newValue);
423
+ if (col.required && !newValue && newValue !== 0) {
424
+ this.validationErrors = new Map(this.validationErrors).set(`${rowIndex}-${colIndex}`, "필수 항목입니다");
425
+ return;
426
+ }
427
+ if (col.validator) {
428
+ const error = col.validator(newValue, row);
429
+ if (error) {
430
+ this.validationErrors = new Map(this.validationErrors).set(`${rowIndex}-${colIndex}`, error);
431
+ return;
432
+ }
433
+ }
434
+ const nextErrors = new Map(this.validationErrors);
435
+ nextErrors.delete(`${rowIndex}-${colIndex}`);
436
+ this.validationErrors = nextErrors;
437
+ if (newValue !== oldValue) {
438
+ this.dispatchEvent(new CustomEvent("row-update", {
439
+ detail: { row, field: col.key, value: newValue, oldValue },
440
+ bubbles: true,
441
+ composed: true
442
+ }));
443
+ }
444
+ this.editingCell = null;
445
+ this.editValue = "";
446
+ }
447
+ _onExpandToggle(rowId) {
448
+ const next = new Set(this.expandedIds);
449
+ const expanded = !next.has(rowId);
450
+ if (expanded) next.add(rowId);
451
+ else next.delete(rowId);
452
+ this.expandedIds = next;
453
+ this.dispatchEvent(new CustomEvent("row-expand", {
454
+ detail: { row: this.data.find((r) => r._id === rowId), expanded },
455
+ bubbles: true,
456
+ composed: true
457
+ }));
458
+ }
459
+ _onAddRowClick() {
460
+ const input = this.shadowRoot?.querySelector(".new-row input");
461
+ input?.focus();
462
+ }
463
+ _onNewRowFocus() {
464
+ }
465
+ _onNewRowKeyDown(e) {
466
+ if (e.key === "Enter") {
467
+ e.preventDefault();
468
+ const inputs = Array.from(this.shadowRoot?.querySelectorAll(".new-row input") ?? []);
469
+ const newRow = {};
470
+ this.columns.forEach((col, i) => {
471
+ if (col.editable !== false && inputs[i]) {
472
+ let val = inputs[i].value;
473
+ if (col.type === "number") val = Number(val);
474
+ newRow[col.key] = val;
475
+ }
476
+ });
477
+ this.dispatchEvent(new CustomEvent("row-create", {
478
+ detail: { row: newRow },
479
+ bubbles: true,
480
+ composed: true
481
+ }));
482
+ inputs.forEach((input) => input.value = "");
483
+ inputs[0]?.focus();
484
+ } else if (e.key === "Tab" && !e.shiftKey) {
485
+ const target = e.target;
486
+ const colIdx = Number(target.dataset.newCol);
487
+ if (colIdx >= this.columns.filter((c) => c.editable !== false).length - 1) {
488
+ e.preventDefault();
489
+ this._onNewRowKeyDown(new KeyboardEvent("keydown", { key: "Enter" }));
490
+ }
491
+ }
492
+ }
493
+ _onRowMenu(row) {
494
+ this.dispatchEvent(new CustomEvent("row-delete", {
495
+ detail: { row },
496
+ bubbles: true,
497
+ composed: true
498
+ }));
499
+ }
500
+ _onPageChange(page) {
501
+ this.dispatchEvent(new CustomEvent("page-change", {
502
+ detail: { page, pageSize: this.pageSize },
503
+ bubbles: true,
504
+ composed: true
505
+ }));
506
+ }
507
+ _onPageSizeChange(pageSize) {
508
+ this.dispatchEvent(new CustomEvent("page-change", {
509
+ detail: { page: 1, pageSize },
510
+ bubbles: true,
511
+ composed: true
512
+ }));
513
+ }
514
+ // --- Helpers ---
515
+ _colSpan() {
516
+ let span = this.columns.length + 1;
517
+ if (this.selectable) span++;
518
+ if (this.expandable) span++;
519
+ return span;
520
+ }
521
+ _getOptionLabel(col, value) {
522
+ return col.options?.find((o) => o.value === String(value))?.label ?? String(value ?? "");
523
+ }
524
+ _getPageNumbers(totalPages) {
525
+ const pages = [];
526
+ const start = Math.max(1, this.currentPage - 2);
527
+ const end = Math.min(totalPages, start + 4);
528
+ for (let i = start; i <= end; i++) pages.push(i);
529
+ return pages;
530
+ }
531
+ _moveToNextEditableCell(reverse) {
532
+ if (!this.editingCell) return;
533
+ let { rowIndex, colIndex } = this.editingCell;
534
+ const editableCols = this.columns.map((c, i) => c.editable ? i : -1).filter((i) => i >= 0);
535
+ const currentIdx = editableCols.indexOf(colIndex);
536
+ if (reverse) {
537
+ if (currentIdx > 0) {
538
+ colIndex = editableCols[currentIdx - 1];
539
+ } else if (rowIndex > 0) {
540
+ rowIndex--;
541
+ colIndex = editableCols[editableCols.length - 1];
542
+ }
543
+ } else {
544
+ if (currentIdx < editableCols.length - 1) {
545
+ colIndex = editableCols[currentIdx + 1];
546
+ } else if (rowIndex < this.data.length - 1) {
547
+ rowIndex++;
548
+ colIndex = editableCols[0];
549
+ }
550
+ }
551
+ const value = this.data[rowIndex]?.[this.columns[colIndex]?.key];
552
+ this._onCellDblClick(rowIndex, colIndex, value);
553
+ }
554
+ _fireSelectionChange() {
555
+ this.dispatchEvent(new CustomEvent("selection-change", {
556
+ detail: { selectedRows: this.getSelectedRows() },
557
+ bubbles: true,
558
+ composed: true
559
+ }));
560
+ }
561
+ // --- Lifecycle ---
562
+ connectedCallback() {
563
+ super.connectedCallback();
564
+ this.addEventListener("keydown", this._onGlobalKeyDown);
565
+ }
566
+ disconnectedCallback() {
567
+ super.disconnectedCallback();
568
+ this.removeEventListener("keydown", this._onGlobalKeyDown);
569
+ }
570
+ async _handleCopy() {
571
+ const rows = this.getSelectedRows();
572
+ if (rows.length === 0) return;
573
+ const tsv = toTSV(rows, this.columns);
574
+ await navigator.clipboard.writeText(tsv);
575
+ }
576
+ async _handlePaste() {
577
+ if (this.editingCell) return;
578
+ const text = await navigator.clipboard.readText();
579
+ if (!text.trim()) return;
580
+ const parsedRows = parseTSV(text, this.columns);
581
+ if (parsedRows.length === 0) return;
582
+ this.dispatchEvent(new CustomEvent("paste", {
583
+ detail: { rows: parsedRows },
584
+ bubbles: true,
585
+ composed: true
586
+ }));
587
+ }
588
+ _moveFocus(dx, dy) {
589
+ if (!this.focusedCell) return;
590
+ const newCol = Math.max(0, Math.min(this.columns.length - 1, this.focusedCell.colIndex + dx));
591
+ const newRow = Math.max(0, Math.min(this.data.length - 1, this.focusedCell.rowIndex + dy));
592
+ this.focusedCell = { rowIndex: newRow, colIndex: newCol };
593
+ }
594
+ _selectAll() {
595
+ this.selectedIds = new Set(this.data.map((r) => r._id));
596
+ this._fireSelectionChange();
597
+ }
598
+ // define helper (follows existing pattern)
599
+ static define(tagName = "u-rich-table") {
600
+ if (!customElements.get(tagName)) {
601
+ customElements.define(tagName, this);
602
+ }
603
+ }
604
+ }
605
+ __decorateClass([
606
+ property({ type: Array })
607
+ ], URichTable.prototype, "columns");
608
+ __decorateClass([
609
+ property({ type: Array })
610
+ ], URichTable.prototype, "data");
611
+ __decorateClass([
612
+ property({ type: Number })
613
+ ], URichTable.prototype, "totalCount");
614
+ __decorateClass([
615
+ property({ type: Number })
616
+ ], URichTable.prototype, "pageSize");
617
+ __decorateClass([
618
+ property({ type: Number })
619
+ ], URichTable.prototype, "currentPage");
620
+ __decorateClass([
621
+ property({ type: Boolean })
622
+ ], URichTable.prototype, "loading");
623
+ __decorateClass([
624
+ property({ type: String })
625
+ ], URichTable.prototype, "emptyMessage");
626
+ __decorateClass([
627
+ property({ type: Boolean })
628
+ ], URichTable.prototype, "selectable");
629
+ __decorateClass([
630
+ property({ type: Boolean })
631
+ ], URichTable.prototype, "editable");
632
+ __decorateClass([
633
+ property({ type: Boolean })
634
+ ], URichTable.prototype, "addable");
635
+ __decorateClass([
636
+ property({ type: Boolean })
637
+ ], URichTable.prototype, "filterable");
638
+ __decorateClass([
639
+ property({ type: Boolean })
640
+ ], URichTable.prototype, "expandable");
641
+ __decorateClass([
642
+ property({ attribute: false })
643
+ ], URichTable.prototype, "detailRenderer");
644
+ __decorateClass([
645
+ state()
646
+ ], URichTable.prototype, "selectedIds");
647
+ __decorateClass([
648
+ state()
649
+ ], URichTable.prototype, "focusedCell");
650
+ __decorateClass([
651
+ state()
652
+ ], URichTable.prototype, "editingCell");
653
+ __decorateClass([
654
+ state()
655
+ ], URichTable.prototype, "editValue");
656
+ __decorateClass([
657
+ state()
658
+ ], URichTable.prototype, "expandedIds");
659
+ __decorateClass([
660
+ state()
661
+ ], URichTable.prototype, "sort");
662
+ __decorateClass([
663
+ state()
664
+ ], URichTable.prototype, "filters");
665
+ __decorateClass([
666
+ state()
667
+ ], URichTable.prototype, "validationErrors");
668
+ __decorateClass([
669
+ state()
670
+ ], URichTable.prototype, "rowErrors");
671
+
672
+ export { URichTable };
@@ -0,0 +1,8 @@
1
+ import { URichTable } from './URichTable.component.js';
2
+ declare global {
3
+ interface HTMLElementTagNameMap {
4
+ 'u-rich-table': URichTable;
5
+ }
6
+ }
7
+ export { URichTable };
8
+ export type { ColumnDef, CellPosition, SortState, FilterState, RichTableEventMap } from './types.js';
@@ -0,0 +1,5 @@
1
+ import { URichTable } from './URichTable.component.js';
2
+
3
+ URichTable.define("u-rich-table");
4
+
5
+ export { URichTable };
@@ -0,0 +1 @@
1
+ export declare const richTableStyles: import('lit').CSSResult;
@@ -0,0 +1,433 @@
1
+ import { css } from 'lit';
2
+
3
+ const richTableStyles = css`
4
+ :host {
5
+ display: block;
6
+ font-family: system-ui, -apple-system, sans-serif;
7
+ font-size: 13px;
8
+ border: 1px solid #e2e8f0;
9
+ border-radius: 8px;
10
+ overflow: hidden;
11
+ }
12
+
13
+ .toolbar {
14
+ display: flex;
15
+ align-items: center;
16
+ gap: 8px;
17
+ padding: 8px 12px;
18
+ background: #f8fafc;
19
+ border-bottom: 1px solid #e2e8f0;
20
+ }
21
+
22
+ .toolbar .selection-info {
23
+ font-size: 12px;
24
+ color: #64748b;
25
+ }
26
+
27
+ .toolbar .search-input {
28
+ padding: 4px 10px;
29
+ border: 1px solid #d1d5db;
30
+ border-radius: 4px;
31
+ font-size: 12px;
32
+ outline: none;
33
+ }
34
+
35
+ .toolbar .search-input:focus {
36
+ border-color: #3b82f6;
37
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
38
+ }
39
+
40
+ .toolbar .btn {
41
+ padding: 4px 10px;
42
+ border: none;
43
+ border-radius: 4px;
44
+ font-size: 12px;
45
+ cursor: pointer;
46
+ color: white;
47
+ }
48
+
49
+ .toolbar .btn-primary { background: #3b82f6; }
50
+ .toolbar .btn-primary:hover { background: #2563eb; }
51
+ .toolbar .btn-success { background: #10b981; }
52
+ .toolbar .btn-success:hover { background: #059669; }
53
+
54
+ table {
55
+ width: 100%;
56
+ border-collapse: collapse;
57
+ }
58
+
59
+ thead th {
60
+ padding: 8px;
61
+ background: #f1f5f9;
62
+ font-weight: 600;
63
+ text-align: left;
64
+ border-bottom: 2px solid #e2e8f0;
65
+ user-select: none;
66
+ position: relative;
67
+ }
68
+
69
+ thead th.sortable {
70
+ cursor: pointer;
71
+ }
72
+
73
+ thead th.sortable:hover {
74
+ background: #e2e8f0;
75
+ }
76
+
77
+ .sort-indicator {
78
+ font-size: 10px;
79
+ color: #94a3b8;
80
+ margin-left: 4px;
81
+ }
82
+
83
+ .filter-row td {
84
+ padding: 4px;
85
+ background: #fefce8;
86
+ border-bottom: 1px solid #e2e8f0;
87
+ }
88
+
89
+ .filter-row input,
90
+ .filter-row select {
91
+ width: 100%;
92
+ padding: 2px 6px;
93
+ border: 1px solid #d1d5db;
94
+ border-radius: 3px;
95
+ font-size: 11px;
96
+ outline: none;
97
+ }
98
+
99
+ tbody tr {
100
+ border-bottom: 1px solid #e2e8f0;
101
+ }
102
+
103
+ tbody tr:hover {
104
+ background: #f8fafc;
105
+ }
106
+
107
+ tbody tr.selected {
108
+ background: #eff6ff;
109
+ }
110
+
111
+ tbody tr.focused td.focused-cell {
112
+ outline: 2px solid #3b82f6;
113
+ outline-offset: -2px;
114
+ }
115
+
116
+ tbody tr.editing {
117
+ background: #fefce8;
118
+ outline: 2px solid #eab308;
119
+ }
120
+
121
+ tbody tr.error {
122
+ background: #fef2f2;
123
+ }
124
+
125
+ tbody td {
126
+ padding: 8px;
127
+ }
128
+
129
+ tbody td .cell-edit-input {
130
+ width: 100%;
131
+ padding: 4px 6px;
132
+ border: 1px solid #3b82f6;
133
+ border-radius: 3px;
134
+ font-size: 13px;
135
+ outline: none;
136
+ }
137
+
138
+ tbody td .cell-edit-input.invalid {
139
+ border-color: #ef4444;
140
+ }
141
+
142
+ .validation-error {
143
+ font-size: 10px;
144
+ color: #ef4444;
145
+ margin-top: 2px;
146
+ }
147
+
148
+ .checkbox-cell {
149
+ width: 40px;
150
+ text-align: center;
151
+ }
152
+
153
+ .expand-cell {
154
+ width: 30px;
155
+ text-align: center;
156
+ cursor: pointer;
157
+ color: #94a3b8;
158
+ }
159
+
160
+ .expand-cell:hover {
161
+ color: #3b82f6;
162
+ }
163
+
164
+ .actions-cell {
165
+ width: 60px;
166
+ text-align: center;
167
+ }
168
+
169
+ .new-row td {
170
+ padding: 4px 8px;
171
+ background: #f0fdf4;
172
+ opacity: 0.8;
173
+ }
174
+
175
+ .new-row input {
176
+ width: 100%;
177
+ padding: 4px 6px;
178
+ border: 1px dashed #86efac;
179
+ border-radius: 3px;
180
+ background: transparent;
181
+ font-size: 12px;
182
+ color: #6b7280;
183
+ outline: none;
184
+ }
185
+
186
+ .new-row input:focus {
187
+ border-color: #10b981;
188
+ background: white;
189
+ opacity: 1;
190
+ }
191
+
192
+ .detail-row td {
193
+ padding: 8px 8px 8px 70px;
194
+ background: #f8fafc;
195
+ border-bottom: 2px solid #e2e8f0;
196
+ }
197
+
198
+ .badge {
199
+ display: inline-block;
200
+ padding: 2px 8px;
201
+ border-radius: 10px;
202
+ font-size: 11px;
203
+ font-weight: 500;
204
+ }
205
+
206
+ .pagination {
207
+ display: flex;
208
+ justify-content: space-between;
209
+ align-items: center;
210
+ padding: 8px 12px;
211
+ background: #f8fafc;
212
+ border-top: 1px solid #e2e8f0;
213
+ font-size: 12px;
214
+ color: #64748b;
215
+ }
216
+
217
+ .pagination .page-buttons {
218
+ display: flex;
219
+ gap: 4px;
220
+ align-items: center;
221
+ }
222
+
223
+ .pagination button {
224
+ padding: 2px 8px;
225
+ border: 1px solid #d1d5db;
226
+ border-radius: 3px;
227
+ background: white;
228
+ font-size: 11px;
229
+ cursor: pointer;
230
+ }
231
+
232
+ .pagination button.active {
233
+ background: #3b82f6;
234
+ color: white;
235
+ border-color: #3b82f6;
236
+ }
237
+
238
+ .pagination select {
239
+ padding: 2px 4px;
240
+ border: 1px solid #d1d5db;
241
+ border-radius: 3px;
242
+ font-size: 11px;
243
+ margin-left: 8px;
244
+ }
245
+
246
+ .loading-overlay {
247
+ display: flex;
248
+ align-items: center;
249
+ justify-content: center;
250
+ padding: 40px;
251
+ color: #6b7280;
252
+ }
253
+
254
+ .empty-message {
255
+ display: flex;
256
+ align-items: center;
257
+ justify-content: center;
258
+ padding: 40px;
259
+ color: #9ca3af;
260
+ }
261
+
262
+ /* ── Dark mode ──
263
+ 외부에서 --u-* CSS 변수가 제공되면 그 값을 사용하고,
264
+ 제공되지 않으면 자체 dark fallback 값을 적용한다. */
265
+
266
+ :host-context([theme="dark"]) {
267
+ border-color: var(--u-border-color, #3D3D3D);
268
+ color: var(--u-txt-color, #D4D4D4);
269
+ }
270
+
271
+ :host-context([theme="dark"]) .toolbar {
272
+ background: var(--u-neutral-200, #1E1E1E);
273
+ border-bottom-color: var(--u-border-color, #3D3D3D);
274
+ }
275
+
276
+ :host-context([theme="dark"]) .toolbar .selection-info {
277
+ color: var(--u-txt-color-weak, #8A8A8A);
278
+ }
279
+
280
+ :host-context([theme="dark"]) .toolbar .search-input {
281
+ background: var(--u-input-bg-color, #1E1E1E);
282
+ color: var(--u-txt-color, #D4D4D4);
283
+ border-color: var(--u-input-border-color, #3D3D3D);
284
+ }
285
+
286
+ :host-context([theme="dark"]) .toolbar .search-input:focus {
287
+ border-color: var(--u-blue-500, #6ba3e3);
288
+ box-shadow: 0 0 0 2px rgba(107, 163, 227, 0.2);
289
+ }
290
+
291
+ :host-context([theme="dark"]) .toolbar .btn-primary {
292
+ background: var(--u-blue-500, #6ba3e3);
293
+ }
294
+
295
+ :host-context([theme="dark"]) .toolbar .btn-primary:hover {
296
+ background: var(--u-blue-600, #87B8F5);
297
+ }
298
+
299
+ :host-context([theme="dark"]) .toolbar .btn-success {
300
+ background: var(--u-green-500, #66B584);
301
+ }
302
+
303
+ :host-context([theme="dark"]) .toolbar .btn-success:hover {
304
+ background: var(--u-green-600, #81D19D);
305
+ }
306
+
307
+ :host-context([theme="dark"]) thead th {
308
+ background: var(--u-neutral-200, #1E1E1E);
309
+ color: var(--u-txt-color, #D4D4D4);
310
+ border-bottom-color: var(--u-border-color, #3D3D3D);
311
+ }
312
+
313
+ :host-context([theme="dark"]) thead th.sortable:hover {
314
+ background: var(--u-neutral-300, #2A2A2A);
315
+ }
316
+
317
+ :host-context([theme="dark"]) .sort-indicator {
318
+ color: var(--u-txt-color-weak, #8A8A8A);
319
+ }
320
+
321
+ :host-context([theme="dark"]) .filter-row td {
322
+ background: var(--u-yellow-0, #2E2200);
323
+ border-bottom-color: var(--u-border-color, #3D3D3D);
324
+ }
325
+
326
+ :host-context([theme="dark"]) .filter-row input,
327
+ :host-context([theme="dark"]) .filter-row select {
328
+ background: var(--u-input-bg-color, #1E1E1E);
329
+ color: var(--u-txt-color, #D4D4D4);
330
+ border-color: var(--u-input-border-color, #3D3D3D);
331
+ }
332
+
333
+ :host-context([theme="dark"]) tbody tr {
334
+ border-bottom-color: var(--u-border-color-weak, #2A2A2A);
335
+ }
336
+
337
+ :host-context([theme="dark"]) tbody tr:hover {
338
+ background: var(--u-neutral-200, #1E1E1E);
339
+ }
340
+
341
+ :host-context([theme="dark"]) tbody tr.selected {
342
+ background: var(--u-blue-0, #0a1e3d);
343
+ }
344
+
345
+ :host-context([theme="dark"]) tbody tr.focused td.focused-cell {
346
+ outline-color: var(--u-blue-500, #6ba3e3);
347
+ }
348
+
349
+ :host-context([theme="dark"]) tbody tr.editing {
350
+ background: var(--u-yellow-0, #2E2200);
351
+ outline-color: var(--u-yellow-500, #D4A712);
352
+ }
353
+
354
+ :host-context([theme="dark"]) tbody tr.error {
355
+ background: var(--u-red-0, #2E0A0A);
356
+ }
357
+
358
+ :host-context([theme="dark"]) tbody td .cell-edit-input {
359
+ background: var(--u-input-bg-color, #1E1E1E);
360
+ color: var(--u-txt-color, #D4D4D4);
361
+ border-color: var(--u-blue-500, #6ba3e3);
362
+ }
363
+
364
+ :host-context([theme="dark"]) tbody td .cell-edit-input.invalid {
365
+ border-color: var(--u-red-500, #D66060);
366
+ }
367
+
368
+ :host-context([theme="dark"]) .validation-error {
369
+ color: var(--u-red-500, #D66060);
370
+ }
371
+
372
+ :host-context([theme="dark"]) .expand-cell {
373
+ color: var(--u-txt-color-weak, #8A8A8A);
374
+ }
375
+
376
+ :host-context([theme="dark"]) .expand-cell:hover {
377
+ color: var(--u-blue-500, #6ba3e3);
378
+ }
379
+
380
+ :host-context([theme="dark"]) .new-row td {
381
+ background: var(--u-green-0, #0D2818);
382
+ }
383
+
384
+ :host-context([theme="dark"]) .new-row input {
385
+ border-color: var(--u-green-500, #66B584);
386
+ color: var(--u-txt-color-weak, #8A8A8A);
387
+ }
388
+
389
+ :host-context([theme="dark"]) .new-row input:focus {
390
+ border-color: var(--u-green-600, #81D19D);
391
+ background: var(--u-input-bg-color, #1E1E1E);
392
+ color: var(--u-txt-color, #D4D4D4);
393
+ }
394
+
395
+ :host-context([theme="dark"]) .detail-row td {
396
+ background: var(--u-neutral-200, #1E1E1E);
397
+ border-bottom-color: var(--u-border-color, #3D3D3D);
398
+ }
399
+
400
+ :host-context([theme="dark"]) .pagination {
401
+ background: var(--u-neutral-200, #1E1E1E);
402
+ border-top-color: var(--u-border-color, #3D3D3D);
403
+ color: var(--u-txt-color-weak, #8A8A8A);
404
+ }
405
+
406
+ :host-context([theme="dark"]) .pagination button {
407
+ background: var(--u-neutral-300, #2A2A2A);
408
+ color: var(--u-txt-color, #D4D4D4);
409
+ border-color: var(--u-border-color, #3D3D3D);
410
+ }
411
+
412
+ :host-context([theme="dark"]) .pagination button.active {
413
+ background: var(--u-blue-500, #6ba3e3);
414
+ color: var(--u-neutral-1000, #FFFFFF);
415
+ border-color: var(--u-blue-500, #6ba3e3);
416
+ }
417
+
418
+ :host-context([theme="dark"]) .pagination select {
419
+ background: var(--u-input-bg-color, #1E1E1E);
420
+ color: var(--u-txt-color, #D4D4D4);
421
+ border-color: var(--u-input-border-color, #3D3D3D);
422
+ }
423
+
424
+ :host-context([theme="dark"]) .loading-overlay {
425
+ color: var(--u-txt-color-weak, #8A8A8A);
426
+ }
427
+
428
+ :host-context([theme="dark"]) .empty-message {
429
+ color: var(--u-txt-color-disabled, #525252);
430
+ }
431
+ `;
432
+
433
+ export { richTableStyles };
@@ -0,0 +1,67 @@
1
+ export interface ColumnDef {
2
+ key: string;
3
+ label: string;
4
+ width?: string;
5
+ sortable?: boolean;
6
+ editable?: boolean;
7
+ required?: boolean;
8
+ type?: 'text' | 'number' | 'date' | 'select' | 'badge';
9
+ options?: {
10
+ value: string;
11
+ label: string;
12
+ }[];
13
+ badgeColors?: Record<string, string>;
14
+ render?: (value: unknown, row: Record<string, unknown>) => string | HTMLElement;
15
+ align?: 'left' | 'center' | 'right';
16
+ filterable?: boolean;
17
+ filterType?: 'text' | 'select';
18
+ validator?: (value: unknown, row: Record<string, unknown>) => string | null;
19
+ clipboardParse?: (text: string) => unknown;
20
+ clipboardFormat?: (value: unknown) => string;
21
+ }
22
+ export interface CellPosition {
23
+ rowIndex: number;
24
+ colIndex: number;
25
+ }
26
+ export interface SortState {
27
+ field: string;
28
+ direction: 'asc' | 'desc';
29
+ }
30
+ export interface FilterState {
31
+ [field: string]: string;
32
+ }
33
+ export interface RichTableEventMap {
34
+ 'selection-change': CustomEvent<{
35
+ selectedRows: Record<string, unknown>[];
36
+ }>;
37
+ 'row-create': CustomEvent<{
38
+ row: Record<string, unknown>;
39
+ }>;
40
+ 'row-update': CustomEvent<{
41
+ row: Record<string, unknown>;
42
+ field: string;
43
+ value: unknown;
44
+ oldValue: unknown;
45
+ }>;
46
+ 'row-delete': CustomEvent<{
47
+ row: Record<string, unknown>;
48
+ }>;
49
+ 'row-expand': CustomEvent<{
50
+ row: Record<string, unknown>;
51
+ expanded: boolean;
52
+ }>;
53
+ 'sort-change': CustomEvent<{
54
+ field: string;
55
+ direction: 'asc' | 'desc' | null;
56
+ }>;
57
+ 'filter-change': CustomEvent<{
58
+ filters: FilterState;
59
+ }>;
60
+ 'page-change': CustomEvent<{
61
+ page: number;
62
+ pageSize: number;
63
+ }>;
64
+ 'paste': CustomEvent<{
65
+ rows: Record<string, unknown>[];
66
+ }>;
67
+ }
@@ -0,0 +1,9 @@
1
+ import { ColumnDef } from '../types.js';
2
+ /**
3
+ * 탭/줄바꿈 구분 텍스트(TSV)를 파싱하여 행 배열로 반환
4
+ */
5
+ export declare function parseTSV(text: string, columns: ColumnDef[]): Record<string, unknown>[];
6
+ /**
7
+ * 행 배열을 탭/줄바꿈 구분 텍스트(TSV)로 변환
8
+ */
9
+ export declare function toTSV(rows: Record<string, unknown>[], columns: ColumnDef[]): string;
@@ -0,0 +1,34 @@
1
+ function parseTSV(text, columns) {
2
+ const lines = text.trim().split("\n");
3
+ return lines.map((line) => {
4
+ const cells = line.split(" ");
5
+ const row = {};
6
+ columns.forEach((col, i) => {
7
+ if (i < cells.length) {
8
+ const raw = cells[i].trim();
9
+ if (col.clipboardParse) {
10
+ row[col.key] = col.clipboardParse(raw);
11
+ } else if (col.type === "number") {
12
+ row[col.key] = Number(raw.replace(/,/g, "")) || 0;
13
+ } else {
14
+ row[col.key] = raw;
15
+ }
16
+ }
17
+ });
18
+ return row;
19
+ });
20
+ }
21
+ function toTSV(rows, columns) {
22
+ const header = columns.map((c) => c.label).join(" ");
23
+ const body = rows.map(
24
+ (row) => columns.map((col) => {
25
+ const value = row[col.key];
26
+ if (col.clipboardFormat) return col.clipboardFormat(value);
27
+ return String(value ?? "");
28
+ }).join(" ")
29
+ ).join("\n");
30
+ return `${header}
31
+ ${body}`;
32
+ }
33
+
34
+ export { parseTSV, toTSV };
package/dist/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export { UDataGrid } from './components/data-grid/UDataGrid';
2
2
  export type { UDataGridColumn, UDataGridProps } from './components/data-grid/UDataGrid.types';
3
3
  export * from './components/data-view/UDataView';
4
4
  export * from './components/simple-sheet/USimpleSheet';
5
+ export * from './components/u-rich-table/URichTable';
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export { UDataGrid } from './components/data-grid/UDataGrid.js';
2
2
  import './components/data-view/UDataView.js';
3
3
  import './components/simple-sheet/USimpleSheet.js';
4
+ import './components/u-rich-table/URichTable.js';
4
5
  export { UDataView } from './components/data-view/UDataView.component.js';
6
+ export { URichTable } from './components/u-rich-table/URichTable.component.js';
5
7
  export { USimpleSheet } from './components/simple-sheet/USimpleSheet.component.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@iyulab/data-components",
3
3
  "description": "iyulab data visualization components",
4
- "version": "0.1.8",
4
+ "version": "0.3.0",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-components",
@@ -46,6 +46,10 @@
46
46
  "./data-grid": {
47
47
  "types": "./dist/components/data-grid/UDataGrid.d.ts",
48
48
  "import": "./dist/components/data-grid/UDataGrid.js"
49
+ },
50
+ "./u-rich-table": {
51
+ "types": "./dist/components/u-rich-table/URichTable.d.ts",
52
+ "import": "./dist/components/u-rich-table/URichTable.js"
49
53
  }
50
54
  },
51
55
  "sideEffects": [
@@ -54,6 +58,7 @@
54
58
  "./dist/components/data-grid/UDataGrid.js",
55
59
  "./dist/components/data-view/UDataView.js",
56
60
  "./dist/components/simple-sheet/USimpleSheet.js",
61
+ "./dist/components/u-rich-table/URichTable.js",
57
62
  "./dist/init.js"
58
63
  ],
59
64
  "scripts": {