@particle-academy/fancy-sheets 0.3.0 → 0.4.0

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,7 +1601,20 @@ function ColumnResizeHandle({ colIndex }) {
1601
1601
  }
1602
1602
  ColumnResizeHandle.displayName = "ColumnResizeHandle";
1603
1603
  function ColumnHeaders() {
1604
- const { columnCount, rowHeight, getColumnWidth } = useSpreadsheet();
1604
+ const { columnCount, rowCount, rowHeight, getColumnWidth, selection, selectRange } = useSpreadsheet();
1605
+ const handleColumnClick = react.useCallback(
1606
+ (colIdx, e) => {
1607
+ if (e.shiftKey) {
1608
+ const activeCol = parseAddress(selection.activeCell).col;
1609
+ const minCol = Math.min(activeCol, colIdx);
1610
+ const maxCol = Math.max(activeCol, colIdx);
1611
+ selectRange(toAddress(0, minCol), toAddress(rowCount - 1, maxCol));
1612
+ } else {
1613
+ selectRange(toAddress(0, colIdx), toAddress(rowCount - 1, colIdx));
1614
+ }
1615
+ },
1616
+ [rowCount, selectRange, selection.activeCell]
1617
+ );
1605
1618
  return /* @__PURE__ */ jsxRuntime.jsxs(
1606
1619
  "div",
1607
1620
  {
@@ -1619,8 +1632,9 @@ function ColumnHeaders() {
1619
1632
  Array.from({ length: columnCount }, (_, i) => /* @__PURE__ */ jsxRuntime.jsxs(
1620
1633
  "div",
1621
1634
  {
1622
- className: "relative flex shrink-0 items-center justify-center border-r border-zinc-300 text-[11px] font-medium text-zinc-500 select-none dark:border-zinc-600 dark:text-zinc-400",
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",
1623
1636
  style: { width: getColumnWidth(i), minWidth: getColumnWidth(i) },
1637
+ onClick: (e) => handleColumnClick(i, e),
1624
1638
  children: [
1625
1639
  columnToLetter(i),
1626
1640
  /* @__PURE__ */ jsxRuntime.jsx(ColumnResizeHandle, { colIndex: i })
@@ -1634,13 +1648,27 @@ function ColumnHeaders() {
1634
1648
  }
1635
1649
  ColumnHeaders.displayName = "ColumnHeaders";
1636
1650
  function RowHeader({ rowIndex }) {
1637
- const { rowHeight } = useSpreadsheet();
1651
+ const { rowHeight, columnCount, selection, selectRange, extendSelection } = useSpreadsheet();
1652
+ const handleClick = react.useCallback(
1653
+ (e) => {
1654
+ if (e.shiftKey) {
1655
+ const activeRow = parseAddress(selection.activeCell).row;
1656
+ const minRow = Math.min(activeRow, rowIndex);
1657
+ const maxRow = Math.max(activeRow, rowIndex);
1658
+ selectRange(toAddress(minRow, 0), toAddress(maxRow, columnCount - 1));
1659
+ } else {
1660
+ selectRange(toAddress(rowIndex, 0), toAddress(rowIndex, columnCount - 1));
1661
+ }
1662
+ },
1663
+ [rowIndex, columnCount, selectRange, selection.activeCell]
1664
+ );
1638
1665
  return /* @__PURE__ */ jsxRuntime.jsx(
1639
1666
  "div",
1640
1667
  {
1641
1668
  "data-fancy-sheets-row-header": "",
1642
- className: "flex shrink-0 items-center justify-center border-r border-b border-zinc-300 bg-zinc-100 text-[11px] font-medium text-zinc-500 select-none dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-400",
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",
1643
1670
  style: { width: 48, minWidth: 48, height: rowHeight },
1671
+ onClick: handleClick,
1644
1672
  children: rowIndex + 1
1645
1673
  }
1646
1674
  );
@@ -1739,9 +1767,9 @@ var Cell = react.memo(function Cell2({ address, row, col }) {
1739
1767
  "data-active": isActive || void 0,
1740
1768
  role: "gridcell",
1741
1769
  className: reactFancy.cn(
1742
- "relative flex items-center truncate border-r border-b border-zinc-200 px-1.5 text-[13px] dark:border-zinc-700",
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",
1743
1771
  isActive && "ring-2 ring-inset ring-blue-500",
1744
- isSelected && !isActive && "bg-blue-500/10"
1772
+ isSelected && !isActive && "bg-blue-50 dark:bg-blue-950/40"
1745
1773
  ),
1746
1774
  style: { width, minWidth: width, height: rowHeight, ...formatStyle },
1747
1775
  onMouseDown: handleMouseDown,
@@ -1884,6 +1912,8 @@ function SpreadsheetGrid({ className }) {
1884
1912
  confirmEdit,
1885
1913
  cancelEdit,
1886
1914
  setCellValue,
1915
+ setFrozenRows,
1916
+ setFrozenCols,
1887
1917
  undo,
1888
1918
  redo
1889
1919
  } = useSpreadsheet();
@@ -1973,66 +2003,114 @@ function SpreadsheetGrid({ className }) {
1973
2003
  const top = row * rowHeight;
1974
2004
  return { left, top };
1975
2005
  })() : null;
1976
- return /* @__PURE__ */ jsxRuntime.jsxs(
1977
- "div",
1978
- {
1979
- ref: containerRef,
1980
- "data-fancy-sheets-grid": "",
1981
- className: reactFancy.cn("relative min-h-0 flex-1 overflow-auto bg-white focus:outline-none dark:bg-zinc-900", className),
1982
- tabIndex: 0,
1983
- onKeyDown: handleKeyDown,
1984
- children: [
1985
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky top-0 z-10", children: /* @__PURE__ */ jsxRuntime.jsx(ColumnHeaders, {}) }),
1986
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
1987
- Array.from({ length: rowCount }, (_, rowIdx) => {
1988
- const isFrozenRow = rowIdx < activeSheet.frozenRows;
1989
- return /* @__PURE__ */ jsxRuntime.jsxs(
2006
+ const handleCopy = react.useCallback(() => {
2007
+ const range = selection.ranges[0];
2008
+ if (range) {
2009
+ const tsv = cellsToTSV(activeSheet.cells, range);
2010
+ navigator.clipboard.writeText(tsv);
2011
+ }
2012
+ }, [selection, activeSheet]);
2013
+ const handlePaste = react.useCallback(() => {
2014
+ navigator.clipboard.readText().then((text) => {
2015
+ if (!text) return;
2016
+ const { values } = tsvToCells(text);
2017
+ const { row: startRow, col: startCol } = parseAddress(selection.activeCell);
2018
+ for (let r = 0; r < values.length; r++) {
2019
+ for (let c = 0; c < values[r].length; c++) {
2020
+ setCellValue(toAddress(startRow + r, startCol + c), values[r][c]);
2021
+ }
2022
+ }
2023
+ });
2024
+ }, [selection, setCellValue]);
2025
+ const handleClearSelection = react.useCallback(() => {
2026
+ const range = selection.ranges[0];
2027
+ if (!range) return;
2028
+ const { start, end } = range;
2029
+ const s = parseAddress(start);
2030
+ const e = parseAddress(end);
2031
+ const minR = Math.min(s.row, e.row), maxR = Math.max(s.row, e.row);
2032
+ const minC = Math.min(s.col, e.col), maxC = Math.max(s.col, e.col);
2033
+ for (let r = minR; r <= maxR; r++) {
2034
+ for (let c = minC; c <= maxC; c++) {
2035
+ setCellValue(toAddress(r, c), "");
2036
+ }
2037
+ }
2038
+ }, [selection, setCellValue]);
2039
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactFancy.ContextMenu, { children: [
2040
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Trigger, { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsxRuntime.jsxs(
2041
+ "div",
2042
+ {
2043
+ ref: containerRef,
2044
+ "data-fancy-sheets-grid": "",
2045
+ className: reactFancy.cn("relative h-full overflow-auto bg-white focus:outline-none dark:bg-zinc-900", className),
2046
+ tabIndex: 0,
2047
+ onKeyDown: handleKeyDown,
2048
+ children: [
2049
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky top-0 z-10", children: /* @__PURE__ */ jsxRuntime.jsx(ColumnHeaders, {}) }),
2050
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
2051
+ Array.from({ length: rowCount }, (_, rowIdx) => {
2052
+ const isFrozenRow = rowIdx < activeSheet.frozenRows;
2053
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2054
+ "div",
2055
+ {
2056
+ className: "flex",
2057
+ style: isFrozenRow ? {
2058
+ position: "sticky",
2059
+ top: rowHeight + rowIdx * rowHeight,
2060
+ zIndex: 8
2061
+ } : void 0,
2062
+ children: [
2063
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky left-0 z-[5]", children: /* @__PURE__ */ jsxRuntime.jsx(RowHeader, { rowIndex: rowIdx }) }),
2064
+ Array.from({ length: columnCount }, (_2, colIdx) => {
2065
+ const addr = toAddress(rowIdx, colIdx);
2066
+ const isFrozenCol = colIdx < activeSheet.frozenCols;
2067
+ return /* @__PURE__ */ jsxRuntime.jsx(
2068
+ "div",
2069
+ {
2070
+ style: isFrozenCol ? {
2071
+ position: "sticky",
2072
+ left: 48 + Array.from({ length: colIdx }, (_3, c) => getColumnWidth(c)).reduce((a, b) => a + b, 0),
2073
+ zIndex: isFrozenRow ? 9 : 6
2074
+ } : void 0,
2075
+ children: /* @__PURE__ */ jsxRuntime.jsx(Cell, { address: addr, row: rowIdx, col: colIdx })
2076
+ },
2077
+ addr
2078
+ );
2079
+ })
2080
+ ]
2081
+ },
2082
+ rowIdx
2083
+ );
2084
+ }),
2085
+ /* @__PURE__ */ jsxRuntime.jsx(SelectionOverlay, {}),
2086
+ editorPosition && /* @__PURE__ */ jsxRuntime.jsx(
1990
2087
  "div",
1991
2088
  {
1992
- className: "flex",
1993
- style: isFrozenRow ? {
1994
- position: "sticky",
1995
- top: rowHeight + rowIdx * rowHeight,
1996
- zIndex: 8,
1997
- backgroundColor: "inherit"
1998
- } : void 0,
1999
- children: [
2000
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky left-0 z-[5]", children: /* @__PURE__ */ jsxRuntime.jsx(RowHeader, { rowIndex: rowIdx }) }),
2001
- Array.from({ length: columnCount }, (_2, colIdx) => {
2002
- const addr = toAddress(rowIdx, colIdx);
2003
- const isFrozenCol = colIdx < activeSheet.frozenCols;
2004
- return /* @__PURE__ */ jsxRuntime.jsx(
2005
- "div",
2006
- {
2007
- style: isFrozenCol ? {
2008
- position: "sticky",
2009
- left: 48 + Array.from({ length: colIdx }, (_3, c) => getColumnWidth(c)).reduce((a, b) => a + b, 0),
2010
- zIndex: isFrozenRow ? 9 : 6,
2011
- backgroundColor: "inherit"
2012
- } : void 0,
2013
- children: /* @__PURE__ */ jsxRuntime.jsx(Cell, { address: addr, row: rowIdx, col: colIdx })
2014
- },
2015
- addr
2016
- );
2017
- })
2018
- ]
2019
- },
2020
- rowIdx
2021
- );
2022
- }),
2023
- /* @__PURE__ */ jsxRuntime.jsx(SelectionOverlay, {}),
2024
- editorPosition && /* @__PURE__ */ jsxRuntime.jsx(
2025
- "div",
2026
- {
2027
- className: "absolute z-20",
2028
- style: { left: editorPosition.left, top: editorPosition.top },
2029
- children: /* @__PURE__ */ jsxRuntime.jsx(CellEditor, {})
2030
- }
2031
- )
2032
- ] })
2033
- ]
2034
- }
2035
- );
2089
+ className: "absolute z-20",
2090
+ style: { left: editorPosition.left, top: editorPosition.top },
2091
+ children: /* @__PURE__ */ jsxRuntime.jsx(CellEditor, {})
2092
+ }
2093
+ )
2094
+ ] })
2095
+ ]
2096
+ }
2097
+ ) }),
2098
+ /* @__PURE__ */ jsxRuntime.jsxs(reactFancy.ContextMenu.Content, { children: [
2099
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: handleCopy, children: "Copy" }),
2100
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: handlePaste, disabled: readOnly, children: "Paste" }),
2101
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Separator, {}),
2102
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: handleClearSelection, disabled: readOnly, children: "Clear cells" }),
2103
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Separator, {}),
2104
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: () => {
2105
+ const row = parseAddress(selection.activeCell).row;
2106
+ setFrozenRows(activeSheet.frozenRows > 0 ? 0 : row);
2107
+ }, disabled: readOnly, children: activeSheet.frozenRows > 0 ? "Unfreeze rows" : "Freeze rows above" }),
2108
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: () => {
2109
+ const col = parseAddress(selection.activeCell).col;
2110
+ setFrozenCols(activeSheet.frozenCols > 0 ? 0 : col);
2111
+ }, disabled: readOnly, children: activeSheet.frozenCols > 0 ? "Unfreeze columns" : "Freeze columns left" })
2112
+ ] })
2113
+ ] });
2036
2114
  }
2037
2115
  SpreadsheetGrid.displayName = "SpreadsheetGrid";
2038
2116
  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";
@@ -2047,6 +2125,8 @@ function DefaultToolbar() {
2047
2125
  confirmEdit,
2048
2126
  startEdit,
2049
2127
  setCellFormat,
2128
+ setFrozenRows,
2129
+ setFrozenCols,
2050
2130
  undo,
2051
2131
  redo,
2052
2132
  canUndo,
@@ -2118,7 +2198,57 @@ function DefaultToolbar() {
2118
2198
  ] })
2119
2199
  },
2120
2200
  align
2121
- ))
2201
+ )),
2202
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-1 h-4 w-px bg-zinc-200 dark:bg-zinc-700" }),
2203
+ /* @__PURE__ */ jsxRuntime.jsx(
2204
+ "button",
2205
+ {
2206
+ className: reactFancy.cn(btnClass, activeSheet.frozenRows > 0 && activeBtnClass),
2207
+ onClick: () => {
2208
+ if (activeSheet.frozenRows > 0) {
2209
+ setFrozenRows(0);
2210
+ } else {
2211
+ const row = selection.activeCell.match(/\d+/);
2212
+ setFrozenRows(row ? parseInt(row[0], 10) - 1 || 1 : 1);
2213
+ }
2214
+ },
2215
+ disabled: readOnly,
2216
+ title: activeSheet.frozenRows > 0 ? `Unfreeze rows (${activeSheet.frozenRows} frozen)` : "Freeze rows above current cell",
2217
+ children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: [
2218
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "9", x2: "21", y2: "9" }),
2219
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "4", x2: "21", y2: "4" }),
2220
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "14", x2: "21", y2: "14", strokeDasharray: "3 3" }),
2221
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "19", x2: "21", y2: "19", strokeDasharray: "3 3" })
2222
+ ] })
2223
+ }
2224
+ ),
2225
+ /* @__PURE__ */ jsxRuntime.jsx(
2226
+ "button",
2227
+ {
2228
+ className: reactFancy.cn(btnClass, activeSheet.frozenCols > 0 && activeBtnClass),
2229
+ onClick: () => {
2230
+ if (activeSheet.frozenCols > 0) {
2231
+ setFrozenCols(0);
2232
+ } else {
2233
+ const colMatch = selection.activeCell.match(/^([A-Z]+)/);
2234
+ if (colMatch) {
2235
+ const col = colMatch[1].split("").reduce((acc, ch) => acc * 26 + ch.charCodeAt(0) - 64, 0) - 1;
2236
+ setFrozenCols(col || 1);
2237
+ } else {
2238
+ setFrozenCols(1);
2239
+ }
2240
+ }
2241
+ },
2242
+ disabled: readOnly,
2243
+ title: activeSheet.frozenCols > 0 ? `Unfreeze columns (${activeSheet.frozenCols} frozen)` : "Freeze columns left of current cell",
2244
+ children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: [
2245
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "3", x2: "9", y2: "21" }),
2246
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "3", x2: "4", y2: "21" }),
2247
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "14", y1: "3", x2: "14", y2: "21", strokeDasharray: "3 3" }),
2248
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "19", y1: "3", x2: "19", y2: "21", strokeDasharray: "3 3" })
2249
+ ] })
2250
+ }
2251
+ )
2122
2252
  ] }),
2123
2253
  /* @__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: [
2124
2254
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-12 shrink-0 text-center text-[11px] font-medium text-zinc-500 dark:text-zinc-400", children: selection.activeCell }),