@iyulab/flex-table 0.22.0 → 0.23.1

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
@@ -67,7 +67,7 @@ npm install @iyulab/flex-table
67
67
  |----------|-----------|------|---------|-------------|
68
68
  | `columns` | — | `ColumnDefinition[]` | `[]` | Column definitions |
69
69
  | `data` | — | `DataRow[]` | `[]` | Data rows (`Record<string, unknown>[]`) |
70
- | `rowHeight` | `row-height` | `number` | `32` | Row height in pixels |
70
+ | `rowHeight` | `row-height` | `number` | `32` | Row height in pixels. Falls back to the `--ft-row-height` token when not set — see [Density](#density-and-header-hierarchy) |
71
71
  | `showRowNumbers` | `show-row-numbers` | `boolean` | `false` | Show row number column |
72
72
  | `theme` | `theme` | `'light' \| 'dark'` | auto | Force theme; auto-detects `prefers-color-scheme` |
73
73
  | `editable` | `editable` | `boolean` | `true` | Global read-only mode when `false` |
@@ -314,6 +314,33 @@ flex-table {
314
314
  }
315
315
  ```
316
316
 
317
+ ### Density and header hierarchy
318
+
319
+ Since 0.23.0 the vertical rhythm and the header's typographic weight are adjustable, so a
320
+ table can be matched to the other tables on the page:
321
+
322
+ ```css
323
+ flex-table {
324
+ --ft-row-height: 32px; /* body row height */
325
+ --ft-cell-padding-block: 6px; /* text placement inside that height */
326
+ --ft-cell-padding-inline: 12px;
327
+ --ft-header-font-size: 14px; /* defaults to --ft-font-size */
328
+ --ft-header-font-weight: 600;
329
+ }
330
+ ```
331
+
332
+ ⚠ **`--ft-row-height` is read once, at first render.** The grid is virtualised — rows are
333
+ positioned at `index × rowHeight`, so the height cannot come from the cascade the way
334
+ padding does. Declare it in a stylesheet that is in effect before the table is attached; to
335
+ change it later, set the `rowHeight` property (or the `row-height` attribute) instead. An
336
+ explicitly set property or attribute always wins over the token, and a value in any unit
337
+ other than `px` is ignored.
338
+
339
+ ⚠ **Row height is fixed, and padding places the text within it.** Cells are single-line
340
+ (`nowrap` + ellipsis) by design. If you reduce `--ft-row-height`, reduce
341
+ `--ft-cell-padding-block` to match — keep `padding-block × 2 + line box ≤ row-height`, or
342
+ the text is clipped at the bottom.
343
+
317
344
  **Without the token sheet nothing changes.** Every reference carries the literal above as
318
345
  its fallback, and the built-in dark theme (`prefers-color-scheme` or `theme="dark"`) still
319
346
  applies — the table remains usable standalone. Referencing the tokens does not add a
@@ -6,24 +6,38 @@ var s = t`
6
6
  :host {
7
7
  --ft-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
8
8
  --ft-font-size: 14px;
9
- --ft-border-color: var(--u-border-color, #e0e0e0);
10
- --ft-bg: var(--u-bg-color, #fff);
11
- --ft-text-color: var(--u-txt-color, #202124);
12
- --ft-header-bg: var(--u-bg-color-raised, #f8f9fa);
13
- --ft-header-hover-bg: var(--u-bg-color-active, #e8eaed);
14
- --ft-header-text-color: var(--u-txt-color, #202124);
15
- --ft-row-even-bg: var(--u-bg-color, #fff);
16
- --ft-row-odd-bg: var(--u-bg-color-raised, #fafafa);
17
- --ft-row-hover-bg: var(--u-bg-color-hover, #f0f4ff);
18
- --ft-active-color: var(--u-primary-color, #1a73e8);
19
- --ft-selection-bg: var(--u-primary-bg-color, #e8f0fe);
20
- --ft-bool-color: var(--u-primary-color, #2196f3);
21
- --ft-sort-indicator-color: var(--u-txt-color-weak, #5f6368);
22
- --ft-editor-bg: var(--u-input-bg-color, #fff);
9
+
10
+ /* --- 치수·위계 축 (기본값 = 종전 렌더값) ---
11
+ 표는 가상 스크롤이라 행 높이가 **JS 로 계산된다**(index * rowHeight).
12
+ 그래서 --ft-row-height CSS 만으로는 닿지 않고, 컴포넌트가 첫 렌더에서
13
+ 판독해 가상화 계산에 넣는다. 그 결과가 rowHeight 프로퍼티의 기본값이다.
14
+
15
+ ⚠여백과 높이의 관계: 행 높이는 **고정**이고 여백은 그 안에서 글자를
16
+ 배치한다. 높이를 줄이면서 여백을 그대로 두면 아래가 잘린다
17
+ (padding-block * 2 + 한 줄 높이 <= row-height 유지할 것). */
18
+ --ft-row-height: 32px;
19
+ --ft-cell-padding-block: 6px;
20
+ --ft-cell-padding-inline: 12px;
21
+ --ft-header-font-size: var(--ft-font-size);
22
+ --ft-header-font-weight: 600;
23
+ --ft-border-color: var(--u-border-color, #E0E0E0);
24
+ --ft-bg: var(--u-bg-color, #FFFFFF);
25
+ --ft-text-color: var(--u-txt-color, #212121);
26
+ --ft-header-bg: var(--u-bg-color-raised, #FAFAFA);
27
+ --ft-header-hover-bg: var(--u-bg-color-active, #EEEEEE);
28
+ --ft-header-text-color: var(--u-txt-color, #212121);
29
+ --ft-row-even-bg: var(--u-bg-color, #FFFFFF);
30
+ --ft-row-odd-bg: var(--u-bg-color-raised, #FAFAFA);
31
+ --ft-row-hover-bg: var(--u-bg-color-hover, #F5F5F5);
32
+ --ft-active-color: var(--u-primary-color, #1976D2);
33
+ --ft-selection-bg: var(--u-primary-bg-color, #E3F2FD);
34
+ --ft-bool-color: var(--u-primary-color, #1976D2);
35
+ --ft-sort-indicator-color: var(--u-txt-color-weak, #757575);
36
+ --ft-editor-bg: var(--u-input-bg-color, #FFFFFF);
23
37
  /* #999 was 2.85 against --ft-bg (#fff); AA needs 4.5. The dark value (#9aa0a6 on
24
38
  #1e1e2e = 6.31) was already fine, so this was a light-only defect. #5f6368 is the
25
39
  same grey the sort indicator uses, which keeps the palette to one family. */
26
- --ft-empty-color: var(--u-txt-color-weak, #5f6368);
40
+ --ft-empty-color: var(--u-txt-color-weak, #757575);
27
41
 
28
42
  /* 상태 색 — 종전에는 규칙 안에 리터럴로 박혀 있어 소비자가 덮을 경로가 없었고
29
43
  테마도 따라오지 않았다(다크에서 라이트용 값 그대로).
@@ -32,11 +46,11 @@ var s = t`
32
46
  아래에 있고 그것이 비쳐야 "어느 행의 어떤 셀"인지 읽힌다. 그래서 면 토큰
33
47
  (--u-*-bg-color)이 아니라 **상태 색에서 알파를 뽑는다** — 부수 효과로 두 테마
34
48
  모두에서 성립하므로 아래 다크 블록에 이 셋을 다시 적을 필요가 없다. */
35
- --ft-invalid-color: var(--u-danger-color, #d93025);
49
+ --ft-invalid-color: var(--u-danger-color, #D32F2F);
36
50
  --ft-invalid-bg: color-mix(in srgb, var(--ft-invalid-color) 8%, transparent);
37
- --ft-drop-color: var(--u-primary-color, #1a73e8);
51
+ --ft-drop-color: var(--u-primary-color, #1976D2);
38
52
  --ft-drop-bg: color-mix(in srgb, var(--ft-drop-color) 15%, transparent);
39
- --ft-find-color: var(--u-warning-color, #f9a825);
53
+ --ft-find-color: var(--u-warning-color, #FDD835);
40
54
  --ft-find-bg: color-mix(in srgb, var(--ft-find-color) 25%, transparent);
41
55
  --ft-find-current-bg: color-mix(in srgb, var(--ft-find-color) 50%, transparent);
42
56
  }
@@ -49,41 +63,41 @@ var s = t`
49
63
  ⇒ 즉 이 표는 시트 없이도 종전처럼 자체 테마를 갖는다(그것이 이 패키지의 계약이었다). */
50
64
  @media (prefers-color-scheme: dark) {
51
65
  :host(:not([theme="light"])) {
52
- --ft-border-color: var(--u-border-color, #3c4043);
53
- --ft-bg: var(--u-bg-color, #1e1e1e);
54
- --ft-text-color: var(--u-txt-color, #e8eaed);
55
- --ft-header-bg: var(--u-bg-color-raised, #292a2d);
56
- --ft-header-hover-bg: var(--u-bg-color-active, #3c4043);
57
- --ft-header-text-color: var(--u-txt-color, #e8eaed);
58
- --ft-row-even-bg: var(--u-bg-color, #1e1e1e);
59
- --ft-row-odd-bg: var(--u-bg-color-raised, #252526);
60
- --ft-row-hover-bg: var(--u-bg-color-hover, #2a2d2e);
61
- --ft-active-color: var(--u-primary-color, #4da3ff);
62
- --ft-selection-bg: var(--u-primary-bg-color, #264f78);
63
- --ft-bool-color: var(--u-primary-color, #64b5f6);
64
- --ft-sort-indicator-color: var(--u-txt-color-weak, #9aa0a6);
65
- --ft-editor-bg: var(--u-input-bg-color, #2d2d2d);
66
- --ft-empty-color: var(--u-txt-color-weak, #9aa0a6);
66
+ --ft-border-color: var(--u-border-color, #E0E0E0);
67
+ --ft-bg: var(--u-bg-color, #FFFFFF);
68
+ --ft-text-color: var(--u-txt-color, #212121);
69
+ --ft-header-bg: var(--u-bg-color-raised, #FAFAFA);
70
+ --ft-header-hover-bg: var(--u-bg-color-active, #EEEEEE);
71
+ --ft-header-text-color: var(--u-txt-color, #212121);
72
+ --ft-row-even-bg: var(--u-bg-color, #FFFFFF);
73
+ --ft-row-odd-bg: var(--u-bg-color-raised, #FAFAFA);
74
+ --ft-row-hover-bg: var(--u-bg-color-hover, #F5F5F5);
75
+ --ft-active-color: var(--u-primary-color, #1976D2);
76
+ --ft-selection-bg: var(--u-primary-bg-color, #E3F2FD);
77
+ --ft-bool-color: var(--u-primary-color, #1976D2);
78
+ --ft-sort-indicator-color: var(--u-txt-color-weak, #757575);
79
+ --ft-editor-bg: var(--u-input-bg-color, #FFFFFF);
80
+ --ft-empty-color: var(--u-txt-color-weak, #757575);
67
81
  }
68
82
  }
69
83
 
70
84
  /* --- Force dark via attribute --- */
71
85
  :host([theme="dark"]) {
72
- --ft-border-color: var(--u-border-color, #3c4043);
73
- --ft-bg: var(--u-bg-color, #1e1e1e);
74
- --ft-text-color: var(--u-txt-color, #e8eaed);
75
- --ft-header-bg: var(--u-bg-color-raised, #292a2d);
76
- --ft-header-hover-bg: var(--u-bg-color-active, #3c4043);
77
- --ft-header-text-color: var(--u-txt-color, #e8eaed);
78
- --ft-row-even-bg: var(--u-bg-color, #1e1e1e);
79
- --ft-row-odd-bg: var(--u-bg-color-raised, #252526);
80
- --ft-row-hover-bg: var(--u-bg-color-hover, #2a2d2e);
81
- --ft-active-color: var(--u-primary-color, #4da3ff);
82
- --ft-selection-bg: var(--u-primary-bg-color, #264f78);
83
- --ft-bool-color: var(--u-primary-color, #64b5f6);
84
- --ft-sort-indicator-color: var(--u-txt-color-weak, #9aa0a6);
85
- --ft-editor-bg: var(--u-input-bg-color, #2d2d2d);
86
- --ft-empty-color: var(--u-txt-color-weak, #9aa0a6);
86
+ --ft-border-color: var(--u-border-color, #E0E0E0);
87
+ --ft-bg: var(--u-bg-color, #FFFFFF);
88
+ --ft-text-color: var(--u-txt-color, #212121);
89
+ --ft-header-bg: var(--u-bg-color-raised, #FAFAFA);
90
+ --ft-header-hover-bg: var(--u-bg-color-active, #EEEEEE);
91
+ --ft-header-text-color: var(--u-txt-color, #212121);
92
+ --ft-row-even-bg: var(--u-bg-color, #FFFFFF);
93
+ --ft-row-odd-bg: var(--u-bg-color-raised, #FAFAFA);
94
+ --ft-row-hover-bg: var(--u-bg-color-hover, #F5F5F5);
95
+ --ft-active-color: var(--u-primary-color, #1976D2);
96
+ --ft-selection-bg: var(--u-primary-bg-color, #E3F2FD);
97
+ --ft-bool-color: var(--u-primary-color, #1976D2);
98
+ --ft-sort-indicator-color: var(--u-txt-color-weak, #757575);
99
+ --ft-editor-bg: var(--u-input-bg-color, #FFFFFF);
100
+ --ft-empty-color: var(--u-txt-color-weak, #757575);
87
101
  }
88
102
 
89
103
  /* --- Layout --- */
@@ -109,10 +123,12 @@ var s = t`
109
123
  .ft-header-cell {
110
124
  position: absolute;
111
125
  top: 0;
112
- padding: 8px 12px;
126
+ /* 머리행은 종전에도 본문보다 상하 2px 넉넉했다 — 그 관계를 유지한다. */
127
+ padding: calc(var(--ft-cell-padding-block) + 2px) var(--ft-cell-padding-inline);
113
128
  background: var(--ft-header-bg);
114
129
  color: var(--ft-header-text-color);
115
- font-weight: 600;
130
+ font-size: var(--ft-header-font-size);
131
+ font-weight: var(--ft-header-font-weight);
116
132
  user-select: none;
117
133
  border-bottom: 2px solid var(--ft-border-color);
118
134
  overflow: hidden;
@@ -214,7 +230,7 @@ var s = t`
214
230
  .ft-cell {
215
231
  position: absolute;
216
232
  top: 0;
217
- padding: 6px 12px;
233
+ padding: var(--ft-cell-padding-block) var(--ft-cell-padding-inline);
218
234
  border-bottom: 1px solid var(--ft-border-color);
219
235
  overflow: hidden;
220
236
  text-overflow: ellipsis;
@@ -248,7 +264,8 @@ var s = t`
248
264
  .ft-editor {
249
265
  width: 100%; height: 100%;
250
266
  border: none; outline: none;
251
- padding: 6px 12px;
267
+ /* 편집기는 셀 위에 겹쳐 뜬다 — 여백이 어긋나면 글자가 튄다. */
268
+ padding: var(--ft-cell-padding-block) var(--ft-cell-padding-inline);
252
269
  font: inherit;
253
270
  color: var(--ft-text-color);
254
271
  background: var(--ft-editor-bg);
@@ -1310,7 +1327,7 @@ function oe(e, t) {
1310
1327
  <sheetData>${n.join("")}</sheetData>
1311
1328
  </worksheet>`;
1312
1329
  }
1313
- function z(e, t) {
1330
+ function se(e, t) {
1314
1331
  let n = oe(e, t);
1315
1332
  return ne([
1316
1333
  {
@@ -1341,24 +1358,24 @@ function z(e, t) {
1341
1358
  }
1342
1359
  //#endregion
1343
1360
  //#region src/export/export.ts
1344
- function B(e, t, n) {
1361
+ function z(e, t, n) {
1345
1362
  switch (n) {
1346
- case "csv": return H(e, t, ",");
1347
- case "tsv": return H(e, t, " ");
1348
- case "json": return W(e, t);
1349
- case "xlsx": return z(e, t);
1363
+ case "csv": return V(e, t, ",");
1364
+ case "tsv": return V(e, t, " ");
1365
+ case "json": return U(e, t);
1366
+ case "xlsx": return se(e, t);
1350
1367
  }
1351
1368
  }
1352
- function V(e, t) {
1369
+ function B(e, t) {
1353
1370
  return e == null ? "" : (t.type === "date" || t.type === "datetime") && e instanceof Date ? e.toISOString() : String(e);
1354
1371
  }
1355
- function H(e, t, n) {
1356
- return [t.map((e) => U(e.header, n)).join(n), ...e.map((e) => t.map((t) => U(V(e[t.key], t), n)).join(n))].join("\n");
1372
+ function V(e, t, n) {
1373
+ return [t.map((e) => H(e.header, n)).join(n), ...e.map((e) => t.map((t) => H(B(e[t.key], t), n)).join(n))].join("\n");
1357
1374
  }
1358
- function U(e, t) {
1375
+ function H(e, t) {
1359
1376
  return e.includes(t) || e.includes("\"") || e.includes("\n") || e.includes("\r") ? "\"" + e.replace(/"/g, "\"\"") + "\"" : e;
1360
1377
  }
1361
- function W(e, t) {
1378
+ function U(e, t) {
1362
1379
  let n = e.map((e) => {
1363
1380
  let n = {};
1364
1381
  for (let r of t) {
@@ -1369,35 +1386,35 @@ function W(e, t) {
1369
1386
  });
1370
1387
  return JSON.stringify(n, null, 2);
1371
1388
  }
1372
- function G(e, t, n) {
1389
+ function W(e, t, n) {
1373
1390
  let r = new Blob([e], { type: n }), i = URL.createObjectURL(r), a = document.createElement("a");
1374
1391
  a.href = i, a.download = t, a.style.display = "none", document.body.appendChild(a), a.click(), document.body.removeChild(a), setTimeout(() => URL.revokeObjectURL(i), 100);
1375
1392
  }
1376
- var K = {
1393
+ var G = {
1377
1394
  csv: "text/csv;charset=utf-8",
1378
1395
  tsv: "text/tab-separated-values;charset=utf-8",
1379
1396
  json: "application/json;charset=utf-8",
1380
1397
  xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
1381
- }, se = {
1398
+ }, ce = {
1382
1399
  csv: ".csv",
1383
1400
  tsv: ".tsv",
1384
1401
  json: ".json",
1385
1402
  xlsx: ".xlsx"
1386
1403
  };
1387
- function ce(e) {
1388
- return K[e];
1389
- }
1390
1404
  function le(e) {
1391
- return se[e];
1405
+ return G[e];
1406
+ }
1407
+ function ue(e) {
1408
+ return ce[e];
1392
1409
  }
1393
1410
  //#endregion
1394
1411
  //#region src/export/xlsx-reader.ts
1395
- function ue(e) {
1412
+ function de(e) {
1396
1413
  for (let t = e.byteLength - 22; t >= 0; t--) if (e.getUint32(t, !0) === 101010256) return t;
1397
1414
  throw Error("XLSX: End of central directory not found — not a valid ZIP file");
1398
1415
  }
1399
- function de(e) {
1400
- let t = ue(e), n = e.getUint32(t + 16, !0), r = e.getUint32(t + 12, !0), i = new TextDecoder("utf-8"), a = [], o = n, s = n + r;
1416
+ function fe(e) {
1417
+ let t = de(e), n = e.getUint32(t + 16, !0), r = e.getUint32(t + 12, !0), i = new TextDecoder("utf-8"), a = [], o = n, s = n + r;
1401
1418
  for (; o < s && e.getUint32(o, !0) === 33639248;) {
1402
1419
  let t = e.getUint16(o + 10, !0), n = e.getUint32(o + 20, !0), r = e.getUint32(o + 24, !0), s = e.getUint16(o + 28, !0), c = e.getUint16(o + 30, !0), l = e.getUint16(o + 32, !0), u = e.getUint32(o + 42, !0), d = i.decode(new Uint8Array(e.buffer, o + 46, s));
1403
1420
  a.push({
@@ -1410,7 +1427,7 @@ function de(e) {
1410
1427
  }
1411
1428
  return a;
1412
1429
  }
1413
- async function fe(e) {
1430
+ async function pe(e) {
1414
1431
  let t = new DecompressionStream("deflate-raw"), n = t.writable.getWriter(), r = t.readable.getReader();
1415
1432
  n.write(e), n.close();
1416
1433
  let i = [];
@@ -1423,40 +1440,40 @@ async function fe(e) {
1423
1440
  for (let e of i) o.set(e, s), s += e.length;
1424
1441
  return o;
1425
1442
  }
1426
- async function pe(e, t) {
1443
+ async function me(e, t) {
1427
1444
  let n = t.localHeaderOffset, r = e.getUint16(n + 26, !0), i = e.getUint16(n + 28, !0), a = n + 30 + r + i, o = new Uint8Array(e.buffer, a, t.compressedSize);
1428
1445
  if (t.compressionMethod === 0) return o.slice();
1429
- if (t.compressionMethod === 8) return fe(o);
1446
+ if (t.compressionMethod === 8) return pe(o);
1430
1447
  throw Error(`XLSX: Unsupported compression method ${t.compressionMethod}`);
1431
1448
  }
1432
- async function me(e) {
1433
- let t = new DataView(e), n = de(t), r = /* @__PURE__ */ new Map();
1434
- for (let e of n) e.compressedSize === 0 && e.name.endsWith("/") || r.set(e.name, await pe(t, e));
1449
+ async function he(e) {
1450
+ let t = new DataView(e), n = fe(t), r = /* @__PURE__ */ new Map();
1451
+ for (let e of n) e.compressedSize === 0 && e.name.endsWith("/") || r.set(e.name, await me(t, e));
1435
1452
  return r;
1436
1453
  }
1437
- var he = new TextDecoder("utf-8");
1438
- function q(e) {
1439
- let t = he.decode(e);
1454
+ var ge = new TextDecoder("utf-8");
1455
+ function K(e) {
1456
+ let t = ge.decode(e);
1440
1457
  return new DOMParser().parseFromString(t, "application/xml");
1441
1458
  }
1442
- function ge(e) {
1459
+ function _e(e) {
1443
1460
  if (!e) return [];
1444
- let t = q(e);
1461
+ let t = K(e);
1445
1462
  return Array.from(t.querySelectorAll("si")).map((e) => Array.from(e.querySelectorAll("t")).map((e) => e.textContent ?? "").join(""));
1446
1463
  }
1447
- function _e(e) {
1464
+ function ve(e) {
1448
1465
  let t = (e - 1) * 864e5 + Date.UTC(1899, 11, 30);
1449
1466
  return new Date(t).toISOString().slice(0, 10);
1450
1467
  }
1451
- function ve(e, t) {
1452
- let n = q(e).querySelectorAll("row");
1468
+ function ye(e, t) {
1469
+ let n = K(e).querySelectorAll("row");
1453
1470
  if (n.length === 0) return [];
1454
1471
  let r = 0, i = /* @__PURE__ */ new Map();
1455
1472
  for (let e of n) {
1456
1473
  let n = parseInt(e.getAttribute("r") ?? "1", 10) - 1, a = /* @__PURE__ */ new Map();
1457
1474
  i.set(n, a);
1458
1475
  for (let n of e.querySelectorAll("c")) {
1459
- let e = ye(n.getAttribute("r") ?? "");
1476
+ let e = be(n.getAttribute("r") ?? "");
1460
1477
  e > r && (r = e);
1461
1478
  let i = n.getAttribute("t") ?? "", o = n.getAttribute("s") ?? "", s = n.querySelector("v")?.textContent ?? "", c = n.querySelector("is > t")?.textContent ?? null, l;
1462
1479
  if (i === "s") l = t[parseInt(s, 10)] ?? "";
@@ -1465,7 +1482,7 @@ function ve(e, t) {
1465
1482
  else if (s === "") l = "";
1466
1483
  else {
1467
1484
  let e = parseFloat(s);
1468
- l = !isNaN(e) && o !== "" && parseInt(o, 10) > 0 && e >= 1 && e <= 73050 ? _e(e) : s;
1485
+ l = !isNaN(e) && o !== "" && parseInt(o, 10) > 0 && e >= 1 && e <= 73050 ? ve(e) : s;
1469
1486
  }
1470
1487
  a.set(e, l);
1471
1488
  }
@@ -1479,13 +1496,13 @@ function ve(e, t) {
1479
1496
  }
1480
1497
  return o;
1481
1498
  }
1482
- function ye(e) {
1499
+ function be(e) {
1483
1500
  let t = 0, n = 0;
1484
1501
  for (; n < e.length && e[n] >= "A" && e[n] <= "Z";) t = t * 26 + (e.charCodeAt(n) - 64), n++;
1485
1502
  return t - 1;
1486
1503
  }
1487
- async function be(e) {
1488
- let t = await me(e), n = ge(t.get("xl/sharedStrings.xml")), r = t.get("xl/worksheets/sheet1.xml");
1504
+ async function xe(e) {
1505
+ let t = await he(e), n = _e(t.get("xl/sharedStrings.xml")), r = t.get("xl/worksheets/sheet1.xml");
1489
1506
  if (!r) {
1490
1507
  for (let [e, n] of t) if (e.toLowerCase().includes("worksheets/sheet")) {
1491
1508
  r = n;
@@ -1493,7 +1510,7 @@ async function be(e) {
1493
1510
  }
1494
1511
  }
1495
1512
  if (!r) throw Error("XLSX: No worksheet found");
1496
- let i = ve(r, n);
1513
+ let i = ye(r, n);
1497
1514
  return i.length === 0 ? {
1498
1515
  headers: [],
1499
1516
  rows: []
@@ -1504,7 +1521,7 @@ async function be(e) {
1504
1521
  }
1505
1522
  //#endregion
1506
1523
  //#region \0@oxc-project+runtime@0.142.0/helpers/esm/decorate.js
1507
- function J(e, t, n, r) {
1524
+ function q(e, t, n, r) {
1508
1525
  var i = arguments.length, a = i < 3 ? t : r === null ? r = Object.getOwnPropertyDescriptor(t, n) : r, o;
1509
1526
  if (typeof Reflect == "object" && typeof Reflect.decorate == "function") a = Reflect.decorate(e, t, n, r);
1510
1527
  else for (var s = e.length - 1; s >= 0; s--) (o = e[s]) && (a = (i < 3 ? o(a) : i > 3 ? o(t, n, a) : o(t, n)) || a);
@@ -1512,16 +1529,16 @@ function J(e, t, n, r) {
1512
1529
  }
1513
1530
  //#endregion
1514
1531
  //#region src/flex-table.ts
1515
- var Y = {
1532
+ var J = {
1516
1533
  eq: "=",
1517
1534
  neq: "≠",
1518
1535
  gt: ">",
1519
1536
  lt: "<",
1520
1537
  gte: "≥",
1521
1538
  lte: "≤"
1522
- }, X = 120, Z = 40, xe = 32, Q = 5, $ = class extends e {
1539
+ }, Y = 120, X = 40, Z = 32, Q = 5, $ = class extends e {
1523
1540
  constructor(...e) {
1524
- 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 = () => {
1541
+ 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.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 = () => {
1525
1542
  this._commitCommentPopup();
1526
1543
  }, 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();
1527
1544
  }
@@ -1535,6 +1552,13 @@ var Y = {
1535
1552
  let t = this._data, n = e !== t;
1536
1553
  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);
1537
1554
  }
1555
+ get rowHeight() {
1556
+ return this._rowHeightExplicit ?? this._rowHeightFromCss ?? Z;
1557
+ }
1558
+ set rowHeight(e) {
1559
+ let t = this.rowHeight;
1560
+ this._rowHeightExplicit = e, this.requestUpdate("rowHeight", t);
1561
+ }
1538
1562
  set selectionMode(e) {
1539
1563
  this._rowSelection.mode = e, this.requestUpdate();
1540
1564
  }
@@ -1870,12 +1894,12 @@ var Y = {
1870
1894
  if (!t) return "";
1871
1895
  let n = this.visibleColumns.slice(t.startCol, t.endCol + 1), r = [];
1872
1896
  for (let e = t.startRow; e <= t.endRow; e++) r.push(this.data[this._toDataIndex(e)]);
1873
- return B(r, n, e);
1897
+ return z(r, n, e);
1874
1898
  }
1875
- return B(this._sortedIndices.map((e) => this.data[e]), this.visibleColumns, e);
1899
+ return z(this._sortedIndices.map((e) => this.data[e]), this.visibleColumns, e);
1876
1900
  }
1877
1901
  exportToFile(e, t) {
1878
- G(this.exportToString(e), t ?? `export${le(e)}`, ce(e));
1902
+ W(this.exportToString(e), t ?? `export${ue(e)}`, le(e));
1879
1903
  }
1880
1904
  setComment(e, t, n) {
1881
1905
  let r = this.getComment(e, t);
@@ -1932,7 +1956,7 @@ var Y = {
1932
1956
  async importFromFile(e) {
1933
1957
  let t = e.name.toLowerCase();
1934
1958
  if (t.endsWith(".xlsx")) {
1935
- let t = await be(await e.arrayBuffer());
1959
+ let t = await xe(await e.arrayBuffer());
1936
1960
  this._applyImportedSheet(t);
1937
1961
  } else if (t.endsWith(".csv") || t.endsWith(".tsv")) {
1938
1962
  let t = O(await e.text());
@@ -2024,8 +2048,8 @@ var Y = {
2024
2048
  return this._columnWidths.get(e);
2025
2049
  }
2026
2050
  _getColWidth(e) {
2027
- let t = this._columnWidths.get(e.key) ?? e.width ?? X;
2028
- return Math.max(t, e.minWidth ?? Z);
2051
+ let t = this._columnWidths.get(e.key) ?? e.width ?? Y;
2052
+ return Math.max(t, e.minWidth ?? X);
2029
2053
  }
2030
2054
  get headerHeight() {
2031
2055
  return this.rowHeight + 8;
@@ -2056,7 +2080,15 @@ var Y = {
2056
2080
  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);
2057
2081
  }
2058
2082
  firstUpdated() {
2059
- this._measureViewport();
2083
+ this._readDensityTokens(), this._measureViewport();
2084
+ }
2085
+ _readDensityTokens() {
2086
+ let e = getComputedStyle(this).getPropertyValue("--ft-row-height").trim();
2087
+ if (!e) return;
2088
+ let t = parseFloat(e);
2089
+ if (!Number.isFinite(t) || t <= 0 || !/px\s*$/.test(e) || t === this._rowHeightFromCss) return;
2090
+ let n = this.rowHeight;
2091
+ this._rowHeightFromCss = t, this._rowHeightExplicit === void 0 && this.requestUpdate("rowHeight", n);
2060
2092
  }
2061
2093
  _updateColOffsets() {
2062
2094
  let e = this.visibleColumns, t = [], n = this._prefixWidth;
@@ -2438,7 +2470,7 @@ var Y = {
2438
2470
  e.preventDefault();
2439
2471
  let n = t[this._activeCell.col];
2440
2472
  if (!n) return !0;
2441
- let r = this._columnWidths.get(n.key) ?? n.width ?? X, i = e.key === "ArrowRight" ? 20 : -20, a = n.minWidth ?? Z, o = Math.max(a, r + i);
2473
+ let r = this._columnWidths.get(n.key) ?? n.width ?? Y, i = e.key === "ArrowRight" ? 20 : -20, a = n.minWidth ?? X, o = Math.max(a, r + i);
2442
2474
  return this._columnWidths.set(n.key, o), this.requestUpdate(), this.dispatchEvent(new CustomEvent("column-resize", {
2443
2475
  detail: {
2444
2476
  key: n.key,
@@ -3173,7 +3205,7 @@ var Y = {
3173
3205
  @keydown=${(e) => {
3174
3206
  e.key === "Escape" && (this._openFilterKey = null), e.stopPropagation();
3175
3207
  }}>
3176
- ${Object.keys(Y).map((e) => n`<option value=${e}>${Y[e]}</option>`)}
3208
+ ${Object.keys(J).map((e) => n`<option value=${e}>${J[e]}</option>`)}
3177
3209
  </select>
3178
3210
  <input class="ft-filter-input ft-num-cond-input" type="number" placeholder="Value"
3179
3211
  .value=${r.value == null ? "" : String(r.value)}
@@ -3284,7 +3316,7 @@ var Y = {
3284
3316
  if (!n) return;
3285
3317
  let r = this.shadowRoot?.querySelectorAll(`.ft-cell[data-col-index="${t}"]`), i = 0, a = this.shadowRoot?.querySelector(`.ft-header-cell[data-col-index="${t}"]`);
3286
3318
  if (a && (i = Math.max(i, a.scrollWidth)), r) for (let e of r) i = Math.max(i, e.scrollWidth);
3287
- let o = n.minWidth ?? Z, s = Math.max(o, i + 8);
3319
+ let o = n.minWidth ?? X, s = Math.max(o, i + 8);
3288
3320
  this._columnWidths.set(n.key, s), this.requestUpdate(), this.dispatchEvent(new CustomEvent("column-resize", {
3289
3321
  detail: {
3290
3322
  key: n.key,
@@ -3297,7 +3329,7 @@ var Y = {
3297
3329
  }
3298
3330
  _onResizeStart(e, t) {
3299
3331
  e.preventDefault(), e.stopPropagation();
3300
- let n = this.visibleColumns[t], r = this._columnWidths.get(n.key) ?? n.width ?? X;
3332
+ let n = this.visibleColumns[t], r = this._columnWidths.get(n.key) ?? n.width ?? Y;
3301
3333
  this._resizing = {
3302
3334
  colIndex: t,
3303
3335
  startX: e.clientX,
@@ -3305,13 +3337,13 @@ var Y = {
3305
3337
  };
3306
3338
  let i = (e) => {
3307
3339
  if (!this._resizing) return;
3308
- let t = e.clientX - this._resizing.startX, r = n.minWidth ?? Z, i = Math.max(r, this._resizing.startWidth + t);
3340
+ let t = e.clientX - this._resizing.startX, r = n.minWidth ?? X, i = Math.max(r, this._resizing.startWidth + t);
3309
3341
  this._columnWidths.set(n.key, i), this.requestUpdate();
3310
3342
  }, a = () => {
3311
3343
  document.removeEventListener("mousemove", i), document.removeEventListener("mouseup", o), this._resizeCleanup = null;
3312
3344
  }, o = () => {
3313
3345
  if (a(), this._resizing) {
3314
- let e = this._columnWidths.get(n.key) ?? X;
3346
+ let e = this._columnWidths.get(n.key) ?? Y;
3315
3347
  this.dispatchEvent(new CustomEvent("column-resize", {
3316
3348
  detail: {
3317
3349
  key: n.key,
@@ -3972,54 +4004,54 @@ var Y = {
3972
4004
  return isNaN(t.getTime()) ? "" : `${t.getFullYear()}-${String(t.getMonth() + 1).padStart(2, "0")}-${String(t.getDate()).padStart(2, "0")}T${String(t.getHours()).padStart(2, "0")}:${String(t.getMinutes()).padStart(2, "0")}`;
3973
4005
  }
3974
4006
  };
3975
- J([a({ type: Array })], $.prototype, "columns", void 0), J([a({
4007
+ q([a({ type: Array })], $.prototype, "columns", void 0), q([a({
3976
4008
  type: Array,
3977
4009
  hasChanged: () => !0
3978
- })], $.prototype, "data", null), J([a({
4010
+ })], $.prototype, "data", null), q([a({
3979
4011
  type: Boolean,
3980
4012
  attribute: "clear-undo-on-data-change"
3981
- })], $.prototype, "clearUndoOnDataChange", void 0), J([a({
4013
+ })], $.prototype, "clearUndoOnDataChange", void 0), q([a({
3982
4014
  type: Boolean,
3983
4015
  attribute: "clear-selection-on-data-change"
3984
- })], $.prototype, "clearSelectionOnDataChange", void 0), J([a({
4016
+ })], $.prototype, "clearSelectionOnDataChange", void 0), q([a({
3985
4017
  type: Number,
3986
4018
  attribute: "row-height"
3987
- })], $.prototype, "rowHeight", void 0), J([a({
4019
+ })], $.prototype, "rowHeight", null), q([a({
3988
4020
  type: Boolean,
3989
4021
  attribute: "show-row-numbers"
3990
- })], $.prototype, "showRowNumbers", void 0), J([a({
4022
+ })], $.prototype, "showRowNumbers", void 0), q([a({
3991
4023
  type: String,
3992
4024
  reflect: !0
3993
- })], $.prototype, "theme", void 0), J([a({
4025
+ })], $.prototype, "theme", void 0), q([a({
3994
4026
  type: Number,
3995
4027
  attribute: "max-rows"
3996
- })], $.prototype, "maxRows", void 0), J([a({ type: Boolean })], $.prototype, "editable", void 0), J([a({
4028
+ })], $.prototype, "maxRows", void 0), q([a({ type: Boolean })], $.prototype, "editable", void 0), q([a({
3997
4029
  type: Boolean,
3998
4030
  attribute: "show-filters"
3999
- })], $.prototype, "showFilters", void 0), J([a({
4031
+ })], $.prototype, "showFilters", void 0), q([a({
4000
4032
  type: Boolean,
4001
4033
  attribute: "show-context-menu"
4002
- })], $.prototype, "showContextMenu", void 0), J([a({
4034
+ })], $.prototype, "showContextMenu", void 0), q([a({
4003
4035
  type: Number,
4004
4036
  attribute: "frozen-rows"
4005
- })], $.prototype, "frozenRows", void 0), J([a({
4037
+ })], $.prototype, "frozenRows", void 0), q([a({
4006
4038
  type: Boolean,
4007
4039
  reflect: !0
4008
- })], $.prototype, "loading", void 0), J([a({
4040
+ })], $.prototype, "loading", void 0), q([a({
4009
4041
  type: Boolean,
4010
4042
  attribute: "import-enabled"
4011
- })], $.prototype, "importEnabled", void 0), J([a({ type: Boolean })], $.prototype, "selectable", void 0), J([a({
4043
+ })], $.prototype, "importEnabled", void 0), q([a({ type: Boolean })], $.prototype, "selectable", void 0), q([a({
4012
4044
  type: String,
4013
4045
  attribute: "selection-mode"
4014
- })], $.prototype, "selectionMode", null), J([a({
4046
+ })], $.prototype, "selectionMode", null), q([a({
4015
4047
  type: String,
4016
4048
  attribute: "data-mode"
4017
- })], $.prototype, "dataMode", void 0), J([a({
4049
+ })], $.prototype, "dataMode", void 0), q([a({
4018
4050
  type: Object,
4019
4051
  attribute: "footer-data"
4020
- })], $.prototype, "footerData", void 0), J([a({
4052
+ })], $.prototype, "footerData", void 0), q([a({
4021
4053
  type: Number,
4022
4054
  attribute: "max-undo-size"
4023
- })], $.prototype, "maxUndoSize", null), J([o()], $.prototype, "_scrollTop", void 0), J([o()], $.prototype, "_scrollLeft", void 0), J([o()], $.prototype, "_viewportHeight", void 0), J([o()], $.prototype, "_viewportWidth", void 0), J([o()], $.prototype, "_activeCell", void 0), J([o()], $.prototype, "_editingCell", void 0), J([o()], $.prototype, "_sortCriteria", void 0), J([o()], $.prototype, "_openFilterKey", void 0), J([o()], $.prototype, "_rowSelectionVersion", void 0), J([o()], $.prototype, "_autocompleteState", void 0), J([o()], $.prototype, "_headerMenu", void 0), J([o()], $.prototype, "_bodyContextMenu", void 0), J([o()], $.prototype, "_commentPopup", void 0), J([o()], $.prototype, "_isDragOver", void 0), $ = J([i("flex-table")], $);
4055
+ })], $.prototype, "maxUndoSize", null), q([o()], $.prototype, "_scrollTop", void 0), q([o()], $.prototype, "_scrollLeft", void 0), q([o()], $.prototype, "_viewportHeight", void 0), q([o()], $.prototype, "_viewportWidth", void 0), q([o()], $.prototype, "_activeCell", void 0), q([o()], $.prototype, "_editingCell", void 0), q([o()], $.prototype, "_sortCriteria", void 0), q([o()], $.prototype, "_openFilterKey", void 0), q([o()], $.prototype, "_rowSelectionVersion", void 0), q([o()], $.prototype, "_autocompleteState", void 0), q([o()], $.prototype, "_headerMenu", void 0), q([o()], $.prototype, "_bodyContextMenu", void 0), q([o()], $.prototype, "_commentPopup", void 0), q([o()], $.prototype, "_isDragOver", void 0), $ = q([i("flex-table")], $);
4024
4056
  //#endregion
4025
- export { f as a, x as i, B as n, D as r, $ as t };
4057
+ export { f as a, x as i, z as n, D as r, $ as t };
@@ -35,7 +35,23 @@ export declare class FlexTable extends LitElement {
35
35
  * (e.g. server-mode grids refreshed via `useODataSource`).
36
36
  */
37
37
  clearSelectionOnDataChange: boolean;
38
- rowHeight: number;
38
+ /**
39
+ * 행 높이(px).
40
+ *
41
+ * ⚠**이 표는 가상 스크롤이라 행 높이가 CSS 가 아니라 여기서 정해진다** — 행은
42
+ * `index * rowHeight` 로 절대 배치되고 셀 높이도 인라인으로 박힌다. 그래서 셀 여백을
43
+ * 아무리 조절해도 행 높이는 따라오지 않는다.
44
+ *
45
+ * 소비자가 표를 **문서 스코프 한 규칙**으로 맞출 수 있도록 CSS 토큰
46
+ * `--ft-row-height` 도 읽는다. 우선순위는 **프로퍼티/속성 > CSS 토큰 > 32px** 이다 —
47
+ * 명시적으로 지정한 쪽이 항상 이긴다.
48
+ */
49
+ get rowHeight(): number;
50
+ set rowHeight(value: number);
51
+ /** 프로퍼티·속성으로 **명시 지정된** 값. 지정 전에는 undefined 라 CSS 가 이긴다. */
52
+ private _rowHeightExplicit?;
53
+ /** `--ft-row-height` 판독 결과. 판독 전·판독 실패 시 기본값. */
54
+ private _rowHeightFromCss;
39
55
  showRowNumbers: boolean;
40
56
  theme: 'light' | 'dark' | undefined;
41
57
  maxRows: number;
@@ -256,6 +272,14 @@ export declare class FlexTable extends LitElement {
256
272
  connectedCallback(): void;
257
273
  disconnectedCallback(): void;
258
274
  protected firstUpdated(): void;
275
+ /**
276
+ * `--ft-row-height` 를 판독해 가상화 계산에 넣는다.
277
+ *
278
+ * ⚠**`_measureViewport` 안에 두지 않는다** — 그쪽은 ResizeObserver 가 호스트 크기가
279
+ * 바뀔 때마다 부른다. `getComputedStyle` 은 그 빈도로 돌릴 것이 아니다. 이 토큰은
280
+ * 문서 스코프의 정적 선언으로 쓰이므로 첫 렌더에 한 번 읽으면 충분하다.
281
+ */
282
+ private _readDensityTokens;
259
283
  private _updateColOffsets;
260
284
  private get visibleColRange();
261
285
  protected willUpdate(changedProperties: Map<string, unknown>): void;
@@ -1,2 +1,2 @@
1
- import { a as e, i as t, n, r, t as i } from "./flex-table-C1F6T8Zj.js";
1
+ import { a as e, i as t, n, r, t as i } from "./flex-table-D-OLUYir.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-C1F6T8Zj.js";
1
+ import { t as e } from "./flex-table-D-OLUYir.js";
2
2
  import t from "react";
3
3
  import { createComponent as n } from "@lit/react";
4
4
  var r = n({
@@ -0,0 +1 @@
1
+ import '../flex-table';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iyulab/flex-table",
3
- "version": "0.22.0",
3
+ "version": "0.23.1",
4
4
  "description": "A minimalist, input-centric data grid web component",
5
5
  "type": "module",
6
6
  "main": "./dist/flex-table.js",