@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.d.cts CHANGED
@@ -16,6 +16,8 @@ interface CellFormat {
16
16
  textAlign?: TextAlign;
17
17
  /** Display format — controls how the value is rendered */
18
18
  displayFormat?: CellDisplayFormat;
19
+ /** Number of decimal places to display (for number/currency/percentage) */
20
+ decimals?: number;
19
21
  }
20
22
  /** A single cell's complete data */
21
23
  interface CellData {
@@ -130,6 +132,8 @@ interface SpreadsheetContextValue {
130
132
  getColumnWidth: (col: number) => number;
131
133
  isCellSelected: (address: string) => boolean;
132
134
  isCellActive: (address: string) => boolean;
135
+ /** @internal drag-to-select state */
136
+ _isDragging: React.RefObject<boolean>;
133
137
  }
134
138
 
135
139
  interface SpreadsheetGridProps {
package/dist/index.d.ts CHANGED
@@ -16,6 +16,8 @@ interface CellFormat {
16
16
  textAlign?: TextAlign;
17
17
  /** Display format — controls how the value is rendered */
18
18
  displayFormat?: CellDisplayFormat;
19
+ /** Number of decimal places to display (for number/currency/percentage) */
20
+ decimals?: number;
19
21
  }
20
22
  /** A single cell's complete data */
21
23
  interface CellData {
@@ -130,6 +132,8 @@ interface SpreadsheetContextValue {
130
132
  getColumnWidth: (col: number) => number;
131
133
  isCellSelected: (address: string) => boolean;
132
134
  isCellActive: (address: string) => boolean;
135
+ /** @internal drag-to-select state */
136
+ _isDragging: React.RefObject<boolean>;
133
137
  }
134
138
 
135
139
  interface SpreadsheetGridProps {
package/dist/index.js CHANGED
@@ -1599,9 +1599,10 @@ function ColumnResizeHandle({ colIndex }) {
1599
1599
  }
1600
1600
  ColumnResizeHandle.displayName = "ColumnResizeHandle";
1601
1601
  function ColumnHeaders() {
1602
- const { columnCount, rowCount, rowHeight, getColumnWidth, selection, selectRange } = useSpreadsheet();
1603
- const handleColumnClick = useCallback(
1602
+ const { columnCount, rowCount, rowHeight, getColumnWidth, selection, selectRange, _isDragging, isCellSelected } = useSpreadsheet();
1603
+ const handleColumnMouseDown = useCallback(
1604
1604
  (colIdx, e) => {
1605
+ if (e.button !== 0) return;
1605
1606
  if (e.shiftKey) {
1606
1607
  const activeCol = parseAddress(selection.activeCell).col;
1607
1608
  const minCol = Math.min(activeCol, colIdx);
@@ -1610,9 +1611,24 @@ function ColumnHeaders() {
1610
1611
  } else {
1611
1612
  selectRange(toAddress(0, colIdx), toAddress(rowCount - 1, colIdx));
1612
1613
  }
1614
+ _isDragging.current = true;
1613
1615
  },
1614
- [rowCount, selectRange, selection.activeCell]
1616
+ [rowCount, selectRange, selection.activeCell, _isDragging]
1615
1617
  );
1618
+ const handleColumnMouseEnter = useCallback(
1619
+ (colIdx) => {
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
+ }
1626
+ },
1627
+ [rowCount, selection.activeCell, selectRange, _isDragging]
1628
+ );
1629
+ const handleMouseUp = useCallback(() => {
1630
+ _isDragging.current = false;
1631
+ }, [_isDragging]);
1616
1632
  return /* @__PURE__ */ jsxs(
1617
1633
  "div",
1618
1634
  {
@@ -1627,28 +1643,38 @@ function ColumnHeaders() {
1627
1643
  style: { width: 48, minWidth: 48 }
1628
1644
  }
1629
1645
  ),
1630
- Array.from({ length: columnCount }, (_, i) => /* @__PURE__ */ jsxs(
1631
- "div",
1632
- {
1633
- 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",
1634
- style: { width: getColumnWidth(i), minWidth: getColumnWidth(i) },
1635
- onClick: (e) => handleColumnClick(i, e),
1636
- children: [
1637
- columnToLetter(i),
1638
- /* @__PURE__ */ jsx(ColumnResizeHandle, { colIndex: i })
1639
- ]
1640
- },
1641
- i
1642
- ))
1646
+ Array.from({ length: columnCount }, (_, i) => {
1647
+ const isColSelected = isCellSelected(toAddress(0, i));
1648
+ return /* @__PURE__ */ jsxs(
1649
+ "div",
1650
+ {
1651
+ className: cn(
1652
+ "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",
1653
+ isColSelected ? "bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300" : "text-zinc-500 dark:text-zinc-400"
1654
+ ),
1655
+ style: { width: getColumnWidth(i), minWidth: getColumnWidth(i) },
1656
+ onMouseDown: (e) => handleColumnMouseDown(i, e),
1657
+ onMouseEnter: () => handleColumnMouseEnter(i),
1658
+ onMouseUp: handleMouseUp,
1659
+ children: [
1660
+ columnToLetter(i),
1661
+ /* @__PURE__ */ jsx(ColumnResizeHandle, { colIndex: i })
1662
+ ]
1663
+ },
1664
+ i
1665
+ );
1666
+ })
1643
1667
  ]
1644
1668
  }
1645
1669
  );
1646
1670
  }
1647
1671
  ColumnHeaders.displayName = "ColumnHeaders";
1648
1672
  function RowHeader({ rowIndex }) {
1649
- const { rowHeight, columnCount, selection, selectRange, extendSelection } = useSpreadsheet();
1650
- const handleClick = useCallback(
1673
+ const { rowHeight, columnCount, selection, selectRange, _isDragging, isCellSelected } = useSpreadsheet();
1674
+ const isRowSelected = isCellSelected(toAddress(rowIndex, 0));
1675
+ const handleMouseDown = useCallback(
1651
1676
  (e) => {
1677
+ if (e.button !== 0) return;
1652
1678
  if (e.shiftKey) {
1653
1679
  const activeRow = parseAddress(selection.activeCell).row;
1654
1680
  const minRow = Math.min(activeRow, rowIndex);
@@ -1657,16 +1683,33 @@ function RowHeader({ rowIndex }) {
1657
1683
  } else {
1658
1684
  selectRange(toAddress(rowIndex, 0), toAddress(rowIndex, columnCount - 1));
1659
1685
  }
1686
+ _isDragging.current = true;
1660
1687
  },
1661
- [rowIndex, columnCount, selectRange, selection.activeCell]
1688
+ [rowIndex, columnCount, selectRange, selection.activeCell, _isDragging]
1662
1689
  );
1690
+ const handleMouseEnter = useCallback(() => {
1691
+ if (_isDragging.current) {
1692
+ const activeRow = parseAddress(selection.activeCell).row;
1693
+ const minRow = Math.min(activeRow, rowIndex);
1694
+ const maxRow = Math.max(activeRow, rowIndex);
1695
+ selectRange(toAddress(minRow, 0), toAddress(maxRow, columnCount - 1));
1696
+ }
1697
+ }, [rowIndex, columnCount, selection.activeCell, selectRange, _isDragging]);
1698
+ const handleMouseUp = useCallback(() => {
1699
+ _isDragging.current = false;
1700
+ }, [_isDragging]);
1663
1701
  return /* @__PURE__ */ jsx(
1664
1702
  "div",
1665
1703
  {
1666
1704
  "data-fancy-sheets-row-header": "",
1667
- 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",
1705
+ className: cn(
1706
+ "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",
1707
+ 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"
1708
+ ),
1668
1709
  style: { width: 48, minWidth: 48, height: rowHeight },
1669
- onClick: handleClick,
1710
+ onMouseDown: handleMouseDown,
1711
+ onMouseEnter: handleMouseEnter,
1712
+ onMouseUp: handleMouseUp,
1670
1713
  children: rowIndex + 1
1671
1714
  }
1672
1715
  );
@@ -1691,21 +1734,24 @@ function serialToDateTimeStr(serial) {
1691
1734
  }
1692
1735
  function isDateFormula(formula) {
1693
1736
  if (!formula) return false;
1694
- const f = formula.toUpperCase();
1695
- return /^(TODAY|NOW|DATE|EDATE)\b/.test(f) || /\b(TODAY|NOW|DATE|EDATE)\s*\(/.test(f);
1737
+ const f = formula.trim().toUpperCase();
1738
+ return /^(TODAY|NOW|DATE|EDATE)\s*\(/.test(f);
1696
1739
  }
1697
1740
  function formatCellValue(val, cell) {
1698
1741
  if (val === null || val === void 0) return "";
1699
1742
  const fmt = cell?.format?.displayFormat;
1700
1743
  if (typeof val === "number") {
1744
+ const dec = cell?.format?.decimals;
1701
1745
  if (fmt === "date") return serialToDateStr(val);
1702
1746
  if (fmt === "datetime") return serialToDateTimeStr(val);
1703
- if (fmt === "percentage") return (val * 100).toFixed(1) + "%";
1704
- if (fmt === "currency") return "$" + val.toFixed(2);
1747
+ if (fmt === "percentage") return (val * 100).toFixed(dec ?? 1) + "%";
1748
+ if (fmt === "currency") return "$" + val.toFixed(dec ?? 2);
1749
+ if (fmt === "number" && dec !== void 0) return val.toFixed(dec);
1705
1750
  if (fmt === "auto" || !fmt) {
1706
1751
  if (cell?.formula && isDateFormula(cell.formula)) {
1707
1752
  return val % 1 === 0 ? serialToDateStr(val) : serialToDateTimeStr(val);
1708
1753
  }
1754
+ if (dec !== void 0) return val.toFixed(dec);
1709
1755
  }
1710
1756
  }
1711
1757
  if (typeof val === "boolean") return val ? "TRUE" : "FALSE";
@@ -1729,7 +1775,8 @@ var Cell = memo(function Cell2({ address, row, col }) {
1729
1775
  rowHeight,
1730
1776
  getColumnWidth,
1731
1777
  isCellSelected,
1732
- isCellActive
1778
+ isCellActive,
1779
+ _isDragging
1733
1780
  } = useSpreadsheet();
1734
1781
  const cell = activeSheet.cells[address];
1735
1782
  const isActive = isCellActive(address);
@@ -1739,6 +1786,7 @@ var Cell = memo(function Cell2({ address, row, col }) {
1739
1786
  const width = getColumnWidth(col);
1740
1787
  const handleMouseDown = useCallback(
1741
1788
  (e) => {
1789
+ if (e.button !== 0) return;
1742
1790
  if (e.shiftKey) {
1743
1791
  extendSelection(address);
1744
1792
  } else if (e.ctrlKey || e.metaKey) {
@@ -1746,9 +1794,18 @@ var Cell = memo(function Cell2({ address, row, col }) {
1746
1794
  } else {
1747
1795
  setSelection(address);
1748
1796
  }
1797
+ _isDragging.current = true;
1749
1798
  },
1750
- [address, setSelection, extendSelection, addSelection]
1799
+ [address, setSelection, extendSelection, addSelection, _isDragging]
1751
1800
  );
1801
+ const handleMouseEnter = useCallback(() => {
1802
+ if (_isDragging.current) {
1803
+ extendSelection(address);
1804
+ }
1805
+ }, [address, extendSelection, _isDragging]);
1806
+ const handleMouseUp = useCallback(() => {
1807
+ _isDragging.current = false;
1808
+ }, [_isDragging]);
1752
1809
  const handleDoubleClick = useCallback(() => {
1753
1810
  if (readOnly) return;
1754
1811
  startEdit();
@@ -1765,12 +1822,14 @@ var Cell = memo(function Cell2({ address, row, col }) {
1765
1822
  "data-active": isActive || void 0,
1766
1823
  role: "gridcell",
1767
1824
  className: cn(
1768
- "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",
1825
+ "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",
1769
1826
  isActive && "ring-2 ring-inset ring-blue-500",
1770
1827
  isSelected && !isActive && "bg-blue-50 dark:bg-blue-950/40"
1771
1828
  ),
1772
1829
  style: { width, minWidth: width, height: rowHeight, ...formatStyle },
1773
1830
  onMouseDown: handleMouseDown,
1831
+ onMouseEnter: handleMouseEnter,
1832
+ onMouseUp: handleMouseUp,
1774
1833
  onDoubleClick: handleDoubleClick,
1775
1834
  children: !isEditing && /* @__PURE__ */ jsx("span", { className: "truncate", children: displayValue })
1776
1835
  }
@@ -1912,6 +1971,7 @@ function SpreadsheetGrid({ className }) {
1912
1971
  setCellValue,
1913
1972
  setFrozenRows,
1914
1973
  setFrozenCols,
1974
+ extendSelection,
1915
1975
  undo,
1916
1976
  redo
1917
1977
  } = useSpreadsheet();
@@ -2135,6 +2195,8 @@ function DefaultToolbar() {
2135
2195
  const isBold = cell?.format?.bold ?? false;
2136
2196
  const isItalic = cell?.format?.italic ?? false;
2137
2197
  const textAlign = cell?.format?.textAlign ?? "left";
2198
+ const displayFormat = cell?.format?.displayFormat ?? "auto";
2199
+ const decimals = cell?.format?.decimals;
2138
2200
  const selectedAddresses = [selection.activeCell];
2139
2201
  const handleFormulaBarChange = (e) => {
2140
2202
  if (editingCell) {
@@ -2246,6 +2308,52 @@ function DefaultToolbar() {
2246
2308
  /* @__PURE__ */ jsx("line", { x1: "19", y1: "3", x2: "19", y2: "21", strokeDasharray: "3 3" })
2247
2309
  ] })
2248
2310
  }
2311
+ ),
2312
+ /* @__PURE__ */ jsx("div", { className: "mx-1 h-4 w-px bg-zinc-200 dark:bg-zinc-700" }),
2313
+ /* @__PURE__ */ jsxs(
2314
+ "select",
2315
+ {
2316
+ 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",
2317
+ value: displayFormat,
2318
+ onChange: (e) => setCellFormat(selectedAddresses, { displayFormat: e.target.value }),
2319
+ disabled: readOnly,
2320
+ title: "Cell format",
2321
+ children: [
2322
+ /* @__PURE__ */ jsx("option", { value: "auto", children: "Auto" }),
2323
+ /* @__PURE__ */ jsx("option", { value: "text", children: "Text" }),
2324
+ /* @__PURE__ */ jsx("option", { value: "number", children: "Number" }),
2325
+ /* @__PURE__ */ jsx("option", { value: "currency", children: "Currency ($)" }),
2326
+ /* @__PURE__ */ jsx("option", { value: "percentage", children: "Percentage (%)" }),
2327
+ /* @__PURE__ */ jsx("option", { value: "date", children: "Date" }),
2328
+ /* @__PURE__ */ jsx("option", { value: "datetime", children: "Date & Time" })
2329
+ ]
2330
+ }
2331
+ ),
2332
+ /* @__PURE__ */ jsxs(
2333
+ "button",
2334
+ {
2335
+ className: btnClass,
2336
+ onClick: () => setCellFormat(selectedAddresses, { decimals: Math.max(0, (decimals ?? 0) - 1) }),
2337
+ disabled: readOnly || (decimals ?? 0) <= 0,
2338
+ title: "Decrease decimal places",
2339
+ children: [
2340
+ /* @__PURE__ */ jsx("span", { className: "text-[10px]", children: ".0" }),
2341
+ /* @__PURE__ */ jsx("span", { className: "text-[8px]", children: "\u2190" })
2342
+ ]
2343
+ }
2344
+ ),
2345
+ /* @__PURE__ */ jsxs(
2346
+ "button",
2347
+ {
2348
+ className: btnClass,
2349
+ onClick: () => setCellFormat(selectedAddresses, { decimals: (decimals ?? 0) + 1 }),
2350
+ disabled: readOnly,
2351
+ title: "Increase decimal places",
2352
+ children: [
2353
+ /* @__PURE__ */ jsx("span", { className: "text-[10px]", children: ".00" }),
2354
+ /* @__PURE__ */ jsx("span", { className: "text-[8px]", children: "\u2192" })
2355
+ ]
2356
+ }
2249
2357
  )
2250
2358
  ] }),
2251
2359
  /* @__PURE__ */ 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: [
@@ -2409,6 +2517,7 @@ function SpreadsheetRoot({
2409
2517
  (address) => state.selection.activeCell === address,
2410
2518
  [state.selection.activeCell]
2411
2519
  );
2520
+ const isDraggingRef = useRef(false);
2412
2521
  const ctx = useMemo(
2413
2522
  () => ({
2414
2523
  workbook: state.workbook,
@@ -2426,7 +2535,8 @@ function SpreadsheetRoot({
2426
2535
  canRedo: state.redoStack.length > 0,
2427
2536
  getColumnWidth,
2428
2537
  isCellSelected,
2429
- isCellActive
2538
+ isCellActive,
2539
+ _isDragging: isDraggingRef
2430
2540
  }),
2431
2541
  [state, activeSheet, columnCount, rowCount, defaultColumnWidth, rowHeight, readOnly, actions, getColumnWidth, isCellSelected, isCellActive]
2432
2542
  );