@iyulab/flex-table 0.34.0 → 0.34.2

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.34.2] - 2026-09-13
4
+
5
+ ### Added
6
+
7
+ - **Development-mode warning when virtual scrolling is silently off.** With 200 or more rows and no
8
+ height constraint the host grows to fit every row, nothing scrolls, and every row is rendered —
9
+ the state the README's "100,000+ rows" premise does not cover. The grid now says so once
10
+ (`[@iyulab/flex-table]`, development builds only) and tells you to size the host so it becomes
11
+ the scroll container.
12
+
13
+ ## [0.34.1] - 2026-09-10
14
+
15
+ ### Changed
16
+
17
+ - **Fallback literals for `--u-txt-color-weak` follow `@iyulab/components` 1.40.0.** They are
18
+ only used when that stylesheet is not loaded; with it, nothing changes here.
19
+
20
+ ### Fixed
21
+
22
+ - **A column pinned to the right was placed against the scrolled content instead of the
23
+ viewport, so it sat outside the visible area exactly when a pin is needed.** Cells are
24
+ positioned absolutely inside a box as wide as the whole row, so the emitted
25
+ `right: ${-scrollLeft + offset}` measured from the content's right edge rather than the
26
+ viewport's: at `scrollLeft: 0` the column rendered past the right border of the scroll area
27
+ and never came into view. The negative offset also pushed the cell beyond that box, which
28
+ extended the scrollable width — measured in Chromium, `scrollWidth` grew 968 -> 1160 -> 1543
29
+ across three scroll steps, so the horizontal scrollbar could not reach its own end.
30
+
31
+ Right pins are now expressed the same way left pins already were, as a `left` offset that
32
+ tracks the viewport, clamped to the column's natural position. A right-pinned column stays
33
+ against the right edge of the scroll area at every scroll offset, several of them stack in
34
+ column order, and a table whose columns fit without overflowing leaves the column where it
35
+ naturally sits rather than inventing a scrollable area. Header, body and footer cells all
36
+ take the same path.
37
+
38
+ `position: sticky` — the usual way to build a pinned column — is not available in this
39
+ layout: every cell is absolutely positioned for horizontal virtualization, and a sticky box
40
+ needs a flow position to stick relative to.
41
+
3
42
  ## [0.34.0] - 2026-09-08
4
43
 
5
44
  ### Fixed
package/README.md CHANGED
@@ -19,7 +19,8 @@ npm install @iyulab/flex-table
19
19
  ## Quick Start
20
20
 
21
21
  ```html
22
- <flex-table id="table" row-height="32" show-row-numbers></flex-table>
22
+ <!-- The table is its own scroll container, so give it a height — see Sizing below. -->
23
+ <flex-table id="table" style="height: 400px" row-height="32" show-row-numbers></flex-table>
23
24
 
24
25
  <script type="module">
25
26
  import '@iyulab/flex-table';
@@ -67,6 +68,31 @@ npm install @iyulab/flex-table
67
68
  - **React Wrapper** — `@iyulab/flex-table/react` subpath for idiomatic React usage
68
69
  - **ARIA** — `role="grid"`, `aria-sort`, `aria-selected`, `aria-readonly`, `aria-invalid`, `aria-rowcount`, `aria-colcount`
69
70
 
71
+ ## Sizing
72
+
73
+ The table is its own scroll container (`overflow: auto` on the host) and it measures the viewport
74
+ from the host's own height. **Give it a height** — directly, or through a constrained parent:
75
+
76
+ ```css
77
+ flex-table { height: 400px; }
78
+ ```
79
+
80
+ ```css
81
+ /* or let a flex parent constrain it */
82
+ .page { height: 100%; display: flex; flex-direction: column; }
83
+ .page flex-table { flex: 1 1 auto; min-height: 0; }
84
+ ```
85
+
86
+ ⚠ **Without a height constraint the virtual scroll has nothing to virtualise against.** The body
87
+ carries the full content height, so an unconstrained host grows to match it and the viewport
88
+ measurement ends up equal to the content — every row is rendered. Measured with 1,000 rows and no
89
+ height: the element becomes ~32,000px tall and all 1,000 rows are in the DOM. The same data in a
90
+ 300px-tall host renders only the visible window (well under a fifth of the rows). That is the
91
+ difference between the "100,000+ rows" claim in Features and a page that stalls.
92
+
93
+ Small fixed data sets are fine unconstrained — the point is that the virtual-scroll guarantee is a
94
+ guarantee about a *constrained* host. `height-model.browser.test.ts` pins both sides of this.
95
+
70
96
  ## Properties
71
97
 
72
98
  | Property | Attribute | Type | Default | Description |
@@ -41,12 +41,12 @@ var l = r`
41
41
  --ft-active-color: var(--u-primary-color, #1976D2);
42
42
  --ft-selection-bg: var(--u-primary-bg-color, #E3F2FD);
43
43
  --ft-bool-color: var(--u-primary-color, #1976D2);
44
- --ft-sort-indicator-color: var(--u-txt-color-weak, #757575);
44
+ --ft-sort-indicator-color: var(--u-txt-color-weak, #616161);
45
45
  --ft-editor-bg: var(--u-input-bg-color, #FFFFFF);
46
46
  /* #999 was 2.85 against --ft-bg (#fff); AA needs 4.5. The dark value (#9aa0a6 on
47
47
  #1e1e2e = 6.31) was already fine, so this was a light-only defect. #5f6368 is the
48
48
  same grey the sort indicator uses, which keeps the palette to one family. */
49
- --ft-empty-color: var(--u-txt-color-weak, #757575);
49
+ --ft-empty-color: var(--u-txt-color-weak, #616161);
50
50
 
51
51
  /* 상태 색 — 종전에는 규칙 안에 리터럴로 박혀 있어 소비자가 덮을 경로가 없었고
52
52
  테마도 따라오지 않았다(다크에서 라이트용 값 그대로).
@@ -85,9 +85,9 @@ var l = r`
85
85
  --ft-active-color: var(--u-primary-color, #1976D2);
86
86
  --ft-selection-bg: var(--u-primary-bg-color, #E3F2FD);
87
87
  --ft-bool-color: var(--u-primary-color, #1976D2);
88
- --ft-sort-indicator-color: var(--u-txt-color-weak, #757575);
88
+ --ft-sort-indicator-color: var(--u-txt-color-weak, #616161);
89
89
  --ft-editor-bg: var(--u-input-bg-color, #FFFFFF);
90
- --ft-empty-color: var(--u-txt-color-weak, #757575);
90
+ --ft-empty-color: var(--u-txt-color-weak, #616161);
91
91
  }
92
92
  }
93
93
 
@@ -106,9 +106,9 @@ var l = r`
106
106
  --ft-active-color: var(--u-primary-color, #1976D2);
107
107
  --ft-selection-bg: var(--u-primary-bg-color, #E3F2FD);
108
108
  --ft-bool-color: var(--u-primary-color, #1976D2);
109
- --ft-sort-indicator-color: var(--u-txt-color-weak, #757575);
109
+ --ft-sort-indicator-color: var(--u-txt-color-weak, #616161);
110
110
  --ft-editor-bg: var(--u-input-bg-color, #FFFFFF);
111
- --ft-empty-color: var(--u-txt-color-weak, #757575);
111
+ --ft-empty-color: var(--u-txt-color-weak, #616161);
112
112
  }
113
113
 
114
114
  /* --- Layout --- */
@@ -1497,7 +1497,7 @@ async function K(e) {
1497
1497
  };
1498
1498
  }
1499
1499
  //#endregion
1500
- //#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
1500
+ //#region \0@oxc-project+runtime@0.149.0/helpers/esm/decorate.js
1501
1501
  function q(e, t, n, r) {
1502
1502
  var i = arguments.length, a = i < 3 ? t : r === null ? r = Object.getOwnPropertyDescriptor(t, n) : r, o;
1503
1503
  if (typeof Reflect == "object" && typeof Reflect.decorate == "function") a = Reflect.decorate(e, t, n, r);
@@ -1515,7 +1515,7 @@ var J = {
1515
1515
  lte: "≤"
1516
1516
  }, Y = 120, X = 40, Z = 32, Q = 5, $ = class extends n {
1517
1517
  constructor(...e) {
1518
- super(...e), this.columns = [], this._data = [], this._inUndoRedo = !1, this.clearUndoOnDataChange = !1, this.clearSelectionOnDataChange = !1, this._rowHeightFromCss = Z, this.showRowNumbers = !1, this.theme = void 0, this.maxRows = 0, this.editable = !0, this.showFilters = !1, this.showContextMenu = !1, this.frozenRows = 0, this.emptyMessage = "No data", this.noMatchingMessage = "No matching data", this.loading = !1, this.importEnabled = !1, this.selectable = !1, this.dataMode = "client", this.footerData = null, this.stylesheets = [], this._scrollTop = 0, this._scrollLeft = 0, this._viewportHeight = 0, this._viewportWidth = 0, this._colLeftOffsets = [], this._totalRowWidth = 0, this._baseStyleSheets = [], this._activeCell = null, this._editingCell = null, this._sortCriteria = [], this._selection = new x(), this._editing = new S(), this._rowSelection = new C(), this._undo = new E(), this._filters = [], this._filteredIndices = [], this._sortedIndices = [], this._openFilterKey = null, this._rowSelectionVersion = 0, this._lastCheckboxRowIndex = null, this._checkboxShiftPending = !1, this._autocompleteState = null, this._headerMenu = null, this._bodyContextMenu = null, this._commentPopup = null, this._viewDirty = !0, this._isDragging = !1, this._wasDrag = !1, this._resizing = null, this._resizeCleanup = null, this._wasHeaderDrag = !1, this._wasRowDrag = !1, this._colDrag = null, this._colDragIndicatorLeft = null, this._fillDrag = null, this._rowDrag = null, this._rowDragIndicatorY = null, this._findState = null, this._columnWidths = /* @__PURE__ */ new Map(), this._hostResizeObserver = null, this._comments = /* @__PURE__ */ new Map(), this._isDragOver = !1, this._onCommentPopupOutsideClick = () => {
1518
+ super(...e), this.columns = [], this._data = [], this._inUndoRedo = !1, this.clearUndoOnDataChange = !1, this.clearSelectionOnDataChange = !1, this._rowHeightFromCss = Z, this.showRowNumbers = !1, this.theme = void 0, this.maxRows = 0, this.editable = !0, this.showFilters = !1, this.showContextMenu = !1, this.frozenRows = 0, this.emptyMessage = "No data", this.noMatchingMessage = "No matching data", this.loading = !1, this.importEnabled = !1, this.selectable = !1, this.dataMode = "client", this.footerData = null, this.stylesheets = [], this._scrollTop = 0, this._scrollLeft = 0, this._viewportHeight = 0, this._viewportWidth = 0, this._colLeftOffsets = [], this._totalRowWidth = 0, this._baseStyleSheets = [], this._activeCell = null, this._editingCell = null, this._sortCriteria = [], this._selection = new x(), this._editing = new S(), this._rowSelection = new C(), this._undo = new E(), this._filters = [], this._filteredIndices = [], this._sortedIndices = [], this._openFilterKey = null, this._rowSelectionVersion = 0, this._lastCheckboxRowIndex = null, this._checkboxShiftPending = !1, this._autocompleteState = null, this._headerMenu = null, this._bodyContextMenu = null, this._commentPopup = null, this._viewDirty = !0, this._isDragging = !1, this._wasDrag = !1, this._resizing = null, this._resizeCleanup = null, this._wasHeaderDrag = !1, this._wasRowDrag = !1, this._colDrag = null, this._colDragIndicatorLeft = null, this._fillDrag = null, this._rowDrag = null, this._rowDragIndicatorY = null, this._findState = null, this._columnWidths = /* @__PURE__ */ new Map(), this._hostResizeObserver = null, this._comments = /* @__PURE__ */ new Map(), this._isDragOver = !1, this._warnedUnconstrained = !1, this._onCommentPopupOutsideClick = () => {
1519
1519
  this._commitCommentPopup();
1520
1520
  }, this._invalidCells = /* @__PURE__ */ new Map(), this._textFilterState = /* @__PURE__ */ new Map(), this._numberFilterState = /* @__PURE__ */ new Map(), this._dateFilterState = /* @__PURE__ */ new Map(), this._emptyFilterState = /* @__PURE__ */ new Map();
1521
1521
  }
@@ -2154,7 +2154,10 @@ var J = {
2154
2154
  }
2155
2155
  _measureViewport() {
2156
2156
  let e = this.clientHeight, t = this.clientWidth;
2157
- e !== this._viewportHeight && (this._viewportHeight = e), t !== this._viewportWidth && (this._viewportWidth = t);
2157
+ e !== this._viewportHeight && (this._viewportHeight = e), t !== this._viewportWidth && (this._viewportWidth = t), this._warnIfUnconstrained();
2158
+ }
2159
+ _warnIfUnconstrained() {
2160
+ this._warnedUnconstrained;
2158
2161
  }
2159
2162
  _onScroll() {
2160
2163
  this._scrollTop = this.scrollTop, this._scrollLeft = this.scrollLeft;
@@ -2787,6 +2790,12 @@ var J = {
2787
2790
  for (let r = t.length - 1; r > e; r--) t[r].pinned === "right" && (n += this._getColWidth(t[r]));
2788
2791
  return n;
2789
2792
  }
2793
+ _getPinnedRightLeft(e) {
2794
+ let t = this.visibleColumns, n = this._colLeftOffsets[e] ?? 0;
2795
+ if (this._viewportWidth <= 0) return n;
2796
+ let r = this._getColWidth(t[e]), i = this._scrollLeft + this._viewportWidth - this._getPinnedRight(e) - r;
2797
+ return Math.min(n, i);
2798
+ }
2790
2799
  _renderHeaderCell(e, t) {
2791
2800
  let n = e.sortable !== !1, r = this._sortCriteria.find((t) => t.key === e.key), o = this._sortCriteria.length > 1 ? this._sortCriteria.findIndex((t) => t.key === e.key) : -1, s = this._filters.some((t) => t.key === e.key), c = e.pinned === "left", l = e.pinned === "right", u = [
2792
2801
  "ft-header-cell",
@@ -2795,7 +2804,7 @@ var J = {
2795
2804
  e.headerAlign === "center" ? "ft-header-align-center" : "",
2796
2805
  e.headerAlign === "end" ? "ft-header-align-end" : ""
2797
2806
  ].filter(Boolean).join(" "), d = n ? r ? r.direction === "asc" ? "ascending" : "descending" : "none" : void 0, f = this._getColWidth(e), p = this.headerHeight, m = this._colLeftOffsets[t] ?? 0, h;
2798
- h = c ? `position: absolute; top: 0; left: ${this._scrollLeft + this._getPinnedLeft(t)}px; width: ${f}px; height: ${p}px; z-index: 4;` : l ? `position: absolute; top: 0; right: ${-this._scrollLeft + this._getPinnedRight(t)}px; width: ${f}px; height: ${p}px; z-index: 4;` : `left: ${m}px; width: ${f}px; height: ${p}px;`;
2807
+ h = c ? `position: absolute; top: 0; left: ${this._scrollLeft + this._getPinnedLeft(t)}px; width: ${f}px; height: ${p}px; z-index: 4;` : l ? `position: absolute; top: 0; left: ${this._getPinnedRightLeft(t)}px; width: ${f}px; height: ${p}px; z-index: 4;` : `left: ${m}px; width: ${f}px; height: ${p}px;`;
2799
2808
  let g = this._colDrag?.active && this._colDrag.colIndex === t, _ = [...u.split(" "), g ? "ft-col-dragging" : ""].filter(Boolean).join(" "), v = this.columns, y = v.findIndex((t) => t.key === e.key), b = [];
2800
2809
  if (y > 0) for (let e = y - 1; e >= 0 && v[e].hidden; e--) b.push(v[e]);
2801
2810
  let x = b.length > 0;
@@ -3802,7 +3811,7 @@ var J = {
3802
3811
  }
3803
3812
  let { start: g, end: _ } = this.visibleRange, v = this._frozenRowCount, y = [];
3804
3813
  for (let e = g; e < _; e++) y.push(this._renderRow(e, s, c, l, (e - v) * this.rowHeight));
3805
- let b = this._renderFillHandle(), x = this._rowDragIndicatorY == null ? "" : i`<div class="ft-row-drop-indicator" style="top:${this._rowDragIndicatorY}px;width:${o + this._prefixWidth}px;"></div>`;
3814
+ let b = this._renderFillHandle(), x = this._rowDragIndicatorY == null ? "" : i`<div class="ft-row-drop-indicator" style="top:${this._rowDragIndicatorY}px;width:${o}px;"></div>`;
3806
3815
  return i`
3807
3816
  ${t}
3808
3817
  ${n}
@@ -3852,7 +3861,7 @@ var J = {
3852
3861
  let u = this.showRowNumbers ? i`<div class="ft-footer-cell ft-row-num"
3853
3862
  style="position: absolute; top: 0; left: ${s + c}px; width: 48px; height: ${a}px; z-index: 2;"></div>` : "", d = [];
3854
3863
  for (let e of n) {
3855
- let t = r[e], n = this._getColWidth(t), o = t.pinned === "right" ? `position: absolute; top: 0; right: ${-s + this._getPinnedRight(e)}px; width: ${n}px; height: ${a}px; z-index: 2;` : `position: absolute; top: 0; left: ${s + this._getPinnedLeft(e)}px; width: ${n}px; height: ${a}px; z-index: 2;`;
3864
+ let t = r[e], n = this._getColWidth(t), o = t.pinned === "right" ? `position: absolute; top: 0; left: ${this._getPinnedRightLeft(e)}px; width: ${n}px; height: ${a}px; z-index: 2;` : `position: absolute; top: 0; left: ${s + this._getPinnedLeft(e)}px; width: ${n}px; height: ${a}px; z-index: 2;`;
3856
3865
  d.push(i`
3857
3866
  <div class="ft-footer-cell ft-pinned" style=${o}>
3858
3867
  ${this.footerData[t.key] ?? ""}</div>
@@ -3901,7 +3910,7 @@ var J = {
3901
3910
  }
3902
3911
  _renderCell(e, t, n, r) {
3903
3912
  let o = this._activeCell?.row === n && this._activeCell?.col === r, s = this._editingCell?.row === n && this._editingCell?.col === r, c = this._selection.isInRange(n, r), l = t.pinned === "left", u = t.pinned === "right", d = l || u, f = this._getColWidth(t), p = this.rowHeight, h = this._colLeftOffsets[r] ?? 0, g;
3904
- g = l ? `position: absolute; top: 0; left: ${this._scrollLeft + this._getPinnedLeft(r)}px; width: ${f}px; height: ${p}px; z-index: 2;` : u ? `position: absolute; top: 0; right: ${-this._scrollLeft + this._getPinnedRight(r)}px; width: ${f}px; height: ${p}px; z-index: 2;` : `left: ${h}px; width: ${f}px; height: ${p}px;`;
3913
+ g = l ? `position: absolute; top: 0; left: ${this._scrollLeft + this._getPinnedLeft(r)}px; width: ${f}px; height: ${p}px; z-index: 2;` : u ? `position: absolute; top: 0; left: ${this._getPinnedRightLeft(r)}px; width: ${f}px; height: ${p}px; z-index: 2;` : `left: ${h}px; width: ${f}px; height: ${p}px;`;
3905
3914
  let _ = !this._isCellEditable(t);
3906
3915
  if (t.conditionalRules && t.conditionalRules.length > 0) {
3907
3916
  let n = e[t.key], r = {};
@@ -330,6 +330,13 @@ export declare class FlexTable extends LitElement {
330
330
  private _toDataIndex;
331
331
  private _focusEditor;
332
332
  private _measureViewport;
333
+ private _warnedUnconstrained;
334
+ /**
335
+ * 개발 모드 사용 안내(HD-61 ⒝): 행이 많은데 호스트가 스크롤 컨테이너가 아니면 — 호스트가 내용
336
+ * 높이만큼 자라 스크롤할 것이 없으면 — 가상화가 꺼진 것이다. 오류가 없어 아무도 모른다.
337
+ * 한 번만, 개발 모드에서만. ResizeObserver 경로에서 불리지만 판정은 프로퍼티 둘을 읽는 것뿐이다.
338
+ */
339
+ private _warnIfUnconstrained;
333
340
  private _onScroll;
334
341
  private _onDocumentClick;
335
342
  private _onContextMenu;
@@ -380,6 +387,20 @@ export declare class FlexTable extends LitElement {
380
387
  private _getPinnedLeft;
381
388
  /** Calculate cumulative right offset for a right-pinned column */
382
389
  private _getPinnedRight;
390
+ /**
391
+ * X offset for a right-pinned column, in the same content coordinate space the
392
+ * left-pinned branch uses (origin = the row/header box, prefix columns included).
393
+ *
394
+ * A CSS `right` cannot express this. Cells are absolutely positioned inside a box
395
+ * whose width is the full row width, so a fixed `right` rides the content instead of
396
+ * the viewport, and a scroll-compensating negative `right` pushes the cell past that
397
+ * box — which extends the scrollable area, so every scroll widens it again.
398
+ *
399
+ * Sticking is therefore expressed as a `left` clamped to the column's natural offset:
400
+ * the column tracks the viewport's right edge while the content is scrolled past it,
401
+ * and rests in place once the viewport reaches it.
402
+ */
403
+ private _getPinnedRightLeft;
383
404
  private _renderHeaderCell;
384
405
  private _onHeaderContextMenu;
385
406
  private _showHiddenBefore;
@@ -1,2 +1,2 @@
1
- import { a as e, i as t, n, r, t as i } from "./flex-table-D4W1C_QA.js";
1
+ import { a as e, i as t, n, r, t as i } from "./flex-table-C-uws8qc.js";
2
2
  export { i as FlexTable, t as RowSelectionState, r as UndoStack, n as exportData, e as renderCell };
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as e } from "./flex-table-D4W1C_QA.js";
1
+ import { t as e } from "./flex-table-C-uws8qc.js";
2
2
  import t from "react";
3
3
  import { createComponent as n } from "@lit/react";
4
4
  var r = n({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iyulab/flex-table",
3
- "version": "0.34.0",
3
+ "version": "0.34.2",
4
4
  "description": "A minimalist, input-centric data grid web component",
5
5
  "type": "module",
6
6
  "main": "./dist/flex-table.js",
@@ -43,7 +43,7 @@
43
43
  "dev": "vite serve demo",
44
44
  "build": "npm run typecheck && vite build && tsc --emitDeclarationOnly",
45
45
  "build:demo": "vite build --config vite.config.demo.ts",
46
- "typecheck": "tsc --noEmit",
46
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
47
47
  "test": "vitest run",
48
48
  "test:watch": "vitest",
49
49
  "test:coverage": "vitest run --coverage",
@@ -91,6 +91,7 @@
91
91
  "@types/react-dom": "^19.2.7",
92
92
  "@typescript-eslint/eslint-plugin": "^8.64.0",
93
93
  "@typescript-eslint/parser": "^8.64.0",
94
+ "@vitest/browser-playwright": "^4.1.10",
94
95
  "eslint": "^10.1.0",
95
96
  "eslint-plugin-lit": "^2.3.1",
96
97
  "jsdom": "^29.1.1",
@@ -98,7 +99,7 @@
98
99
  "react": "^19.2.7",
99
100
  "react-dom": "^19.2.8",
100
101
  "strip-literal": "^3.1.0",
101
- "typescript": "^5.9.3",
102
+ "typescript": "^6.0.2",
102
103
  "vite": "^8.1.4",
103
104
  "vitest": "^4.1.10"
104
105
  }