@iyulab/data-components 0.12.0 → 0.13.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,44 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.13.0] - 2026-08-05
4
+
5
+ ### Added
6
+
7
+ - **Agent skill pack ships with the package** (`skills/iyulab-data-components`).
8
+ Until now the only way to learn the API was to read the emitted `.d.ts` files, which
9
+ made the internal file layout a de-facto contract and left the *combinations* — server
10
+ paging together with selection — undocumented. The reference for `u-rich-table` opens
11
+ with the two things that actually bite: every row needs a unique `_id` (without it a
12
+ single selection looks like "everything is selected"), and the app owns the query
13
+ (`totalCount` is not `data.length`).
14
+
15
+ ```bash
16
+ npx skills add ./node_modules/@iyulab/data-components/skills/iyulab-data-components
17
+ ```
18
+
19
+ - **The public types `u-rich-table` asks for are exported from the package root** —
20
+ `ColumnDef`, `CellPosition`, `SortState`, `FilterState`, `RichTableEventMap`.
21
+ `columns` requires `ColumnDef[]`, but that name could not be imported from the root, so
22
+ consumers had to deep-import an internal path or hand-copy the shape (which then drifts
23
+ silently when the upstream type changes).
24
+
25
+ ```ts
26
+ import type { ColumnDef, RichTableEventMap } from '@iyulab/data-components';
27
+ ```
28
+
29
+ ### Fixed
30
+
31
+ - **`u-data-view`: the layout toggle buttons did nothing.** Grid / list / table were
32
+ rendered but never wired to a click handler, so `mode` could only be changed by setting
33
+ the property — and the "selected" highlight was therefore stuck on `grid` forever.
34
+ The buttons now switch the layout and expose their state via `aria-pressed`.
35
+
36
+ ### Removed
37
+
38
+ - **`u-data-view`: unreachable selected-row styling.** The `.selected` rules were driven
39
+ by an internal index that was never assigned, so they could not apply. Removed along
40
+ with the dead state.
41
+
3
42
  ## [0.12.0] - 2026-08-04
4
43
 
5
44
  ### 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
@@ -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;
@@ -2,7 +2,7 @@ import { styles } from "./UDataView.styles.js";
2
2
  import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateMetadata.js";
3
3
  import __decorate from "../../_virtual/_@oxc-project_runtime@0.142.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
  }
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
@@ -41,7 +41,7 @@ export declare const URichTableReact: import('@lit/react').ReactWebComponent<URi
41
41
  direction: "asc" | "desc" | null;
42
42
  }>>;
43
43
  readonly onFilterChange: EventName<CustomEvent<{
44
- filters: import('./react').FilterState;
44
+ filters: import('.').FilterState;
45
45
  }>>;
46
46
  readonly onPageChange: EventName<CustomEvent<{
47
47
  page: number;
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.13.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",
@@ -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,209 @@
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
+ ## Selection across pages
61
+
62
+ Two facts are deliberately separate, because in server paging they differ:
63
+
64
+ | What you want | Where to read it |
65
+ |---|---|
66
+ | The selected **row objects on this page** | `getSelectedRows()` / `event.detail.selectedRows` |
67
+ | Every selected **identifier**, across all pages visited | `selectedRowIds` / `event.detail.selectedIds` |
68
+
69
+ The component cannot return row objects it was never given, so a bulk action
70
+ spanning pages must work from the identifiers:
71
+
72
+ ```ts
73
+ table.addEventListener('selection-change', e => {
74
+ bulkBar.count = e.detail.selectedIds.length; // accumulated
75
+ preview.rows = e.detail.selectedRows; // this page
76
+ });
77
+
78
+ await deleteAll([...table.selectedRowIds]);
79
+ table.clearSelection();
80
+ ```
81
+
82
+ The header checkbox is scoped to the **current page** — that is what makes it
83
+ truthful when the other pages are not loaded. `select-all` fires alongside
84
+ `selection-change` so the app can distinguish *"the user ticked three rows"*
85
+ from *"the user asked for everything"* and offer a "select all N matching"
86
+ affordance of its own:
87
+
88
+ ```ts
89
+ table.addEventListener('select-all', e => {
90
+ offerSelectEntireQuery.hidden = !e.detail.checked; // e.detail.pageRowIds = this page's ids
91
+ });
92
+ ```
93
+
94
+ `setSelection(ids)` replaces the accumulated set (identifiers not on the current
95
+ page are allowed). It is a no-op when the set is unchanged — without that, the
96
+ natural wiring of *listen to `selection-change` → store → write back* would loop
97
+ forever.
98
+
99
+ ---
100
+
101
+ ## Properties
102
+
103
+ | Property | Type | Default | Reflect | Description |
104
+ |----------|------|---------|---------|-------------|
105
+ | `columns` | `ColumnDef[]` | `[]` | | Column definitions — see *ColumnDef* below |
106
+ | `data` | `Record<string, unknown>[]` | `[]` | | Rows of the **current page**. Give each a unique `_id` |
107
+ | `totalCount` | `number` | `0` | | Total rows the query matches, across all pages |
108
+ | `pageSize` | `number` | `25` | | Rows per page |
109
+ | `currentPage` | `number` | `1` | | 1-based page number |
110
+ | `loading` | `boolean` | `false` | | Shows the loading message instead of rows |
111
+ | `emptyMessage` | `string` | `''` | | Text shown when there are no rows (falls back to the locale string) |
112
+ | `loadingMessage` | `string` | `''` | | Text shown while `loading` |
113
+ | `filterPlaceholder` | `string` | `''` | | Placeholder of the column filter inputs |
114
+ | `filterAllLabel` | `string` | `''` | | Label of the "all" option in `select` filters |
115
+ | `addRowLabel` | `string` | `''` | | Label of the add-row button |
116
+ | `pageInfoFormatter` | `(total, start, end) => string` | locale string | | Builds the pager caption. A function rather than a template, because word order differs per language |
117
+ | `selectable` | `boolean` | `false` | | Renders the selection column |
118
+ | `editable` | `boolean` | `false` | | Enables inline cell editing on columns marked `editable` |
119
+ | `addable` | `boolean` | `false` | | Renders the add-row control |
120
+ | `filterable` | `boolean` | `false` | | Renders the filter row for columns marked `filterable` |
121
+ | `expandable` | `boolean` | `false` | | Renders the expander column; pair with `detailRenderer` |
122
+ | `detailRenderer` | `(row) => TemplateResult` | — | | Renders the expanded detail row |
123
+
124
+ ## Methods
125
+
126
+ | Method | Description |
127
+ |---|---|
128
+ | `getSelectedRows(): Record<string, unknown>[]` | Selected rows **on the current page** |
129
+ | `setSelection(ids: Iterable<string>): void` | Replace the accumulated selection; no-op if unchanged |
130
+ | `clearSelection(): void` | Clear it entirely, across pages |
131
+ | `setRowError(rowId, message): void` | Mark a row as failed (e.g. the server rejected a save) |
132
+ | `clearRowError(rowId): void` | Remove that mark |
133
+
134
+ ## Getters
135
+
136
+ | Getter | Description |
137
+ |---|---|
138
+ | `selectedRowIds: ReadonlySet<string>` | Snapshot of every selected identifier, across pages |
139
+
140
+ ## Events
141
+
142
+ All events bubble and cross shadow boundaries. `RichTableEventMap` types every
143
+ `detail`:
144
+
145
+ ```ts
146
+ import type { RichTableEventMap } from '@iyulab/data-components';
147
+ type SelectionChange = RichTableEventMap['selection-change'];
148
+ ```
149
+
150
+ | Event | `detail` | Fired when |
151
+ |---|---|---|
152
+ | `selection-change` | `{ selectedRows, selectedIds }` | Selection changed by any route |
153
+ | `select-all` | `{ checked, pageRowIds }` | The header checkbox was toggled |
154
+ | `sort-change` | `{ field, direction }` | A sortable header was clicked (`direction` is `null` when cleared) |
155
+ | `filter-change` | `{ filters }` | A column filter changed |
156
+ | `page-change` | `{ page, pageSize }` | The pager or page-size selector moved |
157
+ | `row-update` | `{ row, field, value, oldValue }` | An inline edit was committed |
158
+ | `row-create` | `{ row }` | The add-row control produced a row |
159
+ | `row-delete` | `{ row }` | A row was deleted |
160
+ | `row-expand` | `{ row, expanded }` | A detail row was opened or closed |
161
+ | `paste` | `{ rows }` | TSV was pasted into the grid |
162
+
163
+ ## ColumnDef
164
+
165
+ | Field | Type | Description |
166
+ |---|---|---|
167
+ | `key` | `string` | Property read from the row object |
168
+ | `label` | `string` | Header text |
169
+ | `width` | `string` | CSS width |
170
+ | `type` | `'text'\|'number'\|'date'\|'select'\|'badge'` | Cell renderer and editor |
171
+ | `options` | `{ value, label }[]` | Choices for `type: 'select'` |
172
+ | `badgeColors` | `Record<string, string>` | Value → color for `type: 'badge'` |
173
+ | `align` | `'left'\|'center'\|'right'` | Cell alignment |
174
+ | `sortable` | `boolean` | Header emits `sort-change` |
175
+ | `editable` | `boolean` | Cell is editable when the table is `editable` |
176
+ | `required` | `boolean` | Empty value fails validation |
177
+ | `filterable` | `boolean` | Column appears in the filter row |
178
+ | `filterType` | `'text'\|'select'` | Filter control |
179
+ | `validator` | `(value, row) => string \| null` | Returns an error message, or `null` when valid |
180
+ | `render` | `(value, row) => string \| HTMLElement` | Custom cell rendering |
181
+ | `clipboardParse` | `(text) => unknown` | Parses a pasted cell |
182
+ | `clipboardFormat` | `(value) => string` | Formats a copied cell |
183
+
184
+ ## Slots
185
+
186
+ | Name | Description |
187
+ |------|-------------|
188
+ | `bulk-actions` | Toolbar area shown while rows are selected |
189
+ | `toolbar-end` | Trailing toolbar area, always shown |
190
+
191
+ ## Keyboard
192
+
193
+ | Keys | Action |
194
+ |---|---|
195
+ | Arrow keys | Move the focused cell |
196
+ | `Enter` | Start editing the focused cell / commit and move down |
197
+ | `Escape` | Cancel editing |
198
+ | `Tab` | Commit and move to the next cell |
199
+ | `Space` | Toggle selection of the focused row (when `selectable`) |
200
+ | `Delete` | Emit `row-delete` for every selected row |
201
+ | `Ctrl`/`Cmd` + `A` | Select every row **on this page** — same scope as the header checkbox |
202
+ | `Ctrl`/`Cmd` + `C` | Copy the selection as TSV |
203
+ | `Ctrl`/`Cmd` + `V` | Paste TSV (emits `paste`) |
204
+
205
+ ## Localization
206
+
207
+ `emptyMessage`, `loadingMessage`, `filterPlaceholder`, `filterAllLabel` and
208
+ `addRowLabel` default to `''` and fall back to the package's locale strings —
209
+ 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`.