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