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