@iyulab/flex-table 0.46.0 → 0.48.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,62 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.48.0] - 2026-10-06
4
+
5
+ ### Fixed
6
+
7
+ - **Ctrl+X no longer clears cells that never reached the clipboard.** The keys called the async
8
+ Clipboard API; when the write was refused — no permission, a page outside a secure context, a
9
+ browser without it — the table still cleared the cut range and fired `clipboard-cut`
10
+ (`clipboard-copy` for Ctrl+C). Ctrl+C, Ctrl+X and Ctrl+V now go through the browser's own
11
+ copy/cut/paste and `clipboardData`: they need no clipboard permission, work on any page, and a cut
12
+ range is cleared only once its text is on the clipboard.
13
+ - The context menu's Copy, which still writes through the Clipboard API, fires `clipboard-copy` only
14
+ when the write succeeds; a refused write fires `clipboard-error` alone.
15
+
16
+ ### Changed
17
+
18
+ - `clipboard-error` is now only the context menu's Copy (`action: 'copy'`). Paste no longer reads
19
+ through the Clipboard API, so it has no permission to be refused and never reports `'paste'`.
20
+ - Copy, cut and paste typed in a text field inside the grid (the cell editor, the find panel) are
21
+ that field's own and leave the cells alone.
22
+
23
+ ### Documentation
24
+
25
+ - README: the keyboard table covers every key the grid handles, and the column example uses
26
+ `appearance="plain"` (the `variant` attribute was removed in `@iyulab/components` 2.0).
27
+
28
+ ### Tests
29
+
30
+ - Real key input for Ctrl+C/X/V, Ctrl+Click on a header, and the column menu's Space / Home / End /
31
+ Escape, alongside the rest of the keyboard table.
32
+
33
+ ## [0.47.0] - 2026-10-05
34
+
35
+ ### Changed
36
+
37
+ - **Breaking: `error` from `useODataSource` and `useArraySource` is a structured failure, not a
38
+ string** — `SourceError | null`, where `SourceError` is `{ message, status?, code?, details?, body? }`
39
+ (exported from `@iyulab/flex-table/react` with `SourceErrorDetail`). The message alone lost which
40
+ failure it was, so an app could not tell a 403 the server marked with its own code (for example
41
+ "password change required") from any other 403, or a 404 from a 409, without wrapping `fetcher`
42
+ to watch the status. Now `status` is the HTTP status, `code` the server's rejection code (OData
43
+ `error.code`), `details` the OData `error.details` entries, and `body` the parsed response (or its
44
+ text). A failure with no response — a network error, or a `@odata.nextLink` the hook refused — has
45
+ a `message` only. The message itself is unchanged.
46
+
47
+ Migration: render `error.message` where you rendered `error`
48
+ (`{source.error && <p role="alert">{source.error.message}</p>}`), and compare `error?.message`
49
+ where you compared the string.
50
+
51
+ - **Date and datetime column filters use `u-date-picker`** for their From and To bounds, like the
52
+ cell editor since 0.46. The native date inputs they replace showed the browser's UI language
53
+ (`10/02/2026` in an English browser) while the table shows ISO; the bounds now read and show
54
+ `YYYY-MM-DD` (`YYYY-MM-DD HH:mm`) everywhere, with a calendar beside each. The bounds are labelled
55
+ (**From**, **To**), each calendar stops at the other bound, and either bound may still stay empty.
56
+ A bound now applies when it is committed — Enter, leaving the box, or a day picked in the calendar —
57
+ rather than on every keystroke. While a calendar is open, Escape closes the calendar and the next
58
+ Escape closes the filter. Opening the filter from the column menu focuses the From bound.
59
+
3
60
  ## [0.46.0] - 2026-10-05
4
61
 
5
62
  ### Changed
package/README.md CHANGED
@@ -50,7 +50,7 @@ npm install @iyulab/flex-table
50
50
  - **Range Selection** — Shift+Arrow, Shift+Click for multi-cell selection
51
51
  - **Column Selection** — Ctrl+Click header or `selectColumn()` API
52
52
  - **Row Selection** — Checkbox-based row selection (`selectable`, single/multi mode)
53
- - **Clipboard** — Ctrl+C/X/V with TSV format (Excel/Google Sheets compatible, RFC 4180)
53
+ - **Clipboard** — Ctrl+C/X/V with TSV format (Excel/Google Sheets compatible, RFC 4180), through the browser's own copy/cut/paste — no clipboard permission needed
54
54
  - **Sorting** — Click header to sort (asc/desc/none), Shift+click for multi-sort
55
55
  - **Column Menu** — A 24×24 button in every header opens the column's menu: sort, filter, hide/show, auto-fit, wider/narrower (also opens on header right-click)
56
56
  - **Column Resize** — Drag header border, double-click to auto-fit, Alt+Arrow keyboard resize, or the column menu
@@ -208,7 +208,7 @@ hover) always show them, and they are not printed.
208
208
 
209
209
  ```ts
210
210
  { key: 'actions', label: '', width: 72, editable: false, reveal: 'hover',
211
- render: (_, row) => html`<u-button size="sm" variant="ghost" aria-label="Edit">…</u-button>` }
211
+ render: (_, row) => html`<u-button size="sm" appearance="plain" aria-label="Edit">…</u-button>` }
212
212
  ```
213
213
 
214
214
  ### Merging Repeated Values
@@ -334,7 +334,7 @@ All events use `CustomEvent` with `bubbles: true, composed: true`.
334
334
  | `clipboard-copy` | `{ range, text }` | Range copied as TSV |
335
335
  | `clipboard-cut` | `{ range, text }` | Range cut as TSV |
336
336
  | `clipboard-paste` | `{ changes, addedRows }` | Data pasted from clipboard |
337
- | `clipboard-error` | `{ action, error }` | Clipboard API failed (`action`: `'copy'` or `'paste'`) |
337
+ | `clipboard-error` | `{ action, error }` | The context menu's Copy could not write to the clipboard (`action`: `'copy'`); no `clipboard-copy` follows |
338
338
 
339
339
  ### State Events
340
340
 
@@ -429,13 +429,16 @@ package dependency; CSS custom properties are resolved at render time, not impor
429
429
  |-----|--------|
430
430
  | Arrow keys | Navigate cells |
431
431
  | Tab / Shift+Tab | Move to next/previous cell |
432
- | Enter / F2 | Start editing |
433
- | Escape | Cancel edit / clear selection |
432
+ | Enter / F2 | Start editing (Enter on a non-editable cell fires `row-activate`) |
433
+ | Typing a printable character | Start editing an editable cell with that character |
434
+ | Escape | Cancel edit / clear selection / close the find panel |
434
435
  | Home / End | Row start/end |
435
436
  | Ctrl+Home / Ctrl+End | Table start/end |
436
- | Shift+Arrow | Extend selection range |
437
+ | Shift+Arrow / Shift+Click | Extend selection range |
437
438
  | Ctrl+C / Ctrl+X | Copy/Cut selection as TSV |
438
439
  | Ctrl+V | Paste TSV data |
440
+ | Ctrl+D / Ctrl+R | Fill down / fill right |
441
+ | Ctrl+F / Ctrl+H | Find / find and replace |
439
442
  | Delete / Backspace | Clear selected cells |
440
443
  | Ctrl+Z | Undo |
441
444
  | Ctrl+Shift+Z / Ctrl+Y | Redo |
@@ -684,8 +687,13 @@ button is highlighted while its column has an active filter.
684
687
  - **text**: case-insensitive substring search
685
688
  - **number**: min/max range inputs
686
689
  - **boolean**: All / True / False select
687
- - **date**: from/to date range picker (`<input type="date">`)
688
- - **datetime**: from/to datetime range picker (`<input type="datetime-local">`)
690
+ - **date**: **From** and **To** `u-date-picker`s (`@iyulab/components`) — the cell editor's control, so the
691
+ bounds read and show `YYYY-MM-DD` in every browser language, with a calendar beside each. Either
692
+ bound may stay empty; the end day is inclusive, and each calendar stops at the other bound.
693
+ - **datetime**: the same pickers with a time (`YYYY-MM-DD HH:mm`)
694
+
695
+ A bound applies when it is committed — Enter, leaving the box, or a day picked in its calendar. While a
696
+ calendar is open, Escape closes the calendar; the next Escape closes the filter.
689
697
 
690
698
  Filters set via the UI and the programmatic API (`setFilter()`) share the same filter state. Filter dropdowns automatically flip upward when near the viewport bottom.
691
699
 
@@ -787,7 +795,7 @@ The hook returns:
787
795
  |---|---|
788
796
  | `data` / `totalCount` | Current page rows and the server's total (`@odata.count`). When the server pages its response (`@odata.nextLink`, e.g. a page size smaller than `pageSize`), the hook follows the link until the page is filled; a link outside the request's origin, or one that returns to a page already read, is reported through `error` instead of showing a short page |
789
797
  | `loading` | A request is in flight |
790
- | `error` | Message of the last failed request, or `null`. **Render this** — a failed request otherwise leaves the grid silently empty |
798
+ | `error` | The last failed request, or `null`: `{ message, status?, code?, details?, body? }`. **Render `error.message`** — a failed request otherwise leaves the grid silently empty. Branch on `status` (HTTP), `code` (the server's rejection code, OData `error.code`) and `details` (OData `error.details`); `body` is the parsed response (or its text). A failure with no response — a network error, or a `@odata.nextLink` the hook refused — has a `message` only |
791
799
  | `page` / `setPage` | Zero-based page index |
792
800
  | `sortCriteria` / `onSortChange` | Bind `onSortChange` to the table's `sort-change` event |
793
801
  | `search` / `setSearch` | Current search term and its setter (resets to page 0) |
@@ -1,4 +1,5 @@
1
1
  import type { SortCriteria } from '../core/sorting.js';
2
+ import type { SourceError } from '../core/source-error.js';
2
3
  export interface UseArraySourceOptions<T> {
3
4
  /** 페이지당 행 수. `useODataSource`와 동일 기본값. */
4
5
  pageSize?: number;
@@ -36,7 +37,7 @@ export interface UseArraySourceResult<T> {
36
37
  /** 항상 `false` — 로컬 배열은 동기 처리라 로딩 상태가 없다. `useODataSource`와의 반환 형태 동일성을 위해 유지. */
37
38
  loading: boolean;
38
39
  /** 항상 `null` — 로컬 배열 처리는 실패하지 않는다. 위와 같은 이유로 유지. */
39
- error: string | null;
40
+ error: SourceError | null;
40
41
  page: number;
41
42
  setPage: (page: number) => void;
42
43
  sortCriteria: SortCriteria[];
@@ -0,0 +1,35 @@
1
+ /** OData v4 오류 봉투의 `error.details` 항목 — 필드별 검증 실패 상세. */
2
+ export interface SourceErrorDetail {
3
+ code: string;
4
+ message: string;
5
+ target?: string;
6
+ }
7
+ /**
8
+ * 데이터 소스 훅(`useODataSource`/`useArraySource`)의 실패 값.
9
+ *
10
+ * 문자열 하나가 아니라 구조로 싣는 이유: 소비자는 «어떤 실패인가» 로 갈라야 한다 — 403 중에서도
11
+ * 서버가 코드로 구분한 거절(«비밀번호를 바꿔야 한다»), 404(지워진 행), 409(동시성), 429(제한).
12
+ * 메시지만 남기면 그 정보가 소스 경계에서 사라지고, 소비자는 전송(`fetcher`)을 감싸 상태를
13
+ * 엿보는 것 말고는 길이 없다.
14
+ */
15
+ export interface SourceError {
16
+ /** 보여 줄 문장 — 서버가 준 메시지가 있으면 그것, 없으면 `Request failed (<status>)`. */
17
+ message: string;
18
+ /** HTTP 상태. 응답이 없던 실패(네트워크 오류 · `@odata.nextLink` 검사)에는 없다. */
19
+ status?: number;
20
+ /** 서버가 정한 거절 코드 — OData 오류 봉투의 `error.code`(또는 최상위 `code`). */
21
+ code?: string;
22
+ /** 오류 봉투의 `error.details` 중 `code`·`message` 가 둘 다 문자열인 항목. */
23
+ details?: SourceErrorDetail[];
24
+ /** 응답 본문 — JSON 이면 파싱한 값, 아니면 텍스트. 비어 있으면 없다. */
25
+ body?: unknown;
26
+ }
27
+ /** 실패 응답을 `SourceError` 로 읽는다. 본문을 읽을 수 없어도 상태와 기본 문장은 남긴다. */
28
+ export declare function readFailedResponse(res: Response): Promise<SourceError>;
29
+ /** 실패 응답을 실어 나르는 내부 예외 — `catch` 가 상태를 잃지 않고 `SourceError` 를 꺼낸다. */
30
+ export declare class SourceRequestError extends Error {
31
+ readonly failure: SourceError;
32
+ constructor(failure: SourceError);
33
+ }
34
+ /** 잡힌 예외를 `SourceError` 로 — 응답이 있던 실패는 그 구조 그대로, 나머지는 메시지만. */
35
+ export declare function toSourceError(err: unknown): SourceError;
@@ -567,6 +567,19 @@ var v = r`
567
567
  width: 50%;
568
568
  }
569
569
 
570
+ /* Date bounds stack: side by side, half of the dropdown is narrower than a date (and far narrower
571
+ than a date and a time) plus the calendar button. */
572
+ .ft-filter-range-dates {
573
+ flex-direction: column;
574
+ gap: 6px;
575
+ }
576
+
577
+ .ft-filter-date {
578
+ display: block;
579
+ width: 100%;
580
+ min-width: 0;
581
+ }
582
+
570
583
  .ft-filter-actions {
571
584
  margin-top: 6px;
572
585
  text-align: right;
@@ -1842,16 +1855,20 @@ function K(e, t, n, r) {
1842
1855
  }
1843
1856
  //#endregion
1844
1857
  //#region src/flex-table.ts
1845
- var q = {
1858
+ var q = [
1859
+ "copy",
1860
+ "cut",
1861
+ "paste"
1862
+ ], J = {
1846
1863
  eq: "=",
1847
1864
  neq: "≠",
1848
1865
  gt: ">",
1849
1866
  lt: "<",
1850
1867
  gte: "≥",
1851
1868
  lte: "≤"
1852
- }, J = 120, Y = 40, X = 20, Z = 32, Q = 5, qe = 200, $ = class extends n {
1869
+ }, Y = 120, X = 40, qe = 20, Je = 32, Z = 5, Ye = 200, Q = class extends n {
1853
1870
  constructor(...e) {
1854
- super(...e), this.columns = [], this._data = [], this._inUndoRedo = !1, this.clearUndoOnDataChange = !1, this.clearSelectionOnDataChange = !1, this._rowHeightFromCss = Z, this._targetFloor = 0, 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 ne(), this._editing = new re(), this._rowSelection = new j(), this._undo = new M(), 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 = () => {
1871
+ super(...e), this.columns = [], this._data = [], this._inUndoRedo = !1, this.clearUndoOnDataChange = !1, this.clearSelectionOnDataChange = !1, this._rowHeightFromCss = Je, this._targetFloor = 0, 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 ne(), this._editing = new re(), this._rowSelection = new j(), this._undo = new M(), 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 = () => {
1855
1872
  this._commitCommentPopup();
1856
1873
  }, 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();
1857
1874
  }
@@ -1866,7 +1883,7 @@ var q = {
1866
1883
  this._data = e, n && !this._inUndoRedo && (this.clearUndoOnDataChange && this._undo.clear(), this.clearSelectionOnDataChange && this._rowSelection.selectedCount > 0 && (this._rowSelection.deselectAll(), this._rowSelectionVersion++, this._dispatchRowSelectionEvent())), this.requestUpdate("data", t);
1867
1884
  }
1868
1885
  get rowHeight() {
1869
- let e = this._rowHeightExplicit ?? this._rowHeightFromCss ?? Z;
1886
+ let e = this._rowHeightExplicit ?? this._rowHeightFromCss ?? Je;
1870
1887
  return Math.max(e, this._targetFloor);
1871
1888
  }
1872
1889
  set rowHeight(e) {
@@ -2391,8 +2408,8 @@ var q = {
2391
2408
  return this._columnWidths.get(e);
2392
2409
  }
2393
2410
  _getColWidth(e) {
2394
- let t = this._columnWidths.get(e.key) ?? e.width ?? J;
2395
- return Math.max(t, e.minWidth ?? Y);
2411
+ let t = this._columnWidths.get(e.key) ?? e.width ?? Y;
2412
+ return Math.max(t, e.minWidth ?? X);
2396
2413
  }
2397
2414
  get headerHeight() {
2398
2415
  return this.rowHeight + 8;
@@ -2418,17 +2435,21 @@ var q = {
2418
2435
  return Math.max(0, this._visibleRowCount - this._frozenRowCount) * this.rowHeight;
2419
2436
  }
2420
2437
  get visibleRange() {
2421
- let e = this._frozenRowCount, t = e + Math.floor(Math.max(0, this._scrollTop) / this.rowHeight), n = Math.max(e, t - Q), r = Math.max(0, this._viewportHeight - this.headerHeight - this.frozenRowsHeight), i = Math.ceil(r / this.rowHeight) + 1;
2438
+ let e = this._frozenRowCount, t = e + Math.floor(Math.max(0, this._scrollTop) / this.rowHeight), n = Math.max(e, t - Z), r = Math.max(0, this._viewportHeight - this.headerHeight - this.frozenRowsHeight), i = Math.ceil(r / this.rowHeight) + 1;
2422
2439
  return {
2423
2440
  start: n,
2424
- end: Math.min(this._visibleRowCount, t + i + Q)
2441
+ end: Math.min(this._visibleRowCount, t + i + Z)
2425
2442
  };
2426
2443
  }
2427
2444
  connectedCallback() {
2428
- super.connectedCallback(), this._onScroll = this._onScroll.bind(this), this._onKeyDown = this._onKeyDown.bind(this), this._onDocumentClick = this._onDocumentClick.bind(this), this._onContextMenu = this._onContextMenu.bind(this), this._onDragover = this._onDragover.bind(this), this._onDragleave = this._onDragleave.bind(this), this._onDrop = this._onDrop.bind(this), this.addEventListener("scroll", this._onScroll, { passive: !0 }), this.addEventListener("keydown", this._onKeyDown), this.addEventListener("contextmenu", this._onContextMenu), this.addEventListener("dragover", this._onDragover), this.addEventListener("dragleave", this._onDragleave), this.addEventListener("drop", this._onDrop), this.hasAttribute("tabindex") || this.setAttribute("tabindex", "0"), this.setAttribute("role", "grid"), typeof ResizeObserver < "u" && (this._hostResizeObserver = new ResizeObserver(() => this._measureViewport()), this._hostResizeObserver.observe(this));
2445
+ super.connectedCallback(), this._onScroll = this._onScroll.bind(this), this._onKeyDown = this._onKeyDown.bind(this), this._onDocumentClick = this._onDocumentClick.bind(this), this._onContextMenu = this._onContextMenu.bind(this), this._onDragover = this._onDragover.bind(this), this._onDragleave = this._onDragleave.bind(this), this._onDrop = this._onDrop.bind(this), this._onClipboard = this._onClipboard.bind(this), this.addEventListener("scroll", this._onScroll, { passive: !0 }), this.addEventListener("keydown", this._onKeyDown);
2446
+ for (let e of q) this.addEventListener(e, this._onClipboard);
2447
+ this.addEventListener("contextmenu", this._onContextMenu), this.addEventListener("dragover", this._onDragover), this.addEventListener("dragleave", this._onDragleave), this.addEventListener("drop", this._onDrop), this.hasAttribute("tabindex") || this.setAttribute("tabindex", "0"), this.setAttribute("role", "grid"), typeof ResizeObserver < "u" && (this._hostResizeObserver = new ResizeObserver(() => this._measureViewport()), this._hostResizeObserver.observe(this));
2429
2448
  }
2430
2449
  disconnectedCallback() {
2431
- super.disconnectedCallback(), this.removeEventListener("scroll", this._onScroll), this.removeEventListener("keydown", this._onKeyDown), this.removeEventListener("contextmenu", this._onContextMenu), this.removeEventListener("dragover", this._onDragover), this.removeEventListener("dragleave", this._onDragleave), this.removeEventListener("drop", this._onDrop), document.removeEventListener("click", this._onDocumentClick), document.removeEventListener("mousedown", this._onCommentPopupOutsideClick), this._isDragging = !1, this._wasDrag = !1, this._resizeCleanup &&= (this._resizeCleanup(), null), this._hostResizeObserver &&= (this._hostResizeObserver.disconnect(), null);
2450
+ super.disconnectedCallback(), this.removeEventListener("scroll", this._onScroll), this.removeEventListener("keydown", this._onKeyDown);
2451
+ for (let e of q) this.removeEventListener(e, this._onClipboard);
2452
+ this.removeEventListener("contextmenu", this._onContextMenu), this.removeEventListener("dragover", this._onDragover), this.removeEventListener("dragleave", this._onDragleave), this.removeEventListener("drop", this._onDrop), document.removeEventListener("click", this._onDocumentClick), document.removeEventListener("mousedown", this._onCommentPopupOutsideClick), this._isDragging = !1, this._wasDrag = !1, this._resizeCleanup &&= (this._resizeCleanup(), null), this._hostResizeObserver &&= (this._hostResizeObserver.disconnect(), null);
2432
2453
  }
2433
2454
  firstUpdated() {
2434
2455
  this._readDensityTokens(), this._measureViewport(), this.shadowRoot && Array.isArray(this.shadowRoot.adoptedStyleSheets) && (this._baseStyleSheets = [...this.shadowRoot.adoptedStyleSheets]), this._syncStylesheets();
@@ -2464,13 +2485,13 @@ var q = {
2464
2485
  i = r;
2465
2486
  break;
2466
2487
  }
2467
- i = Math.max(0, i - Q);
2488
+ i = Math.max(0, i - Z);
2468
2489
  let a = e.length;
2469
2490
  for (let e = i; e < t.length; e++) if (t[e] > r) {
2470
2491
  a = e;
2471
2492
  break;
2472
2493
  }
2473
- return a = Math.min(e.length, a + Q), {
2494
+ return a = Math.min(e.length, a + Z), {
2474
2495
  start: i,
2475
2496
  end: a
2476
2497
  };
@@ -2530,7 +2551,7 @@ var q = {
2530
2551
  e !== this._viewportHeight && (this._viewportHeight = e), t !== this._viewportWidth && (this._viewportWidth = t), this._warnIfUnconstrained();
2531
2552
  }
2532
2553
  _warnIfUnconstrained() {
2533
- this._warnedUnconstrained || process.env.NODE_ENV === "production" || this._visibleRowCount < qe || this.scrollHeight > this.clientHeight + 1 || (this._warnedUnconstrained = !0, console.warn(`[@iyulab/flex-table] ${this._visibleRowCount} rows but the host has no height constraint — it grew to fit every row, so virtual scrolling is off and all rows are rendered. Give the host a height (e.g. height: 400px, or flex: 1 1 auto with min-height: 0 inside a sized parent) so it becomes the scroll container.`));
2554
+ this._warnedUnconstrained || process.env.NODE_ENV === "production" || this._visibleRowCount < Ye || this.scrollHeight > this.clientHeight + 1 || (this._warnedUnconstrained = !0, console.warn(`[@iyulab/flex-table] ${this._visibleRowCount} rows but the host has no height constraint — it grew to fit every row, so virtual scrolling is off and all rows are rendered. Give the host a height (e.g. height: 400px, or flex: 1 1 auto with min-height: 0 inside a sized parent) so it becomes the scroll container.`));
2534
2555
  }
2535
2556
  _onScroll() {
2536
2557
  this._scrollTop = this.scrollTop, this._scrollLeft = this.scrollLeft;
@@ -2766,7 +2787,7 @@ var q = {
2766
2787
  _onEditorKeyDown(e) {
2767
2788
  if (_(e)) return;
2768
2789
  let t = e.currentTarget;
2769
- if (!(t?.localName === "u-date-picker" && (e.composedPath().some((e) => e instanceof Element && e.part?.contains("popover")) || e.key === "Escape" && Ye(t)))) {
2790
+ if (!(t?.localName === "u-date-picker" && (e.composedPath().some((e) => e instanceof Element && e.part?.contains("popover")) || e.key === "Escape" && $(t)))) {
2770
2791
  if (this._autocompleteState && this._autocompleteState.candidates.length > 0) {
2771
2792
  if (e.key === "ArrowDown") {
2772
2793
  e.preventDefault(), e.stopPropagation();
@@ -2822,7 +2843,7 @@ var q = {
2822
2843
  this._activeCell = this._selection.activeCell ? { ...this._selection.activeCell } : null, this._scrollToActiveCell(), this._dispatchSelectionEvent();
2823
2844
  }
2824
2845
  _onKeyDown(e) {
2825
- if (this._editing.current) return;
2846
+ if (this._editing.current || this._openFilterKey && e.composedPath().some((e) => e instanceof Element && e.classList.contains("ft-filter-dropdown"))) return;
2826
2847
  let t = this.visibleColumns;
2827
2848
  if ((e.ctrlKey || e.metaKey) && !e.altKey) {
2828
2849
  let t = e.key.toLowerCase();
@@ -2874,9 +2895,9 @@ var q = {
2874
2895
  switch (e.key.toLowerCase()) {
2875
2896
  case "z": return e.preventDefault(), e.shiftKey ? this.redo() : this.undo(), !0;
2876
2897
  case "y": return e.preventDefault(), this.redo(), !0;
2877
- case "c": return e.preventDefault(), this._handleCopy(!1), !0;
2878
- case "x": return e.preventDefault(), this._handleCopy(this.editable), !0;
2879
- case "v": return e.preventDefault(), this.editable && this._handlePaste(), !0;
2898
+ case "c":
2899
+ case "x":
2900
+ case "v": return !0;
2880
2901
  case "d": return e.preventDefault(), this.editable && this._handleFillDown(), !0;
2881
2902
  case "r": return e.preventDefault(), this.editable && this._handleFillRight(), !0;
2882
2903
  case "f": return e.preventDefault(), this._toggleFindPanel("find"), !0;
@@ -2888,12 +2909,12 @@ var q = {
2888
2909
  if (!e.altKey || !this._activeCell || e.key !== "ArrowLeft" && e.key !== "ArrowRight") return !1;
2889
2910
  e.preventDefault();
2890
2911
  let n = t[this._activeCell.col];
2891
- return n && this._resizeColumnBy(n.key, e.key === "ArrowRight" ? X : -20), !0;
2912
+ return n && this._resizeColumnBy(n.key, e.key === "ArrowRight" ? qe : -20), !0;
2892
2913
  }
2893
2914
  _resizeColumnBy(e, t) {
2894
2915
  let n = this.visibleColumns.findIndex((t) => t.key === e), r = this.visibleColumns[n];
2895
2916
  if (!r) return;
2896
- let i = this._columnWidths.get(r.key) ?? r.width ?? J, a = r.minWidth ?? Y, o = Math.max(a, i + t);
2917
+ let i = this._columnWidths.get(r.key) ?? r.width ?? Y, a = r.minWidth ?? X, o = Math.max(a, i + t);
2897
2918
  this._columnWidths.set(r.key, o), this.requestUpdate(), this.dispatchEvent(new CustomEvent("column-resize", {
2898
2919
  detail: {
2899
2920
  key: r.key,
@@ -2919,35 +2940,55 @@ var q = {
2919
2940
  default: return !1;
2920
2941
  }
2921
2942
  }
2922
- async _handleCopy(e) {
2923
- let t = this._selection.getEffectiveRange();
2924
- if (!t) return;
2925
- let n = this.visibleColumns, r = oe(this._sortedIndices.map((e) => this.data[e]), n, t);
2926
- try {
2927
- await navigator.clipboard.writeText(r);
2928
- } catch (e) {
2929
- this.dispatchEvent(new CustomEvent("clipboard-error", {
2930
- detail: {
2931
- action: "copy",
2932
- error: e
2933
- },
2934
- bubbles: !0,
2935
- composed: !0
2936
- }));
2943
+ _selectionTsv() {
2944
+ let e = this._selection.getEffectiveRange();
2945
+ return e ? {
2946
+ range: e,
2947
+ text: oe(this._sortedIndices.map((e) => this.data[e]), this.visibleColumns, e)
2948
+ } : null;
2949
+ }
2950
+ _onClipboard(e) {
2951
+ let t = e.composedPath()[0];
2952
+ if (t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || t instanceof HTMLElement && t.isContentEditable || !e.clipboardData) return;
2953
+ if (e.type === "paste") {
2954
+ if (!this.editable || !this._activeCell) return;
2955
+ e.preventDefault(), this._pasteText(e.clipboardData.getData("text/plain"));
2956
+ return;
2937
2957
  }
2938
- e && this._clearRange(t), this.dispatchEvent(new CustomEvent(e ? "clipboard-cut" : "clipboard-copy", {
2958
+ let n = this._selectionTsv();
2959
+ n && (e.preventDefault(), e.clipboardData.setData("text/plain", n.text), this._afterCopy(n.range, n.text, e.type === "cut" && this.editable));
2960
+ }
2961
+ async _copyFromMenu() {
2962
+ let e = this._selectionTsv();
2963
+ if (e) {
2964
+ try {
2965
+ await navigator.clipboard.writeText(e.text);
2966
+ } catch (e) {
2967
+ this.dispatchEvent(new CustomEvent("clipboard-error", {
2968
+ detail: {
2969
+ action: "copy",
2970
+ error: e
2971
+ },
2972
+ bubbles: !0,
2973
+ composed: !0
2974
+ }));
2975
+ return;
2976
+ }
2977
+ this._afterCopy(e.range, e.text, !1);
2978
+ }
2979
+ }
2980
+ _afterCopy(e, t, n) {
2981
+ n && this._clearRange(e), this.dispatchEvent(new CustomEvent(n ? "clipboard-cut" : "clipboard-copy", {
2939
2982
  detail: {
2940
- range: t,
2941
- text: r
2983
+ range: e,
2984
+ text: t
2942
2985
  },
2943
2986
  bubbles: !0,
2944
2987
  composed: !0
2945
2988
  }));
2946
2989
  }
2947
- async _handlePaste() {
2990
+ _pasteText(e) {
2948
2991
  if (!this._activeCell) return;
2949
- let e = await this._readClipboardText();
2950
- if (e == null) return;
2951
2992
  let t = h(e);
2952
2993
  if (t.length === 0) return;
2953
2994
  let n = this._expandRowsForPaste(this._activeCell.row, t.length), r = this._applyPasteData(this._activeCell, t);
@@ -2975,20 +3016,6 @@ var q = {
2975
3016
  composed: !0
2976
3017
  }));
2977
3018
  }
2978
- async _readClipboardText() {
2979
- try {
2980
- return await navigator.clipboard.readText();
2981
- } catch (e) {
2982
- return this.dispatchEvent(new CustomEvent("clipboard-error", {
2983
- detail: {
2984
- action: "paste",
2985
- error: e
2986
- },
2987
- bubbles: !0,
2988
- composed: !0
2989
- })), null;
2990
- }
2991
- }
2992
3019
  _selectPastedRange(e, t) {
2993
3020
  let n = Math.max(...t.map((e) => e.length)), r = Math.min(e.row + t.length, this._visibleRowCount) - 1, i = Math.min(e.col + n, this.visibleColumns.length) - 1;
2994
3021
  (r !== e.row || i !== e.col) && (this._selection.setDimensions(this._visibleRowCount, this.visibleColumns.length), this._selection.setActive(e.row, e.col), this._selection.setActiveWithRange(r, i), this._activeCell = {
@@ -3349,8 +3376,9 @@ var q = {
3349
3376
  _openFilterFromMenu(e) {
3350
3377
  this._headerMenu = null, this._openFilterKey = e, requestAnimationFrame(() => {
3351
3378
  document.addEventListener("click", this._onDocumentClick, { once: !0 });
3352
- }), this.updateComplete.then(() => {
3353
- this.shadowRoot?.querySelector(".ft-filter-dropdown input, .ft-filter-dropdown select")?.focus();
3379
+ }), this.updateComplete.then(async () => {
3380
+ let e = this.shadowRoot?.querySelector(".ft-filter-dropdown input, .ft-filter-dropdown select, .ft-filter-dropdown u-date-picker");
3381
+ await e?.updateComplete, e?.focus();
3354
3382
  });
3355
3383
  }
3356
3384
  _showHiddenBefore(e) {
@@ -3386,7 +3414,7 @@ var q = {
3386
3414
  ${r.map((e) => c("show", A("showColumn", { header: e.label }), () => this._setColumnHidden(e.key, !1)))}
3387
3415
  <div class="ft-header-menu-separator" role="separator"></div>
3388
3416
  ${c("autofit", A("autoFitWidth"), () => this._autoFitColumn(e))}
3389
- ${c("wider", A("wider"), () => this._resizeColumnBy(e, X), !0)}
3417
+ ${c("wider", A("wider"), () => this._resizeColumnBy(e, qe), !0)}
3390
3418
  ${c("narrower", A("narrower"), () => this._resizeColumnBy(e, -20), !0)}
3391
3419
  </div>
3392
3420
  `;
@@ -3409,7 +3437,7 @@ var q = {
3409
3437
  style="position: fixed; left: ${n}px; top: ${r}px; z-index: var(--u-layer-floating, 1000);"
3410
3438
  @mousedown=${(e) => e.stopPropagation()}
3411
3439
  @keydown=${(e) => this._onMenuKeydown(e, ".ft-body-context-menu", l)}>
3412
- ${d(A("copy"), () => this._handleCopy(!1))}
3440
+ ${d(A("copy"), () => this._copyFromMenu())}
3413
3441
  ${f}
3414
3442
  ${d(A("insertRowAbove"), () => this.addRow(void 0, t))}
3415
3443
  ${d(A("insertRowBelow"), () => this.addRow(void 0, t + 1))}
@@ -3675,7 +3703,7 @@ var q = {
3675
3703
  @keydown=${(e) => {
3676
3704
  e.key === "Escape" && (this._openFilterKey = null), e.stopPropagation();
3677
3705
  }}>
3678
- ${Object.keys(q).map((e) => i`<option value=${e}>${q[e]}</option>`)}
3706
+ ${Object.keys(J).map((e) => i`<option value=${e}>${J[e]}</option>`)}
3679
3707
  </select>
3680
3708
  <input class="ft-filter-input ft-num-cond-input" type="text" inputmode="decimal" placeholder=${A("valuePlaceholder")}
3681
3709
  .value=${n.text ?? (n.value == null ? "" : P(n.value))}
@@ -3737,21 +3765,21 @@ var q = {
3737
3765
  return n ? n.error : null;
3738
3766
  }
3739
3767
  _renderDateFilter(e) {
3740
- let t = e.type === "datetime" ? "datetime-local" : "date", n = this._dateFilterState.get(e.key) ?? {};
3768
+ let t = e.type === "datetime", n = this._dateFilterState.get(e.key) ?? {}, r = (e) => e?.slice(0, 10), o = (e) => {
3769
+ e.key === "Escape" && $(e.currentTarget) || (e.stopPropagation(), e.key === "Escape" && (this._openFilterKey = null));
3770
+ };
3741
3771
  return i`
3742
- <div class="ft-filter-range">
3743
- <input class="ft-filter-input" type=${t} placeholder=${A("fromPlaceholder")}
3744
- .value=${n.from ?? ""}
3745
- @input=${(t) => this._applyDateFilter(e.key, t, "from")}
3746
- @keydown=${(e) => {
3747
- e.key === "Escape" && (this._openFilterKey = null), e.stopPropagation();
3748
- }}>
3749
- <input class="ft-filter-input" type=${t} placeholder=${A("toPlaceholder")}
3750
- .value=${n.to ?? ""}
3751
- @input=${(t) => this._applyDateFilter(e.key, t, "to")}
3752
- @keydown=${(e) => {
3753
- e.key === "Escape" && (this._openFilterKey = null), e.stopPropagation();
3754
- }}>
3772
+ <div class="ft-filter-range ft-filter-range-dates">
3773
+ <u-date-picker class="ft-filter-date" size="sm" clearable
3774
+ mode=${t ? "datetime" : "date"} label=${A("fromPlaceholder")}
3775
+ .value=${n.from ?? ""} max=${r(n.to) ?? a}
3776
+ @change=${(t) => this._applyDateFilter(e.key, t, "from")}
3777
+ @keydown=${o}></u-date-picker>
3778
+ <u-date-picker class="ft-filter-date" size="sm" clearable
3779
+ mode=${t ? "datetime" : "date"} label=${A("toPlaceholder")}
3780
+ .value=${n.to ?? ""} min=${r(n.from) ?? a}
3781
+ @change=${(t) => this._applyDateFilter(e.key, t, "to")}
3782
+ @keydown=${o}></u-date-picker>
3755
3783
  </div>
3756
3784
  ${this._renderEmptyFilterRow(e.key, () => {
3757
3785
  this._dateFilterState.delete(e.key);
@@ -3759,7 +3787,7 @@ var q = {
3759
3787
  `;
3760
3788
  }
3761
3789
  _applyDateFilter(e, t, n) {
3762
- let r = t.target.value, i = this._dateFilterState.get(e) ?? {};
3790
+ let r = t.target.value ?? "", i = this._dateFilterState.get(e) ?? {};
3763
3791
  r === "" ? delete i[n] : i[n] = r, this._dateFilterState.set(e, i), this._emptyFilterState.delete(e), this._applyFilterForKey(e);
3764
3792
  }
3765
3793
  _renderBooleanFilter(e) {
@@ -3787,7 +3815,7 @@ var q = {
3787
3815
  if (!n) return;
3788
3816
  let r = this.shadowRoot?.querySelectorAll(`.ft-cell[data-col-index="${t}"]`), i = 0, a = this.shadowRoot?.querySelector(`.ft-header-cell[data-col-index="${t}"]`);
3789
3817
  if (a && (i = Math.max(i, a.scrollWidth)), r) for (let e of r) i = Math.max(i, e.scrollWidth);
3790
- let o = n.minWidth ?? Y, s = Math.max(o, i + 8);
3818
+ let o = n.minWidth ?? X, s = Math.max(o, i + 8);
3791
3819
  this._columnWidths.set(n.key, s), this.requestUpdate(), this.dispatchEvent(new CustomEvent("column-resize", {
3792
3820
  detail: {
3793
3821
  key: n.key,
@@ -3800,7 +3828,7 @@ var q = {
3800
3828
  }
3801
3829
  _onResizeStart(e, t) {
3802
3830
  e.preventDefault(), e.stopPropagation();
3803
- let n = this.visibleColumns[t], r = this._columnWidths.get(n.key) ?? n.width ?? J;
3831
+ let n = this.visibleColumns[t], r = this._columnWidths.get(n.key) ?? n.width ?? Y;
3804
3832
  this._resizing = {
3805
3833
  colIndex: t,
3806
3834
  startX: e.clientX,
@@ -3808,13 +3836,13 @@ var q = {
3808
3836
  };
3809
3837
  let i = (e) => {
3810
3838
  if (!this._resizing) return;
3811
- let t = e.clientX - this._resizing.startX, r = n.minWidth ?? Y, i = Math.max(r, this._resizing.startWidth + t);
3839
+ let t = e.clientX - this._resizing.startX, r = n.minWidth ?? X, i = Math.max(r, this._resizing.startWidth + t);
3812
3840
  this._columnWidths.set(n.key, i), this.requestUpdate();
3813
3841
  }, a = () => {
3814
3842
  document.removeEventListener("mousemove", i), document.removeEventListener("mouseup", o), this._resizeCleanup = null;
3815
3843
  }, o = () => {
3816
3844
  if (a(), this._resizing) {
3817
- let e = this._columnWidths.get(n.key) ?? J;
3845
+ let e = this._columnWidths.get(n.key) ?? Y;
3818
3846
  this.dispatchEvent(new CustomEvent("column-resize", {
3819
3847
  detail: {
3820
3848
  key: n.key,
@@ -4450,7 +4478,7 @@ var q = {
4450
4478
  return i`
4451
4479
  <u-date-picker class="ft-editor ft-editor-${t.type}" size="sm"
4452
4480
  mode=${e ? "datetime" : "date"} ?confirm=${e}
4453
- .value=${Je(n, e)}
4481
+ .value=${Xe(n, e)}
4454
4482
  @keydown=${this._onEditorKeyDown}
4455
4483
  @change=${() => this._commitEdit()}
4456
4484
  @blur=${() => this._commitEdit()}></u-date-picker>
@@ -4497,62 +4525,62 @@ var q = {
4497
4525
  `;
4498
4526
  }
4499
4527
  };
4500
- K([s({ type: Array })], $.prototype, "columns", void 0), K([s({
4528
+ K([s({ type: Array })], Q.prototype, "columns", void 0), K([s({
4501
4529
  type: Array,
4502
4530
  hasChanged: () => !0
4503
- })], $.prototype, "data", null), K([s({
4531
+ })], Q.prototype, "data", null), K([s({
4504
4532
  type: Boolean,
4505
4533
  attribute: "clear-undo-on-data-change"
4506
- })], $.prototype, "clearUndoOnDataChange", void 0), K([s({
4534
+ })], Q.prototype, "clearUndoOnDataChange", void 0), K([s({
4507
4535
  type: Boolean,
4508
4536
  attribute: "clear-selection-on-data-change"
4509
- })], $.prototype, "clearSelectionOnDataChange", void 0), K([s({
4537
+ })], Q.prototype, "clearSelectionOnDataChange", void 0), K([s({
4510
4538
  type: Number,
4511
4539
  attribute: "row-height"
4512
- })], $.prototype, "rowHeight", null), K([s({
4540
+ })], Q.prototype, "rowHeight", null), K([s({
4513
4541
  type: Boolean,
4514
4542
  attribute: "show-row-numbers"
4515
- })], $.prototype, "showRowNumbers", void 0), K([s({
4543
+ })], Q.prototype, "showRowNumbers", void 0), K([s({
4516
4544
  type: String,
4517
4545
  reflect: !0
4518
- })], $.prototype, "theme", void 0), K([s({
4546
+ })], Q.prototype, "theme", void 0), K([s({
4519
4547
  type: Number,
4520
4548
  attribute: "max-rows"
4521
- })], $.prototype, "maxRows", void 0), K([s({ type: Boolean })], $.prototype, "editable", void 0), K([s({
4549
+ })], Q.prototype, "maxRows", void 0), K([s({ type: Boolean })], Q.prototype, "editable", void 0), K([s({
4522
4550
  type: Boolean,
4523
4551
  attribute: "show-filters"
4524
- })], $.prototype, "showFilters", void 0), K([s({
4552
+ })], Q.prototype, "showFilters", void 0), K([s({
4525
4553
  type: Boolean,
4526
4554
  attribute: "show-context-menu"
4527
- })], $.prototype, "showContextMenu", void 0), K([s({
4555
+ })], Q.prototype, "showContextMenu", void 0), K([s({
4528
4556
  type: Number,
4529
4557
  attribute: "frozen-rows"
4530
- })], $.prototype, "frozenRows", void 0), K([s({
4558
+ })], Q.prototype, "frozenRows", void 0), K([s({
4531
4559
  type: String,
4532
4560
  attribute: "empty-message"
4533
- })], $.prototype, "emptyMessage", void 0), K([s({
4561
+ })], Q.prototype, "emptyMessage", void 0), K([s({
4534
4562
  type: String,
4535
4563
  attribute: "no-matching-message"
4536
- })], $.prototype, "noMatchingMessage", void 0), K([s({
4564
+ })], Q.prototype, "noMatchingMessage", void 0), K([s({
4537
4565
  type: Boolean,
4538
4566
  reflect: !0
4539
- })], $.prototype, "loading", void 0), K([s({
4567
+ })], Q.prototype, "loading", void 0), K([s({
4540
4568
  type: Boolean,
4541
4569
  attribute: "import-enabled"
4542
- })], $.prototype, "importEnabled", void 0), K([s({ type: Boolean })], $.prototype, "selectable", void 0), K([s({
4570
+ })], Q.prototype, "importEnabled", void 0), K([s({ type: Boolean })], Q.prototype, "selectable", void 0), K([s({
4543
4571
  type: String,
4544
4572
  attribute: "selection-mode"
4545
- })], $.prototype, "selectionMode", null), K([s({
4573
+ })], Q.prototype, "selectionMode", null), K([s({
4546
4574
  type: String,
4547
4575
  attribute: "data-mode"
4548
- })], $.prototype, "dataMode", void 0), K([s({
4576
+ })], Q.prototype, "dataMode", void 0), K([s({
4549
4577
  type: Object,
4550
4578
  attribute: "footer-data"
4551
- })], $.prototype, "footerData", void 0), K([s({ attribute: !1 })], $.prototype, "stylesheets", void 0), K([s({
4579
+ })], Q.prototype, "footerData", void 0), K([s({ attribute: !1 })], Q.prototype, "stylesheets", void 0), K([s({
4552
4580
  type: Number,
4553
4581
  attribute: "max-undo-size"
4554
- })], $.prototype, "maxUndoSize", null), K([c()], $.prototype, "_scrollTop", void 0), K([c()], $.prototype, "_scrollLeft", void 0), K([c()], $.prototype, "_viewportHeight", void 0), K([c()], $.prototype, "_viewportWidth", void 0), K([c()], $.prototype, "_activeCell", void 0), K([c()], $.prototype, "_editingCell", void 0), K([c()], $.prototype, "_sortCriteria", void 0), K([c()], $.prototype, "_openFilterKey", void 0), K([c()], $.prototype, "_rowSelectionVersion", void 0), K([c()], $.prototype, "_autocompleteState", void 0), K([c()], $.prototype, "_headerMenu", void 0), K([c()], $.prototype, "_bodyContextMenu", void 0), K([c()], $.prototype, "_commentPopup", void 0), K([c()], $.prototype, "_isDragOver", void 0), $ = K([o("flex-table")], $);
4555
- function Je(e, t) {
4582
+ })], Q.prototype, "maxUndoSize", null), K([c()], Q.prototype, "_scrollTop", void 0), K([c()], Q.prototype, "_scrollLeft", void 0), K([c()], Q.prototype, "_viewportHeight", void 0), K([c()], Q.prototype, "_viewportWidth", void 0), K([c()], Q.prototype, "_activeCell", void 0), K([c()], Q.prototype, "_editingCell", void 0), K([c()], Q.prototype, "_sortCriteria", void 0), K([c()], Q.prototype, "_openFilterKey", void 0), K([c()], Q.prototype, "_rowSelectionVersion", void 0), K([c()], Q.prototype, "_autocompleteState", void 0), K([c()], Q.prototype, "_headerMenu", void 0), K([c()], Q.prototype, "_bodyContextMenu", void 0), K([c()], Q.prototype, "_commentPopup", void 0), K([c()], Q.prototype, "_isDragOver", void 0), Q = K([o("flex-table")], Q);
4583
+ function Xe(e, t) {
4556
4584
  if (t) {
4557
4585
  let t = ce(e);
4558
4586
  return /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(t) ? t.replace(" ", "T") : void 0;
@@ -4560,7 +4588,7 @@ function Je(e, t) {
4560
4588
  let n = se(e);
4561
4589
  return /^\d{4}-\d{2}-\d{2}$/.test(n) ? n : void 0;
4562
4590
  }
4563
- function Ye(e) {
4591
+ function $(e) {
4564
4592
  try {
4565
4593
  return e.matches(":state(open)");
4566
4594
  } catch {
@@ -4568,4 +4596,4 @@ function Ye(e) {
4568
4596
  }
4569
4597
  }
4570
4598
  //#endregion
4571
- export { j as a, M as i, G as n, k as o, V as r, C as s, $ as t };
4599
+ export { j as a, M as i, G as n, k as o, V as r, C as s, Q as t };
@@ -393,9 +393,20 @@ export declare class FlexTable extends LitElement {
393
393
  /** Widen (positive) or narrow (negative) a visible column by `delta` px, never below its minimum. */
394
394
  private _resizeColumnBy;
395
395
  private _handleNavigation;
396
- private _handleCopy;
397
- private _handlePaste;
398
- private _readClipboardText;
396
+ /**
397
+ * The selection as TSV in display (sorted) order, or `null` without a selection.
398
+ */
399
+ private _selectionTsv;
400
+ /**
401
+ * The browser's copy / cut / paste on the grid. Events from a text field inside the grid (the cell
402
+ * editor, the find panel, a filter input) are that field's own and pass through untouched.
403
+ */
404
+ private _onClipboard;
405
+ /** The context menu's Copy — no clipboard event to fill, so it goes through the async Clipboard API. */
406
+ private _copyFromMenu;
407
+ /** Runs once the text is on the clipboard — only then is a cut range cleared. */
408
+ private _afterCopy;
409
+ private _pasteText;
399
410
  /**
400
411
  * After a paste, the cells it wrote are the selection — as in spreadsheets — so the reader sees
401
412
  * what changed and can copy, clear or undo it as one block. The active cell stays where the
@@ -492,6 +503,12 @@ export declare class FlexTable extends LitElement {
492
503
  private _numberFilterState;
493
504
  private _dateFilterState;
494
505
  private _emptyFilterState;
506
+ /**
507
+ * From/to bounds as two `u-date-picker`s — the cell editor's control, for the same reason: the native
508
+ * date inputs show the browser's UI language (`10/02/2026` in an English browser) while the table
509
+ * shows ISO. Two pickers, not one `u-date-range-picker`: either bound may be left open ("from this
510
+ * day on"), which a range value cannot say. Each bound's calendar stops at the other bound.
511
+ */
495
512
  private _renderDateFilter;
496
513
  private _applyDateFilter;
497
514
  private _renderBooleanFilter;
@@ -1,2 +1,2 @@
1
- import { a as e, i as t, n, o as r, r as i, s as a, t as o } from "./flex-table-DIQhqj12.js";
1
+ import { a as e, i as t, n, o as r, r as i, s as a, t as o } from "./flex-table-B8ehfxqn.js";
2
2
  export { o as FlexTable, e as RowSelectionState, t as UndoStack, n as effectiveAlign, i as exportData, r as flexTableLocale, a as renderCell };
@@ -1,4 +1,5 @@
1
1
  import type { SortCriteria } from '../core/sorting.js';
2
+ import type { SourceError } from '../core/source-error.js';
2
3
  export interface UseODataSourceOptions {
3
4
  pageSize?: number;
4
5
  defaultOrderBy?: string;
@@ -43,7 +44,11 @@ export interface UseODataSourceResult<T> {
43
44
  data: T[];
44
45
  totalCount: number;
45
46
  loading: boolean;
46
- error: string | null;
47
+ /**
48
+ * 마지막 요청의 실패, 없으면 `null`. 화면에는 `error.message` 를 그린다. «어떤 실패인가» 는
49
+ * `status`(HTTP) · `code`(서버의 거절 코드) · `details` 로 가른다 — 401 은 `onUnauthorized` 도 부른다.
50
+ */
51
+ error: SourceError | null;
47
52
  page: number;
48
53
  setPage: (page: number) => void;
49
54
  sortCriteria: SortCriteria[];
package/dist/react.d.ts CHANGED
@@ -69,3 +69,4 @@ export { useODataSource } from './odata/use-odata-source.js';
69
69
  export type { UseODataSourceOptions, UseODataSourceResult } from './odata/types.js';
70
70
  export { useArraySource } from './array/use-array-source.js';
71
71
  export type { UseArraySourceOptions, UseArraySourceResult } from './array/types.js';
72
+ export type { SourceError, SourceErrorDetail } from './core/source-error.js';
package/dist/react.js CHANGED
@@ -1,47 +1,74 @@
1
- import { t as e } from "./flex-table-DIQhqj12.js";
1
+ import { t as e } from "./flex-table-B8ehfxqn.js";
2
2
  import { i as t, t as n } from "./query-Dw4iO0ir.js";
3
3
  import { t as r } from "./view-DiLr-TdY.js";
4
4
  import i, { useCallback as a, useEffect as o, useMemo as s, useRef as c, useState as l } from "react";
5
5
  import { createComponent as u } from "@lit/react";
6
+ //#region src/core/source-error.ts
7
+ async function d(e) {
8
+ let t = await e.text().catch(() => ""), n = {
9
+ message: `Request failed (${e.status})`,
10
+ status: e.status
11
+ };
12
+ if (!t) return n;
13
+ let r = t;
14
+ try {
15
+ r = JSON.parse(t);
16
+ } catch {}
17
+ if (n.body = r, !r || typeof r != "object") return n;
18
+ let i = r, a = i.error && typeof i.error == "object" ? i.error : i;
19
+ typeof a.message == "string" && a.message ? n.message = a.message : typeof i.message == "string" && i.message && (n.message = i.message), typeof a.code == "string" && a.code && (n.code = a.code);
20
+ let o = f(a.details);
21
+ return o && (n.details = o), n;
22
+ }
23
+ function f(e) {
24
+ if (!Array.isArray(e)) return;
25
+ let t = e.filter((e) => {
26
+ if (!e || typeof e != "object") return !1;
27
+ let t = e;
28
+ return typeof t.code == "string" && typeof t.message == "string";
29
+ });
30
+ return t.length > 0 ? t : void 0;
31
+ }
32
+ var p = class extends Error {
33
+ constructor(e) {
34
+ super(e.message), this.name = "SourceRequestError", this.failure = e;
35
+ }
36
+ };
37
+ function m(e) {
38
+ return e instanceof p ? e.failure : e instanceof Error ? { message: e.message } : { message: String(e) };
39
+ }
40
+ //#endregion
6
41
  //#region src/odata/use-odata-source.ts
7
- function d(e, r = {}) {
8
- let { pageSize: i = 20, defaultOrderBy: s, initialPage: u = 0, initialSearch: d = "", initialSort: f, fixedFilter: p, baseUrl: m, fetcher: h = fetch, onUnauthorized: g, enabled: _ = !0 } = r, v = p ? JSON.stringify(p) : "", [y, b] = l([]), [x, S] = l(0), [C, w] = l(!1), [T, E] = l(null), D = t({
42
+ function h(e, r = {}) {
43
+ let { pageSize: i = 20, defaultOrderBy: s, initialPage: u = 0, initialSearch: f = "", initialSort: h, fixedFilter: g, baseUrl: _, fetcher: v = fetch, onUnauthorized: y, enabled: b = !0 } = r, x = g ? JSON.stringify(g) : "", [S, C] = l([]), [w, T] = l(0), [E, D] = l(!1), [O, k] = l(null), A = t({
9
44
  defaultOrderBy: s,
10
45
  initialPage: u,
11
- initialSearch: d,
12
- initialSort: f
13
- }), [O, k] = l(D.page), [A, j] = l(D.sortCriteria), [M, N] = l(D.search), [P, F] = l(0), [I, L] = l(v);
14
- v !== I && (L(v), k(0));
15
- let R = c(null), z = a(() => {
16
- F((e) => e + 1);
17
- }, []), B = a((e) => {
18
- N(e), k(0);
19
- }, []), V = a((e) => {
46
+ initialSearch: f,
47
+ initialSort: h
48
+ }), [j, M] = l(A.page), [N, P] = l(A.sortCriteria), [F, I] = l(A.search), [L, R] = l(0), [z, B] = l(x);
49
+ x !== z && (B(x), M(0));
50
+ let V = c(null), H = a(() => {
51
+ R((e) => e + 1);
52
+ }, []), U = a((e) => {
53
+ I(e), M(0);
54
+ }, []), W = a((e) => {
20
55
  let t = e.detail?.criteria;
21
- t && (j(t), k(0));
56
+ t && (P(t), M(0));
22
57
  }, []);
23
58
  return o(() => {
24
- if (R.current?.abort(), !_) return;
59
+ if (V.current?.abort(), !b) return;
25
60
  let t = new AbortController();
26
- R.current = t, w(!0), E(null);
61
+ V.current = t, D(!0), k(null);
27
62
  let r = n({
28
- page: O,
63
+ page: j,
29
64
  pageSize: i,
30
- sortCriteria: A,
65
+ sortCriteria: N,
31
66
  defaultOrderBy: s,
32
- search: M,
33
- fixedFilter: p
34
- }), a = `${m ?? window.location.origin}${e}${r}`, o = async (e) => {
35
- let n = await h(e, { signal: t.signal });
36
- if (!n.ok) {
37
- n.status === 401 && g && g(n);
38
- let e = await n.text().catch(() => ""), t = `Request failed (${n.status})`;
39
- try {
40
- let n = JSON.parse(e);
41
- t = n?.error?.message ?? n?.message ?? t;
42
- } catch {}
43
- throw Error(t);
44
- }
67
+ search: F,
68
+ fixedFilter: g
69
+ }), a = `${_ ?? window.location.origin}${e}${r}`, o = async (e) => {
70
+ let n = await v(e, { signal: t.signal });
71
+ if (!n.ok) throw n.status === 401 && y && y(n), new p(await d(n));
45
72
  return n.json();
46
73
  };
47
74
  return (async () => {
@@ -61,42 +88,42 @@ function d(e, r = {}) {
61
88
  })().then(({ count: e, rows: n }) => {
62
89
  if (t.signal.aborted) return;
63
90
  let r = e ?? 0;
64
- b(n), S(r), E(null);
91
+ C(n), T(r), k(null);
65
92
  let a = r === 0 ? 0 : Math.ceil(r / i) - 1;
66
- O > a && k(a);
93
+ j > a && M(a);
67
94
  }).catch((e) => {
68
- t.signal.aborted || e.name !== "AbortError" && (E(e.message), b([]), S(0));
95
+ t.signal.aborted || e.name !== "AbortError" && (k(m(e)), C([]), T(0));
69
96
  }).finally(() => {
70
- t.signal.aborted || w(!1);
97
+ t.signal.aborted || D(!1);
71
98
  }), () => t.abort();
72
99
  }, [
73
- _,
100
+ b,
74
101
  e,
75
- O,
102
+ j,
76
103
  i,
77
- A,
78
- M,
79
- v,
104
+ N,
105
+ F,
106
+ x,
80
107
  s,
81
- P,
82
- m
108
+ L,
109
+ _
83
110
  ]), {
84
- data: y,
85
- totalCount: x,
86
- loading: C || !_,
87
- error: T,
88
- page: O,
89
- setPage: k,
90
- sortCriteria: A,
91
- onSortChange: V,
92
- search: M,
93
- setSearch: B,
94
- refresh: z
111
+ data: S,
112
+ totalCount: w,
113
+ loading: E || !b,
114
+ error: O,
115
+ page: j,
116
+ setPage: M,
117
+ sortCriteria: N,
118
+ onSortChange: W,
119
+ search: F,
120
+ setSearch: U,
121
+ refresh: H
95
122
  };
96
123
  }
97
124
  //#endregion
98
125
  //#region src/array/use-array-source.ts
99
- function f(e, n = {}) {
126
+ function g(e, n = {}) {
100
127
  let { pageSize: i = 20, defaultOrderBy: o, initialPage: c = 0, initialSearch: u = "", initialSort: d, columns: f, searchFields: p } = n, m = t({
101
128
  defaultOrderBy: o,
102
129
  initialPage: c,
@@ -137,7 +164,7 @@ function f(e, n = {}) {
137
164
  refresh: C
138
165
  };
139
166
  }
140
- var p = u({
167
+ var _ = u({
141
168
  tagName: "flex-table",
142
169
  elementClass: e,
143
170
  react: i,
@@ -176,4 +203,4 @@ var p = u({
176
203
  }
177
204
  });
178
205
  //#endregion
179
- export { p as FlexTableReact, f as useArraySource, d as useODataSource };
206
+ export { _ as FlexTableReact, g as useArraySource, h as useODataSource };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iyulab/flex-table",
3
- "version": "0.46.0",
3
+ "version": "0.48.0",
4
4
  "description": "A minimalist, input-centric data grid web component",
5
5
  "type": "module",
6
6
  "main": "./dist/flex-table.js",
@@ -45,6 +45,7 @@
45
45
  "build": "npm run typecheck && vite build && tsc --emitDeclarationOnly",
46
46
  "build:demo": "vite build --config vite.config.demo.ts",
47
47
  "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
48
+ "pretest": "node -e \"if(require('fs').existsSync('../../scripts/sibling-dist-check.js'))require('child_process').execFileSync('node',['../../scripts/sibling-dist-check.js'],{stdio:'inherit'})\"",
48
49
  "test": "vitest run",
49
50
  "test:watch": "vitest",
50
51
  "lint": "eslint src/",
@@ -127,7 +127,7 @@ Server paging from OData or an in-memory array uses the same binding — see `re
127
127
  - Styles for elements returned by `render` must be passed through the `stylesheets` property
128
128
  (constructable `CSSStyleSheet[]`); document CSS does not cross the shadow boundary.
129
129
  - `useODataSource` `fetcher` / `onUnauthorized` must be stable references (`useCallback`).
130
- - Always render `error` from `useODataSource`; a failed request otherwise leaves the grid empty.
130
+ - Always render `error.message` from `useODataSource`; a failed request otherwise leaves the grid empty. Branch on `error.status` / `error.code` rather than parsing the message.
131
131
 
132
132
  ## References
133
133
 
@@ -159,7 +159,7 @@ All are `CustomEvent`s with `bubbles: true, composed: true`. `row`/`index` are d
159
159
  | `selection-change` | `{ selectedIndices, selectedRows }` |
160
160
  | `clipboard-copy` / `clipboard-cut` | `{ range, text }` (TSV) |
161
161
  | `clipboard-paste` | `{ changes, addedRows }` |
162
- | `clipboard-error` | `{ action: 'copy' \| 'paste', error }` |
162
+ | `clipboard-error` | `{ action: 'copy', error }` — the context menu's Copy could not write; no `clipboard-copy` follows |
163
163
  | `fill-handle-apply` | `{ sourceRange, targetRange, cells }` |
164
164
  | `find-replace` | `{ type: 'replace' \| 'replace-all', cells }` |
165
165
  | `comment-change` | `{ dataIndex, colKey, text }` |
@@ -73,7 +73,7 @@ function Orders() {
73
73
  return (
74
74
  <>
75
75
  <input value={source.search} onChange={(e) => source.setSearch(e.target.value)} />
76
- {source.error && <p role="alert">{source.error}</p>}
76
+ {source.error && <p role="alert">{source.error.message}</p>}
77
77
  <FlexTableReact<Order>
78
78
  dataMode="server"
79
79
  columns={columns}
@@ -111,7 +111,7 @@ function Orders() {
111
111
  |---|---|
112
112
  | `data`, `totalCount` | Current page and `@odata.count`; `@odata.nextLink` is followed to fill a page |
113
113
  | `loading` | Request in flight |
114
- | `error` | `string \| null` — render it |
114
+ | `error` | `SourceError \| null` — `{ message, status?, code?, details?, body? }`; render `error.message`, branch on `status` / `code` (OData `error.code`) |
115
115
  | `page`, `setPage` | Zero-based |
116
116
  | `sortCriteria`, `onSortChange` | Pass `onSortChange` to the table's `sort-change` (resets to page 0) |
117
117
  | `search`, `setSearch` | Literal text (resets to page 0) |