@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 +139 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +139 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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) =>
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
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,
|
|
1650
|
-
const
|
|
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:
|
|
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
|
-
|
|
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)\
|
|
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
|
);
|