@iyulab/data-components 0.1.3 → 0.1.6

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.
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  데이터 표시 및 입력을 위한 웹 컴포넌트 라이브러리.
4
4
 
5
+ **[Live Demo](https://iyulab.github.io/node-data-components/)**
6
+
5
7
  - **USimpleSheet** — 엑셀 호환 스프레드시트 입력 컴포넌트 (Lit)
6
8
  - **UDataView** — Grid / List / Table 뷰 전환 컴포넌트 (Lit)
7
9
  - **UDataGrid** — OData 기반 서버 사이드 데이터 그리드 (React + DevExtreme)
@@ -175,6 +175,85 @@ const styles = css`
175
175
  color: var(--u-txt-color-weak);
176
176
  font-size: 1rem;
177
177
  }
178
+
179
+ /* ── Dark mode ──
180
+ 외부에서 --u-* CSS 변수가 제공되면 그 값을 사용하고,
181
+ 제공되지 않으면 자체 dark fallback 값을 적용한다. */
182
+
183
+ :host-context([theme="dark"]) .toolbar {
184
+ background: var(--u-bg-color, #121212);
185
+ border-color: var(--u-border-color, #3D3D3D);
186
+ }
187
+
188
+ :host-context([theme="dark"]) .view-toggles u-button[active] {
189
+ background: var(--u-blue-600, #87B8F5);
190
+ color: var(--u-neutral-0, #000000);
191
+ }
192
+
193
+ :host-context([theme="dark"]) .info {
194
+ color: var(--u-txt-color-weak, #8A8A8A);
195
+ }
196
+
197
+ :host-context([theme="dark"]) .card {
198
+ background: var(--u-bg-color, #121212);
199
+ border-color: var(--u-border-color, #3D3D3D);
200
+ }
201
+
202
+ :host-context([theme="dark"]) .card:hover {
203
+ border-color: var(--u-blue-600, #87B8F5);
204
+ box-shadow: 0 4px 12px var(--u-shadow-color-weak, rgba(0, 0, 0, 0.25));
205
+ }
206
+
207
+ :host-context([theme="dark"]) .card.selected {
208
+ border-color: var(--u-blue-600, #87B8F5);
209
+ background: var(--u-bg-color-active, #3D3D3D);
210
+ box-shadow: 0 0 0 3px var(--u-blue-200, #2B4F7E);
211
+ }
212
+
213
+ :host-context([theme="dark"]) .card-field .label {
214
+ color: var(--u-txt-color-weak, #8A8A8A);
215
+ }
216
+
217
+ :host-context([theme="dark"]) .card-field .value {
218
+ color: var(--u-txt-color, #D4D4D4);
219
+ }
220
+
221
+ :host-context([theme="dark"]) .table-wrapper {
222
+ border-color: var(--u-border-color, #3D3D3D);
223
+ }
224
+
225
+ :host-context([theme="dark"]) table {
226
+ background: var(--u-bg-color, #121212);
227
+ }
228
+
229
+ :host-context([theme="dark"]) thead {
230
+ background: var(--u-neutral-50, #0A0A0A);
231
+ }
232
+
233
+ :host-context([theme="dark"]) th {
234
+ color: var(--u-txt-color-weak, #8A8A8A);
235
+ border-bottom-color: var(--u-border-color, #3D3D3D);
236
+ }
237
+
238
+ :host-context([theme="dark"]) tbody tr:hover {
239
+ background: var(--u-bg-color-hover, #2A2A2A);
240
+ }
241
+
242
+ :host-context([theme="dark"]) tbody tr.selected {
243
+ background: var(--u-bg-color-active, #3D3D3D);
244
+ }
245
+
246
+ :host-context([theme="dark"]) tbody tr:not(:last-child) {
247
+ border-bottom-color: var(--u-border-color-weak, #2A2A2A);
248
+ }
249
+
250
+ :host-context([theme="dark"]) td {
251
+ color: var(--u-txt-color, #D4D4D4);
252
+ }
253
+
254
+ :host-context([theme="dark"]) .empty {
255
+ color: var(--u-txt-color-weak, #8A8A8A);
256
+ }
178
257
  `;
179
258
 
180
259
  export { styles };
@@ -9,6 +9,10 @@ export interface SheetColumn {
9
9
  width?: number;
10
10
  /** 읽기 전용 열 */
11
11
  readonly?: boolean;
12
+ /** 선택 가능한 옵션 목록 (정적 배열 또는 동적 콜백) */
13
+ options?: string[] | ((row: number, col: number) => string[]);
14
+ /** true이면 목록에 있는 값만 입력 가능 (기본: false = 자유 입력 허용) */
15
+ strict?: boolean;
12
16
  }
13
17
  /**
14
18
  * USimpleSheet - 심플 스프레드시트 컴포넌트
@@ -44,6 +48,9 @@ export declare class USimpleSheet extends UElement {
44
48
  private _editing;
45
49
  private _editVal;
46
50
  private _replaceOnEdit;
51
+ private _dropdownItems;
52
+ private _dropdownIndex;
53
+ private _isDropdownClick;
47
54
  private _data;
48
55
  private _colWidths;
49
56
  private _isMouseSelecting;
@@ -79,6 +86,7 @@ export declare class USimpleSheet extends UElement {
79
86
  private _onInputChange;
80
87
  private _onInputKeyDown;
81
88
  private _onInputBlur;
89
+ private _onDropdownItemMouseDown;
82
90
  private _onTableMouseDown;
83
91
  private _onTableMouseOver;
84
92
  private _onTableDblClick;
@@ -112,6 +120,8 @@ export declare class USimpleSheet extends UElement {
112
120
  private _scrollCellIntoView;
113
121
  private _getCellFromEvent;
114
122
  private _isColReadonly;
123
+ private _getColOptions;
124
+ private _filterOptions;
115
125
  private _emitChange;
116
126
  /** 현재 데이터를 2D 배열로 반환 */
117
127
  getData(): string[][];
@@ -32,6 +32,9 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
32
32
  this._editing = null;
33
33
  this._editVal = "";
34
34
  this._replaceOnEdit = false;
35
+ this._dropdownItems = [];
36
+ this._dropdownIndex = -1;
37
+ this._isDropdownClick = false;
35
38
  // @state() 없음 — 직접 requestUpdate() 호출
36
39
  this._data = [];
37
40
  this._colWidths = [];
@@ -180,6 +183,7 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
180
183
  return;
181
184
  }
182
185
  if (!this.readonly && !this._isColReadonly(anchor.col) && e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
186
+ e.preventDefault();
183
187
  this._startEdit(anchor.row, anchor.col, e.key);
184
188
  }
185
189
  };
@@ -188,14 +192,46 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
188
192
  // ──────────────────────────────────────────
189
193
  this._onInputChange = (e) => {
190
194
  this._editVal = e.target.value;
195
+ if (this._editing) {
196
+ const options = this._getColOptions(this._editing.row, this._editing.col);
197
+ if (options) {
198
+ const filtered = this._filterOptions(options, this._editVal);
199
+ this._dropdownItems = filtered;
200
+ this._dropdownIndex = filtered.length > 0 ? 0 : -1;
201
+ }
202
+ }
191
203
  };
192
204
  this._onInputKeyDown = (e) => {
193
205
  e.stopPropagation();
206
+ const hasDropdown = this._dropdownItems.length > 0;
194
207
  if (e.key === "Escape") {
195
208
  e.preventDefault();
196
209
  this._cancelEdit();
197
210
  return;
198
211
  }
212
+ if (hasDropdown && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
213
+ e.preventDefault();
214
+ const len = this._dropdownItems.length;
215
+ if (e.key === "ArrowDown") {
216
+ this._dropdownIndex = (this._dropdownIndex + 1) % len;
217
+ } else {
218
+ this._dropdownIndex = (this._dropdownIndex - 1 + len) % len;
219
+ }
220
+ this.updateComplete.then(() => {
221
+ const dropdown = this.renderRoot.querySelector(".cell-dropdown");
222
+ const highlighted = this.renderRoot.querySelector(".dropdown-item.highlighted");
223
+ if (dropdown && highlighted) {
224
+ const dRect = dropdown.getBoundingClientRect();
225
+ const hRect = highlighted.getBoundingClientRect();
226
+ if (hRect.bottom > dRect.bottom) dropdown.scrollTop += hRect.bottom - dRect.bottom;
227
+ else if (hRect.top < dRect.top) dropdown.scrollTop -= dRect.top - hRect.top;
228
+ }
229
+ });
230
+ return;
231
+ }
232
+ if (hasDropdown && this._dropdownIndex >= 0 && (e.key === "Enter" || e.key === "Tab")) {
233
+ this._editVal = this._dropdownItems[this._dropdownIndex];
234
+ }
199
235
  if (e.key === "Enter") {
200
236
  e.preventDefault();
201
237
  const pos = this._editing;
@@ -248,10 +284,20 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
248
284
  }
249
285
  };
250
286
  this._onInputBlur = () => {
287
+ if (this._isDropdownClick) {
288
+ this._isDropdownClick = false;
289
+ return;
290
+ }
251
291
  if (this._editing) {
252
292
  this._commitEdit();
253
293
  }
254
294
  };
295
+ this._onDropdownItemMouseDown = (e, value) => {
296
+ e.preventDefault();
297
+ this._isDropdownClick = true;
298
+ this._editVal = value;
299
+ this._commitEdit();
300
+ };
255
301
  // ──────────────────────────────────────────
256
302
  // 마우스 이벤트
257
303
  // ──────────────────────────────────────────
@@ -579,6 +625,10 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
579
625
  isAnchor ? "anchor" : "",
580
626
  isEditing ? "editing" : ""
581
627
  ].filter(Boolean).join(" ");
628
+ const hasOptions = isEditing && this._getColOptions(r, c) !== null;
629
+ const showDropdown = isEditing && this._dropdownItems.length > 0;
630
+ const isStrict = this.columns?.[c]?.strict ?? false;
631
+ const noMatch = isEditing && hasOptions && this._dropdownItems.length === 0 && this._editVal !== "";
582
632
  return html`
583
633
  <td
584
634
  class=${classes}
@@ -593,6 +643,20 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
593
643
  @keydown=${this._onInputKeyDown}
594
644
  @blur=${this._onInputBlur}
595
645
  />
646
+ ${showDropdown ? html`
647
+ <div class="cell-dropdown">
648
+ ${this._dropdownItems.map((item, i) => html`
649
+ <div
650
+ class="dropdown-item ${i === this._dropdownIndex ? "highlighted" : ""}"
651
+ @mousedown=${(e) => this._onDropdownItemMouseDown(e, item)}
652
+ >${item}</div>
653
+ `)}
654
+ </div>
655
+ ` : noMatch && isStrict ? html`
656
+ <div class="cell-dropdown">
657
+ <div class="dropdown-empty">일치하는 항목 없음</div>
658
+ </div>
659
+ ` : ""}
596
660
  ` : value}
597
661
  </td>
598
662
  `;
@@ -692,6 +756,15 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
692
756
  this._editVal = typedChar !== void 0 ? typedChar : this._data[row]?.[col] ?? "";
693
757
  this._editing = { row, col };
694
758
  this._select(row, col);
759
+ const options = this._getColOptions(row, col);
760
+ if (options) {
761
+ const filtered = this._filterOptions(options, this._editVal);
762
+ this._dropdownItems = filtered;
763
+ this._dropdownIndex = filtered.length > 0 ? 0 : -1;
764
+ } else {
765
+ this._dropdownItems = [];
766
+ this._dropdownIndex = -1;
767
+ }
695
768
  this.updateComplete.then(() => {
696
769
  const input = this.renderRoot.querySelector(".cell-input");
697
770
  if (input) {
@@ -708,11 +781,26 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
708
781
  _commitEdit() {
709
782
  if (!this._editing) return;
710
783
  const { row, col } = this._editing;
784
+ const colDef = this.columns?.[col];
785
+ if (colDef?.strict && colDef?.options && this._editVal !== "") {
786
+ const allOptions = this._getColOptions(row, col) ?? [];
787
+ if (!allOptions.includes(this._editVal)) {
788
+ this._editing = null;
789
+ this._dropdownItems = [];
790
+ this._dropdownIndex = -1;
791
+ this._isDropdownClick = false;
792
+ this.requestUpdate();
793
+ this._containerEl?.focus();
794
+ return;
795
+ }
796
+ }
711
797
  const prevVal = this._data[row]?.[col] ?? "";
712
798
  const newData = this._data.map((r) => [...r]);
713
799
  newData[row][col] = this._editVal;
714
800
  this._data = newData;
715
801
  this._editing = null;
802
+ this._dropdownItems = [];
803
+ this._dropdownIndex = -1;
716
804
  if (this._editVal !== prevVal) {
717
805
  this._pushHistory();
718
806
  this._emitChange();
@@ -722,6 +810,8 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
722
810
  }
723
811
  _cancelEdit() {
724
812
  this._editing = null;
813
+ this._dropdownItems = [];
814
+ this._dropdownIndex = -1;
725
815
  this.requestUpdate();
726
816
  this._containerEl?.focus();
727
817
  }
@@ -766,6 +856,16 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
766
856
  _isColReadonly(col) {
767
857
  return this.columns?.[col]?.readonly ?? false;
768
858
  }
859
+ _getColOptions(row, col) {
860
+ const colDef = this.columns?.[col];
861
+ if (!colDef?.options) return null;
862
+ return typeof colDef.options === "function" ? colDef.options(row, col) : colDef.options;
863
+ }
864
+ _filterOptions(options, query) {
865
+ if (!query) return options;
866
+ const lower = query.toLowerCase();
867
+ return options.filter((o) => o.toLowerCase().includes(lower));
868
+ }
769
869
  _emitChange() {
770
870
  this.emit("change", { data: this._data });
771
871
  }
@@ -850,6 +950,12 @@ __decorateClass([
850
950
  __decorateClass([
851
951
  state()
852
952
  ], _USimpleSheet.prototype, "_replaceOnEdit");
953
+ __decorateClass([
954
+ state()
955
+ ], _USimpleSheet.prototype, "_dropdownItems");
956
+ __decorateClass([
957
+ state()
958
+ ], _USimpleSheet.prototype, "_dropdownIndex");
853
959
  let USimpleSheet = _USimpleSheet;
854
960
 
855
961
  export { USimpleSheet };
@@ -197,36 +197,112 @@ const styles = css`
197
197
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
198
198
  }
199
199
 
200
+ /* Dropdown overlay */
201
+ .cell-dropdown {
202
+ position: absolute;
203
+ top: 100%;
204
+ left: -1px;
205
+ width: calc(100% + 2px);
206
+ min-width: 120px;
207
+ max-height: 200px;
208
+ overflow-y: auto;
209
+ background: var(--u-bg-color, #fff);
210
+ border: 1px solid var(--u-border-color, #e2e8f0);
211
+ border-top: none;
212
+ border-radius: 0 0 4px 4px;
213
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
214
+ z-index: 20;
215
+ }
216
+
217
+ .dropdown-item {
218
+ padding: 4px 8px;
219
+ font-size: 13px;
220
+ color: var(--u-txt-color, #0f172a);
221
+ cursor: pointer;
222
+ white-space: nowrap;
223
+ overflow: hidden;
224
+ text-overflow: ellipsis;
225
+ }
226
+
227
+ .dropdown-item:hover {
228
+ background: var(--u-neutral-100, #f1f5f9);
229
+ }
230
+
231
+ .dropdown-item.highlighted {
232
+ background: var(--u-blue-50, #eff6ff);
233
+ color: var(--u-blue-800, #1e40af);
234
+ }
235
+
236
+ .dropdown-empty {
237
+ padding: 6px 8px;
238
+ font-size: 12px;
239
+ color: var(--u-txt-color-weak, #64748b);
240
+ font-style: italic;
241
+ }
242
+
200
243
  /* Readonly mode */
201
244
  :host([readonly]) .cell {
202
245
  cursor: default;
203
246
  }
204
247
 
205
248
  /* ── Dark mode ──
206
- 헤더/셀 배경·텍스트는 CSS 변수(--u-neutral-*, --u-txt-color 등)가
207
- 테마에 따라 자동 처리되므로 선택 상태와 포커스 효과만 재정의한다. */
249
+ 외부에서 --u-* CSS 변수가 제공되면 그 값을 사용하고,
250
+ 제공되지 않으면 자체 dark fallback 값을 적용한다. */
251
+
252
+ :host-context([theme="dark"]) .sheet-container {
253
+ background: var(--u-bg-color, #121212);
254
+ border-color: var(--u-border-color, #3D3D3D);
255
+ }
208
256
 
209
257
  :host-context([theme="dark"]) .sheet-container:focus-within {
210
258
  border-color: var(--u-blue-500, #6ba3e3);
211
259
  box-shadow: 0 0 0 2px var(--u-blue-100, #1e3a5f);
212
260
  }
213
261
 
262
+ :host-context([theme="dark"]) .corner {
263
+ background: var(--u-neutral-100, #121212);
264
+ border-color: var(--u-border-color, #3D3D3D);
265
+ }
266
+
267
+ :host-context([theme="dark"]) .corner:hover {
268
+ background: var(--u-neutral-200, #1E1E1E);
269
+ }
270
+
271
+ :host-context([theme="dark"]) .col-header {
272
+ background: var(--u-neutral-100, #121212);
273
+ color: var(--u-txt-color-weak, #8A8A8A);
274
+ border-color: var(--u-border-color, #3D3D3D);
275
+ }
276
+
214
277
  :host-context([theme="dark"]) .col-header.col-selected {
215
278
  background: var(--u-blue-100, #1e3a5f);
216
279
  color: var(--u-blue-800, #c2deff);
217
280
  }
218
281
 
282
+ :host-context([theme="dark"]) .row-num {
283
+ background: var(--u-neutral-50, #0A0A0A);
284
+ color: var(--u-txt-color-weak, #8A8A8A);
285
+ border-right-color: var(--u-border-color, #3D3D3D);
286
+ border-bottom-color: var(--u-border-color-weak, #2A2A2A);
287
+ }
288
+
219
289
  :host-context([theme="dark"]) .row-num.row-selected {
220
290
  background: var(--u-blue-100, #1e3a5f);
221
291
  color: var(--u-blue-800, #c2deff);
222
292
  }
223
293
 
294
+ :host-context([theme="dark"]) .cell {
295
+ color: var(--u-txt-color, #D4D4D4);
296
+ border-right-color: var(--u-border-color-weak, #2A2A2A);
297
+ border-bottom-color: var(--u-border-color-weak, #2A2A2A);
298
+ }
299
+
224
300
  :host-context([theme="dark"]) .cell.selected {
225
301
  background: var(--u-blue-0, #0a1e3d);
226
302
  }
227
303
 
228
304
  :host-context([theme="dark"]) .cell.anchor {
229
- background: var(--u-bg-color);
305
+ background: var(--u-bg-color, #121212);
230
306
  outline-color: var(--u-blue-500, #6ba3e3);
231
307
  }
232
308
 
@@ -235,11 +311,34 @@ const styles = css`
235
311
  }
236
312
 
237
313
  :host-context([theme="dark"]) .cell-input {
238
- background: var(--u-bg-color);
239
- color: var(--u-txt-color);
314
+ background: var(--u-bg-color, #121212);
315
+ color: var(--u-txt-color, #D4D4D4);
240
316
  outline-color: var(--u-blue-500, #6ba3e3);
241
317
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
242
318
  }
319
+
320
+ :host-context([theme="dark"]) .cell-dropdown {
321
+ background: var(--u-bg-color, #121212);
322
+ border-color: var(--u-border-color, #3D3D3D);
323
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
324
+ }
325
+
326
+ :host-context([theme="dark"]) .dropdown-item {
327
+ color: var(--u-txt-color, #D4D4D4);
328
+ }
329
+
330
+ :host-context([theme="dark"]) .dropdown-item:hover {
331
+ background: var(--u-neutral-100, #1E1E1E);
332
+ }
333
+
334
+ :host-context([theme="dark"]) .dropdown-item.highlighted {
335
+ background: var(--u-blue-100, #1e3a5f);
336
+ color: var(--u-blue-800, #c2deff);
337
+ }
338
+
339
+ :host-context([theme="dark"]) .dropdown-empty {
340
+ color: var(--u-txt-color-weak, #8A8A8A);
341
+ }
243
342
  `;
244
343
 
245
344
  export { styles };
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.3",
4
+ "version": "0.1.6",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-components",