@lensui/lens-table 0.1.8 → 0.1.10

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
@@ -96,6 +96,7 @@ export function Example() {
96
96
  | `locale` | `'zh-CN' \| 'en-US' \| LocaleConfig` | `'zh-CN'` | 国际化配置。传字符串时使用内置中英文文案;传对象时可通过`language` 指定语言,并通过 `labels` 覆盖文案。 |
97
97
  | `borderless` | `boolean` | `false` | 是否弱化竖向边框。开启后隐藏单元格之间的竖线,但保留横向分隔线。 |
98
98
  | `verticalBorderless` | `boolean` | `false` | 已兼容保留,建议改用 `borderless`。 |
99
+ | `horizontalBorderless` | `boolean` | `false` | 是否隐藏横向行分隔线及上下边框,同时保留纵向列分隔线。可与 `striped` 配合使用。 |
99
100
  | `striped` | `boolean \| string` | `false` | 是否显示斑马纹行背景。传`true` 使用主题变量,传颜色字符串可自定义斑马纹颜色,例如 `'#f6faff'`。 |
100
101
  | `highlight` | `{ editedCells?: boolean \| string; insertedRows?: boolean \| string }` | `{}` | 高亮反馈配置。`editedCells` 控制编辑过的单元格底色,`insertedRows` 控制乐观插入行底色;传 `true` 使用默认色,传颜色字符串可自定义。 |
101
102
  | `cellSpans` | `TableCellSpan[] \| ((ctx) => TableCellSpan \| { rowSpan?: number; colSpan?: number } \| null)` | `[]` | 合并主体单元格配置。可传数组,也可传函数按单元格生成规则。推荐使用 `rowKey` 定位;横向合并会限制在同一个固定列分区内,工具列不会参与合并。 |
package/dist/index.cjs CHANGED
@@ -692,6 +692,18 @@ function DateEditor({ editor, value: draft, style, locale, labels, onChange, onC
692
692
  );
693
693
  }
694
694
 
695
+ // src/core/typography.ts
696
+ var CELL_FONT = "13px Inter, ui-sans-serif, system-ui, sans-serif";
697
+ function getCellBaselineOffset(context) {
698
+ context.save();
699
+ context.textBaseline = "alphabetic";
700
+ const metrics = context.measureText("Mg");
701
+ context.restore();
702
+ const ascent = metrics.fontBoundingBoxAscent;
703
+ const descent = metrics.fontBoundingBoxDescent;
704
+ return Number.isFinite(ascent) && Number.isFinite(descent) ? (ascent - descent) / 2 : 0;
705
+ }
706
+
695
707
  // src/components/editors/TextEditor.tsx
696
708
  var import_jsx_runtime6 = require("react/jsx-runtime");
697
709
  function TextEditor({ value, type, style, labels, onChange, onCommit, onCancel }) {
@@ -705,6 +717,7 @@ function TextEditor({ value, type, style, labels, onChange, onCommit, onCancel }
705
717
  "input",
706
718
  {
707
719
  type,
720
+ style: { font: CELL_FONT, textAlign: style.textAlign },
708
721
  value,
709
722
  autoFocus: true,
710
723
  onChange: (event) => onChange(event.target.value),
@@ -929,7 +942,7 @@ function ellipsizeText(context, text, maxWidth) {
929
942
  return `${text.slice(0, start)}${ellipsis}`;
930
943
  }
931
944
  function paintGrid(options) {
932
- const { context: ctx, width, height, pixelRatio, scrollLeft, scrollTop, rowHeight, headerHeight, fixedHeader, verticalBorderless, striped, columnDraggable, sortState, filterValues, hoveredHeaderAction, rows, columns, metrics, range, selection, editing, hoveredRowIndex, selectionRange, selectedRowKeys, rowSelectionMode, selectedColumnKeys, highlightEditedCells, highlightInsertedRows, insertedRowKeys, editedCellKeys, cellAnnotations, getRowKey, rowDragPreview, columnDropTarget } = options;
945
+ const { context: ctx, width, height, pixelRatio, scrollLeft, scrollTop, rowHeight, headerHeight, fixedHeader, verticalBorderless, horizontalBorderless, striped, columnDraggable, sortState, filterValues, hoveredHeaderAction, rows, columns, metrics, range, selection, editing, hoveredRowIndex, selectionRange, selectedRowKeys, rowSelectionMode, selectedColumnKeys, highlightEditedCells, highlightInsertedRows, insertedRowKeys, editedCellKeys, cellAnnotations, getRowKey, rowDragPreview, columnDropTarget } = options;
933
946
  const headerLeafTop = options.headerLeafTop ?? 0;
934
947
  const headerLeafHeight = options.headerLeafHeight ?? headerHeight;
935
948
  const colors = { ...COLORS, ...options.colors };
@@ -946,7 +959,8 @@ function paintGrid(options) {
946
959
  ctx.clearRect(0, 0, width, height);
947
960
  ctx.fillStyle = colors.background;
948
961
  ctx.fillRect(0, 0, width, height);
949
- ctx.font = "13px Inter, ui-sans-serif, system-ui, sans-serif";
962
+ ctx.font = CELL_FONT;
963
+ const cellBaselineOffset = getCellBaselineOffset(ctx);
950
964
  ctx.textBaseline = "middle";
951
965
  const leftFixedWidth = columns.reduce((value, column, index) => column.fixed === "left" ? value + metrics[index].width : value, 0);
952
966
  const rightFixedWidth = columns.reduce((value, column, index) => column.fixed === "right" ? value + metrics[index].width : value, 0);
@@ -1051,7 +1065,7 @@ function paintGrid(options) {
1051
1065
  ctx.fillStyle = colors.grid;
1052
1066
  if (!verticalBorderless) ctx.fillRect(x + cellWidth - 1, y, 1, cellHeight);
1053
1067
  if (rowIndex === rowBelowDragGap) ctx.fillRect(x, y, cellWidth, 1);
1054
- if (!options.suppressLastRowBottomBorder || rowIndex !== rows.length - 1) {
1068
+ if (!horizontalBorderless && (!options.suppressLastRowBottomBorder || rowIndex !== rows.length - 1)) {
1055
1069
  ctx.fillRect(x, y + cellHeight - 1, cellWidth, 1);
1056
1070
  }
1057
1071
  if (rowSpan > 1) {
@@ -1145,9 +1159,10 @@ function paintGrid(options) {
1145
1159
  ctx.rect(x + 2, y + 1, Math.max(0, cellWidth - 4), cellHeight - 2);
1146
1160
  ctx.clip();
1147
1161
  ctx.fillStyle = cellStyle?.color ?? colors.text;
1148
- ctx.font = "13px Inter, ui-sans-serif, system-ui, sans-serif";
1162
+ ctx.font = CELL_FONT;
1163
+ ctx.textBaseline = "alphabetic";
1149
1164
  ctx.textAlign = column.align === "right" ? "right" : column.align === "center" ? "center" : "left";
1150
- ctx.fillText(label, textX, y + cellHeight / 2);
1165
+ ctx.fillText(label, textX, y + cellHeight / 2 + cellBaselineOffset);
1151
1166
  ctx.restore();
1152
1167
  }
1153
1168
  }
@@ -1191,7 +1206,7 @@ function paintGrid(options) {
1191
1206
  }
1192
1207
  ctx.fillStyle = colors.grid;
1193
1208
  if (!verticalBorderless) ctx.fillRect(x + metric.width - 1, headerY, 1, headerHeight);
1194
- ctx.fillRect(x, headerY + headerHeight - 1, metric.width, 1);
1209
+ if (!horizontalBorderless) ctx.fillRect(x, headerY + headerHeight - 1, metric.width, 1);
1195
1210
  const column = columns[columnIndex];
1196
1211
  if (headerLeafTop === 0 && !column.rowSelection && !column.rowDragHandle && !column.rowNumber && !column.renderHeader && column.title) {
1197
1212
  ctx.font = "600 13px Inter, ui-sans-serif, system-ui, sans-serif";
@@ -1227,12 +1242,12 @@ function paintGrid(options) {
1227
1242
  ctx.fillRect(rightFixedLeft - 1, 0, 1, height);
1228
1243
  }
1229
1244
  ctx.fillStyle = colors.grid;
1230
- ctx.fillRect(0, 0, width, 1);
1245
+ if (!horizontalBorderless) ctx.fillRect(0, 0, width, 1);
1231
1246
  if (!verticalBorderless) {
1232
1247
  ctx.fillRect(0, 0, 1, height);
1233
1248
  ctx.fillRect(width - 1, 0, 1, height);
1234
1249
  }
1235
- if (!options.suppressFrameBottomBorder) ctx.fillRect(0, height - 1, width, 1);
1250
+ if (!horizontalBorderless && !options.suppressFrameBottomBorder) ctx.fillRect(0, height - 1, width, 1);
1236
1251
  if (selectionRange) {
1237
1252
  const rowStart = Math.min(selectionRange.anchor.rowIndex, selectionRange.focus.rowIndex);
1238
1253
  const rowEnd = Math.max(selectionRange.anchor.rowIndex, selectionRange.focus.rowIndex);
@@ -1536,6 +1551,7 @@ function TableInner({
1536
1551
  locale = "zh-CN",
1537
1552
  borderless,
1538
1553
  verticalBorderless,
1554
+ horizontalBorderless = false,
1539
1555
  striped = false,
1540
1556
  highlight,
1541
1557
  cellSpans = [],
@@ -1698,7 +1714,8 @@ function TableInner({
1698
1714
  const hoveredRowIndexRef = (0, import_react5.useRef)(null);
1699
1715
  const [viewport, setViewport] = (0, import_react5.useState)({ width: 0, height: typeof height === "number" ? height : FALLBACK_HEIGHT });
1700
1716
  const [horizontalScrollbarHeight, setHorizontalScrollbarHeight] = (0, import_react5.useState)(0);
1701
- const resolvedHeight = typeof height === "number" ? height : viewport.height || FALLBACK_HEIGHT;
1717
+ const [availableHeight, setAvailableHeight] = (0, import_react5.useState)(FALLBACK_HEIGHT);
1718
+ const resolvedHeight = typeof height === "number" ? height : availableHeight;
1702
1719
  const [scrollPosition, setScrollPosition] = (0, import_react5.useState)({ left: 0, top: 0 });
1703
1720
  const scrollRef = (0, import_react5.useRef)({ left: 0, top: 0 });
1704
1721
  const [rowKeys, setRowKeys] = useControllableKeys(selectedRowKeys, defaultSelectedRowKeys, (keys) => {
@@ -2215,8 +2232,8 @@ function TableInner({
2215
2232
  const scroll = scrollRef.current;
2216
2233
  const rowScrollTop = fixedHeader ? scroll.top : Math.max(0, scroll.top - bodyTop);
2217
2234
  const range = isVirtualized ? getViewportRange(scroll.left, rowScrollTop, viewport.width, bodyViewportHeight, rows.length, rowHeight, metrics, virtualOverscan) : { rowStart: 0, rowEnd: rows.length, columnStart: 0, columnEnd: columns.length };
2218
- paintGrid({ context, width: viewport.width, height: renderHeight, pixelRatio: ratio, scrollLeft: scroll.left, scrollTop: scroll.top, rowHeight, headerHeight, headerLeafTop, headerLeafHeight, bodyTop, suppressLastRowBottomBorder: bottomSummaryHeight > 0, suppressFrameBottomBorder: bottomSummaryHeight > 0, fixedHeader, verticalBorderless: !hasVerticalBorders, striped: hasStripedRows, columnDraggable, sortState, filterValues, hoveredHeaderAction: hoveredHeaderActionRef.current, rows, columns, metrics, range, selection, editing, hoveredRowIndex: hoveredRowIndexRef.current, selectionRange, selectedRowKeys: selectedRowKeySet, rowSelectionMode, selectedColumnKeys: selectedColumnKeySet, cellSpans: cellSpanLookup.covered, maxRowSpan: cellSpanLookup.maxRowSpan, highlightEditedCells: highlightsEditedCells, highlightInsertedRows: highlightsInsertedRows, insertedRowKeys: insertedRowKeySet, editedCellKeys, cellAnnotations, getRowKey, rowDragPreview, columnDropTarget, colors: themeColors });
2219
- }, [bodyTop, bodyViewportHeight, bottomSummaryHeight, cellAnnotations, cellSpanLookup, columnDraggable, columnDropTarget, columns, editedCellHighlightColor, editedCellKeys, editing, renderHeight, filterValues, fixedHeader, getRowKey, hasStripedRows, hasVerticalBorders, headerDepth, headerHeight, headerLeafHeight, headerLeafTop, highlightsEditedCells, highlightsInsertedRows, insertedRowHighlightColor, insertedRowKeySet, isVirtualized, metrics, rowDragPreview, rowHeight, rowSelectionMode, rows, selectedColumnKeySet, selectedRowKeySet, selection, selectionRange, sortState, stripedColor, viewport.width, virtualOverscan]);
2235
+ paintGrid({ context, width: viewport.width, height: renderHeight, pixelRatio: ratio, scrollLeft: scroll.left, scrollTop: scroll.top, rowHeight, headerHeight, headerLeafTop, headerLeafHeight, bodyTop, suppressLastRowBottomBorder: bottomSummaryHeight > 0, suppressFrameBottomBorder: bottomSummaryHeight > 0, fixedHeader, verticalBorderless: !hasVerticalBorders, horizontalBorderless, striped: hasStripedRows, columnDraggable, sortState, filterValues, hoveredHeaderAction: hoveredHeaderActionRef.current, rows, columns, metrics, range, selection, editing, hoveredRowIndex: hoveredRowIndexRef.current, selectionRange, selectedRowKeys: selectedRowKeySet, rowSelectionMode, selectedColumnKeys: selectedColumnKeySet, cellSpans: cellSpanLookup.covered, maxRowSpan: cellSpanLookup.maxRowSpan, highlightEditedCells: highlightsEditedCells, highlightInsertedRows: highlightsInsertedRows, insertedRowKeys: insertedRowKeySet, editedCellKeys, cellAnnotations, getRowKey, rowDragPreview, columnDropTarget, colors: themeColors });
2236
+ }, [bodyTop, bodyViewportHeight, bottomSummaryHeight, cellAnnotations, cellSpanLookup, columnDraggable, columnDropTarget, columns, editedCellHighlightColor, editedCellKeys, editing, renderHeight, filterValues, fixedHeader, getRowKey, hasStripedRows, hasVerticalBorders, headerDepth, headerHeight, headerLeafHeight, headerLeafTop, horizontalBorderless, highlightsEditedCells, highlightsInsertedRows, insertedRowHighlightColor, insertedRowKeySet, isVirtualized, metrics, rowDragPreview, rowHeight, rowSelectionMode, rows, selectedColumnKeySet, selectedRowKeySet, selection, selectionRange, sortState, stripedColor, viewport.width, virtualOverscan]);
2220
2237
  const scheduleDraw = (0, import_react5.useCallback)(() => {
2221
2238
  if (frameRef.current === null) frameRef.current = requestAnimationFrame(draw);
2222
2239
  }, [draw]);
@@ -2375,7 +2392,15 @@ function TableInner({
2375
2392
  (0, import_react5.useLayoutEffect)(() => {
2376
2393
  const element = scrollerRef.current;
2377
2394
  if (!element) return;
2395
+ const root = element.parentElement;
2378
2396
  const update = () => {
2397
+ if (root && typeof height !== "number") {
2398
+ const renderedHeight = root.style.height;
2399
+ root.style.height = height;
2400
+ const nextAvailableHeight = root.clientHeight || FALLBACK_HEIGHT;
2401
+ root.style.height = renderedHeight;
2402
+ setAvailableHeight(nextAvailableHeight);
2403
+ }
2379
2404
  const measuredHeight = element.clientHeight || (typeof height === "number" ? height : FALLBACK_HEIGHT);
2380
2405
  setViewport({ width: element.clientWidth, height: measuredHeight });
2381
2406
  setHorizontalScrollbarHeight(Math.max(0, element.offsetHeight - element.clientHeight));
@@ -2387,6 +2412,7 @@ function TableInner({
2387
2412
  }
2388
2413
  const observer = new ResizeObserver(update);
2389
2414
  observer.observe(element);
2415
+ if (root?.parentElement) observer.observe(root.parentElement);
2390
2416
  return () => observer.disconnect();
2391
2417
  }, [height]);
2392
2418
  (0, import_react5.useEffect)(() => {
@@ -3571,11 +3597,21 @@ function TableInner({
3571
3597
  const visibleRight = Math.min(cellLeft + cellWidth, horizontalEnd, viewport.width);
3572
3598
  const visibleTop = Math.max(cellTop, verticalStart);
3573
3599
  const visibleBottom = Math.min(cellTop + cellHeight, renderHeight);
3600
+ if (column.editor && column.editor.type !== "text") {
3601
+ return {
3602
+ left: visibleLeft,
3603
+ top: visibleTop,
3604
+ width: Math.max(0, visibleRight - visibleLeft),
3605
+ height: Math.max(0, visibleBottom - visibleTop)
3606
+ };
3607
+ }
3574
3608
  return {
3575
- left: visibleLeft,
3576
- top: visibleTop,
3577
- width: Math.max(0, visibleRight - visibleLeft),
3578
- height: Math.max(0, visibleBottom - visibleTop)
3609
+ left: cellLeft,
3610
+ top: cellTop,
3611
+ width: cellWidth,
3612
+ height: cellHeight,
3613
+ textAlign: column.align ?? "left",
3614
+ clipPath: `inset(${Math.max(0, visibleTop - cellTop)}px ${Math.max(0, cellLeft + cellWidth - visibleRight)}px ${Math.max(0, cellTop + cellHeight - visibleBottom)}px ${Math.max(0, visibleLeft - cellLeft)}px)`
3579
3615
  };
3580
3616
  }, [bodyTop, columns, editing, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getScrollableColumnLeft, leftFixedOffsets, metrics, renderHeight, rightFixedOffsets, rightFixedWidth, rowHeight, viewport.width]);
3581
3617
  const handleColumnPointerDown = (0, import_react5.useCallback)((event) => {
@@ -4451,7 +4487,7 @@ function TableInner({
4451
4487
  };
4452
4488
  const currentScroll = scrollRef.current;
4453
4489
  const scrollerBottom = Math.max(0, bottomSummaryHeight - horizontalScrollbarHeight);
4454
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: `rvg-root${!hasVerticalBorders ? " is-vertical-borderless" : ""}${hasStripedRows ? " is-striped" : ""}${insertBusy ? " is-inserting" : ""}${deleteBusy ? " is-deleting" : ""} ${loading && !hasCompletedLoadRef.current && !hasCustomLoading ? "is-initial-loading" : ""} ${className}`, style: rootStyle, children: [
4490
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: `rvg-root${!hasVerticalBorders ? " is-vertical-borderless" : ""}${horizontalBorderless ? " is-horizontal-borderless" : ""}${hasStripedRows ? " is-striped" : ""}${insertBusy ? " is-inserting" : ""}${deleteBusy ? " is-deleting" : ""} ${loading && !hasCompletedLoadRef.current && !hasCustomLoading ? "is-initial-loading" : ""} ${className}`, style: rootStyle, children: [
4455
4491
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { ref: scrollerRef, className: `rvg-scroller${contextMenu ? " is-context-menu-open" : ""}`, style: { bottom: scrollerBottom }, onScroll: handleScroll, onCopy: handleCopy, onContextMenu: handleContextMenu, tabIndex: 0, role: "grid", "aria-label": ariaLabel, "aria-rowcount": rows.length, "aria-colcount": columns.length, onKeyDown: handleKeyDown, children: [
4456
4492
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
4457
4493
  "canvas",
package/dist/index.css CHANGED
@@ -105,6 +105,9 @@
105
105
  content: "";
106
106
  pointer-events: none;
107
107
  }
108
+ .rvg-root.is-horizontal-borderless .rvg-text-layer::before {
109
+ display: none;
110
+ }
108
111
  .rvg-vertical-scrollbar,
109
112
  .rvg-horizontal-scrollbar {
110
113
  position: absolute;
@@ -294,6 +297,10 @@
294
297
  .rvg-summary-row.is-top {
295
298
  border-bottom: 1px solid var(--rvg-color-grid);
296
299
  }
300
+ .rvg-root.is-horizontal-borderless .rvg-summary-row {
301
+ border-top: 0;
302
+ border-bottom: 0;
303
+ }
297
304
  .rvg-summary-scroll-clip {
298
305
  position: absolute;
299
306
  top: 0;
@@ -746,6 +753,9 @@
746
753
  background: var(--rvg-color-header-bg);
747
754
  box-sizing: border-box;
748
755
  }
756
+ .rvg-root.is-horizontal-borderless .rvg-header-group {
757
+ border-bottom: 0;
758
+ }
749
759
  .rvg-header-group.is-leaf {
750
760
  z-index: 2;
751
761
  }
@@ -1644,15 +1654,17 @@
1644
1654
  box-sizing: border-box;
1645
1655
  }
1646
1656
  .rvg-native-editor.has-clear {
1647
- padding-right: 32px;
1657
+ padding-right: 10px;
1648
1658
  }
1649
1659
  .rvg-native-editor input {
1650
1660
  min-width: 0;
1651
1661
  width: 100%;
1652
1662
  height: 100%;
1663
+ margin: 0;
1653
1664
  padding: 0;
1654
- transform: translateY(-1px);
1655
1665
  flex: 1;
1666
+ appearance: none;
1667
+ border-radius: 0;
1656
1668
  border: 0;
1657
1669
  outline: 0;
1658
1670
  background: transparent;
@@ -2419,12 +2431,18 @@
2419
2431
  .rvg-root.is-vertical-borderless .rvg-loading-row span {
2420
2432
  border-right: 0;
2421
2433
  }
2434
+ .rvg-root.is-horizontal-borderless .rvg-loading-row span {
2435
+ border-bottom: 0;
2436
+ }
2422
2437
  .rvg-root.is-striped .rvg-loading-row:nth-child(even) span {
2423
2438
  background: var(--rvg-color-stripe) !important;
2424
2439
  }
2425
2440
  .rvg-root.is-vertical-borderless .rvg-initial-loading::after {
2426
2441
  border-left: 0;
2427
2442
  }
2443
+ .rvg-root.is-horizontal-borderless .rvg-initial-loading::after {
2444
+ border-bottom: 0;
2445
+ }
2428
2446
  .rvg-loading-row i {
2429
2447
  display: block;
2430
2448
  height: 7px;
@@ -2528,3 +2546,14 @@
2528
2546
  opacity: 1;
2529
2547
  }
2530
2548
  }
2549
+ .rvg-native-editor input[type=number] {
2550
+ -moz-appearance: textfield;
2551
+ }
2552
+ .rvg-native-editor input::-webkit-inner-spin-button,
2553
+ .rvg-native-editor input::-webkit-outer-spin-button {
2554
+ margin: 0;
2555
+ -webkit-appearance: none;
2556
+ }
2557
+ .rvg-native-editor .rvg-clear-trigger {
2558
+ background: var(--rvg-color-bg);
2559
+ }
package/dist/index.d.cts CHANGED
@@ -347,6 +347,8 @@ interface TableProps<Row extends object> {
347
347
  borderless?: boolean;
348
348
  /** @deprecated Use borderless instead. */
349
349
  verticalBorderless?: boolean;
350
+ /** Hide horizontal row and frame separators while keeping vertical grid lines. */
351
+ horizontalBorderless?: boolean;
350
352
  /** Paint alternating row backgrounds. Pass a color string to customize the stripe color. */
351
353
  striped?: boolean | string;
352
354
  /** Visual highlight feedback configuration. All entries default to false. */
package/dist/index.d.ts CHANGED
@@ -347,6 +347,8 @@ interface TableProps<Row extends object> {
347
347
  borderless?: boolean;
348
348
  /** @deprecated Use borderless instead. */
349
349
  verticalBorderless?: boolean;
350
+ /** Hide horizontal row and frame separators while keeping vertical grid lines. */
351
+ horizontalBorderless?: boolean;
350
352
  /** Paint alternating row backgrounds. Pass a color string to customize the stripe color. */
351
353
  striped?: boolean | string;
352
354
  /** Visual highlight feedback configuration. All entries default to false. */
package/dist/index.js CHANGED
@@ -662,6 +662,18 @@ function DateEditor({ editor, value: draft, style, locale, labels, onChange, onC
662
662
  );
663
663
  }
664
664
 
665
+ // src/core/typography.ts
666
+ var CELL_FONT = "13px Inter, ui-sans-serif, system-ui, sans-serif";
667
+ function getCellBaselineOffset(context) {
668
+ context.save();
669
+ context.textBaseline = "alphabetic";
670
+ const metrics = context.measureText("Mg");
671
+ context.restore();
672
+ const ascent = metrics.fontBoundingBoxAscent;
673
+ const descent = metrics.fontBoundingBoxDescent;
674
+ return Number.isFinite(ascent) && Number.isFinite(descent) ? (ascent - descent) / 2 : 0;
675
+ }
676
+
665
677
  // src/components/editors/TextEditor.tsx
666
678
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
667
679
  function TextEditor({ value, type, style, labels, onChange, onCommit, onCancel }) {
@@ -675,6 +687,7 @@ function TextEditor({ value, type, style, labels, onChange, onCommit, onCancel }
675
687
  "input",
676
688
  {
677
689
  type,
690
+ style: { font: CELL_FONT, textAlign: style.textAlign },
678
691
  value,
679
692
  autoFocus: true,
680
693
  onChange: (event) => onChange(event.target.value),
@@ -899,7 +912,7 @@ function ellipsizeText(context, text, maxWidth) {
899
912
  return `${text.slice(0, start)}${ellipsis}`;
900
913
  }
901
914
  function paintGrid(options) {
902
- const { context: ctx, width, height, pixelRatio, scrollLeft, scrollTop, rowHeight, headerHeight, fixedHeader, verticalBorderless, striped, columnDraggable, sortState, filterValues, hoveredHeaderAction, rows, columns, metrics, range, selection, editing, hoveredRowIndex, selectionRange, selectedRowKeys, rowSelectionMode, selectedColumnKeys, highlightEditedCells, highlightInsertedRows, insertedRowKeys, editedCellKeys, cellAnnotations, getRowKey, rowDragPreview, columnDropTarget } = options;
915
+ const { context: ctx, width, height, pixelRatio, scrollLeft, scrollTop, rowHeight, headerHeight, fixedHeader, verticalBorderless, horizontalBorderless, striped, columnDraggable, sortState, filterValues, hoveredHeaderAction, rows, columns, metrics, range, selection, editing, hoveredRowIndex, selectionRange, selectedRowKeys, rowSelectionMode, selectedColumnKeys, highlightEditedCells, highlightInsertedRows, insertedRowKeys, editedCellKeys, cellAnnotations, getRowKey, rowDragPreview, columnDropTarget } = options;
903
916
  const headerLeafTop = options.headerLeafTop ?? 0;
904
917
  const headerLeafHeight = options.headerLeafHeight ?? headerHeight;
905
918
  const colors = { ...COLORS, ...options.colors };
@@ -916,7 +929,8 @@ function paintGrid(options) {
916
929
  ctx.clearRect(0, 0, width, height);
917
930
  ctx.fillStyle = colors.background;
918
931
  ctx.fillRect(0, 0, width, height);
919
- ctx.font = "13px Inter, ui-sans-serif, system-ui, sans-serif";
932
+ ctx.font = CELL_FONT;
933
+ const cellBaselineOffset = getCellBaselineOffset(ctx);
920
934
  ctx.textBaseline = "middle";
921
935
  const leftFixedWidth = columns.reduce((value, column, index) => column.fixed === "left" ? value + metrics[index].width : value, 0);
922
936
  const rightFixedWidth = columns.reduce((value, column, index) => column.fixed === "right" ? value + metrics[index].width : value, 0);
@@ -1021,7 +1035,7 @@ function paintGrid(options) {
1021
1035
  ctx.fillStyle = colors.grid;
1022
1036
  if (!verticalBorderless) ctx.fillRect(x + cellWidth - 1, y, 1, cellHeight);
1023
1037
  if (rowIndex === rowBelowDragGap) ctx.fillRect(x, y, cellWidth, 1);
1024
- if (!options.suppressLastRowBottomBorder || rowIndex !== rows.length - 1) {
1038
+ if (!horizontalBorderless && (!options.suppressLastRowBottomBorder || rowIndex !== rows.length - 1)) {
1025
1039
  ctx.fillRect(x, y + cellHeight - 1, cellWidth, 1);
1026
1040
  }
1027
1041
  if (rowSpan > 1) {
@@ -1115,9 +1129,10 @@ function paintGrid(options) {
1115
1129
  ctx.rect(x + 2, y + 1, Math.max(0, cellWidth - 4), cellHeight - 2);
1116
1130
  ctx.clip();
1117
1131
  ctx.fillStyle = cellStyle?.color ?? colors.text;
1118
- ctx.font = "13px Inter, ui-sans-serif, system-ui, sans-serif";
1132
+ ctx.font = CELL_FONT;
1133
+ ctx.textBaseline = "alphabetic";
1119
1134
  ctx.textAlign = column.align === "right" ? "right" : column.align === "center" ? "center" : "left";
1120
- ctx.fillText(label, textX, y + cellHeight / 2);
1135
+ ctx.fillText(label, textX, y + cellHeight / 2 + cellBaselineOffset);
1121
1136
  ctx.restore();
1122
1137
  }
1123
1138
  }
@@ -1161,7 +1176,7 @@ function paintGrid(options) {
1161
1176
  }
1162
1177
  ctx.fillStyle = colors.grid;
1163
1178
  if (!verticalBorderless) ctx.fillRect(x + metric.width - 1, headerY, 1, headerHeight);
1164
- ctx.fillRect(x, headerY + headerHeight - 1, metric.width, 1);
1179
+ if (!horizontalBorderless) ctx.fillRect(x, headerY + headerHeight - 1, metric.width, 1);
1165
1180
  const column = columns[columnIndex];
1166
1181
  if (headerLeafTop === 0 && !column.rowSelection && !column.rowDragHandle && !column.rowNumber && !column.renderHeader && column.title) {
1167
1182
  ctx.font = "600 13px Inter, ui-sans-serif, system-ui, sans-serif";
@@ -1197,12 +1212,12 @@ function paintGrid(options) {
1197
1212
  ctx.fillRect(rightFixedLeft - 1, 0, 1, height);
1198
1213
  }
1199
1214
  ctx.fillStyle = colors.grid;
1200
- ctx.fillRect(0, 0, width, 1);
1215
+ if (!horizontalBorderless) ctx.fillRect(0, 0, width, 1);
1201
1216
  if (!verticalBorderless) {
1202
1217
  ctx.fillRect(0, 0, 1, height);
1203
1218
  ctx.fillRect(width - 1, 0, 1, height);
1204
1219
  }
1205
- if (!options.suppressFrameBottomBorder) ctx.fillRect(0, height - 1, width, 1);
1220
+ if (!horizontalBorderless && !options.suppressFrameBottomBorder) ctx.fillRect(0, height - 1, width, 1);
1206
1221
  if (selectionRange) {
1207
1222
  const rowStart = Math.min(selectionRange.anchor.rowIndex, selectionRange.focus.rowIndex);
1208
1223
  const rowEnd = Math.max(selectionRange.anchor.rowIndex, selectionRange.focus.rowIndex);
@@ -1506,6 +1521,7 @@ function TableInner({
1506
1521
  locale = "zh-CN",
1507
1522
  borderless,
1508
1523
  verticalBorderless,
1524
+ horizontalBorderless = false,
1509
1525
  striped = false,
1510
1526
  highlight,
1511
1527
  cellSpans = [],
@@ -1668,7 +1684,8 @@ function TableInner({
1668
1684
  const hoveredRowIndexRef = useRef2(null);
1669
1685
  const [viewport, setViewport] = useState4({ width: 0, height: typeof height === "number" ? height : FALLBACK_HEIGHT });
1670
1686
  const [horizontalScrollbarHeight, setHorizontalScrollbarHeight] = useState4(0);
1671
- const resolvedHeight = typeof height === "number" ? height : viewport.height || FALLBACK_HEIGHT;
1687
+ const [availableHeight, setAvailableHeight] = useState4(FALLBACK_HEIGHT);
1688
+ const resolvedHeight = typeof height === "number" ? height : availableHeight;
1672
1689
  const [scrollPosition, setScrollPosition] = useState4({ left: 0, top: 0 });
1673
1690
  const scrollRef = useRef2({ left: 0, top: 0 });
1674
1691
  const [rowKeys, setRowKeys] = useControllableKeys(selectedRowKeys, defaultSelectedRowKeys, (keys) => {
@@ -2185,8 +2202,8 @@ function TableInner({
2185
2202
  const scroll = scrollRef.current;
2186
2203
  const rowScrollTop = fixedHeader ? scroll.top : Math.max(0, scroll.top - bodyTop);
2187
2204
  const range = isVirtualized ? getViewportRange(scroll.left, rowScrollTop, viewport.width, bodyViewportHeight, rows.length, rowHeight, metrics, virtualOverscan) : { rowStart: 0, rowEnd: rows.length, columnStart: 0, columnEnd: columns.length };
2188
- paintGrid({ context, width: viewport.width, height: renderHeight, pixelRatio: ratio, scrollLeft: scroll.left, scrollTop: scroll.top, rowHeight, headerHeight, headerLeafTop, headerLeafHeight, bodyTop, suppressLastRowBottomBorder: bottomSummaryHeight > 0, suppressFrameBottomBorder: bottomSummaryHeight > 0, fixedHeader, verticalBorderless: !hasVerticalBorders, striped: hasStripedRows, columnDraggable, sortState, filterValues, hoveredHeaderAction: hoveredHeaderActionRef.current, rows, columns, metrics, range, selection, editing, hoveredRowIndex: hoveredRowIndexRef.current, selectionRange, selectedRowKeys: selectedRowKeySet, rowSelectionMode, selectedColumnKeys: selectedColumnKeySet, cellSpans: cellSpanLookup.covered, maxRowSpan: cellSpanLookup.maxRowSpan, highlightEditedCells: highlightsEditedCells, highlightInsertedRows: highlightsInsertedRows, insertedRowKeys: insertedRowKeySet, editedCellKeys, cellAnnotations, getRowKey, rowDragPreview, columnDropTarget, colors: themeColors });
2189
- }, [bodyTop, bodyViewportHeight, bottomSummaryHeight, cellAnnotations, cellSpanLookup, columnDraggable, columnDropTarget, columns, editedCellHighlightColor, editedCellKeys, editing, renderHeight, filterValues, fixedHeader, getRowKey, hasStripedRows, hasVerticalBorders, headerDepth, headerHeight, headerLeafHeight, headerLeafTop, highlightsEditedCells, highlightsInsertedRows, insertedRowHighlightColor, insertedRowKeySet, isVirtualized, metrics, rowDragPreview, rowHeight, rowSelectionMode, rows, selectedColumnKeySet, selectedRowKeySet, selection, selectionRange, sortState, stripedColor, viewport.width, virtualOverscan]);
2205
+ paintGrid({ context, width: viewport.width, height: renderHeight, pixelRatio: ratio, scrollLeft: scroll.left, scrollTop: scroll.top, rowHeight, headerHeight, headerLeafTop, headerLeafHeight, bodyTop, suppressLastRowBottomBorder: bottomSummaryHeight > 0, suppressFrameBottomBorder: bottomSummaryHeight > 0, fixedHeader, verticalBorderless: !hasVerticalBorders, horizontalBorderless, striped: hasStripedRows, columnDraggable, sortState, filterValues, hoveredHeaderAction: hoveredHeaderActionRef.current, rows, columns, metrics, range, selection, editing, hoveredRowIndex: hoveredRowIndexRef.current, selectionRange, selectedRowKeys: selectedRowKeySet, rowSelectionMode, selectedColumnKeys: selectedColumnKeySet, cellSpans: cellSpanLookup.covered, maxRowSpan: cellSpanLookup.maxRowSpan, highlightEditedCells: highlightsEditedCells, highlightInsertedRows: highlightsInsertedRows, insertedRowKeys: insertedRowKeySet, editedCellKeys, cellAnnotations, getRowKey, rowDragPreview, columnDropTarget, colors: themeColors });
2206
+ }, [bodyTop, bodyViewportHeight, bottomSummaryHeight, cellAnnotations, cellSpanLookup, columnDraggable, columnDropTarget, columns, editedCellHighlightColor, editedCellKeys, editing, renderHeight, filterValues, fixedHeader, getRowKey, hasStripedRows, hasVerticalBorders, headerDepth, headerHeight, headerLeafHeight, headerLeafTop, horizontalBorderless, highlightsEditedCells, highlightsInsertedRows, insertedRowHighlightColor, insertedRowKeySet, isVirtualized, metrics, rowDragPreview, rowHeight, rowSelectionMode, rows, selectedColumnKeySet, selectedRowKeySet, selection, selectionRange, sortState, stripedColor, viewport.width, virtualOverscan]);
2190
2207
  const scheduleDraw = useCallback2(() => {
2191
2208
  if (frameRef.current === null) frameRef.current = requestAnimationFrame(draw);
2192
2209
  }, [draw]);
@@ -2345,7 +2362,15 @@ function TableInner({
2345
2362
  useLayoutEffect2(() => {
2346
2363
  const element = scrollerRef.current;
2347
2364
  if (!element) return;
2365
+ const root = element.parentElement;
2348
2366
  const update = () => {
2367
+ if (root && typeof height !== "number") {
2368
+ const renderedHeight = root.style.height;
2369
+ root.style.height = height;
2370
+ const nextAvailableHeight = root.clientHeight || FALLBACK_HEIGHT;
2371
+ root.style.height = renderedHeight;
2372
+ setAvailableHeight(nextAvailableHeight);
2373
+ }
2349
2374
  const measuredHeight = element.clientHeight || (typeof height === "number" ? height : FALLBACK_HEIGHT);
2350
2375
  setViewport({ width: element.clientWidth, height: measuredHeight });
2351
2376
  setHorizontalScrollbarHeight(Math.max(0, element.offsetHeight - element.clientHeight));
@@ -2357,6 +2382,7 @@ function TableInner({
2357
2382
  }
2358
2383
  const observer = new ResizeObserver(update);
2359
2384
  observer.observe(element);
2385
+ if (root?.parentElement) observer.observe(root.parentElement);
2360
2386
  return () => observer.disconnect();
2361
2387
  }, [height]);
2362
2388
  useEffect2(() => {
@@ -3541,11 +3567,21 @@ function TableInner({
3541
3567
  const visibleRight = Math.min(cellLeft + cellWidth, horizontalEnd, viewport.width);
3542
3568
  const visibleTop = Math.max(cellTop, verticalStart);
3543
3569
  const visibleBottom = Math.min(cellTop + cellHeight, renderHeight);
3570
+ if (column.editor && column.editor.type !== "text") {
3571
+ return {
3572
+ left: visibleLeft,
3573
+ top: visibleTop,
3574
+ width: Math.max(0, visibleRight - visibleLeft),
3575
+ height: Math.max(0, visibleBottom - visibleTop)
3576
+ };
3577
+ }
3544
3578
  return {
3545
- left: visibleLeft,
3546
- top: visibleTop,
3547
- width: Math.max(0, visibleRight - visibleLeft),
3548
- height: Math.max(0, visibleBottom - visibleTop)
3579
+ left: cellLeft,
3580
+ top: cellTop,
3581
+ width: cellWidth,
3582
+ height: cellHeight,
3583
+ textAlign: column.align ?? "left",
3584
+ clipPath: `inset(${Math.max(0, visibleTop - cellTop)}px ${Math.max(0, cellLeft + cellWidth - visibleRight)}px ${Math.max(0, cellTop + cellHeight - visibleBottom)}px ${Math.max(0, visibleLeft - cellLeft)}px)`
3549
3585
  };
3550
3586
  }, [bodyTop, columns, editing, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getScrollableColumnLeft, leftFixedOffsets, metrics, renderHeight, rightFixedOffsets, rightFixedWidth, rowHeight, viewport.width]);
3551
3587
  const handleColumnPointerDown = useCallback2((event) => {
@@ -4421,7 +4457,7 @@ function TableInner({
4421
4457
  };
4422
4458
  const currentScroll = scrollRef.current;
4423
4459
  const scrollerBottom = Math.max(0, bottomSummaryHeight - horizontalScrollbarHeight);
4424
- return /* @__PURE__ */ jsxs7("div", { className: `rvg-root${!hasVerticalBorders ? " is-vertical-borderless" : ""}${hasStripedRows ? " is-striped" : ""}${insertBusy ? " is-inserting" : ""}${deleteBusy ? " is-deleting" : ""} ${loading && !hasCompletedLoadRef.current && !hasCustomLoading ? "is-initial-loading" : ""} ${className}`, style: rootStyle, children: [
4460
+ return /* @__PURE__ */ jsxs7("div", { className: `rvg-root${!hasVerticalBorders ? " is-vertical-borderless" : ""}${horizontalBorderless ? " is-horizontal-borderless" : ""}${hasStripedRows ? " is-striped" : ""}${insertBusy ? " is-inserting" : ""}${deleteBusy ? " is-deleting" : ""} ${loading && !hasCompletedLoadRef.current && !hasCustomLoading ? "is-initial-loading" : ""} ${className}`, style: rootStyle, children: [
4425
4461
  /* @__PURE__ */ jsxs7("div", { ref: scrollerRef, className: `rvg-scroller${contextMenu ? " is-context-menu-open" : ""}`, style: { bottom: scrollerBottom }, onScroll: handleScroll, onCopy: handleCopy, onContextMenu: handleContextMenu, tabIndex: 0, role: "grid", "aria-label": ariaLabel, "aria-rowcount": rows.length, "aria-colcount": columns.length, onKeyDown: handleKeyDown, children: [
4426
4462
  /* @__PURE__ */ jsx7(
4427
4463
  "canvas",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensui/lens-table",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "A lightweight, canvas-rendered React table component for LensUI.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",