@iyulab/flex-table 0.41.2 → 0.43.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,31 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.43.0] - 2026-10-04
4
+
5
+ ### Changed
6
+
7
+ - **Breaking: `useODataSource`'s `onUnauthorized` is called on `401` only.** It was also called on
8
+ `403`, so an app that sends the user to sign in from this hook (its documented use) bounced a
9
+ signed-in user without permission back to the sign-in page — and, after signing in, to the same
10
+ list and the same `403`. A `403` now surfaces only as `error`, like any other failed request.
11
+ Migration: if you handled `403` in `onUnauthorized`, read it from `error` instead.
12
+
13
+ ## [0.42.0] - 2026-10-04
14
+
15
+ ### Added
16
+
17
+ - **`reveal: 'hover'` column option** — the column's content shows only while its row is hovered,
18
+ holds focus or is selected; for a row-actions column, so a list does not repeat the same buttons
19
+ on every row. The content stays focusable (Tab reveals it), touch screens always show it, and it
20
+ is not printed.
21
+
22
+ ### Changed
23
+
24
+ - **The virtual scroll renders only the rows it needs above the view.** The first row in view was
25
+ computed as if the header and the frozen rows scrolled away with the body, so up to
26
+ (header + frozen rows) ÷ row height extra rows were rendered above the view — ten with eight frozen
27
+ rows. What is visible does not change.
28
+
3
29
  ## [0.41.2] - 2026-10-03
4
30
 
5
31
  ### Fixed
package/README.md CHANGED
@@ -151,6 +151,7 @@ interface ColumnDefinition {
151
151
  validator?: CellValidator; // Validate before commit: (value, row, col) => string | null
152
152
  conditionalRules?: ConditionalRule[]; // Per-cell style rules, see below
153
153
  mergeRepeated?: boolean | ((row, previousRow, col) => boolean); // Merge runs of repeated values, see below
154
+ reveal?: 'always' | 'hover'; // 'hover': show only while the row is hovered, focused or selected (row actions)
154
155
  }
155
156
  ```
156
157
 
@@ -198,6 +199,18 @@ const columns: ColumnDefinition<Order>[] = [
198
199
 
199
200
  Rules are evaluated in order and combined; later matching rules override earlier ones for overlapping style properties.
200
201
 
202
+ ### Row actions that appear on hover
203
+
204
+ A column of row actions (edit, a "more" menu) repeated on every row turns a list into a wall of
205
+ buttons. `reveal: 'hover'` shows that column's content only while its row is hovered, holds focus or
206
+ is selected. The controls stay in the DOM and focusable — Tab reveals them — touch screens (no
207
+ hover) always show them, and they are not printed.
208
+
209
+ ```ts
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>` }
212
+ ```
213
+
201
214
  ### Merging Repeated Values
202
215
 
203
216
  A list of child rows under a parent — order lines under an order, boxes under a shipment — repeats the parent's columns on every line. `mergeRepeated` draws each run of repeated values as one merged cell: the value shows once, on the run's first row, and the lines between the run's rows are dropped.
@@ -712,7 +725,7 @@ const source = useODataSource('/api/orders', {
712
725
  | `fixedFilter` | — | Filter always applied in addition to search. Changing it resets the page to 0 — see below |
713
726
  | `baseUrl` | `window.location.origin` | Override the request origin (proxy/BFF setups) |
714
727
  | `fetcher` | global `fetch` | Custom transport — pass a wrapper that injects auth headers |
715
- | `onUnauthorized` | — | Called on `401`/`403` responses, before the generic error is set |
728
+ | `onUnauthorized` | — | Called on `401` responses, before the generic error is set. A `403` (signed in, not permitted) does not call it — it surfaces as `error` |
716
729
  | `enabled` | `true` | While `false`, no request is made and `loading` stays `true` — see below |
717
730
 
718
731
  `fetcher`/`onUnauthorized` should be stable references (e.g. wrap in `useCallback`) — they are intentionally excluded from the hook's internal effect dependencies to avoid refetch loops on every render.
@@ -918,6 +918,28 @@ var y = r`
918
918
  outline: 2px solid var(--ft-find-color);
919
919
  outline-offset: -2px;
920
920
  }
921
+
922
+ /* reveal: 'hover' — a row-actions column shows while the row is hovered, focused or selected.
923
+ opacity (not display/visibility) keeps the controls focusable, so Tab reveals them. */
924
+ .ft-cell.ft-reveal-hover > * {
925
+ opacity: 0;
926
+ transition: opacity var(--u-duration-fast, 140ms) ease-out;
927
+ }
928
+ .ft-row:hover .ft-cell.ft-reveal-hover > *,
929
+ .ft-row:focus-within .ft-cell.ft-reveal-hover > *,
930
+ .ft-row-selected .ft-cell.ft-reveal-hover > *,
931
+ .ft-cell.ft-reveal-hover.ft-active > * {
932
+ opacity: 1;
933
+ }
934
+ @media (hover: none) {
935
+ .ft-cell.ft-reveal-hover > * { opacity: 1; }
936
+ }
937
+ @media (prefers-reduced-motion: reduce) {
938
+ .ft-cell.ft-reveal-hover > * { transition: none; }
939
+ }
940
+ @media print {
941
+ .ft-cell.ft-reveal-hover > * { visibility: hidden; }
942
+ }
921
943
  `;
922
944
  //#endregion
923
945
  //#region src/renderers/format.ts
@@ -2270,10 +2292,10 @@ var q = {
2270
2292
  return Math.max(0, this._visibleRowCount - this._frozenRowCount) * this.rowHeight;
2271
2293
  }
2272
2294
  get visibleRange() {
2273
- let e = this._frozenRowCount, t = Math.max(0, this._scrollTop - this.headerHeight - this.frozenRowsHeight), n = e + Math.floor(t / this.rowHeight), r = Math.max(e, n - Q), i = Math.ceil(this._viewportHeight / this.rowHeight);
2295
+ 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;
2274
2296
  return {
2275
- start: r,
2276
- end: Math.min(this._visibleRowCount, n + i + Q)
2297
+ start: n,
2298
+ end: Math.min(this._visibleRowCount, t + i + Q)
2277
2299
  };
2278
2300
  }
2279
2301
  connectedCallback() {
@@ -4232,7 +4254,8 @@ var q = {
4232
4254
  S ? "ft-find-current" : x ? "ft-find-match" : "",
4233
4255
  o ? `ft-merge-${o.headParity}` : "",
4234
4256
  o?.continues ? "ft-merge-continued" : "",
4235
- o?.opensDown ? "ft-merge-open" : ""
4257
+ o?.opensDown ? "ft-merge-open" : "",
4258
+ t.reveal === "hover" ? "ft-reveal-hover" : ""
4236
4259
  ].filter(Boolean).join(" "), T = this._toDataIndex(n), E = this._comments.get(T)?.get(t.key) ?? null, D = E !== null, O = D ? i`<div class="ft-comment-indicator" title=${E}></div>` : a;
4237
4260
  return i`
4238
4261
  <div class=${D ? C + " ft-has-comment" : C}
@@ -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-CZPubYVD.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-BdnkC234.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 };
@@ -115,6 +115,15 @@ export interface ColumnDefinition<T = DataRow> {
115
115
  * the top of the scrolled view and below frozen rows, so it never scrolls out of sight.
116
116
  */
117
117
  mergeRepeated?: boolean | ((row: T, previousRow: T, col: ColumnDefinition<T>) => boolean);
118
+ /**
119
+ * When the column's content shows.
120
+ * - `'always'` (default)
121
+ * - `'hover'`: only while the row is hovered, holds focus, or is selected — for a row-actions
122
+ * column (edit, a "more" menu), so a list does not repeat the same buttons on every row. The
123
+ * content stays in the DOM and keyboard focus reveals it; on touch screens (no hover) it is
124
+ * always shown, and it is not printed.
125
+ */
126
+ reveal?: 'always' | 'hover';
118
127
  }
119
128
  /** Style applied to a cell by a conditional formatting rule */
120
129
  export interface CellStyle {
@@ -23,7 +23,10 @@ export interface UseODataSourceOptions {
23
23
  baseUrl?: string;
24
24
  /** 커스텀 fetch transport(예: 인증 헤더를 주입하는 `HttpClient` 래퍼). 기본값: 전역 `fetch`. */
25
25
  fetcher?: (input: string, init: RequestInit) => Promise<Response>;
26
- /** 응답이 401/403일 때 호출(세션 만료 리다이렉트 등). 호출 후에도 기존 에러 처리는 계속 진행된다. */
26
+ /**
27
+ * 응답이 401 일 때 호출(세션 만료 리다이렉트 등). 호출 후에도 기존 에러 처리는 계속 진행된다.
28
+ * 403(인증됐지만 권한 없음)에는 부르지 않는다 — 그 실패는 `error` 로 드러난다.
29
+ */
27
30
  onUnauthorized?: (response: Response) => void;
28
31
  /**
29
32
  * `false` 인 동안 요청하지 않는다. 기본값 `true`.
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as e } from "./flex-table-CZPubYVD.js";
1
+ import { t as e } from "./flex-table-BdnkC234.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";
@@ -34,7 +34,7 @@ function d(e, r = {}) {
34
34
  }), a = `${m ?? window.location.origin}${e}${r}`, o = async (e) => {
35
35
  let n = await h(e, { signal: t.signal });
36
36
  if (!n.ok) {
37
- (n.status === 401 || n.status === 403) && g && g(n);
37
+ n.status === 401 && g && g(n);
38
38
  let e = await n.text().catch(() => ""), t = `Request failed (${n.status})`;
39
39
  try {
40
40
  let n = JSON.parse(e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iyulab/flex-table",
3
- "version": "0.41.2",
3
+ "version": "0.43.0",
4
4
  "description": "A minimalist, input-centric data grid web component",
5
5
  "type": "module",
6
6
  "main": "./dist/flex-table.js",
@@ -4,7 +4,6 @@ description: Spreadsheet-grade data grid web component (`<flex-table>`, built wi
4
4
  license: MIT
5
5
  metadata:
6
6
  author: iyulab
7
- version: "0.40.1"
8
7
  ---
9
8
 
10
9
  # @iyulab/flex-table
@@ -66,6 +66,7 @@ Replacing `data` with the same array reference does not trigger the clear-on-cha
66
66
  | `validator` | `CellValidator<T>` | `(value, row, col) => string \| null \| undefined` |
67
67
  | `conditionalRules` | `ConditionalRule<T>[]` | `{ when(value, row, col): boolean, style: CellStyle }` |
68
68
  | `mergeRepeated` | `boolean \| (row, previousRow, col) => boolean` | Draw a run of repeated values as one merged cell; values, sort, filter and export are unchanged |
69
+ | `reveal` | `'always' \| 'hover'` | `'hover'`: content shows only while the row is hovered, focused or selected — for a row-actions column; stays focusable, always shown without hover, not printed |
69
70
 
70
71
  `CellStyle` = `{ background?, color?, fontWeight?: 'bold' | 'normal', fontStyle?: 'italic' | 'normal' }`.
71
72
 
@@ -102,7 +102,7 @@ function Orders() {
102
102
  | `fixedFilter` | — | odata-query filter object, always applied; value change resets to page 0 (compared by value) |
103
103
  | `baseUrl` | `window.location.origin` | For proxy/BFF setups |
104
104
  | `fetcher` | global `fetch` | `(input, init) => Promise<Response>`; keep stable |
105
- | `onUnauthorized` | — | `(response) => void` on 401/403; keep stable |
105
+ | `onUnauthorized` | — | `(response) => void` on 401 only (403 surfaces as `error`); keep stable |
106
106
  | `enabled` | `true` | While `false`: no request, `loading` stays `true`, `refresh()` is a no-op, in-flight request is cancelled |
107
107
 
108
108
  ### Result