@iyulab/data-components 0.12.0 → 0.14.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,95 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.14.0] - 2026-08-07
4
+
5
+ ### Added
6
+
7
+ - **`u-rich-table` gained a `row-activate` event** — the most common grid interaction, "click a
8
+ row to see its detail," had no way to reach the app. `selection-change` is checkbox-only and
9
+ conflating it with "the row being viewed" breaks multi-select screens; `row-expand` announces
10
+ that a detail row opened but gives the app no slot to render into. Clicking a cell now always
11
+ fires `row-activate` (`editable` columns included — a single click never enters edit mode, only
12
+ `dblclick` does), and so does `Enter` on a focused cell in a non-`editable` column (an
13
+ `editable` column's `Enter` still starts editing, unchanged). `detail: { row, id, via: 'click' |
14
+ 'keyboard' }`. Purely additive — no existing behavior changes.
15
+
16
+ ### Fixed
17
+
18
+ - 🔴**`u-rich-table`: giving the host a height made the pagination unreachable.** The component
19
+ declared no vertical layout, so under a height constraint the content overflowed at its natural
20
+ size while `:host` clipped with `overflow: hidden` — and because the pagination is the last child,
21
+ it was the first thing to disappear. It stayed in the DOM; it was simply neither visible nor
22
+ scrollable to. *The table rendered, but page 2 could not be reached.*
23
+
24
+ That left consumers with two options, both of which cost something the grid is for: constrain the
25
+ height and lose paging, or leave it unconstrained and let 50 rows become a 2,000px document whose
26
+ header scrolls away.
27
+
28
+ The host is now a column flex container: the toolbar and pagination hold their positions and only
29
+ the row area scrolls. The header row is `sticky`, so column names stay put while rows scroll —
30
+ the boundary line under it is drawn with an inset `box-shadow` rather than `border-bottom`,
31
+ because under `border-collapse: collapse` cell borders belong to the table and do not follow a
32
+ stuck header.
33
+
34
+ **Unconstrained use is unchanged**: with no height, `flex: 1 1 auto` follows content height and
35
+ `overflow: auto` adds no scrollbar. Verified in a real browser at both sizes.
36
+
37
+ - 🔴**`sideEffects` omitted this package's own entry barrel, so bundlers dropped every element
38
+ registration.** The barrel that `exports["."]` resolves to exists solely to register the custom
39
+ elements, but it was not in the `sideEffects` allowlist. A consumer writing
40
+ `import '@iyulab/data-components'` — the form this package's own documentation recommends — had the module
41
+ elided entirely in a production build. The failure is silent: the build succeeds with no warning,
42
+ the tags remain in the DOM, and an unregistered custom element renders nothing.
43
+
44
+ Registration modules were already listed correctly. That was not enough: a dropped barrel means
45
+ they are never reached.
46
+
47
+ The `./react` subpath entry had the same gap. This also explains a second symptom:
48
+ `querySelector` still returns an element for an unregistered tag, so consumer code calling a
49
+ component method on it fails with `is not a function` rather than returning null.
50
+
51
+ Both the source-resolved and published-artifact forms of every affected entry point are now
52
+ declared, so workspace consumers and installed consumers get the same guarantee.
53
+
54
+ ## [0.13.0] - 2026-08-05
55
+
56
+ ### Added
57
+
58
+ - **Agent skill pack ships with the package** (`skills/iyulab-data-components`).
59
+ Until now the only way to learn the API was to read the emitted `.d.ts` files, which
60
+ made the internal file layout a de-facto contract and left the *combinations* — server
61
+ paging together with selection — undocumented. The reference for `u-rich-table` opens
62
+ with the two things that actually bite: every row needs a unique `_id` (without it a
63
+ single selection looks like "everything is selected"), and the app owns the query
64
+ (`totalCount` is not `data.length`).
65
+
66
+ ```bash
67
+ npx skills add ./node_modules/@iyulab/data-components/skills/iyulab-data-components
68
+ ```
69
+
70
+ - **The public types `u-rich-table` asks for are exported from the package root** —
71
+ `ColumnDef`, `CellPosition`, `SortState`, `FilterState`, `RichTableEventMap`.
72
+ `columns` requires `ColumnDef[]`, but that name could not be imported from the root, so
73
+ consumers had to deep-import an internal path or hand-copy the shape (which then drifts
74
+ silently when the upstream type changes).
75
+
76
+ ```ts
77
+ import type { ColumnDef, RichTableEventMap } from '@iyulab/data-components';
78
+ ```
79
+
80
+ ### Fixed
81
+
82
+ - **`u-data-view`: the layout toggle buttons did nothing.** Grid / list / table were
83
+ rendered but never wired to a click handler, so `mode` could only be changed by setting
84
+ the property — and the "selected" highlight was therefore stuck on `grid` forever.
85
+ The buttons now switch the layout and expose their state via `aria-pressed`.
86
+
87
+ ### Removed
88
+
89
+ - **`u-data-view`: unreachable selected-row styling.** The `.selected` rules were driven
90
+ by an internal index that was never assigned, so they could not apply. Removed along
91
+ with the dead state.
92
+
3
93
  ## [0.12.0] - 2026-08-04
4
94
 
5
95
  ### Fixed
package/README.md CHANGED
@@ -18,6 +18,23 @@
18
18
  npm install @iyulab/data-components
19
19
  ```
20
20
 
21
+ ## 🤖 Skills Usage
22
+
23
+ 이 패키지는 AI 코딩 에이전트가 라이브러리를 이해하고 사용하도록 돕는
24
+ [Agent Skill](https://agentskills.io/)을 포함합니다.
25
+
26
+ **GitHub 경유 (권장):**
27
+
28
+ ```bash
29
+ npx skills add iyulab/node-data-components
30
+ ```
31
+
32
+ **로컬 `node_modules` 경유:**
33
+
34
+ ```bash
35
+ npx skills add ./node_modules/@iyulab/data-components/skills/iyulab-data-components
36
+ ```
37
+
21
38
  ## Quick Start
22
39
 
23
40
  ### USimpleSheet
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.142.0/helpers/esm/decorate.js
1
+ //#region \0@oxc-project+runtime@0.143.0/helpers/esm/decorate.js
2
2
  function __decorate(decorators, target, key, desc) {
3
3
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
4
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.142.0/helpers/esm/decorateMetadata.js
1
+ //#region \0@oxc-project+runtime@0.143.0/helpers/esm/decorateMetadata.js
2
2
  function __decorateMetadata(k, v) {
3
3
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
4
4
  }
@@ -34,9 +34,17 @@ export declare class UDataView extends UElement {
34
34
  renderCard?: (item: DataItem, index: number) => TemplateResult;
35
35
  /** 커스텀 셀 렌더 함수 (table용) */
36
36
  renderCell?: (item: DataItem, column: DataColumn, index: number) => TemplateResult | string;
37
- private selectedIndex;
38
37
  render(): TemplateResult<1>;
39
38
  private renderToolbar;
39
+ /**
40
+ * 레이아웃 전환 버튼.
41
+ *
42
+ * ⚠**선택 상태를 `variant`+`color` 로 나타낸다.** 종전에는 `?active` 로 이 시트의
43
+ * `u-button[active]` 규칙(배경 한 줄)을 켰는데, 그 규칙은 자기 주석에 *"주색 위의 글자
44
+ * 대비가 3.45~3.68 로 최선이 아니다"* 라고 적고 있었다. `UButton` 자신의 `variant`·`color`
45
+ * 를 쓰면 그 대비 계약을 컴포넌트가 책임진다.
46
+ * 접근성은 `aria-pressed` 가 나른다 — 색만으로는 토글 상태가 보조기술에 닿지 않는다.
47
+ */
40
48
  private renderViewButton;
41
49
  private renderContent;
42
50
  private renderGrid;
@@ -1,8 +1,8 @@
1
1
  import { styles } from "./UDataView.styles.js";
2
- import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateMetadata.js";
3
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorate.js";
2
+ import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.js";
3
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js";
4
4
  import { html } from "lit";
5
- import { customElement, property, state } from "lit/decorators.js";
5
+ import { customElement, property } from "lit/decorators.js";
6
6
  import "@iyulab/components/dist/components/icon/UIcon.js";
7
7
  import "@iyulab/components/dist/components/button/UButton.js";
8
8
  import { UElement } from "@iyulab/components/dist/components/UElement.js";
@@ -14,7 +14,6 @@ var UDataView = class UDataView extends UElement {
14
14
  this.mode = "grid";
15
15
  this.gridMinWidth = "200px";
16
16
  this.gap = "1rem";
17
- this.selectedIndex = null;
18
17
  }
19
18
  static {
20
19
  this.styles = [super.styles, styles];
@@ -41,11 +40,27 @@ var UDataView = class UDataView extends UElement {
41
40
  </div>
42
41
  `;
43
42
  }
43
+ /**
44
+ * 레이아웃 전환 버튼.
45
+ *
46
+ * ⚠**선택 상태를 `variant`+`color` 로 나타낸다.** 종전에는 `?active` 로 이 시트의
47
+ * `u-button[active]` 규칙(배경 한 줄)을 켰는데, 그 규칙은 자기 주석에 *"주색 위의 글자
48
+ * 대비가 3.45~3.68 로 최선이 아니다"* 라고 적고 있었다. `UButton` 자신의 `variant`·`color`
49
+ * 를 쓰면 그 대비 계약을 컴포넌트가 책임진다.
50
+ * 접근성은 `aria-pressed` 가 나른다 — 색만으로는 토글 상태가 보조기술에 닿지 않는다.
51
+ */
44
52
  renderViewButton(mode, icon, label) {
53
+ const selected = this.mode === mode;
45
54
  return html`
46
55
  <u-button
47
- ?active=${this.mode === mode}
56
+ variant=${selected ? "solid" : "ghost"}
57
+ color=${selected ? "primary" : "neutral"}
48
58
  title=${label}
59
+ aria-label=${label}
60
+ aria-pressed=${selected ? "true" : "false"}
61
+ @click=${() => {
62
+ this.mode = mode;
63
+ }}
49
64
  >
50
65
  <u-icon lib="bootstrap" name=${icon}></u-icon>
51
66
  </u-button>
@@ -89,9 +104,7 @@ var UDataView = class UDataView extends UElement {
89
104
  </thead>
90
105
  <tbody>
91
106
  ${this.items.map((item, index) => html`
92
- <tr
93
- class=${this.selectedIndex === index ? "selected" : ""}
94
- >
107
+ <tr>
95
108
  ${cols.map((col) => html`
96
109
  <td>${this.getCellContent(item, col, index)}</td>
97
110
  `)}
@@ -105,9 +118,7 @@ var UDataView = class UDataView extends UElement {
105
118
  renderGridItem(item, index) {
106
119
  const content = this.renderCard ? this.renderCard(item, index) : this.renderDefaultCard(item);
107
120
  return html`
108
- <div
109
- class="card ${this.selectedIndex === index ? "selected" : ""}"
110
- >
121
+ <div class="card">
111
122
  ${content}
112
123
  </div>
113
124
  `;
@@ -115,9 +126,7 @@ var UDataView = class UDataView extends UElement {
115
126
  renderListItem(item, index) {
116
127
  const content = this.renderCard ? this.renderCard(item, index) : this.renderDefaultCard(item);
117
128
  return html`
118
- <div
119
- class="card list-card ${this.selectedIndex === index ? "selected" : ""}"
120
- >
129
+ <div class="card list-card">
121
130
  ${content}
122
131
  </div>
123
132
  `;
@@ -168,7 +177,6 @@ __decorate([property({ type: String }), __decorateMetadata("design:type", Object
168
177
  __decorate([property({ type: String }), __decorateMetadata("design:type", Object)], UDataView.prototype, "gap", void 0);
169
178
  __decorate([property({ attribute: false }), __decorateMetadata("design:type", Function)], UDataView.prototype, "renderCard", void 0);
170
179
  __decorate([property({ attribute: false }), __decorateMetadata("design:type", Function)], UDataView.prototype, "renderCell", void 0);
171
- __decorate([state(), __decorateMetadata("design:type", Object)], UDataView.prototype, "selectedIndex", void 0);
172
180
  UDataView = __decorate([customElement("u-data-view")], UDataView);
173
181
  //#endregion
174
182
  export { UDataView };
@@ -44,15 +44,10 @@ var styles = [css`
44
44
  gap: 0.25rem;
45
45
  }
46
46
 
47
- .view-toggles u-button[active] {
48
- background: var(--u-primary-color, #1976D2);
49
- /* 예외 1"주색 위의 글자" 역할 토큰이 없다.
50
- 역할 층의 유채색은 전경 5단(--u-primary-color-*)뿐이고,위에 얹는 글자색을
51
- 가리키는 토큰은 없다. 실측: 라이트 #FFFFFF/#1E88E5 = 3.68, 다크 #000000/#2A659D
52
- = 3.45 — 다크는 흰 글자였다면 6.09 였다. 즉 현재 값이 최선이 아니지만, 팔레트를
53
- 직접 바꾸면 게시된 시각이 움직이므로 토큰 추가 결정까지 현행을 유지한다. */
54
- color: var(--u-neutral-0, #FFFFFF);
55
- }
47
+ /* 선택된 레이아웃 버튼의 표기는 UButton 의 variant+color 가 맡는다(cycle-244).
48
+ 종전의 u-button[active] 규칙은 배경만 바꾸면서 그 위 글자 대비가 3.45~3.68 이라고
49
+ 스스로 적고 있었다컴포넌트의 계약을 우회하지 않고 그쪽에 위임한다.
50
+ (주석 백틱 금지: css 태그드 템플릿을 자리에서 끝낸다) */
56
51
 
57
52
  .info {
58
53
  font-size: 0.875rem;
@@ -90,12 +85,6 @@ var styles = [css`
90
85
  transform: translateY(-2px);
91
86
  }
92
87
 
93
- .card.selected {
94
- border-color: var(--u-primary-color, #1976D2);
95
- background: var(--u-bg-color-active, #EEEEEE);
96
- box-shadow: 0 0 0 3px var(--u-primary-color-weakest, #90CAF9);
97
- }
98
-
99
88
  .card-content {
100
89
  display: flex;
101
90
  flex-direction: column;
@@ -177,10 +166,6 @@ var styles = [css`
177
166
  background: var(--u-bg-color-hover, #F5F5F5);
178
167
  }
179
168
 
180
- tbody tr.selected {
181
- background: var(--u-bg-color-active, #EEEEEE);
182
- }
183
-
184
169
  tbody tr:not(:last-child) {
185
170
  border-bottom: 1px solid var(--u-border-color-weak, #EEEEEE);
186
171
  }
@@ -1,5 +1,5 @@
1
- import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateMetadata.js";
2
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorate.js";
1
+ import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.js";
2
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js";
3
3
  import { messages } from "../../utilities/messages.js";
4
4
  import { styles } from "./USimpleSheet.styles.js";
5
5
  import { html } from "lit";
@@ -56,8 +56,10 @@ var USimpleSheet = class USimpleSheet extends UElement {
56
56
  if (e.ctrlKey || e.metaKey) {
57
57
  if (e.key === "z" || e.key === "Z") {
58
58
  e.preventDefault();
59
- if (!this.readonly) if (e.shiftKey) this._redo();
60
- else this._undo();
59
+ if (!this.readonly) {
60
+ if (e.shiftKey) this._redo();
61
+ else this._undo();
62
+ }
61
63
  return;
62
64
  }
63
65
  if (e.key === "y" || e.key === "Y") {
@@ -109,9 +111,10 @@ var USimpleSheet = class USimpleSheet extends UElement {
109
111
  }
110
112
  if (e.key === "Tab") {
111
113
  e.preventDefault();
112
- if (e.shiftKey) if (anchor.col === 0 && anchor.row > 0) this._select(anchor.row - 1, this._colCount - 1);
113
- else this._select(anchor.row, Math.max(0, anchor.col - 1));
114
- else if (anchor.col === this._colCount - 1) {
114
+ if (e.shiftKey) {
115
+ if (anchor.col === 0 && anchor.row > 0) this._select(anchor.row - 1, this._colCount - 1);
116
+ else this._select(anchor.row, Math.max(0, anchor.col - 1));
117
+ } else if (anchor.col === this._colCount - 1) {
115
118
  if (anchor.row < this._rowCount - 1) this._select(anchor.row + 1, 0);
116
119
  } else this._select(anchor.row, anchor.col + 1);
117
120
  return;
@@ -220,11 +223,13 @@ var USimpleSheet = class USimpleSheet extends UElement {
220
223
  e.preventDefault();
221
224
  const pos = this._editing;
222
225
  this._commitEdit();
223
- if (e.shiftKey) if (pos.col === 0 && pos.row > 0) this._select(pos.row - 1, this._colCount - 1);
224
- else this._select(pos.row, Math.max(0, pos.col - 1));
225
- else if (pos.col === this._colCount - 1) if (pos.row < this._rowCount - 1) this._select(pos.row + 1, 0);
226
- else this._select(pos.row, pos.col);
227
- else this._select(pos.row, pos.col + 1);
226
+ if (e.shiftKey) {
227
+ if (pos.col === 0 && pos.row > 0) this._select(pos.row - 1, this._colCount - 1);
228
+ else this._select(pos.row, Math.max(0, pos.col - 1));
229
+ } else if (pos.col === this._colCount - 1) {
230
+ if (pos.row < this._rowCount - 1) this._select(pos.row + 1, 0);
231
+ else this._select(pos.row, pos.col);
232
+ } else this._select(pos.row, pos.col + 1);
228
233
  return;
229
234
  }
230
235
  if (e.key === "ArrowUp") {
@@ -131,6 +131,12 @@ export declare class URichTable extends LitElement {
131
131
  private _onSortClick;
132
132
  private _onFilterChange;
133
133
  private _onCellClick;
134
+ /**
135
+ * 「선택」(체크박스)과 별개로 「보고 있는 행」을 앱에 알린다. 셀 단일 클릭은
136
+ * `editable` 열에서도 편집에 들어가지 않으므로(dblclick만 진입) 항상 안전하게 낼 수 있다.
137
+ * 키보드 경로(Enter)는 `editable` 열에서 이미 편집 진입 신호이므로 그 경우는 내지 않는다.
138
+ */
139
+ private _fireRowActivate;
134
140
  private _onCellDblClick;
135
141
  private _onEditKeyDown;
136
142
  private _onCellEditConfirm;
@@ -1,5 +1,5 @@
1
- import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateMetadata.js";
2
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorate.js";
1
+ import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.js";
2
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js";
3
3
  import { messages } from "../../utilities/messages.js";
4
4
  import { richTableStyles } from "./styles.js";
5
5
  import { parseTSV, toTSV } from "./utils/clipboard.js";
@@ -73,7 +73,7 @@ var URichTable = class URichTable extends LitElement {
73
73
  if (col?.editable) {
74
74
  const value = this.data[this.focusedCell.rowIndex]?.[col.key];
75
75
  this._onCellDblClick(this.focusedCell.rowIndex, this.focusedCell.colIndex, value);
76
- }
76
+ } else this._fireRowActivate(this.focusedCell.rowIndex, "keyboard");
77
77
  }
78
78
  if (e.key === " " && this.selectable) {
79
79
  e.preventDefault();
@@ -178,14 +178,16 @@ var URichTable = class URichTable extends LitElement {
178
178
  render() {
179
179
  return html`
180
180
  ${this._renderToolbar()}
181
- <table>
182
- ${this._renderHeader()}
183
- <tbody>
184
- ${this.filterable ? this._renderFilterRow() : ""}
185
- ${this._renderBody()}
186
- ${this.addable ? this._renderNewRow() : ""}
187
- </tbody>
188
- </table>
181
+ <div class="table-wrap">
182
+ <table>
183
+ ${this._renderHeader()}
184
+ <tbody>
185
+ ${this.filterable ? this._renderFilterRow() : ""}
186
+ ${this._renderBody()}
187
+ ${this.addable ? this._renderNewRow() : ""}
188
+ </tbody>
189
+ </table>
190
+ </div>
189
191
  ${this._renderPagination()}
190
192
  `;
191
193
  }
@@ -452,12 +454,13 @@ var URichTable = class URichTable extends LitElement {
452
454
  this._fireSelectionChange();
453
455
  }
454
456
  _onSortClick(field) {
455
- if (this.sort?.field === field) if (this.sort.direction === "asc") this.sort = {
456
- field,
457
- direction: "desc"
458
- };
459
- else this.sort = null;
460
- else this.sort = {
457
+ if (this.sort?.field === field) {
458
+ if (this.sort.direction === "asc") this.sort = {
459
+ field,
460
+ direction: "desc"
461
+ };
462
+ else this.sort = null;
463
+ } else this.sort = {
461
464
  field,
462
465
  direction: "asc"
463
466
  };
@@ -494,6 +497,25 @@ var URichTable = class URichTable extends LitElement {
494
497
  colIndex: colIdx
495
498
  };
496
499
  this._lastSelectedIndex = rowIdx;
500
+ this._fireRowActivate(rowIdx, "click");
501
+ }
502
+ /**
503
+ * 「선택」(체크박스)과 별개로 「보고 있는 행」을 앱에 알린다. 셀 단일 클릭은
504
+ * `editable` 열에서도 편집에 들어가지 않으므로(dblclick만 진입) 항상 안전하게 낼 수 있다.
505
+ * 키보드 경로(Enter)는 `editable` 열에서 이미 편집 진입 신호이므로 그 경우는 내지 않는다.
506
+ */
507
+ _fireRowActivate(rowIdx, via) {
508
+ const row = this.data[rowIdx];
509
+ if (!row) return;
510
+ this.dispatchEvent(new CustomEvent("row-activate", {
511
+ detail: {
512
+ row,
513
+ id: this._rowId(row, rowIdx),
514
+ via
515
+ },
516
+ bubbles: true,
517
+ composed: true
518
+ }));
497
519
  }
498
520
  _onCellDblClick(rowIdx, colIdx, value) {
499
521
  this.editingCell = {
@@ -16,7 +16,16 @@ var richTableStyles = css`
16
16
  --dc-muted-color: var(--u-txt-color-weak, #757575); /* 보조 텍스트 · 라벨 */
17
17
  --dc-icon-color: var(--u-txt-color-weak, #757575); /* 정렬 표시 · 확장 · 행 메뉴 */
18
18
  --dc-empty-color: var(--u-txt-color-weak, #757575); /* 빈 상태 안내문 */
19
- display: block;
19
+ /* ── 세로 레이아웃 ──
20
+ 🔴**높이 제약은 데이터 그리드의 기본 사용 형태다** — 「조회 조건 + 결과 목록」이 한 화면에
21
+ 들어가야 하고, 결과가 몇 건이든 조건 영역과 페이지 이동은 항상 같은 자리에 있어야 한다.
22
+ 종전에는 «display: block» 이라 호스트에 높이를 주면 내용이 자연 높이로 넘쳤고,
23
+ «overflow: hidden» 때문에 **넘친 부분에 도달할 방법이 없었다.** 페이지네이션이 마지막
24
+ 자식이므로 **가장 먼저 사라진다** ⇒ *표는 그려지는데 2페이지로 갈 수 없었다.*
25
+ ⚠**높이를 주지 않는 사용은 그대로다** — «flex: 1 1 auto» 는 제약이 없으면 내용 높이를
26
+ 따르고 «overflow: auto» 는 넘치지 않으면 스크롤바를 만들지 않는다. */
27
+ display: flex;
28
+ flex-direction: column;
20
29
  font-family: system-ui, -apple-system, sans-serif;
21
30
  font-size: 13px;
22
31
  color: var(--u-txt-color, #212121);
@@ -25,7 +34,15 @@ var richTableStyles = css`
25
34
  overflow: hidden;
26
35
  }
27
36
 
37
+ /* 행 영역만 스크롤한다 — 툴바와 페이지네이션은 자리를 지킨다. */
38
+ .table-wrap {
39
+ flex: 1 1 auto;
40
+ overflow: auto;
41
+ min-height: 0; /* ⚠flex 아이템의 기본 min-height:auto 는 «줄어들지 않음»이라 이것이 없으면 넘친다 */
42
+ }
43
+
28
44
  .toolbar {
45
+ flex: 0 0 auto;
29
46
  display: flex;
30
47
  align-items: center;
31
48
  gap: 8px;
@@ -79,9 +96,19 @@ var richTableStyles = css`
79
96
  background: var(--u-bg-color-raised, #FAFAFA);
80
97
  font-weight: 600;
81
98
  text-align: left;
82
- border-bottom: 2px solid var(--u-border-color, #E0E0E0);
83
99
  user-select: none;
84
- position: relative;
100
+
101
+ /* ⚠**아래 경계선을 «border-bottom» 으로 그리지 않는다.** «border-collapse: collapse» 에서
102
+ 셀 테두리는 테이블이 소유하므로 **헤더만 sticky 로 띄우면 함께 따라오지 않는다** —
103
+ 스크롤하는 순간 구분선이 사라지고 행이 헤더 밑으로 비쳐 지나간다. «box-shadow» 는
104
+ 요소가 그리므로 sticky 를 따라온다. */
105
+ box-shadow: inset 0 -2px 0 var(--u-border-color, #E0E0E0);
106
+
107
+ /* 행 영역이 스크롤되는 동안 열 이름은 남는다 — 「어느 열이 무엇인지」가 이 컴포넌트의
108
+ 스크롤 모델에서 잃으면 안 되는 것이다. */
109
+ position: sticky;
110
+ top: 0;
111
+ z-index: 1;
85
112
  }
86
113
 
87
114
  thead th.sortable {
@@ -222,6 +249,7 @@ var richTableStyles = css`
222
249
  }
223
250
 
224
251
  .pagination {
252
+ flex: 0 0 auto;
225
253
  display: flex;
226
254
  justify-content: space-between;
227
255
  align-items: center;
@@ -66,6 +66,16 @@ export interface RichTableEventMap {
66
66
  row: Record<string, unknown>;
67
67
  expanded: boolean;
68
68
  }>;
69
+ /**
70
+ * 사용자가 행을 "열었다" — 셀 클릭 또는 포커스된 행에서의 `Enter`. `selectable` 과 무관하다:
71
+ * 선택은 "무엇을 처리할까", 활성화는 "무엇을 볼까"이다. `editable` 열에서 `Enter` 는 이미
72
+ * 셀 편집 진입 신호이므로 그 경우는 내지 않는다(`via: 'keyboard'` 는 비-editable 열에서만 발생).
73
+ */
74
+ 'row-activate': CustomEvent<{
75
+ row: Record<string, unknown>;
76
+ id: string;
77
+ via: 'click' | 'keyboard';
78
+ }>;
69
79
  'sort-change': CustomEvent<{
70
80
  field: string;
71
81
  direction: 'asc' | 'desc' | null;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,15 @@
1
1
  export * from './components/data-view/UDataView';
2
2
  export * from './components/simple-sheet/USimpleSheet';
3
3
  export * from './components/u-rich-table/URichTable';
4
+ /**
5
+ * `URichTable` 의 공개 API 가 요구하는 타입.
6
+ *
7
+ * ⚠**이 줄이 없으면 «타입이 있는데 쓸 수 없는 컴포넌트»가 된다** — `columns` 는
8
+ * `ColumnDef[]` 를 요구하는데 소비자는 그 이름을 루트에서 import 할 수 없어, 내부 경로를
9
+ * 딥임포트하거나(내부 배치가 곧 공개 계약이 된다) 같은 모양을 손으로 복제해야 했다
10
+ * (업스트림이 필드를 바꿔도 모른 채 조용히 갈라진다).
11
+ *
12
+ * ★다른 두 컴포넌트는 타입이 **같은 파일**에 있어 위 `export *` 로 이미 나간다 —
13
+ * `URichTable` 만 타입을 `types.ts` 로 분리해 배럴이 지나치고 있었다.
14
+ */
15
+ export type { ColumnDef, CellPosition, SortState, FilterState, RichTableEventMap, } from './components/u-rich-table/types';
package/dist/react.d.ts CHANGED
@@ -36,12 +36,17 @@ export declare const URichTableReact: import('@lit/react').ReactWebComponent<URi
36
36
  row: Record<string, unknown>;
37
37
  expanded: boolean;
38
38
  }>>;
39
+ readonly onRowActivate: EventName<CustomEvent<{
40
+ row: Record<string, unknown>;
41
+ id: string;
42
+ via: "click" | "keyboard";
43
+ }>>;
39
44
  readonly onSortChange: EventName<CustomEvent<{
40
45
  field: string;
41
46
  direction: "asc" | "desc" | null;
42
47
  }>>;
43
48
  readonly onFilterChange: EventName<CustomEvent<{
44
- filters: import('./react').FilterState;
49
+ filters: import('.').FilterState;
45
50
  }>>;
46
51
  readonly onPageChange: EventName<CustomEvent<{
47
52
  page: number;
package/dist/react.js CHANGED
@@ -39,6 +39,7 @@ var URichTableReact = createComponent({
39
39
  onRowUpdate: "row-update",
40
40
  onRowDelete: "row-delete",
41
41
  onRowExpand: "row-expand",
42
+ onRowActivate: "row-activate",
42
43
  onSortChange: "sort-change",
43
44
  onFilterChange: "filter-change",
44
45
  onPageChange: "page-change",
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.12.0",
4
+ "version": "0.14.0",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-components",
@@ -15,6 +15,7 @@
15
15
  },
16
16
  "files": [
17
17
  "dist",
18
+ "skills",
18
19
  "LICENSE",
19
20
  "README.md",
20
21
  "CHANGELOG.md",
@@ -22,6 +23,9 @@
22
23
  ],
23
24
  "type": "module",
24
25
  "types": "./dist/index.d.ts",
26
+ "engines": {
27
+ "node": ">=20"
28
+ },
25
29
  "exports": {
26
30
  ".": {
27
31
  "types": "./dist/index.d.ts",
@@ -38,16 +42,20 @@
38
42
  "./u-rich-table": "./dist/components/u-rich-table/URichTable.js"
39
43
  },
40
44
  "sideEffects": [
41
- "./src/utilities/shadowDomProtection.ts",
45
+ "./dist/components/data-view/UDataView.js",
42
46
  "./src/components/data-view/UDataView.ts",
47
+ "./dist/components/simple-sheet/USimpleSheet.js",
43
48
  "./src/components/simple-sheet/USimpleSheet.ts",
49
+ "./dist/components/u-rich-table/URichTable.js",
44
50
  "./src/components/u-rich-table/URichTable.ts",
51
+ "./dist/index.js",
52
+ "./src/index.ts",
53
+ "./dist/init.js",
45
54
  "./src/init.ts",
55
+ "./dist/react.js",
56
+ "./src/react.ts",
46
57
  "./dist/utilities/shadowDomProtection.js",
47
- "./dist/components/data-view/UDataView.js",
48
- "./dist/components/simple-sheet/USimpleSheet.js",
49
- "./dist/components/u-rich-table/URichTable.js",
50
- "./dist/init.js"
58
+ "./src/utilities/shadowDomProtection.ts"
51
59
  ],
52
60
  "scripts": {
53
61
  "start": "vite",
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: iyulab-data-components
3
+ description: Data-oriented web components built on Lit — a server-paged rich table with inline editing and cross-page selection, a spreadsheet-like grid, and a multi-layout data viewer. Use when working with @iyulab/data-components package.
4
+ license: MIT
5
+ metadata:
6
+ author: iyulab
7
+ version: "0.13.0"
8
+ ---
9
+
10
+ # @iyulab/data-components
11
+
12
+ Three custom elements for displaying and editing tabular data. Built on
13
+ [Lit](https://lit.dev/); usable from any framework or plain HTML.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install @iyulab/data-components
19
+ ```
20
+
21
+ ## Which component do I use?
22
+
23
+ | Tag | Use it when | Reference |
24
+ |---|---|---|
25
+ | `u-rich-table` | The rows come from a **server** — paging, sorting and filtering are the app's job, and you need selection, inline editing or expandable detail rows | [rich-table.md](references/components/rich-table.md) |
26
+ | `u-simple-sheet` | The data is a **free-form grid** of cells the user types into, spreadsheet-style (fill handle, ranges, clipboard) | [simple-sheet.md](references/components/simple-sheet.md) |
27
+ | `u-data-view` | You are **displaying** records and want to switch between grid / list / table layouts without rebuilding markup | [data-view.md](references/components/data-view.md) |
28
+
29
+ `u-rich-table` and `u-simple-sheet` overlap only superficially. The table is
30
+ **record-oriented** (a row is an object with named columns, and the app owns the
31
+ query); the sheet is **cell-oriented** (a row is an array of strings, and the
32
+ component owns the grid).
33
+
34
+ ## Importing
35
+
36
+ Import the barrel to register every element:
37
+
38
+ ```ts
39
+ import '@iyulab/data-components';
40
+ ```
41
+
42
+ …or import one element to keep the bundle small:
43
+
44
+ ```ts
45
+ import '@iyulab/data-components/dist/components/u-rich-table/URichTable.js';
46
+ ```
47
+
48
+ Registration is a **module side effect** (`@customElement`), so the import must
49
+ not be elided — do not mark this package as side-effect-free in a bundler
50
+ config, or the elements silently never register.
51
+
52
+ ## Types
53
+
54
+ Every type the public API asks for is exported from the package root:
55
+
56
+ ```ts
57
+ import type {
58
+ ColumnDef, CellPosition, SortState, FilterState, RichTableEventMap,
59
+ } from '@iyulab/data-components';
60
+ ```
61
+
62
+ Do **not** deep-import from `dist/components/...` for types. The subpath exists,
63
+ but it turns the internal layout into a contract you depend on.
64
+
65
+ ## Theming
66
+
67
+ Colors come from the `@iyulab/components` design tokens (`--u-*`) and follow the
68
+ active theme automatically. Per-component knobs are listed in each reference
69
+ document under *CSS Custom Properties*.
@@ -0,0 +1,63 @@
1
+ # u-data-view
2
+
3
+ ```ts
4
+ import '@iyulab/data-components/dist/components/data-view/UDataView.js';
5
+ ```
6
+
7
+ **Tag:** `u-data-view`
8
+
9
+ Read-only viewer that renders the same records as a card **grid**, a **list**, or
10
+ a **table**, without the app rebuilding markup per layout.
11
+
12
+ ```html
13
+ <u-data-view mode="list"></u-data-view>
14
+ ```
15
+
16
+ ```ts
17
+ const view = document.querySelector('u-data-view')!;
18
+ view.items = records;
19
+ view.columns = [ // used by mode="table"; inferred from the first item when omitted
20
+ { key: 'name', label: 'Name' },
21
+ { key: 'owner', label: 'Owner', width: '160px' },
22
+ ];
23
+ ```
24
+
25
+ A toolbar above the content shows the item count and one button per layout;
26
+ clicking a button sets `mode`. `mode` is also a plain property, so the app can
27
+ set it directly and persist the user's choice.
28
+
29
+ Switching layout is a **local** interaction — the component does not emit an
30
+ event for it. Read `view.mode` when you need the current layout.
31
+
32
+ Cell and card rendering can be replaced without giving up the layout logic:
33
+
34
+ ```ts
35
+ view.renderCard = (item, index) => html`<strong>${item.name}</strong>`;
36
+ view.renderCell = (item, column) => column.key === 'size' ? formatBytes(item.size) : item[column.key];
37
+ ```
38
+
39
+ ## Display only
40
+
41
+ `u-data-view` renders records; it does not select, sort, page or emit events.
42
+ When the user needs to act on rows — selection, inline edit, server paging — use
43
+ [`u-rich-table`](./rich-table.md).
44
+
45
+ ## Properties
46
+
47
+ | Property | Type | Default | Reflect | Description |
48
+ |----------|------|---------|---------|-------------|
49
+ | `items` | `DataItem[]` | `[]` | | Records to display. `DataItem` is `Record<string, any>` — the component deliberately does not constrain the app's domain type |
50
+ | `mode` | `'grid'\|'list'\|'table'` | `'grid'` | | Current layout |
51
+ | `columns` | `DataColumn[]` | — | | Columns for `mode="table"`; inferred from the first item when omitted |
52
+ | `gridMinWidth` | `string` | `'200px'` | | Minimum card width in `mode="grid"` (CSS length) |
53
+ | `gap` | `string` | `'1rem'` | | Gap between cards or rows (CSS length) |
54
+ | `renderCard` | `(item, index) => TemplateResult` | — | | Replaces card content in `grid` / `list` |
55
+ | `renderCell` | `(item, column, index) => TemplateResult \| string` | — | | Replaces cell content in `table` |
56
+
57
+ ## DataColumn
58
+
59
+ | Field | Type | Description |
60
+ |---|---|---|
61
+ | `key` | `string` | Property read from the item |
62
+ | `label` | `string` | Header text (defaults to `key`) |
63
+ | `width` | `string` | CSS width |
@@ -0,0 +1,228 @@
1
+ # u-rich-table
2
+
3
+ ```ts
4
+ import '@iyulab/data-components/dist/components/u-rich-table/URichTable.js';
5
+ ```
6
+
7
+ **Tag:** `u-rich-table`
8
+
9
+ Record-oriented table for **server-paged** data: selection that survives paging,
10
+ inline editing with validation, column filters, expandable detail rows and
11
+ TSV clipboard paste.
12
+
13
+ ```html
14
+ <u-rich-table selectable editable filterable></u-rich-table>
15
+ ```
16
+
17
+ ```ts
18
+ const table = document.querySelector('u-rich-table')!;
19
+ table.columns = [
20
+ { key: 'name', label: 'Name', sortable: true, editable: true, required: true },
21
+ { key: 'state', label: 'State', type: 'badge', badgeColors: { open: 'green' } },
22
+ ];
23
+ table.data = rows; // the CURRENT page only
24
+ table.totalCount = 1240; // the total the query matches
25
+ table.currentPage = 1;
26
+ table.pageSize = 25;
27
+ ```
28
+
29
+ ---
30
+
31
+ ## Read this first: give every row an `_id`
32
+
33
+ The component does **not** assign row identity. Selection, expansion and row
34
+ errors are all tracked by `row._id`.
35
+
36
+ ```ts
37
+ table.data = rows.map(r => ({ ...r, _id: r.documentNo }));
38
+ ```
39
+
40
+ If `_id` is missing, every row's identity is `undefined`, a `Set` holds exactly
41
+ one of those — so **selecting one row appears to select all of them**. The
42
+ component falls back to the row's *position* and warns once on the console, but
43
+ position-based identity moves the selection to a different row as soon as the
44
+ data is re-sorted or re-paged.
45
+
46
+ ## Read this second: the app owns the query
47
+
48
+ `u-rich-table` never fetches, sorts, filters or slices. It renders the page you
49
+ give it and tells you what the user asked for:
50
+
51
+ ```ts
52
+ table.addEventListener('page-change', e => load({ page: e.detail.page, size: e.detail.pageSize }));
53
+ table.addEventListener('sort-change', e => load({ sort: e.detail.field, dir: e.detail.direction }));
54
+ table.addEventListener('filter-change', e => load({ filters: e.detail.filters }));
55
+ ```
56
+
57
+ `totalCount` is what the pager counts — not `data.length`. Setting `data` alone
58
+ produces a table that believes it holds every matching record.
59
+
60
+ ## Sizing: give the host a height
61
+
62
+ The component manages its own vertical layout. Constrain the host and the row
63
+ area is what scrolls — the toolbar and the pager keep their positions, and the
64
+ header row stays visible while rows move under it:
65
+
66
+ ```css
67
+ u-rich-table { height: calc(100vh - 280px); }
68
+ ```
69
+
70
+ That is the shape a query screen wants: filters above, the pager always in the
71
+ same place, and column names readable no matter how far down you are.
72
+
73
+ Leave the height off and the table grows to its content instead — no inner
74
+ scrollbar, the page scrolls. Both are supported; pick per screen. What you
75
+ should **not** do is wrap it in your own `overflow: auto` container, which puts
76
+ the header and toolbar back inside the scrolling region.
77
+
78
+ ## Selection across pages
79
+
80
+ Two facts are deliberately separate, because in server paging they differ:
81
+
82
+ | What you want | Where to read it |
83
+ |---|---|
84
+ | The selected **row objects on this page** | `getSelectedRows()` / `event.detail.selectedRows` |
85
+ | Every selected **identifier**, across all pages visited | `selectedRowIds` / `event.detail.selectedIds` |
86
+
87
+ The component cannot return row objects it was never given, so a bulk action
88
+ spanning pages must work from the identifiers:
89
+
90
+ ```ts
91
+ table.addEventListener('selection-change', e => {
92
+ bulkBar.count = e.detail.selectedIds.length; // accumulated
93
+ preview.rows = e.detail.selectedRows; // this page
94
+ });
95
+
96
+ await deleteAll([...table.selectedRowIds]);
97
+ table.clearSelection();
98
+ ```
99
+
100
+ The header checkbox is scoped to the **current page** — that is what makes it
101
+ truthful when the other pages are not loaded. `select-all` fires alongside
102
+ `selection-change` so the app can distinguish *"the user ticked three rows"*
103
+ from *"the user asked for everything"* and offer a "select all N matching"
104
+ affordance of its own:
105
+
106
+ ```ts
107
+ table.addEventListener('select-all', e => {
108
+ offerSelectEntireQuery.hidden = !e.detail.checked; // e.detail.pageRowIds = this page's ids
109
+ });
110
+ ```
111
+
112
+ `setSelection(ids)` replaces the accumulated set (identifiers not on the current
113
+ page are allowed). It is a no-op when the set is unchanged — without that, the
114
+ natural wiring of *listen to `selection-change` → store → write back* would loop
115
+ forever.
116
+
117
+ ---
118
+
119
+ ## Properties
120
+
121
+ | Property | Type | Default | Reflect | Description |
122
+ |----------|------|---------|---------|-------------|
123
+ | `columns` | `ColumnDef[]` | `[]` | | Column definitions — see *ColumnDef* below |
124
+ | `data` | `Record<string, unknown>[]` | `[]` | | Rows of the **current page**. Give each a unique `_id` |
125
+ | `totalCount` | `number` | `0` | | Total rows the query matches, across all pages |
126
+ | `pageSize` | `number` | `25` | | Rows per page |
127
+ | `currentPage` | `number` | `1` | | 1-based page number |
128
+ | `loading` | `boolean` | `false` | | Shows the loading message instead of rows |
129
+ | `emptyMessage` | `string` | `''` | | Text shown when there are no rows (falls back to the locale string) |
130
+ | `loadingMessage` | `string` | `''` | | Text shown while `loading` |
131
+ | `filterPlaceholder` | `string` | `''` | | Placeholder of the column filter inputs |
132
+ | `filterAllLabel` | `string` | `''` | | Label of the "all" option in `select` filters |
133
+ | `addRowLabel` | `string` | `''` | | Label of the add-row button |
134
+ | `pageInfoFormatter` | `(total, start, end) => string` | locale string | | Builds the pager caption. A function rather than a template, because word order differs per language |
135
+ | `selectable` | `boolean` | `false` | | Renders the selection column |
136
+ | `editable` | `boolean` | `false` | | Enables inline cell editing on columns marked `editable` |
137
+ | `addable` | `boolean` | `false` | | Renders the add-row control |
138
+ | `filterable` | `boolean` | `false` | | Renders the filter row for columns marked `filterable` |
139
+ | `expandable` | `boolean` | `false` | | Renders the expander column; pair with `detailRenderer` |
140
+ | `detailRenderer` | `(row) => TemplateResult` | — | | Renders the expanded detail row |
141
+
142
+ ## Methods
143
+
144
+ | Method | Description |
145
+ |---|---|
146
+ | `getSelectedRows(): Record<string, unknown>[]` | Selected rows **on the current page** |
147
+ | `setSelection(ids: Iterable<string>): void` | Replace the accumulated selection; no-op if unchanged |
148
+ | `clearSelection(): void` | Clear it entirely, across pages |
149
+ | `setRowError(rowId, message): void` | Mark a row as failed (e.g. the server rejected a save) |
150
+ | `clearRowError(rowId): void` | Remove that mark |
151
+
152
+ ## Getters
153
+
154
+ | Getter | Description |
155
+ |---|---|
156
+ | `selectedRowIds: ReadonlySet<string>` | Snapshot of every selected identifier, across pages |
157
+
158
+ ## Events
159
+
160
+ All events bubble and cross shadow boundaries. `RichTableEventMap` types every
161
+ `detail`:
162
+
163
+ ```ts
164
+ import type { RichTableEventMap } from '@iyulab/data-components';
165
+ type SelectionChange = RichTableEventMap['selection-change'];
166
+ ```
167
+
168
+ | Event | `detail` | Fired when |
169
+ |---|---|---|
170
+ | `selection-change` | `{ selectedRows, selectedIds }` | Selection changed by any route |
171
+ | `select-all` | `{ checked, pageRowIds }` | The header checkbox was toggled |
172
+ | `sort-change` | `{ field, direction }` | A sortable header was clicked (`direction` is `null` when cleared) |
173
+ | `filter-change` | `{ filters }` | A column filter changed |
174
+ | `page-change` | `{ page, pageSize }` | The pager or page-size selector moved |
175
+ | `row-update` | `{ row, field, value, oldValue }` | An inline edit was committed |
176
+ | `row-create` | `{ row }` | The add-row control produced a row |
177
+ | `row-delete` | `{ row }` | A row was deleted |
178
+ | `row-expand` | `{ row, expanded }` | A detail row was opened or closed |
179
+ | `row-activate` | `{ row, id, via }` | A row was clicked, or `Enter` was pressed on a focused non-editable cell (`via` is `'click'` or `'keyboard'`). Independent of `selectable` — selection is "what to act on", activation is "what to view" |
180
+ | `paste` | `{ rows }` | TSV was pasted into the grid |
181
+
182
+ ## ColumnDef
183
+
184
+ | Field | Type | Description |
185
+ |---|---|---|
186
+ | `key` | `string` | Property read from the row object |
187
+ | `label` | `string` | Header text |
188
+ | `width` | `string` | CSS width |
189
+ | `type` | `'text'\|'number'\|'date'\|'select'\|'badge'` | Cell renderer and editor |
190
+ | `options` | `{ value, label }[]` | Choices for `type: 'select'` |
191
+ | `badgeColors` | `Record<string, string>` | Value → color for `type: 'badge'` |
192
+ | `align` | `'left'\|'center'\|'right'` | Cell alignment |
193
+ | `sortable` | `boolean` | Header emits `sort-change` |
194
+ | `editable` | `boolean` | Cell is editable when the table is `editable` |
195
+ | `required` | `boolean` | Empty value fails validation |
196
+ | `filterable` | `boolean` | Column appears in the filter row |
197
+ | `filterType` | `'text'\|'select'` | Filter control |
198
+ | `validator` | `(value, row) => string \| null` | Returns an error message, or `null` when valid |
199
+ | `render` | `(value, row) => string \| HTMLElement` | Custom cell rendering |
200
+ | `clipboardParse` | `(text) => unknown` | Parses a pasted cell |
201
+ | `clipboardFormat` | `(value) => string` | Formats a copied cell |
202
+
203
+ ## Slots
204
+
205
+ | Name | Description |
206
+ |------|-------------|
207
+ | `bulk-actions` | Toolbar area shown while rows are selected |
208
+ | `toolbar-end` | Trailing toolbar area, always shown |
209
+
210
+ ## Keyboard
211
+
212
+ | Keys | Action |
213
+ |---|---|
214
+ | Arrow keys | Move the focused cell |
215
+ | `Enter` | Editable cell: start editing / commit and move down. Non-editable cell: emit `row-activate` |
216
+ | `Escape` | Cancel editing |
217
+ | `Tab` | Commit and move to the next cell |
218
+ | `Space` | Toggle selection of the focused row (when `selectable`) |
219
+ | `Delete` | Emit `row-delete` for every selected row |
220
+ | `Ctrl`/`Cmd` + `A` | Select every row **on this page** — same scope as the header checkbox |
221
+ | `Ctrl`/`Cmd` + `C` | Copy the selection as TSV |
222
+ | `Ctrl`/`Cmd` + `V` | Paste TSV (emits `paste`) |
223
+
224
+ ## Localization
225
+
226
+ `emptyMessage`, `loadingMessage`, `filterPlaceholder`, `filterAllLabel` and
227
+ `addRowLabel` default to `''` and fall back to the package's locale strings —
228
+ set them only to override the translation for a specific table.
@@ -0,0 +1,107 @@
1
+ # u-simple-sheet
2
+
3
+ ```ts
4
+ import '@iyulab/data-components/dist/components/simple-sheet/USimpleSheet.js';
5
+ ```
6
+
7
+ **Tag:** `u-simple-sheet`
8
+
9
+ Spreadsheet-like grid of text cells: range selection, fill handle, clipboard
10
+ copy/paste, per-column dropdowns, computed columns, display formatting,
11
+ resizable columns and undo/redo history.
12
+
13
+ ```html
14
+ <u-simple-sheet rows="30" cols="6"></u-simple-sheet>
15
+ ```
16
+
17
+ ```ts
18
+ const sheet = document.querySelector('u-simple-sheet')!;
19
+ sheet.columns = [
20
+ { key: 'item', label: 'Item', width: 180 },
21
+ { key: 'qty', label: 'Qty', format: { maximumFractionDigits: 0 } },
22
+ { key: 'price', label: 'Price', format: { style: 'currency', currency: 'USD' } },
23
+ { key: 'total', label: 'Total', compute: (row, data) => String(+data[row][1] * +data[row][2]) },
24
+ { key: 'state', label: 'State', options: ['draft', 'sent'], strict: true },
25
+ ];
26
+ sheet.addEventListener('change', e => save(sheet.getDataAsObjects()));
27
+ ```
28
+
29
+ The data is a **2-D array of strings** — the sheet stores what the user typed and
30
+ never coerces it. `format` changes only what is displayed; `getData()` returns
31
+ the raw values.
32
+
33
+ ## Cell-oriented, not record-oriented
34
+
35
+ Use `u-simple-sheet` when the grid itself is the input surface (an estimate, a
36
+ bulk-entry form, a paste target). When rows are records fetched page by page
37
+ from a server, use [`u-rich-table`](./rich-table.md) instead.
38
+
39
+ ## Properties
40
+
41
+ | Property | Type | Default | Reflect | Description |
42
+ |----------|------|---------|---------|-------------|
43
+ | `data` | `string[][]` | `[]` | | Initial cell values, row-major |
44
+ | `columns` | `SheetColumn[]` | — | | Column definitions; omit for generic `A`, `B`, `C`… columns |
45
+ | `rows` | `number` | `20` | | Row count when `data` is shorter |
46
+ | `cols` | `number` | `10` | | Column count when neither `data` nor `columns` says otherwise |
47
+ | `readonly` | `boolean` | `false` | | Blocks editing, paste and fill |
48
+ | `noMatchMessage` | `string` | `''` | | Text shown when a `strict` dropdown has no match (falls back to the locale string) |
49
+ | `theme` | `'light'\|'dark'` | — | ✓ | Forces a theme. Unset follows the ancestor `theme`/`data-theme` context (`:host-context`, Chromium only) |
50
+
51
+ ## Methods
52
+
53
+ | Method | Description |
54
+ |---|---|
55
+ | `getData(): string[][]` | Current values as a copied 2-D array |
56
+ | `getDataAsObjects(): Record<string, string>[]` | Rows keyed by `SheetColumn.key` (or `A`, `B`, `C`… when `columns` is unset) |
57
+ | `setData(data: string[][]): void` | Replace all values and re-render |
58
+ | `setCell(row, col, value): void` | Set one cell — recomputes, pushes history and emits `change` |
59
+ | `getSelection(): { minRow, maxRow, minCol, maxCol } \| null` | Current range, normalized |
60
+ | `setSelection(range): void` | Select a range programmatically (clamped to the grid) |
61
+ | `selectAll(): void` | Select every cell |
62
+
63
+ ## Getters
64
+
65
+ | Getter | Description |
66
+ |---|---|
67
+ | `canUndo: boolean` | History has an earlier state |
68
+ | `canRedo: boolean` | History has a later state |
69
+
70
+ ## Events
71
+
72
+ | Event | `detail` | Fired when |
73
+ |---|---|---|
74
+ | `change` | `{ data: string[][] }` | Any edit, paste, fill or `setCell()` committed a value |
75
+
76
+ ## SheetColumn
77
+
78
+ | Field | Type | Description |
79
+ |---|---|---|
80
+ | `key` | `string` | Object key used by `getDataAsObjects()` |
81
+ | `label` | `string` | Header text |
82
+ | `width` | `number` | Column width in px |
83
+ | `readonly` | `boolean` | Column cannot be edited |
84
+ | `options` | `string[] \| ((row, col) => string[])` | Dropdown choices, static or per cell |
85
+ | `strict` | `boolean` | Only listed options may be entered (default: free text) |
86
+ | `compute` | `(rowIndex, data) => string` | Derived value. Makes the column read-only; evaluated left→right, top→bottom |
87
+ | `format` | `Intl.NumberFormatOptions \| ((value, rowIndex) => string)` | Display formatting only — the stored value is untouched |
88
+
89
+ ## Keyboard
90
+
91
+ | Keys | Action |
92
+ |---|---|
93
+ | Arrow keys | Move the active cell |
94
+ | `Shift` + arrows | Extend the selection |
95
+ | `Home` / `End` | Jump to the first / last column |
96
+ | `PageUp` / `PageDown` | Jump a screenful of rows |
97
+ | `Enter` / `F2` | Start editing; `Enter` again commits and moves down |
98
+ | `Escape` | Cancel editing |
99
+ | `Tab` | Commit and move right (`Shift` + `Tab` moves left) |
100
+ | `Delete` / `Backspace` | Clear the selected cells |
101
+ | `Ctrl`/`Cmd` + `C` / `V` | Copy / paste the selection as TSV |
102
+ | `Ctrl`/`Cmd` + `Z` | Undo (`Shift` + `Ctrl`/`Cmd` + `Z` redoes) |
103
+ | `Ctrl`/`Cmd` + `Y` | Redo |
104
+ | `Ctrl`/`Cmd` + `D` / `R` | Fill down / fill right from the selection |
105
+ | `Ctrl`/`Cmd` + `A` | Select every cell |
106
+
107
+ Cut (`Ctrl`/`Cmd` + `X`) is not handled — copy, then `Delete`.