@iyulab/flex-table 0.19.0 → 0.20.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/README.md CHANGED
@@ -105,9 +105,11 @@ interface ColumnDefinition {
105
105
  sortable?: boolean; // Enable sorting (default: true)
106
106
  editable?: boolean; // Per-column edit control (follows global editable)
107
107
  pinned?: 'left' | 'right'; // Freeze column during horizontal scroll
108
+ format?: string | ((value, row, col) => string); // Display format, see "format vs renderer" below
108
109
  renderer?: CellRenderer; // Custom cell render: (value, row, col) => TemplateResult | string
109
110
  editor?: CellEditor; // Custom cell editor: (value, row, col) => TemplateResult
110
111
  validator?: CellValidator; // Validate before commit: (value, row, col) => string | null
112
+ conditionalRules?: ConditionalRule[]; // Per-cell style rules, see below
111
113
  }
112
114
  ```
113
115
 
@@ -115,6 +117,42 @@ The `editor` callback must return a Lit `TemplateResult` containing an input ele
115
117
 
116
118
  The `validator` callback returns `null` if valid, or an error message string. On failure, the cell shows a red border for 3 seconds and a `validation-error` event is dispatched.
117
119
 
120
+ ### `format` vs `renderer`
121
+
122
+ Both control how a cell's raw value is displayed, but they differ in what they replace:
123
+
124
+ - **`format`**: a plain string pattern (Excel-style, e.g. `'#,##0.00'`, `'0.00%'`, `'$#,##0'`, `'yyyy-MM-dd'`) or a `(value) => string` function. Only the *displayed text* changes — editing, sorting, filtering, and export all keep operating on the raw underlying value. Use this for number/date/currency display formatting.
125
+ - **`renderer`**: a `(value, row, col) => TemplateResult | string` function that replaces the cell's rendered content entirely — badges, links, icons, multi-field composites. Sorting/filtering still use the raw value, but the visual output is fully custom.
126
+
127
+ ```typescript
128
+ const columns: ColumnDefinition<Order>[] = [
129
+ { key: 'total', header: 'Total', format: '#,##0.00' }, // "1,234.50"
130
+ { key: 'placedAt', header: 'Placed', format: 'yyyy-MM-dd' }, // date pattern
131
+ { key: 'status', header: 'Status', renderer: (v) => html`<span class="badge badge-${v}">${v}</span>` },
132
+ ];
133
+ ```
134
+
135
+ If both are set on the same column, `renderer` takes precedence — `format` has no effect once a custom `renderer` fully controls the cell's output.
136
+
137
+ ### Conditional Formatting
138
+
139
+ `conditionalRules` applies a style to a cell when its `when` predicate matches — a declarative alternative to writing a `renderer` just to color-code status/threshold values:
140
+
141
+ ```typescript
142
+ const columns: ColumnDefinition<Order>[] = [
143
+ {
144
+ key: 'status',
145
+ header: 'Status',
146
+ conditionalRules: [
147
+ { when: (v) => v === 'overdue', style: { color: '#dc2626', fontWeight: 'bold' } },
148
+ { when: (v) => v === 'paid', style: { color: '#16a34a' } },
149
+ ],
150
+ },
151
+ ];
152
+ ```
153
+
154
+ Rules are evaluated in order and combined; later matching rules override earlier ones for overlapping style properties.
155
+
118
156
  ## Methods
119
157
 
120
158
  ### Row Operations
@@ -312,6 +350,27 @@ function App() {
312
350
 
313
351
  All `<flex-table>` properties are available as React props, and all custom events are mapped to `on*` callbacks (e.g., `cell-edit-commit` → `onCellEditCommit`).
314
352
 
353
+ #### Imperative API via `ref`
354
+
355
+ `FlexTableReact` forwards `ref` to the underlying `FlexTable` custom element instance, so all [Methods](#methods) (`addRow`, `deleteRows`, `selectAll`, `setFilter`, etc.) are reachable without re-rendering the whole table:
356
+
357
+ ```tsx
358
+ import { useRef } from 'react';
359
+ import { FlexTableReact, type FlexTable } from '@iyulab/flex-table/react';
360
+
361
+ function App() {
362
+ const tableRef = useRef<FlexTable>(null);
363
+
364
+ return (
365
+ <>
366
+ <button onClick={() => tableRef.current?.addRow({ name: '', age: 0 })}>Add row</button>
367
+ <button onClick={() => tableRef.current?.deleteRows()}>Delete selected</button>
368
+ <FlexTableReact ref={tableRef} columns={columns} data={data} selectable />
369
+ </>
370
+ );
371
+ }
372
+ ```
373
+
315
374
  #### Typed rows (generics)
316
375
 
317
376
  `FlexTableReact` and `ColumnDefinition` are generic over your row type — no `as unknown as` casts needed in `data`, `columns`, or callbacks:
@@ -321,6 +321,24 @@ var s = t`
321
321
  border-radius: 4px;
322
322
  }
323
323
 
324
+ .ft-loading-overlay {
325
+ position: absolute;
326
+ inset: 0;
327
+ z-index: 90;
328
+ background: rgba(255, 255, 255, 0.5);
329
+ pointer-events: none;
330
+ }
331
+
332
+ @media (prefers-color-scheme: dark) {
333
+ :host(:not([theme="light"])) .ft-loading-overlay {
334
+ background: rgba(30, 30, 30, 0.5);
335
+ }
336
+ }
337
+
338
+ :host([theme="dark"]) .ft-loading-overlay {
339
+ background: rgba(30, 30, 30, 0.5);
340
+ }
341
+
324
342
  .ft-row-num-header,
325
343
  .ft-row-num {
326
344
  position: absolute;
@@ -1480,7 +1498,7 @@ var Y = {
1480
1498
  lte: "≤"
1481
1499
  }, X = 120, Z = 40, xe = 32, Q = 5, $ = class extends e {
1482
1500
  constructor(...e) {
1483
- super(...e), this.columns = [], this._data = [], this._inUndoRedo = !1, this.clearUndoOnDataChange = !1, this.clearSelectionOnDataChange = !1, this.rowHeight = xe, this.showRowNumbers = !1, this.theme = void 0, this.maxRows = 0, this.editable = !0, this.showFilters = !1, this.showContextMenu = !1, this.frozenRows = 0, this.importEnabled = !1, this.selectable = !1, this.dataMode = "client", this.footerData = null, this._scrollTop = 0, this._scrollLeft = 0, this._viewportHeight = 0, this._viewportWidth = 0, this._colLeftOffsets = [], this._totalRowWidth = 0, this._activeCell = null, this._editingCell = null, this._sortCriteria = [], this._selection = new y(), this._editing = new b(), this._rowSelection = new x(), this._undo = new D(), this._filters = [], this._filteredIndices = [], this._sortedIndices = [], this._openFilterKey = null, this._rowSelectionVersion = 0, 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._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 = () => {
1501
+ super(...e), this.columns = [], this._data = [], this._inUndoRedo = !1, this.clearUndoOnDataChange = !1, this.clearSelectionOnDataChange = !1, this.rowHeight = xe, this.showRowNumbers = !1, this.theme = void 0, this.maxRows = 0, this.editable = !0, this.showFilters = !1, this.showContextMenu = !1, this.frozenRows = 0, this.loading = !1, this.importEnabled = !1, this.selectable = !1, this.dataMode = "client", this.footerData = null, this._scrollTop = 0, this._scrollLeft = 0, this._viewportHeight = 0, this._viewportWidth = 0, this._colLeftOffsets = [], this._totalRowWidth = 0, this._activeCell = null, this._editingCell = null, this._sortCriteria = [], this._selection = new y(), this._editing = new b(), this._rowSelection = new x(), this._undo = new D(), this._filters = [], this._filteredIndices = [], this._sortedIndices = [], this._openFilterKey = null, this._rowSelectionVersion = 0, 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._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 = () => {
1484
1502
  this._commitCommentPopup();
1485
1503
  }, 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();
1486
1504
  }
@@ -2055,7 +2073,7 @@ var Y = {
2055
2073
  updated(e) {
2056
2074
  this._focusEditor(), this._adjustFilterDropdown();
2057
2075
  let t = String(this._visibleRowCount), n = String(this.visibleColumns.length);
2058
- if (this.getAttribute("aria-rowcount") !== t && this.setAttribute("aria-rowcount", t), this.getAttribute("aria-colcount") !== n && this.setAttribute("aria-colcount", n), e.has("_commentPopup") && this._commentPopup) {
2076
+ if (this.getAttribute("aria-rowcount") !== t && this.setAttribute("aria-rowcount", t), this.getAttribute("aria-colcount") !== n && this.setAttribute("aria-colcount", n), this.loading ? this.getAttribute("aria-busy") !== "true" && this.setAttribute("aria-busy", "true") : this.hasAttribute("aria-busy") && this.removeAttribute("aria-busy"), e.has("_commentPopup") && this._commentPopup) {
2059
2077
  let e = this.shadowRoot?.querySelector(".ft-comment-popup textarea");
2060
2078
  e && (e.value = this.getComment(this._commentPopup.dataIndex, this._commentPopup.colKey) ?? "", e.focus(), e.select());
2061
2079
  }
@@ -3676,12 +3694,12 @@ var Y = {
3676
3694
  `;
3677
3695
  }
3678
3696
  render() {
3679
- let e = this.visibleColumns, t = this.importEnabled && this._isDragOver ? n`<div class="ft-import-overlay">Drop file to import (.xlsx / .csv)</div>` : r;
3680
- if (e.length === 0) return n`${t}<div class="ft-empty">No columns defined</div>`;
3681
- let i = this.headerHeight, a = this._totalRowWidth, { start: o, end: s } = this.visibleColRange, c = [];
3682
- for (let t = 0; t < e.length; t++) (e[t].pinned === "left" || e[t].pinned === "right") && (t < o || t >= s) && c.push(t);
3683
- let l = 0, u = this._scrollLeft, d = this.selectable ? n`<div class="ft-checkbox-header"
3684
- style="position: absolute; top: 0; left: ${u + l}px; width: 36px; height: ${i}px; z-index: 4;">
3697
+ let e = this.visibleColumns, t = this.importEnabled && this._isDragOver ? n`<div class="ft-import-overlay">Drop file to import (.xlsx / .csv)</div>` : r, i = this.loading ? n`<div class="ft-loading-overlay"></div>` : r;
3698
+ if (e.length === 0) return n`${t}${i}<div class="ft-empty">No columns defined</div>`;
3699
+ let a = this.headerHeight, o = this._totalRowWidth, { start: s, end: c } = this.visibleColRange, l = [];
3700
+ for (let t = 0; t < e.length; t++) (e[t].pinned === "left" || e[t].pinned === "right") && (t < s || t >= c) && l.push(t);
3701
+ let u = 0, d = this._scrollLeft, f = this.selectable ? n`<div class="ft-checkbox-header"
3702
+ style="position: absolute; top: 0; left: ${d + u}px; width: 36px; height: ${a}px; z-index: 4;">
3685
3703
  ${this._rowSelection.mode === "multi" ? n`
3686
3704
  <input type="checkbox"
3687
3705
  .checked=${this._rowSelection.isAllSelected}
@@ -3689,38 +3707,40 @@ var Y = {
3689
3707
  @change=${this._onSelectAllChange}>
3690
3708
  ` : ""}
3691
3709
  </div>` : "";
3692
- this.selectable && (l += 36);
3693
- let f = this.showRowNumbers ? n`<div class="ft-row-num-header"
3694
- style="position: absolute; top: 0; left: ${u + l}px; width: 48px; height: ${i}px; z-index: 4;">#</div>` : "", p = [];
3695
- for (let t of c) p.push(this._renderHeaderCell(e[t], t));
3696
- for (let t = o; t < s; t++) p.push(this._renderHeaderCell(e[t], t));
3697
- let m = this._colDragIndicatorLeft == null ? "" : n`<div class="ft-drop-indicator" style="left:${this._colDragIndicatorLeft}px"></div>`;
3710
+ this.selectable && (u += 36);
3711
+ let p = this.showRowNumbers ? n`<div class="ft-row-num-header"
3712
+ style="position: absolute; top: 0; left: ${d + u}px; width: 48px; height: ${a}px; z-index: 4;">#</div>` : "", m = [];
3713
+ for (let t of l) m.push(this._renderHeaderCell(e[t], t));
3714
+ for (let t = s; t < c; t++) m.push(this._renderHeaderCell(e[t], t));
3715
+ let h = this._colDragIndicatorLeft == null ? "" : n`<div class="ft-drop-indicator" style="left:${this._colDragIndicatorLeft}px"></div>`;
3698
3716
  if (this.data.length === 0 || this._visibleRowCount === 0) return n`
3699
- <div class="ft-header" role="row" style="width: ${a}px; height: ${i}px;">
3700
- ${d}${f}
3701
- ${p}
3717
+ ${i}
3718
+ <div class="ft-header" role="row" style="width: ${o}px; height: ${a}px;">
3719
+ ${f}${p}
3702
3720
  ${m}
3721
+ ${h}
3703
3722
  </div>
3704
3723
  <div class="ft-empty">${this.data.length === 0 ? "No data" : "No matching data"}</div>
3705
3724
  `;
3706
- let { start: h, end: g } = this.visibleRange, _ = this._frozenRowCount, v = [];
3707
- for (let e = h; e < g; e++) v.push(this._renderRow(e, o, s, c, (e - _) * this.rowHeight));
3708
- let y = this._renderFillHandle(), b = this._rowDragIndicatorY == null ? "" : n`<div class="ft-row-drop-indicator" style="top:${this._rowDragIndicatorY}px;width:${a + this._prefixWidth}px;"></div>`;
3725
+ let { start: g, end: _ } = this.visibleRange, v = this._frozenRowCount, y = [];
3726
+ for (let e = g; e < _; e++) y.push(this._renderRow(e, s, c, l, (e - v) * this.rowHeight));
3727
+ let b = this._renderFillHandle(), x = this._rowDragIndicatorY == null ? "" : n`<div class="ft-row-drop-indicator" style="top:${this._rowDragIndicatorY}px;width:${o + this._prefixWidth}px;"></div>`;
3709
3728
  return n`
3710
3729
  ${t}
3730
+ ${i}
3711
3731
  ${this._renderFindPanel()}
3712
- <div class="ft-header" role="row" style="width: ${a}px; height: ${i}px;">
3713
- ${d}${f}
3714
- ${p}
3732
+ <div class="ft-header" role="row" style="width: ${o}px; height: ${a}px;">
3733
+ ${f}${p}
3715
3734
  ${m}
3735
+ ${h}
3716
3736
  </div>
3717
- ${this._renderFrozenRows(o, s, c)}
3718
- <div class="ft-body" style="height: ${this.totalBodyHeight}px; width: ${a}px;">
3719
- ${v}
3737
+ ${this._renderFrozenRows(s, c, l)}
3738
+ <div class="ft-body" style="height: ${this.totalBodyHeight}px; width: ${o}px;">
3739
+ ${y}
3720
3740
  </div>
3721
- ${this.footerData ? this._renderFooter(o, s, c) : ""}
3741
+ ${this.footerData ? this._renderFooter(s, c, l) : ""}
3742
+ ${x}
3722
3743
  ${b}
3723
- ${y}
3724
3744
  ${this._renderHeaderContextMenu()}
3725
3745
  ${this._renderBodyContextMenu()}
3726
3746
  ${this._renderCommentPopup()}
@@ -3946,6 +3966,9 @@ J([a({ type: Array })], $.prototype, "columns", void 0), J([a({
3946
3966
  type: Number,
3947
3967
  attribute: "frozen-rows"
3948
3968
  })], $.prototype, "frozenRows", void 0), J([a({
3969
+ type: Boolean,
3970
+ reflect: !0
3971
+ })], $.prototype, "loading", void 0), J([a({
3949
3972
  type: Boolean,
3950
3973
  attribute: "import-enabled"
3951
3974
  })], $.prototype, "importEnabled", void 0), J([a({ type: Boolean })], $.prototype, "selectable", void 0), J([a({
@@ -45,6 +45,12 @@ export declare class FlexTable extends LitElement {
45
45
  showContextMenu: boolean;
46
46
  /** Number of rows to freeze at the top (always visible during vertical scroll). */
47
47
  frozenRows: number;
48
+ /**
49
+ * True일 때 그리드 위에 로딩 오버레이를 표시하고 host에 `aria-busy="true"`를 반영한다.
50
+ * `useODataSource()`가 반환하는 `loading`과 자연 연동하도록 설계됨:
51
+ * `<FlexTableReact loading={source.loading} .../>`.
52
+ */
53
+ loading: boolean;
48
54
  /** Enable file drag-and-drop import (.xlsx, .csv). */
49
55
  importEnabled: boolean;
50
56
  /** Enable row-level checkbox selection. */
@@ -1,2 +1,2 @@
1
- import { a as e, i as t, n, r, t as i } from "./flex-table-BQc2PU5u.js";
1
+ import { a as e, i as t, n, r, t as i } from "./flex-table-C_7PqhEb.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-BQc2PU5u.js";
1
+ import { t as e } from "./flex-table-C_7PqhEb.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.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "A minimalist, input-centric data grid web component",
5
5
  "type": "module",
6
6
  "main": "./dist/flex-table.js",