@particle-academy/fancy-sheets 0.3.1 → 0.4.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/dist/index.d.cts CHANGED
@@ -130,6 +130,8 @@ interface SpreadsheetContextValue {
130
130
  getColumnWidth: (col: number) => number;
131
131
  isCellSelected: (address: string) => boolean;
132
132
  isCellActive: (address: string) => boolean;
133
+ /** @internal drag-to-select state */
134
+ _isDragging: React.RefObject<boolean>;
133
135
  }
134
136
 
135
137
  interface SpreadsheetGridProps {
package/dist/index.d.ts CHANGED
@@ -130,6 +130,8 @@ interface SpreadsheetContextValue {
130
130
  getColumnWidth: (col: number) => number;
131
131
  isCellSelected: (address: string) => boolean;
132
132
  isCellActive: (address: string) => boolean;
133
+ /** @internal drag-to-select state */
134
+ _isDragging: React.RefObject<boolean>;
133
135
  }
134
136
 
135
137
  interface SpreadsheetGridProps {
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createContext, memo, useCallback, useContext, useRef, useEffect, useMemo, useState, useReducer } from 'react';
2
- import { cn } from '@particle-academy/react-fancy';
2
+ import { cn, ContextMenu } from '@particle-academy/react-fancy';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
 
5
5
  // src/components/Spreadsheet/Spreadsheet.tsx
@@ -1599,15 +1599,36 @@ function ColumnResizeHandle({ colIndex }) {
1599
1599
  }
1600
1600
  ColumnResizeHandle.displayName = "ColumnResizeHandle";
1601
1601
  function ColumnHeaders() {
1602
- const { columnCount, rowCount, rowHeight, getColumnWidth, selectRange } = useSpreadsheet();
1603
- const handleColumnClick = useCallback(
1602
+ const { columnCount, rowCount, rowHeight, getColumnWidth, selection, selectRange, _isDragging } = useSpreadsheet();
1603
+ const handleColumnMouseDown = useCallback(
1604
+ (colIdx, e) => {
1605
+ if (e.button !== 0) return;
1606
+ if (e.shiftKey) {
1607
+ const activeCol = parseAddress(selection.activeCell).col;
1608
+ const minCol = Math.min(activeCol, colIdx);
1609
+ const maxCol = Math.max(activeCol, colIdx);
1610
+ selectRange(toAddress(0, minCol), toAddress(rowCount - 1, maxCol));
1611
+ } else {
1612
+ selectRange(toAddress(0, colIdx), toAddress(rowCount - 1, colIdx));
1613
+ }
1614
+ _isDragging.current = true;
1615
+ },
1616
+ [rowCount, selectRange, selection.activeCell, _isDragging]
1617
+ );
1618
+ const handleColumnMouseEnter = useCallback(
1604
1619
  (colIdx) => {
1605
- const start = toAddress(0, colIdx);
1606
- const end = toAddress(rowCount - 1, colIdx);
1607
- selectRange(start, end);
1620
+ if (_isDragging.current) {
1621
+ const activeCol = parseAddress(selection.activeCell).col;
1622
+ const minCol = Math.min(activeCol, colIdx);
1623
+ const maxCol = Math.max(activeCol, colIdx);
1624
+ selectRange(toAddress(0, minCol), toAddress(rowCount - 1, maxCol));
1625
+ }
1608
1626
  },
1609
- [rowCount, selectRange]
1627
+ [rowCount, selection.activeCell, selectRange, _isDragging]
1610
1628
  );
1629
+ const handleMouseUp = useCallback(() => {
1630
+ _isDragging.current = false;
1631
+ }, [_isDragging]);
1611
1632
  return /* @__PURE__ */ jsxs(
1612
1633
  "div",
1613
1634
  {
@@ -1627,7 +1648,9 @@ function ColumnHeaders() {
1627
1648
  {
1628
1649
  className: "relative flex shrink-0 cursor-pointer items-center justify-center border-r border-zinc-300 text-[11px] font-medium text-zinc-500 select-none hover:bg-zinc-200 dark:border-zinc-600 dark:text-zinc-400 dark:hover:bg-zinc-700",
1629
1650
  style: { width: getColumnWidth(i), minWidth: getColumnWidth(i) },
1630
- onClick: () => handleColumnClick(i),
1651
+ onMouseDown: (e) => handleColumnMouseDown(i, e),
1652
+ onMouseEnter: () => handleColumnMouseEnter(i),
1653
+ onMouseUp: handleMouseUp,
1631
1654
  children: [
1632
1655
  columnToLetter(i),
1633
1656
  /* @__PURE__ */ jsx(ColumnResizeHandle, { colIndex: i })
@@ -1641,19 +1664,42 @@ function ColumnHeaders() {
1641
1664
  }
1642
1665
  ColumnHeaders.displayName = "ColumnHeaders";
1643
1666
  function RowHeader({ rowIndex }) {
1644
- const { rowHeight, columnCount, selectRange } = useSpreadsheet();
1645
- const handleClick = useCallback(() => {
1646
- const start = toAddress(rowIndex, 0);
1647
- const end = toAddress(rowIndex, columnCount - 1);
1648
- selectRange(start, end);
1649
- }, [rowIndex, columnCount, selectRange]);
1667
+ const { rowHeight, columnCount, selection, selectRange, _isDragging } = useSpreadsheet();
1668
+ const handleMouseDown = useCallback(
1669
+ (e) => {
1670
+ if (e.button !== 0) return;
1671
+ if (e.shiftKey) {
1672
+ const activeRow = parseAddress(selection.activeCell).row;
1673
+ const minRow = Math.min(activeRow, rowIndex);
1674
+ const maxRow = Math.max(activeRow, rowIndex);
1675
+ selectRange(toAddress(minRow, 0), toAddress(maxRow, columnCount - 1));
1676
+ } else {
1677
+ selectRange(toAddress(rowIndex, 0), toAddress(rowIndex, columnCount - 1));
1678
+ }
1679
+ _isDragging.current = true;
1680
+ },
1681
+ [rowIndex, columnCount, selectRange, selection.activeCell, _isDragging]
1682
+ );
1683
+ const handleMouseEnter = useCallback(() => {
1684
+ if (_isDragging.current) {
1685
+ const activeRow = parseAddress(selection.activeCell).row;
1686
+ const minRow = Math.min(activeRow, rowIndex);
1687
+ const maxRow = Math.max(activeRow, rowIndex);
1688
+ selectRange(toAddress(minRow, 0), toAddress(maxRow, columnCount - 1));
1689
+ }
1690
+ }, [rowIndex, columnCount, selection.activeCell, selectRange, _isDragging]);
1691
+ const handleMouseUp = useCallback(() => {
1692
+ _isDragging.current = false;
1693
+ }, [_isDragging]);
1650
1694
  return /* @__PURE__ */ jsx(
1651
1695
  "div",
1652
1696
  {
1653
1697
  "data-fancy-sheets-row-header": "",
1654
1698
  className: "flex shrink-0 cursor-pointer items-center justify-center border-r border-b border-zinc-300 bg-zinc-100 text-[11px] font-medium text-zinc-500 select-none hover:bg-zinc-200 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700",
1655
1699
  style: { width: 48, minWidth: 48, height: rowHeight },
1656
- onClick: handleClick,
1700
+ onMouseDown: handleMouseDown,
1701
+ onMouseEnter: handleMouseEnter,
1702
+ onMouseUp: handleMouseUp,
1657
1703
  children: rowIndex + 1
1658
1704
  }
1659
1705
  );
@@ -1716,7 +1762,8 @@ var Cell = memo(function Cell2({ address, row, col }) {
1716
1762
  rowHeight,
1717
1763
  getColumnWidth,
1718
1764
  isCellSelected,
1719
- isCellActive
1765
+ isCellActive,
1766
+ _isDragging
1720
1767
  } = useSpreadsheet();
1721
1768
  const cell = activeSheet.cells[address];
1722
1769
  const isActive = isCellActive(address);
@@ -1726,6 +1773,7 @@ var Cell = memo(function Cell2({ address, row, col }) {
1726
1773
  const width = getColumnWidth(col);
1727
1774
  const handleMouseDown = useCallback(
1728
1775
  (e) => {
1776
+ if (e.button !== 0) return;
1729
1777
  if (e.shiftKey) {
1730
1778
  extendSelection(address);
1731
1779
  } else if (e.ctrlKey || e.metaKey) {
@@ -1733,9 +1781,18 @@ var Cell = memo(function Cell2({ address, row, col }) {
1733
1781
  } else {
1734
1782
  setSelection(address);
1735
1783
  }
1784
+ _isDragging.current = true;
1736
1785
  },
1737
- [address, setSelection, extendSelection, addSelection]
1786
+ [address, setSelection, extendSelection, addSelection, _isDragging]
1738
1787
  );
1788
+ const handleMouseEnter = useCallback(() => {
1789
+ if (_isDragging.current) {
1790
+ extendSelection(address);
1791
+ }
1792
+ }, [address, extendSelection, _isDragging]);
1793
+ const handleMouseUp = useCallback(() => {
1794
+ _isDragging.current = false;
1795
+ }, [_isDragging]);
1739
1796
  const handleDoubleClick = useCallback(() => {
1740
1797
  if (readOnly) return;
1741
1798
  startEdit();
@@ -1752,12 +1809,14 @@ var Cell = memo(function Cell2({ address, row, col }) {
1752
1809
  "data-active": isActive || void 0,
1753
1810
  role: "gridcell",
1754
1811
  className: cn(
1755
- "relative flex items-center truncate border-r border-b border-zinc-200 px-1.5 text-[13px] dark:border-zinc-700",
1812
+ "relative flex items-center truncate border-r border-b border-zinc-200 bg-white px-1.5 text-[13px] select-none dark:border-zinc-700 dark:bg-zinc-900",
1756
1813
  isActive && "ring-2 ring-inset ring-blue-500",
1757
- isSelected && !isActive && "bg-blue-500/10"
1814
+ isSelected && !isActive && "bg-blue-50 dark:bg-blue-950/40"
1758
1815
  ),
1759
1816
  style: { width, minWidth: width, height: rowHeight, ...formatStyle },
1760
1817
  onMouseDown: handleMouseDown,
1818
+ onMouseEnter: handleMouseEnter,
1819
+ onMouseUp: handleMouseUp,
1761
1820
  onDoubleClick: handleDoubleClick,
1762
1821
  children: !isEditing && /* @__PURE__ */ jsx("span", { className: "truncate", children: displayValue })
1763
1822
  }
@@ -1897,6 +1956,9 @@ function SpreadsheetGrid({ className }) {
1897
1956
  confirmEdit,
1898
1957
  cancelEdit,
1899
1958
  setCellValue,
1959
+ setFrozenRows,
1960
+ setFrozenCols,
1961
+ extendSelection,
1900
1962
  undo,
1901
1963
  redo
1902
1964
  } = useSpreadsheet();
@@ -1986,66 +2048,114 @@ function SpreadsheetGrid({ className }) {
1986
2048
  const top = row * rowHeight;
1987
2049
  return { left, top };
1988
2050
  })() : null;
1989
- return /* @__PURE__ */ jsxs(
1990
- "div",
1991
- {
1992
- ref: containerRef,
1993
- "data-fancy-sheets-grid": "",
1994
- className: cn("relative min-h-0 flex-1 overflow-auto bg-white focus:outline-none dark:bg-zinc-900", className),
1995
- tabIndex: 0,
1996
- onKeyDown: handleKeyDown,
1997
- children: [
1998
- /* @__PURE__ */ jsx("div", { className: "sticky top-0 z-10", children: /* @__PURE__ */ jsx(ColumnHeaders, {}) }),
1999
- /* @__PURE__ */ jsxs("div", { className: "relative", children: [
2000
- Array.from({ length: rowCount }, (_, rowIdx) => {
2001
- const isFrozenRow = rowIdx < activeSheet.frozenRows;
2002
- return /* @__PURE__ */ jsxs(
2051
+ const handleCopy = useCallback(() => {
2052
+ const range = selection.ranges[0];
2053
+ if (range) {
2054
+ const tsv = cellsToTSV(activeSheet.cells, range);
2055
+ navigator.clipboard.writeText(tsv);
2056
+ }
2057
+ }, [selection, activeSheet]);
2058
+ const handlePaste = useCallback(() => {
2059
+ navigator.clipboard.readText().then((text) => {
2060
+ if (!text) return;
2061
+ const { values } = tsvToCells(text);
2062
+ const { row: startRow, col: startCol } = parseAddress(selection.activeCell);
2063
+ for (let r = 0; r < values.length; r++) {
2064
+ for (let c = 0; c < values[r].length; c++) {
2065
+ setCellValue(toAddress(startRow + r, startCol + c), values[r][c]);
2066
+ }
2067
+ }
2068
+ });
2069
+ }, [selection, setCellValue]);
2070
+ const handleClearSelection = useCallback(() => {
2071
+ const range = selection.ranges[0];
2072
+ if (!range) return;
2073
+ const { start, end } = range;
2074
+ const s = parseAddress(start);
2075
+ const e = parseAddress(end);
2076
+ const minR = Math.min(s.row, e.row), maxR = Math.max(s.row, e.row);
2077
+ const minC = Math.min(s.col, e.col), maxC = Math.max(s.col, e.col);
2078
+ for (let r = minR; r <= maxR; r++) {
2079
+ for (let c = minC; c <= maxC; c++) {
2080
+ setCellValue(toAddress(r, c), "");
2081
+ }
2082
+ }
2083
+ }, [selection, setCellValue]);
2084
+ return /* @__PURE__ */ jsxs(ContextMenu, { children: [
2085
+ /* @__PURE__ */ jsx(ContextMenu.Trigger, { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsxs(
2086
+ "div",
2087
+ {
2088
+ ref: containerRef,
2089
+ "data-fancy-sheets-grid": "",
2090
+ className: cn("relative h-full overflow-auto bg-white focus:outline-none dark:bg-zinc-900", className),
2091
+ tabIndex: 0,
2092
+ onKeyDown: handleKeyDown,
2093
+ children: [
2094
+ /* @__PURE__ */ jsx("div", { className: "sticky top-0 z-10", children: /* @__PURE__ */ jsx(ColumnHeaders, {}) }),
2095
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
2096
+ Array.from({ length: rowCount }, (_, rowIdx) => {
2097
+ const isFrozenRow = rowIdx < activeSheet.frozenRows;
2098
+ return /* @__PURE__ */ jsxs(
2099
+ "div",
2100
+ {
2101
+ className: "flex",
2102
+ style: isFrozenRow ? {
2103
+ position: "sticky",
2104
+ top: rowHeight + rowIdx * rowHeight,
2105
+ zIndex: 8
2106
+ } : void 0,
2107
+ children: [
2108
+ /* @__PURE__ */ jsx("div", { className: "sticky left-0 z-[5]", children: /* @__PURE__ */ jsx(RowHeader, { rowIndex: rowIdx }) }),
2109
+ Array.from({ length: columnCount }, (_2, colIdx) => {
2110
+ const addr = toAddress(rowIdx, colIdx);
2111
+ const isFrozenCol = colIdx < activeSheet.frozenCols;
2112
+ return /* @__PURE__ */ jsx(
2113
+ "div",
2114
+ {
2115
+ style: isFrozenCol ? {
2116
+ position: "sticky",
2117
+ left: 48 + Array.from({ length: colIdx }, (_3, c) => getColumnWidth(c)).reduce((a, b) => a + b, 0),
2118
+ zIndex: isFrozenRow ? 9 : 6
2119
+ } : void 0,
2120
+ children: /* @__PURE__ */ jsx(Cell, { address: addr, row: rowIdx, col: colIdx })
2121
+ },
2122
+ addr
2123
+ );
2124
+ })
2125
+ ]
2126
+ },
2127
+ rowIdx
2128
+ );
2129
+ }),
2130
+ /* @__PURE__ */ jsx(SelectionOverlay, {}),
2131
+ editorPosition && /* @__PURE__ */ jsx(
2003
2132
  "div",
2004
2133
  {
2005
- className: "flex",
2006
- style: isFrozenRow ? {
2007
- position: "sticky",
2008
- top: rowHeight + rowIdx * rowHeight,
2009
- zIndex: 8,
2010
- backgroundColor: "inherit"
2011
- } : void 0,
2012
- children: [
2013
- /* @__PURE__ */ jsx("div", { className: "sticky left-0 z-[5]", children: /* @__PURE__ */ jsx(RowHeader, { rowIndex: rowIdx }) }),
2014
- Array.from({ length: columnCount }, (_2, colIdx) => {
2015
- const addr = toAddress(rowIdx, colIdx);
2016
- const isFrozenCol = colIdx < activeSheet.frozenCols;
2017
- return /* @__PURE__ */ jsx(
2018
- "div",
2019
- {
2020
- style: isFrozenCol ? {
2021
- position: "sticky",
2022
- left: 48 + Array.from({ length: colIdx }, (_3, c) => getColumnWidth(c)).reduce((a, b) => a + b, 0),
2023
- zIndex: isFrozenRow ? 9 : 6,
2024
- backgroundColor: "inherit"
2025
- } : void 0,
2026
- children: /* @__PURE__ */ jsx(Cell, { address: addr, row: rowIdx, col: colIdx })
2027
- },
2028
- addr
2029
- );
2030
- })
2031
- ]
2032
- },
2033
- rowIdx
2034
- );
2035
- }),
2036
- /* @__PURE__ */ jsx(SelectionOverlay, {}),
2037
- editorPosition && /* @__PURE__ */ jsx(
2038
- "div",
2039
- {
2040
- className: "absolute z-20",
2041
- style: { left: editorPosition.left, top: editorPosition.top },
2042
- children: /* @__PURE__ */ jsx(CellEditor, {})
2043
- }
2044
- )
2045
- ] })
2046
- ]
2047
- }
2048
- );
2134
+ className: "absolute z-20",
2135
+ style: { left: editorPosition.left, top: editorPosition.top },
2136
+ children: /* @__PURE__ */ jsx(CellEditor, {})
2137
+ }
2138
+ )
2139
+ ] })
2140
+ ]
2141
+ }
2142
+ ) }),
2143
+ /* @__PURE__ */ jsxs(ContextMenu.Content, { children: [
2144
+ /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: handleCopy, children: "Copy" }),
2145
+ /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: handlePaste, disabled: readOnly, children: "Paste" }),
2146
+ /* @__PURE__ */ jsx(ContextMenu.Separator, {}),
2147
+ /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: handleClearSelection, disabled: readOnly, children: "Clear cells" }),
2148
+ /* @__PURE__ */ jsx(ContextMenu.Separator, {}),
2149
+ /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => {
2150
+ const row = parseAddress(selection.activeCell).row;
2151
+ setFrozenRows(activeSheet.frozenRows > 0 ? 0 : row);
2152
+ }, disabled: readOnly, children: activeSheet.frozenRows > 0 ? "Unfreeze rows" : "Freeze rows above" }),
2153
+ /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => {
2154
+ const col = parseAddress(selection.activeCell).col;
2155
+ setFrozenCols(activeSheet.frozenCols > 0 ? 0 : col);
2156
+ }, disabled: readOnly, children: activeSheet.frozenCols > 0 ? "Unfreeze columns" : "Freeze columns left" })
2157
+ ] })
2158
+ ] });
2049
2159
  }
2050
2160
  SpreadsheetGrid.displayName = "SpreadsheetGrid";
2051
2161
  var btnClass = "inline-flex items-center justify-center rounded px-2 py-1 text-[12px] font-medium text-zinc-600 transition-colors hover:bg-zinc-100 disabled:opacity-40 dark:text-zinc-300 dark:hover:bg-zinc-800";
@@ -2346,6 +2456,7 @@ function SpreadsheetRoot({
2346
2456
  (address) => state.selection.activeCell === address,
2347
2457
  [state.selection.activeCell]
2348
2458
  );
2459
+ const isDraggingRef = useRef(false);
2349
2460
  const ctx = useMemo(
2350
2461
  () => ({
2351
2462
  workbook: state.workbook,
@@ -2363,7 +2474,8 @@ function SpreadsheetRoot({
2363
2474
  canRedo: state.redoStack.length > 0,
2364
2475
  getColumnWidth,
2365
2476
  isCellSelected,
2366
- isCellActive
2477
+ isCellActive,
2478
+ _isDragging: isDraggingRef
2367
2479
  }),
2368
2480
  [state, activeSheet, columnCount, rowCount, defaultColumnWidth, rowHeight, readOnly, actions, getColumnWidth, isCellSelected, isCellActive]
2369
2481
  );