@particle-academy/fancy-sheets 0.4.0 → 0.4.2

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,9 +1601,10 @@ function ColumnResizeHandle({ colIndex }) {
1601
1601
  }
1602
1602
  ColumnResizeHandle.displayName = "ColumnResizeHandle";
1603
1603
  function ColumnHeaders() {
1604
- const { columnCount, rowCount, rowHeight, getColumnWidth, selection, selectRange } = useSpreadsheet();
1605
- const handleColumnClick = react.useCallback(
1604
+ const { columnCount, rowCount, rowHeight, getColumnWidth, selection, selectRange, _isDragging, isCellSelected } = useSpreadsheet();
1605
+ const handleColumnMouseDown = react.useCallback(
1606
1606
  (colIdx, e) => {
1607
+ if (e.button !== 0) return;
1607
1608
  if (e.shiftKey) {
1608
1609
  const activeCol = parseAddress(selection.activeCell).col;
1609
1610
  const minCol = Math.min(activeCol, colIdx);
@@ -1612,9 +1613,24 @@ function ColumnHeaders() {
1612
1613
  } else {
1613
1614
  selectRange(toAddress(0, colIdx), toAddress(rowCount - 1, colIdx));
1614
1615
  }
1616
+ _isDragging.current = true;
1615
1617
  },
1616
- [rowCount, selectRange, selection.activeCell]
1618
+ [rowCount, selectRange, selection.activeCell, _isDragging]
1617
1619
  );
1620
+ const handleColumnMouseEnter = react.useCallback(
1621
+ (colIdx) => {
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
+ }
1628
+ },
1629
+ [rowCount, selection.activeCell, selectRange, _isDragging]
1630
+ );
1631
+ const handleMouseUp = react.useCallback(() => {
1632
+ _isDragging.current = false;
1633
+ }, [_isDragging]);
1618
1634
  return /* @__PURE__ */ jsxRuntime.jsxs(
1619
1635
  "div",
1620
1636
  {
@@ -1629,28 +1645,38 @@ function ColumnHeaders() {
1629
1645
  style: { width: 48, minWidth: 48 }
1630
1646
  }
1631
1647
  ),
1632
- Array.from({ length: columnCount }, (_, i) => /* @__PURE__ */ jsxRuntime.jsxs(
1633
- "div",
1634
- {
1635
- 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",
1636
- style: { width: getColumnWidth(i), minWidth: getColumnWidth(i) },
1637
- onClick: (e) => handleColumnClick(i, e),
1638
- children: [
1639
- columnToLetter(i),
1640
- /* @__PURE__ */ jsxRuntime.jsx(ColumnResizeHandle, { colIndex: i })
1641
- ]
1642
- },
1643
- i
1644
- ))
1648
+ Array.from({ length: columnCount }, (_, i) => {
1649
+ const isColSelected = isCellSelected(toAddress(0, i));
1650
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1651
+ "div",
1652
+ {
1653
+ className: reactFancy.cn(
1654
+ "relative flex shrink-0 cursor-pointer items-center justify-center border-r border-zinc-300 text-[11px] font-medium select-none hover:bg-zinc-200 dark:border-zinc-600 dark:hover:bg-zinc-700",
1655
+ isColSelected ? "bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300" : "text-zinc-500 dark:text-zinc-400"
1656
+ ),
1657
+ style: { width: getColumnWidth(i), minWidth: getColumnWidth(i) },
1658
+ onMouseDown: (e) => handleColumnMouseDown(i, e),
1659
+ onMouseEnter: () => handleColumnMouseEnter(i),
1660
+ onMouseUp: handleMouseUp,
1661
+ children: [
1662
+ columnToLetter(i),
1663
+ /* @__PURE__ */ jsxRuntime.jsx(ColumnResizeHandle, { colIndex: i })
1664
+ ]
1665
+ },
1666
+ i
1667
+ );
1668
+ })
1645
1669
  ]
1646
1670
  }
1647
1671
  );
1648
1672
  }
1649
1673
  ColumnHeaders.displayName = "ColumnHeaders";
1650
1674
  function RowHeader({ rowIndex }) {
1651
- const { rowHeight, columnCount, selection, selectRange, extendSelection } = useSpreadsheet();
1652
- const handleClick = react.useCallback(
1675
+ const { rowHeight, columnCount, selection, selectRange, _isDragging, isCellSelected } = useSpreadsheet();
1676
+ const isRowSelected = isCellSelected(toAddress(rowIndex, 0));
1677
+ const handleMouseDown = react.useCallback(
1653
1678
  (e) => {
1679
+ if (e.button !== 0) return;
1654
1680
  if (e.shiftKey) {
1655
1681
  const activeRow = parseAddress(selection.activeCell).row;
1656
1682
  const minRow = Math.min(activeRow, rowIndex);
@@ -1659,16 +1685,33 @@ function RowHeader({ rowIndex }) {
1659
1685
  } else {
1660
1686
  selectRange(toAddress(rowIndex, 0), toAddress(rowIndex, columnCount - 1));
1661
1687
  }
1688
+ _isDragging.current = true;
1662
1689
  },
1663
- [rowIndex, columnCount, selectRange, selection.activeCell]
1690
+ [rowIndex, columnCount, selectRange, selection.activeCell, _isDragging]
1664
1691
  );
1692
+ const handleMouseEnter = react.useCallback(() => {
1693
+ if (_isDragging.current) {
1694
+ const activeRow = parseAddress(selection.activeCell).row;
1695
+ const minRow = Math.min(activeRow, rowIndex);
1696
+ const maxRow = Math.max(activeRow, rowIndex);
1697
+ selectRange(toAddress(minRow, 0), toAddress(maxRow, columnCount - 1));
1698
+ }
1699
+ }, [rowIndex, columnCount, selection.activeCell, selectRange, _isDragging]);
1700
+ const handleMouseUp = react.useCallback(() => {
1701
+ _isDragging.current = false;
1702
+ }, [_isDragging]);
1665
1703
  return /* @__PURE__ */ jsxRuntime.jsx(
1666
1704
  "div",
1667
1705
  {
1668
1706
  "data-fancy-sheets-row-header": "",
1669
- 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",
1707
+ className: reactFancy.cn(
1708
+ "flex shrink-0 cursor-pointer items-center justify-center border-r border-b border-zinc-300 text-[11px] font-medium select-none hover:bg-zinc-200 dark:border-zinc-600 dark:hover:bg-zinc-700",
1709
+ isRowSelected ? "bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300" : "bg-zinc-100 text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400"
1710
+ ),
1670
1711
  style: { width: 48, minWidth: 48, height: rowHeight },
1671
- onClick: handleClick,
1712
+ onMouseDown: handleMouseDown,
1713
+ onMouseEnter: handleMouseEnter,
1714
+ onMouseUp: handleMouseUp,
1672
1715
  children: rowIndex + 1
1673
1716
  }
1674
1717
  );
@@ -1693,21 +1736,24 @@ function serialToDateTimeStr(serial) {
1693
1736
  }
1694
1737
  function isDateFormula(formula) {
1695
1738
  if (!formula) return false;
1696
- const f = formula.toUpperCase();
1697
- return /^(TODAY|NOW|DATE|EDATE)\b/.test(f) || /\b(TODAY|NOW|DATE|EDATE)\s*\(/.test(f);
1739
+ const f = formula.trim().toUpperCase();
1740
+ return /^(TODAY|NOW|DATE|EDATE)\s*\(/.test(f);
1698
1741
  }
1699
1742
  function formatCellValue(val, cell) {
1700
1743
  if (val === null || val === void 0) return "";
1701
1744
  const fmt = cell?.format?.displayFormat;
1702
1745
  if (typeof val === "number") {
1746
+ const dec = cell?.format?.decimals;
1703
1747
  if (fmt === "date") return serialToDateStr(val);
1704
1748
  if (fmt === "datetime") return serialToDateTimeStr(val);
1705
- if (fmt === "percentage") return (val * 100).toFixed(1) + "%";
1706
- if (fmt === "currency") return "$" + val.toFixed(2);
1749
+ if (fmt === "percentage") return (val * 100).toFixed(dec ?? 1) + "%";
1750
+ if (fmt === "currency") return "$" + val.toFixed(dec ?? 2);
1751
+ if (fmt === "number" && dec !== void 0) return val.toFixed(dec);
1707
1752
  if (fmt === "auto" || !fmt) {
1708
1753
  if (cell?.formula && isDateFormula(cell.formula)) {
1709
1754
  return val % 1 === 0 ? serialToDateStr(val) : serialToDateTimeStr(val);
1710
1755
  }
1756
+ if (dec !== void 0) return val.toFixed(dec);
1711
1757
  }
1712
1758
  }
1713
1759
  if (typeof val === "boolean") return val ? "TRUE" : "FALSE";
@@ -1731,7 +1777,8 @@ var Cell = react.memo(function Cell2({ address, row, col }) {
1731
1777
  rowHeight,
1732
1778
  getColumnWidth,
1733
1779
  isCellSelected,
1734
- isCellActive
1780
+ isCellActive,
1781
+ _isDragging
1735
1782
  } = useSpreadsheet();
1736
1783
  const cell = activeSheet.cells[address];
1737
1784
  const isActive = isCellActive(address);
@@ -1741,6 +1788,7 @@ var Cell = react.memo(function Cell2({ address, row, col }) {
1741
1788
  const width = getColumnWidth(col);
1742
1789
  const handleMouseDown = react.useCallback(
1743
1790
  (e) => {
1791
+ if (e.button !== 0) return;
1744
1792
  if (e.shiftKey) {
1745
1793
  extendSelection(address);
1746
1794
  } else if (e.ctrlKey || e.metaKey) {
@@ -1748,9 +1796,18 @@ var Cell = react.memo(function Cell2({ address, row, col }) {
1748
1796
  } else {
1749
1797
  setSelection(address);
1750
1798
  }
1799
+ _isDragging.current = true;
1751
1800
  },
1752
- [address, setSelection, extendSelection, addSelection]
1801
+ [address, setSelection, extendSelection, addSelection, _isDragging]
1753
1802
  );
1803
+ const handleMouseEnter = react.useCallback(() => {
1804
+ if (_isDragging.current) {
1805
+ extendSelection(address);
1806
+ }
1807
+ }, [address, extendSelection, _isDragging]);
1808
+ const handleMouseUp = react.useCallback(() => {
1809
+ _isDragging.current = false;
1810
+ }, [_isDragging]);
1754
1811
  const handleDoubleClick = react.useCallback(() => {
1755
1812
  if (readOnly) return;
1756
1813
  startEdit();
@@ -1767,12 +1824,14 @@ var Cell = react.memo(function Cell2({ address, row, col }) {
1767
1824
  "data-active": isActive || void 0,
1768
1825
  role: "gridcell",
1769
1826
  className: reactFancy.cn(
1770
- "relative flex items-center truncate border-r border-b border-zinc-200 bg-white px-1.5 text-[13px] dark:border-zinc-700 dark:bg-zinc-900",
1827
+ "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",
1771
1828
  isActive && "ring-2 ring-inset ring-blue-500",
1772
1829
  isSelected && !isActive && "bg-blue-50 dark:bg-blue-950/40"
1773
1830
  ),
1774
1831
  style: { width, minWidth: width, height: rowHeight, ...formatStyle },
1775
1832
  onMouseDown: handleMouseDown,
1833
+ onMouseEnter: handleMouseEnter,
1834
+ onMouseUp: handleMouseUp,
1776
1835
  onDoubleClick: handleDoubleClick,
1777
1836
  children: !isEditing && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: displayValue })
1778
1837
  }
@@ -1914,6 +1973,7 @@ function SpreadsheetGrid({ className }) {
1914
1973
  setCellValue,
1915
1974
  setFrozenRows,
1916
1975
  setFrozenCols,
1976
+ extendSelection,
1917
1977
  undo,
1918
1978
  redo
1919
1979
  } = useSpreadsheet();
@@ -2137,6 +2197,8 @@ function DefaultToolbar() {
2137
2197
  const isBold = cell?.format?.bold ?? false;
2138
2198
  const isItalic = cell?.format?.italic ?? false;
2139
2199
  const textAlign = cell?.format?.textAlign ?? "left";
2200
+ const displayFormat = cell?.format?.displayFormat ?? "auto";
2201
+ const decimals = cell?.format?.decimals;
2140
2202
  const selectedAddresses = [selection.activeCell];
2141
2203
  const handleFormulaBarChange = (e) => {
2142
2204
  if (editingCell) {
@@ -2248,6 +2310,52 @@ function DefaultToolbar() {
2248
2310
  /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "19", y1: "3", x2: "19", y2: "21", strokeDasharray: "3 3" })
2249
2311
  ] })
2250
2312
  }
2313
+ ),
2314
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-1 h-4 w-px bg-zinc-200 dark:bg-zinc-700" }),
2315
+ /* @__PURE__ */ jsxRuntime.jsxs(
2316
+ "select",
2317
+ {
2318
+ className: "h-6 rounded border border-zinc-200 bg-transparent px-1 text-[11px] text-zinc-600 outline-none hover:border-zinc-300 dark:border-zinc-700 dark:text-zinc-400 dark:hover:border-zinc-600",
2319
+ value: displayFormat,
2320
+ onChange: (e) => setCellFormat(selectedAddresses, { displayFormat: e.target.value }),
2321
+ disabled: readOnly,
2322
+ title: "Cell format",
2323
+ children: [
2324
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "auto", children: "Auto" }),
2325
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "text", children: "Text" }),
2326
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "number", children: "Number" }),
2327
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "currency", children: "Currency ($)" }),
2328
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "percentage", children: "Percentage (%)" }),
2329
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "date", children: "Date" }),
2330
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "datetime", children: "Date & Time" })
2331
+ ]
2332
+ }
2333
+ ),
2334
+ /* @__PURE__ */ jsxRuntime.jsxs(
2335
+ "button",
2336
+ {
2337
+ className: btnClass,
2338
+ onClick: () => setCellFormat(selectedAddresses, { decimals: Math.max(0, (decimals ?? 0) - 1) }),
2339
+ disabled: readOnly || (decimals ?? 0) <= 0,
2340
+ title: "Decrease decimal places",
2341
+ children: [
2342
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px]", children: ".0" }),
2343
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[8px]", children: "\u2190" })
2344
+ ]
2345
+ }
2346
+ ),
2347
+ /* @__PURE__ */ jsxRuntime.jsxs(
2348
+ "button",
2349
+ {
2350
+ className: btnClass,
2351
+ onClick: () => setCellFormat(selectedAddresses, { decimals: (decimals ?? 0) + 1 }),
2352
+ disabled: readOnly,
2353
+ title: "Increase decimal places",
2354
+ children: [
2355
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px]", children: ".00" }),
2356
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[8px]", children: "\u2192" })
2357
+ ]
2358
+ }
2251
2359
  )
2252
2360
  ] }),
2253
2361
  /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-fancy-sheets-formula-bar": "", className: "flex items-center gap-2 border-b border-zinc-200 px-2 py-1 dark:border-zinc-700", children: [
@@ -2411,6 +2519,7 @@ function SpreadsheetRoot({
2411
2519
  (address) => state.selection.activeCell === address,
2412
2520
  [state.selection.activeCell]
2413
2521
  );
2522
+ const isDraggingRef = react.useRef(false);
2414
2523
  const ctx = react.useMemo(
2415
2524
  () => ({
2416
2525
  workbook: state.workbook,
@@ -2428,7 +2537,8 @@ function SpreadsheetRoot({
2428
2537
  canRedo: state.redoStack.length > 0,
2429
2538
  getColumnWidth,
2430
2539
  isCellSelected,
2431
- isCellActive
2540
+ isCellActive,
2541
+ _isDragging: isDraggingRef
2432
2542
  }),
2433
2543
  [state, activeSheet, columnCount, rowCount, defaultColumnWidth, rowHeight, readOnly, actions, getColumnWidth, isCellSelected, isCellActive]
2434
2544
  );