@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 +90 -0
- package/README.md +17 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.142.0 → _@oxc-project_runtime@0.143.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.142.0 → _@oxc-project_runtime@0.143.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/components/data-view/UDataView.d.ts +9 -1
- package/dist/components/data-view/UDataView.js +23 -15
- package/dist/components/data-view/UDataView.styles.js +4 -19
- package/dist/components/simple-sheet/USimpleSheet.js +17 -12
- package/dist/components/u-rich-table/URichTable.d.ts +6 -0
- package/dist/components/u-rich-table/URichTable.js +39 -17
- package/dist/components/u-rich-table/styles.js +31 -3
- package/dist/components/u-rich-table/types.d.ts +10 -0
- package/dist/index.d.ts +12 -0
- package/dist/react.d.ts +6 -1
- package/dist/react.js +1 -0
- package/package.json +14 -6
- package/skills/iyulab-data-components/SKILL.md +69 -0
- package/skills/iyulab-data-components/references/components/data-view.md +63 -0
- package/skills/iyulab-data-components/references/components/rich-table.md +228 -0
- package/skills/iyulab-data-components/references/components/simple-sheet.md +107 -0
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.
|
|
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.
|
|
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.
|
|
3
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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.
|
|
2
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
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)
|
|
60
|
-
|
|
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)
|
|
113
|
-
|
|
114
|
-
|
|
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)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
else
|
|
227
|
-
|
|
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.
|
|
2
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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)
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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.
|
|
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
|
-
"./
|
|
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
|
-
"./
|
|
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`.
|