@rowsncolumns/spreadsheet 10.2.72 → 10.2.74
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/cjs/api/evaluate-formula.d.ts +17 -0
- package/dist/cjs/api/evaluate-formula.d.ts.map +1 -0
- package/dist/cjs/api/evaluate-formula.js +141 -0
- package/dist/cjs/api/go-to.d.ts +19 -0
- package/dist/cjs/api/go-to.d.ts.map +1 -0
- package/dist/cjs/api/go-to.js +49 -0
- package/dist/cjs/atoms/evaluate-formula-dialog.atom.d.ts +36 -0
- package/dist/cjs/atoms/evaluate-formula-dialog.atom.d.ts.map +1 -0
- package/dist/cjs/atoms/evaluate-formula-dialog.atom.js +36 -0
- package/dist/cjs/atoms/go-to-dialog.atom.d.ts +36 -0
- package/dist/cjs/atoms/go-to-dialog.atom.d.ts.map +1 -0
- package/dist/cjs/atoms/go-to-dialog.atom.js +34 -0
- package/dist/cjs/atoms/index.d.ts +4 -0
- package/dist/cjs/atoms/index.d.ts.map +1 -1
- package/dist/cjs/atoms/index.js +4 -0
- package/dist/cjs/atoms/paste-special-dialog.atom.d.ts +29 -0
- package/dist/cjs/atoms/paste-special-dialog.atom.d.ts.map +1 -0
- package/dist/cjs/atoms/paste-special-dialog.atom.js +34 -0
- package/dist/cjs/atoms/remove-duplicates-dialog.atom.d.ts +45 -0
- package/dist/cjs/atoms/remove-duplicates-dialog.atom.d.ts.map +1 -0
- package/dist/cjs/atoms/remove-duplicates-dialog.atom.js +37 -0
- package/dist/cjs/components/canvas-grid/index.d.ts.map +1 -1
- package/dist/cjs/components/canvas-grid/index.js +109 -3
- package/dist/cjs/components/canvas-grid/typings.d.ts +54 -8
- package/dist/cjs/components/canvas-grid/typings.d.ts.map +1 -1
- package/dist/cjs/components/context-menu/index.d.ts +16 -3
- package/dist/cjs/components/context-menu/index.d.ts.map +1 -1
- package/dist/cjs/components/context-menu/index.js +112 -5
- package/dist/cjs/components/context-menu/total-row-functions.d.ts +32 -0
- package/dist/cjs/components/context-menu/total-row-functions.d.ts.map +1 -0
- package/dist/cjs/components/context-menu/total-row-functions.js +62 -0
- package/dist/cjs/components/evaluate-formula-dialog/index.d.ts +34 -0
- package/dist/cjs/components/evaluate-formula-dialog/index.d.ts.map +1 -0
- package/dist/cjs/components/evaluate-formula-dialog/index.js +106 -0
- package/dist/cjs/components/go-to-dialog/index.d.ts +35 -0
- package/dist/cjs/components/go-to-dialog/index.d.ts.map +1 -0
- package/dist/cjs/components/go-to-dialog/index.js +93 -0
- package/dist/cjs/components/paste-special-dialog/index.d.ts +29 -0
- package/dist/cjs/components/paste-special-dialog/index.d.ts.map +1 -0
- package/dist/cjs/components/paste-special-dialog/index.js +99 -0
- package/dist/cjs/components/remove-duplicates-dialog/index.d.ts +37 -0
- package/dist/cjs/components/remove-duplicates-dialog/index.d.ts.map +1 -0
- package/dist/cjs/components/remove-duplicates-dialog/index.js +91 -0
- package/dist/cjs/components/toolbar/index.d.ts +1 -0
- package/dist/cjs/components/toolbar/index.d.ts.map +1 -1
- package/dist/cjs/components/toolbar/index.js +1 -0
- package/dist/cjs/components/toolbar/text-rotation-selector.d.ts +29 -0
- package/dist/cjs/components/toolbar/text-rotation-selector.d.ts.map +1 -0
- package/dist/cjs/components/toolbar/text-rotation-selector.js +78 -0
- package/dist/cjs/components/toolbar/text-wrap-selector.d.ts +7 -1
- package/dist/cjs/components/toolbar/text-wrap-selector.d.ts.map +1 -1
- package/dist/cjs/components/toolbar/text-wrap-selector.js +10 -3
- package/dist/cjs/hooks/use-copy-paste-cut.d.ts +20 -1
- package/dist/cjs/hooks/use-copy-paste-cut.d.ts.map +1 -1
- package/dist/cjs/hooks/use-copy-paste-cut.js +27 -21
- package/dist/cjs/hooks/use-keyboard-handler.d.ts +17 -2
- package/dist/cjs/hooks/use-keyboard-handler.d.ts.map +1 -1
- package/dist/cjs/hooks/use-keyboard-handler.js +186 -21
- package/dist/cjs/index.d.ts +6 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +7 -1
- package/dist/cjs/release-date.js +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/cjs/types.d.ts +173 -1
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/esm/api/evaluate-formula.d.ts +17 -0
- package/dist/esm/api/evaluate-formula.d.ts.map +1 -0
- package/dist/esm/api/evaluate-formula.js +109 -0
- package/dist/esm/api/go-to.d.ts +19 -0
- package/dist/esm/api/go-to.d.ts.map +1 -0
- package/dist/esm/api/go-to.js +41 -0
- package/dist/esm/atoms/evaluate-formula-dialog.atom.d.ts +36 -0
- package/dist/esm/atoms/evaluate-formula-dialog.atom.d.ts.map +1 -0
- package/dist/esm/atoms/evaluate-formula-dialog.atom.js +25 -0
- package/dist/esm/atoms/go-to-dialog.atom.d.ts +36 -0
- package/dist/esm/atoms/go-to-dialog.atom.d.ts.map +1 -0
- package/dist/esm/atoms/go-to-dialog.atom.js +25 -0
- package/dist/esm/atoms/index.d.ts +4 -0
- package/dist/esm/atoms/index.d.ts.map +1 -1
- package/dist/esm/atoms/index.js +4 -0
- package/dist/esm/atoms/paste-special-dialog.atom.d.ts +29 -0
- package/dist/esm/atoms/paste-special-dialog.atom.d.ts.map +1 -0
- package/dist/esm/atoms/paste-special-dialog.atom.js +25 -0
- package/dist/esm/atoms/remove-duplicates-dialog.atom.d.ts +45 -0
- package/dist/esm/atoms/remove-duplicates-dialog.atom.d.ts.map +1 -0
- package/dist/esm/atoms/remove-duplicates-dialog.atom.js +26 -0
- package/dist/esm/components/canvas-grid/index.d.ts.map +1 -1
- package/dist/esm/components/canvas-grid/index.js +107 -3
- package/dist/esm/components/canvas-grid/typings.d.ts +54 -8
- package/dist/esm/components/canvas-grid/typings.d.ts.map +1 -1
- package/dist/esm/components/context-menu/index.d.ts +16 -3
- package/dist/esm/components/context-menu/index.d.ts.map +1 -1
- package/dist/esm/components/context-menu/index.js +114 -7
- package/dist/esm/components/context-menu/total-row-functions.d.ts +32 -0
- package/dist/esm/components/context-menu/total-row-functions.d.ts.map +1 -0
- package/dist/esm/components/context-menu/total-row-functions.js +50 -0
- package/dist/esm/components/evaluate-formula-dialog/index.d.ts +34 -0
- package/dist/esm/components/evaluate-formula-dialog/index.d.ts.map +1 -0
- package/dist/esm/components/evaluate-formula-dialog/index.js +86 -0
- package/dist/esm/components/go-to-dialog/index.d.ts +35 -0
- package/dist/esm/components/go-to-dialog/index.d.ts.map +1 -0
- package/dist/esm/components/go-to-dialog/index.js +84 -0
- package/dist/esm/components/paste-special-dialog/index.d.ts +29 -0
- package/dist/esm/components/paste-special-dialog/index.d.ts.map +1 -0
- package/dist/esm/components/paste-special-dialog/index.js +90 -0
- package/dist/esm/components/remove-duplicates-dialog/index.d.ts +37 -0
- package/dist/esm/components/remove-duplicates-dialog/index.d.ts.map +1 -0
- package/dist/esm/components/remove-duplicates-dialog/index.js +82 -0
- package/dist/esm/components/toolbar/index.d.ts +1 -0
- package/dist/esm/components/toolbar/index.d.ts.map +1 -1
- package/dist/esm/components/toolbar/index.js +1 -0
- package/dist/esm/components/toolbar/text-rotation-selector.d.ts +29 -0
- package/dist/esm/components/toolbar/text-rotation-selector.d.ts.map +1 -0
- package/dist/esm/components/toolbar/text-rotation-selector.js +71 -0
- package/dist/esm/components/toolbar/text-wrap-selector.d.ts +7 -1
- package/dist/esm/components/toolbar/text-wrap-selector.d.ts.map +1 -1
- package/dist/esm/components/toolbar/text-wrap-selector.js +10 -3
- package/dist/esm/hooks/use-copy-paste-cut.d.ts +20 -1
- package/dist/esm/hooks/use-copy-paste-cut.d.ts.map +1 -1
- package/dist/esm/hooks/use-copy-paste-cut.js +10 -4
- package/dist/esm/hooks/use-keyboard-handler.d.ts +17 -2
- package/dist/esm/hooks/use-keyboard-handler.d.ts.map +1 -1
- package/dist/esm/hooks/use-keyboard-handler.js +173 -21
- package/dist/esm/index.d.ts +6 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +5 -0
- package/dist/esm/release-date.js +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/esm/types.d.ts +173 -1
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/spreadsheet.min.css +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { NamedRange, SheetRange } from "../../types";
|
|
3
|
+
export type GoToDialogProps = {
|
|
4
|
+
/** Controls visibility. */
|
|
5
|
+
open: boolean;
|
|
6
|
+
onOpenChange(open: boolean): void;
|
|
7
|
+
/** Defined names listed in the "Go to" box; picking one fills the reference. */
|
|
8
|
+
namedRanges?: NamedRange[] | null;
|
|
9
|
+
/** Shown as the placeholder of the reference box (the current location). */
|
|
10
|
+
currentAddress?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Resolves the typed text to a range, or `undefined` when it is not a
|
|
13
|
+
* valid reference / name (the dialog then shows Excel's error).
|
|
14
|
+
*/
|
|
15
|
+
resolve(reference: string): SheetRange | undefined;
|
|
16
|
+
/** Receives the resolved range; the host selects and scrolls to it. */
|
|
17
|
+
onNavigate(range: SheetRange): void;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Excel's Go To dialog (`Ctrl+G` / `F5`): type a cell or range reference,
|
|
21
|
+
* optionally sheet-qualified, or pick a defined name, and the selection
|
|
22
|
+
* jumps there. Resolution is delegated to `resolve` so the dialog stays
|
|
23
|
+
* UI-only.
|
|
24
|
+
*/
|
|
25
|
+
export declare const GoToDialog: React.MemoExoticComponent<({ open, onOpenChange, namedRanges, currentAddress, resolve, onNavigate, }: GoToDialogProps) => React.JSX.Element>;
|
|
26
|
+
/**
|
|
27
|
+
* Host-mounted, atom-driven wrapper around {@link GoToDialog}. Mount it once
|
|
28
|
+
* in the host alongside the other editor dialogs; the grid opens it on
|
|
29
|
+
* `Ctrl+G` / `F5` (unless the host handles `onRequestGoTo` itself).
|
|
30
|
+
*/
|
|
31
|
+
export declare const GoToDialogHost: {
|
|
32
|
+
(): React.JSX.Element | null;
|
|
33
|
+
displayName: string;
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/go-to-dialog/index.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAQ3D,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE1D,MAAM,MAAM,eAAe,GAAG;IAC5B,2BAA2B;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IAClC,gFAAgF;IAChF,WAAW,CAAC,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC;IAClC,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;IACnD,uEAAuE;IACvE,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;CACrC,CAAC;AAIF;;;;;GAKG;AACH,eAAO,MAAM,UAAU,wGAQlB,eAAe,uBA4GnB,CAAC;AAUF;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;CA8B1B,CAAC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Button, cn, Dialog, DialogBody, DialogCloseButton, DialogContent, DialogTitle, Input, Separator, } from "@rowsncolumns/ui";
|
|
2
|
+
import { cellToAddress } from "@rowsncolumns/utils";
|
|
3
|
+
import React, { memo, useCallback, useState } from "react";
|
|
4
|
+
import { resolveGoToReference } from "../../api/go-to";
|
|
5
|
+
import { useGoToDialogState, useRegisterGoToDialogHost, } from "../../atoms/go-to-dialog.atom";
|
|
6
|
+
const INVALID_REFERENCE = "Reference is not valid.";
|
|
7
|
+
/**
|
|
8
|
+
* Excel's Go To dialog (`Ctrl+G` / `F5`): type a cell or range reference,
|
|
9
|
+
* optionally sheet-qualified, or pick a defined name, and the selection
|
|
10
|
+
* jumps there. Resolution is delegated to `resolve` so the dialog stays
|
|
11
|
+
* UI-only.
|
|
12
|
+
*/
|
|
13
|
+
export const GoToDialog = memo(({ open, onOpenChange, namedRanges, currentAddress, resolve, onNavigate, }) => {
|
|
14
|
+
const [reference, setReference] = useState("");
|
|
15
|
+
const [error, setError] = useState(undefined);
|
|
16
|
+
const navigate = useCallback((text) => {
|
|
17
|
+
const range = resolve(text);
|
|
18
|
+
if (!range) {
|
|
19
|
+
setError(INVALID_REFERENCE);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
onNavigate(range);
|
|
23
|
+
onOpenChange(false);
|
|
24
|
+
}, [resolve, onNavigate, onOpenChange]);
|
|
25
|
+
const names = (namedRanges ?? []).filter((range) => range.range && !range.hidden);
|
|
26
|
+
return (React.createElement(Dialog, { open: open, onOpenChange: onOpenChange, modal: true },
|
|
27
|
+
React.createElement(DialogContent, { className: "w-[360px] max-w-[92vw] pointer-events-auto" },
|
|
28
|
+
React.createElement(DialogTitle, null, "Go To"),
|
|
29
|
+
React.createElement(DialogCloseButton, null),
|
|
30
|
+
React.createElement(DialogBody, { className: "flex flex-col gap-3" },
|
|
31
|
+
React.createElement("form", { className: "flex flex-col gap-3", onSubmit: (e) => {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
navigate(reference);
|
|
34
|
+
} },
|
|
35
|
+
React.createElement("div", { className: "flex flex-col gap-1" },
|
|
36
|
+
React.createElement("span", { className: "text-xs font-medium" }, "Go to:"),
|
|
37
|
+
React.createElement("div", { role: "listbox", "aria-label": "Defined names", className: "h-[140px] overflow-auto rounded border border-rnc-border text-xs" }, names.length === 0 ? (React.createElement("div", { className: "px-2 py-1.5 text-rnc-foreground/60" }, "No defined names")) : (names.map((range) => (React.createElement("div", { key: String(range.namedRangeId), role: "option", "aria-selected": reference === range.name, className: cn("px-2 py-1 cursor-default select-none hover:bg-rnc-accent", reference === range.name && "bg-rnc-accent"), onClick: () => {
|
|
38
|
+
setReference(range.name);
|
|
39
|
+
setError(undefined);
|
|
40
|
+
}, onDoubleClick: () => navigate(range.name) }, range.name)))))),
|
|
41
|
+
React.createElement("label", { className: "flex flex-col gap-1 text-xs font-medium" },
|
|
42
|
+
"Reference:",
|
|
43
|
+
React.createElement(Input, { autoFocus: true, value: reference, placeholder: currentAddress, "aria-invalid": error ? true : undefined, onChange: (e) => {
|
|
44
|
+
setReference(e.target.value);
|
|
45
|
+
setError(undefined);
|
|
46
|
+
} })),
|
|
47
|
+
error ? (React.createElement("div", { role: "alert", className: "text-xs text-rnc-destructive" }, error)) : null,
|
|
48
|
+
React.createElement(Separator, null),
|
|
49
|
+
React.createElement("div", { className: "flex justify-end gap-2" },
|
|
50
|
+
React.createElement(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onOpenChange(false) }, "Cancel"),
|
|
51
|
+
React.createElement(Button, { type: "submit", size: "sm", disabled: !reference.trim() }, "OK")))))));
|
|
52
|
+
});
|
|
53
|
+
GoToDialog.displayName = "GoToDialog";
|
|
54
|
+
const currentAddressOf = (request) => {
|
|
55
|
+
const address = cellToAddress(request.activeCell) ?? "";
|
|
56
|
+
const sheetName = request.getSheetName?.(request.sheetId);
|
|
57
|
+
return sheetName ? `${sheetName}!${address}` : address;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Host-mounted, atom-driven wrapper around {@link GoToDialog}. Mount it once
|
|
61
|
+
* in the host alongside the other editor dialogs; the grid opens it on
|
|
62
|
+
* `Ctrl+G` / `F5` (unless the host handles `onRequestGoTo` itself).
|
|
63
|
+
*/
|
|
64
|
+
export const GoToDialogHost = () => {
|
|
65
|
+
useRegisterGoToDialogHost();
|
|
66
|
+
const [request, setRequest] = useGoToDialogState();
|
|
67
|
+
const close = useCallback(() => setRequest(null), [setRequest]);
|
|
68
|
+
const resolve = useCallback((reference) => request
|
|
69
|
+
? resolveGoToReference(reference, {
|
|
70
|
+
sheetId: request.sheetId,
|
|
71
|
+
namedRanges: request.namedRanges,
|
|
72
|
+
getSheetId: request.getSheetId,
|
|
73
|
+
rowCount: request.rowCount,
|
|
74
|
+
columnCount: request.columnCount,
|
|
75
|
+
})
|
|
76
|
+
: undefined, [request]);
|
|
77
|
+
if (!request)
|
|
78
|
+
return null;
|
|
79
|
+
return (React.createElement(GoToDialog, { open: true, onOpenChange: (open) => {
|
|
80
|
+
if (!open)
|
|
81
|
+
close();
|
|
82
|
+
}, namedRanges: request.namedRanges, currentAddress: currentAddressOf(request), resolve: resolve, onNavigate: request.onNavigate }));
|
|
83
|
+
};
|
|
84
|
+
GoToDialogHost.displayName = "GoToDialogHost";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { PasteSpecialOptions } from "../../hooks/use-copy-paste-cut";
|
|
3
|
+
export type PasteSpecialDialogProps = {
|
|
4
|
+
/** Controls visibility. */
|
|
5
|
+
open: boolean;
|
|
6
|
+
onOpenChange(open: boolean): void;
|
|
7
|
+
/**
|
|
8
|
+
* Emits the assembled request. The host routes it to the grid's
|
|
9
|
+
* `paste(options)`; the dialog never touches the clipboard itself.
|
|
10
|
+
*/
|
|
11
|
+
onApply(options: PasteSpecialOptions): void;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Excel's Paste Special dialog: what to paste (all / formulas / values /
|
|
15
|
+
* formats), an arithmetic operation against the destination, skip blanks and
|
|
16
|
+
* transpose. Every choice composes into one `PasteSpecialOptions` request.
|
|
17
|
+
*/
|
|
18
|
+
export declare const PasteSpecialDialog: React.MemoExoticComponent<({ open, onOpenChange, onApply }: PasteSpecialDialogProps) => React.JSX.Element>;
|
|
19
|
+
/**
|
|
20
|
+
* Host-mounted, atom-driven wrapper around {@link PasteSpecialDialog}. Mount
|
|
21
|
+
* it once in the host alongside the other editor dialogs; the grid opens it
|
|
22
|
+
* from the context menu ("Paste special…") and `Ctrl+Alt+V`. Remounted per
|
|
23
|
+
* request so each opening starts from Excel's defaults.
|
|
24
|
+
*/
|
|
25
|
+
export declare const PasteSpecialDialogHost: {
|
|
26
|
+
(): React.JSX.Element | null;
|
|
27
|
+
displayName: string;
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/paste-special-dialog/index.tsx"],"names":[],"mappings":"AASA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAM3D,OAAO,KAAK,EAEV,mBAAmB,EACpB,MAAM,gCAAgC,CAAC;AAoBxC,MAAM,MAAM,uBAAuB,GAAG;IACpC,2BAA2B;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IAClC;;;OAGG;IACH,OAAO,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAAC;CAC7C,CAAC;AAyCF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,8DACK,uBAAuB,uBA2F1D,CAAC;AAIF;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB;;;CAclC,CAAC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Button, Dialog, DialogBody, DialogCloseButton, DialogContent, DialogTitle, Separator, } from "@rowsncolumns/ui";
|
|
2
|
+
import React, { memo, useCallback, useState } from "react";
|
|
3
|
+
import { usePasteSpecialDialogState, useRegisterPasteSpecialDialogHost, } from "../../atoms/paste-special-dialog.atom";
|
|
4
|
+
const PASTE_MODES = [
|
|
5
|
+
{ value: "all", label: "All" },
|
|
6
|
+
{ value: "formulas", label: "Formulas" },
|
|
7
|
+
{ value: "values", label: "Values" },
|
|
8
|
+
{ value: "formats", label: "Formats" },
|
|
9
|
+
];
|
|
10
|
+
const OPERATIONS = [
|
|
11
|
+
{ value: "none", label: "None" },
|
|
12
|
+
{ value: "add", label: "Add" },
|
|
13
|
+
{ value: "subtract", label: "Subtract" },
|
|
14
|
+
{ value: "multiply", label: "Multiply" },
|
|
15
|
+
{ value: "divide", label: "Divide" },
|
|
16
|
+
];
|
|
17
|
+
const RadioGroup = ({ name, legend, value, options, onChange, disabled, }) => (React.createElement("fieldset", { className: "flex flex-col gap-1 min-w-0 disabled:opacity-50", disabled: disabled },
|
|
18
|
+
React.createElement("legend", { className: "text-xs font-medium mb-1" }, legend),
|
|
19
|
+
options.map((option) => (React.createElement("label", { key: option.value, className: "flex items-center gap-2 text-xs py-0.5 cursor-pointer" },
|
|
20
|
+
React.createElement("input", { type: "radio", name: name, value: option.value, checked: value === option.value, disabled: disabled, onChange: () => onChange(option.value) }),
|
|
21
|
+
option.label)))));
|
|
22
|
+
/**
|
|
23
|
+
* Excel's Paste Special dialog: what to paste (all / formulas / values /
|
|
24
|
+
* formats), an arithmetic operation against the destination, skip blanks and
|
|
25
|
+
* transpose. Every choice composes into one `PasteSpecialOptions` request.
|
|
26
|
+
*/
|
|
27
|
+
export const PasteSpecialDialog = memo(({ open, onOpenChange, onApply }) => {
|
|
28
|
+
const [paste, setPaste] = useState("all");
|
|
29
|
+
const [operation, setOperation] = useState("none");
|
|
30
|
+
const [skipBlanks, setSkipBlanks] = useState(false);
|
|
31
|
+
const [transpose, setTranspose] = useState(false);
|
|
32
|
+
// Excel greys Operation out under Paste → Formats (a formats paste never
|
|
33
|
+
// changes destination values); the choice is kept but not applied.
|
|
34
|
+
const operationDisabled = paste === "formats";
|
|
35
|
+
const commit = useCallback(() => {
|
|
36
|
+
onApply({
|
|
37
|
+
paste,
|
|
38
|
+
operation: operation === "none" || operationDisabled ? undefined : operation,
|
|
39
|
+
skipBlanks,
|
|
40
|
+
transpose,
|
|
41
|
+
});
|
|
42
|
+
onOpenChange(false);
|
|
43
|
+
}, [
|
|
44
|
+
onApply,
|
|
45
|
+
onOpenChange,
|
|
46
|
+
paste,
|
|
47
|
+
operation,
|
|
48
|
+
operationDisabled,
|
|
49
|
+
skipBlanks,
|
|
50
|
+
transpose,
|
|
51
|
+
]);
|
|
52
|
+
return (React.createElement(Dialog, { open: open, onOpenChange: onOpenChange, modal: true },
|
|
53
|
+
React.createElement(DialogContent, { className: "w-[380px] max-w-[92vw] pointer-events-auto" },
|
|
54
|
+
React.createElement(DialogTitle, null, "Paste Special"),
|
|
55
|
+
React.createElement(DialogCloseButton, null),
|
|
56
|
+
React.createElement(DialogBody, { className: "flex flex-col gap-4" },
|
|
57
|
+
React.createElement("div", { className: "grid grid-cols-2 gap-4" },
|
|
58
|
+
React.createElement(RadioGroup, { name: "paste-special-paste", legend: "Paste", value: paste, options: PASTE_MODES, onChange: setPaste }),
|
|
59
|
+
React.createElement(RadioGroup, { name: "paste-special-operation", legend: "Operation", value: operation, options: OPERATIONS, onChange: setOperation, disabled: operationDisabled })),
|
|
60
|
+
React.createElement("div", { className: "flex flex-col gap-1" },
|
|
61
|
+
React.createElement("label", { className: "flex items-center gap-2 text-xs py-0.5 cursor-pointer" },
|
|
62
|
+
React.createElement("input", { type: "checkbox", checked: skipBlanks, onChange: (e) => setSkipBlanks(e.target.checked) }),
|
|
63
|
+
"Skip blanks"),
|
|
64
|
+
React.createElement("label", { className: "flex items-center gap-2 text-xs py-0.5 cursor-pointer" },
|
|
65
|
+
React.createElement("input", { type: "checkbox", checked: transpose, onChange: (e) => setTranspose(e.target.checked) }),
|
|
66
|
+
"Transpose")),
|
|
67
|
+
React.createElement(Separator, null),
|
|
68
|
+
React.createElement("div", { className: "flex justify-end gap-2" },
|
|
69
|
+
React.createElement(Button, { variant: "outline", size: "sm", onClick: () => onOpenChange(false) }, "Cancel"),
|
|
70
|
+
React.createElement(Button, { size: "sm", onClick: commit }, "OK"))))));
|
|
71
|
+
});
|
|
72
|
+
PasteSpecialDialog.displayName = "PasteSpecialDialog";
|
|
73
|
+
/**
|
|
74
|
+
* Host-mounted, atom-driven wrapper around {@link PasteSpecialDialog}. Mount
|
|
75
|
+
* it once in the host alongside the other editor dialogs; the grid opens it
|
|
76
|
+
* from the context menu ("Paste special…") and `Ctrl+Alt+V`. Remounted per
|
|
77
|
+
* request so each opening starts from Excel's defaults.
|
|
78
|
+
*/
|
|
79
|
+
export const PasteSpecialDialogHost = () => {
|
|
80
|
+
useRegisterPasteSpecialDialogHost();
|
|
81
|
+
const [request, setRequest] = usePasteSpecialDialogState();
|
|
82
|
+
const close = useCallback(() => setRequest(null), [setRequest]);
|
|
83
|
+
if (!request)
|
|
84
|
+
return null;
|
|
85
|
+
return (React.createElement(PasteSpecialDialog, { open: true, onOpenChange: (open) => {
|
|
86
|
+
if (!open)
|
|
87
|
+
close();
|
|
88
|
+
}, onApply: request.onApply }));
|
|
89
|
+
};
|
|
90
|
+
PasteSpecialDialogHost.displayName = "PasteSpecialDialogHost";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type RemoveDuplicatesDialogColumn } from "../../atoms/remove-duplicates-dialog.atom";
|
|
3
|
+
import type { RemoveDuplicatesOptions } from "../../types";
|
|
4
|
+
export type { RemoveDuplicatesDialogColumn };
|
|
5
|
+
export type RemoveDuplicatesDialogProps = {
|
|
6
|
+
/** Controls visibility. */
|
|
7
|
+
open: boolean;
|
|
8
|
+
onOpenChange(open: boolean): void;
|
|
9
|
+
/** Columns of the range, in sheet order. All start checked. */
|
|
10
|
+
columns: RemoveDuplicatesDialogColumn[];
|
|
11
|
+
/** Initial state of "My data has headers". */
|
|
12
|
+
initialHasHeader?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Emits the chosen compare columns and header flag. The host routes these
|
|
15
|
+
* to `onRemoveDuplicates`; the dialog never touches state directly.
|
|
16
|
+
*/
|
|
17
|
+
onApply(options: RemoveDuplicatesOptions): void;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Excel's "Remove Duplicates" dialog: a "My data has headers" toggle plus a
|
|
21
|
+
* checklist of the columns that decide whether two rows are duplicates.
|
|
22
|
+
* Unchecked columns still travel with their row — only the comparison
|
|
23
|
+
* narrows.
|
|
24
|
+
*/
|
|
25
|
+
export declare const RemoveDuplicatesDialog: React.MemoExoticComponent<({ open, onOpenChange, columns, initialHasHeader, onApply, }: RemoveDuplicatesDialogProps) => React.JSX.Element>;
|
|
26
|
+
/**
|
|
27
|
+
* Host-mounted, atom-driven wrapper around {@link RemoveDuplicatesDialog}.
|
|
28
|
+
* Mount this once in the host (alongside the other editor dialogs); the
|
|
29
|
+
* context menu opens it via `useOpenRemoveDuplicatesDialog`. While no host is
|
|
30
|
+
* mounted the context menu keeps its one-click de-dupe, so adopting the
|
|
31
|
+
* dialog is opt-in per host.
|
|
32
|
+
*/
|
|
33
|
+
export declare const RemoveDuplicatesDialogHost: {
|
|
34
|
+
(): React.JSX.Element;
|
|
35
|
+
displayName: string;
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/remove-duplicates-dialog/index.tsx"],"names":[],"mappings":"AAWA,OAAO,KAA0D,MAAM,OAAO,CAAC;AAE/E,OAAO,EACL,KAAK,4BAA4B,EAGlC,MAAM,2CAA2C,CAAC;AACnD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAE3D,YAAY,EAAE,4BAA4B,EAAE,CAAC;AAE7C,MAAM,MAAM,2BAA2B,GAAG;IACxC,2BAA2B;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IAClC,+DAA+D;IAC/D,OAAO,EAAE,4BAA4B,EAAE,CAAC;IACxC,8CAA8C;IAC9C,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;OAGG;IACH,OAAO,CAAC,OAAO,EAAE,uBAAuB,GAAG,IAAI,CAAC;CACjD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,0FAO9B,2BAA2B,uBAqI/B,CAAC;AAIF;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B;;;CAetC,CAAC"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Cross1Icon } from "@rowsncolumns/icons";
|
|
2
|
+
import { Button, Dialog, DialogBody, DialogCloseButton, DialogContent, DialogDescription, DialogTitle, Separator, } from "@rowsncolumns/ui";
|
|
3
|
+
import React, { memo, useCallback, useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { useRegisterRemoveDuplicatesDialogHost, useRemoveDuplicatesDialogState, } from "../../atoms/remove-duplicates-dialog.atom";
|
|
5
|
+
/**
|
|
6
|
+
* Excel's "Remove Duplicates" dialog: a "My data has headers" toggle plus a
|
|
7
|
+
* checklist of the columns that decide whether two rows are duplicates.
|
|
8
|
+
* Unchecked columns still travel with their row — only the comparison
|
|
9
|
+
* narrows.
|
|
10
|
+
*/
|
|
11
|
+
export const RemoveDuplicatesDialog = memo(({ open, onOpenChange, columns, initialHasHeader = false, onApply, }) => {
|
|
12
|
+
const allColumnIndexes = useMemo(() => columns.map((column) => column.columnIndex), [columns]);
|
|
13
|
+
const [hasHeader, setHasHeader] = useState(initialHasHeader);
|
|
14
|
+
const [checked, setChecked] = useState(() => new Set(allColumnIndexes));
|
|
15
|
+
// Re-seed when reopened for a different range.
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (open) {
|
|
18
|
+
setHasHeader(initialHasHeader);
|
|
19
|
+
setChecked(new Set(allColumnIndexes));
|
|
20
|
+
}
|
|
21
|
+
// Only reset on open transitions.
|
|
22
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
23
|
+
}, [open]);
|
|
24
|
+
const toggleColumn = useCallback((columnIndex) => {
|
|
25
|
+
setChecked((prev) => {
|
|
26
|
+
const next = new Set(prev);
|
|
27
|
+
if (next.has(columnIndex)) {
|
|
28
|
+
next.delete(columnIndex);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
next.add(columnIndex);
|
|
32
|
+
}
|
|
33
|
+
return next;
|
|
34
|
+
});
|
|
35
|
+
}, []);
|
|
36
|
+
const commit = useCallback(() => {
|
|
37
|
+
const columnIndices = allColumnIndexes.filter((columnIndex) => checked.has(columnIndex));
|
|
38
|
+
onApply({ columnIndices, hasHeader });
|
|
39
|
+
onOpenChange(false);
|
|
40
|
+
}, [allColumnIndexes, checked, hasHeader, onApply, onOpenChange]);
|
|
41
|
+
return (React.createElement(Dialog, { open: open, onOpenChange: onOpenChange },
|
|
42
|
+
React.createElement(DialogContent, { className: "w-[420px] max-w-[92vw] pointer-events-auto" },
|
|
43
|
+
React.createElement(DialogTitle, null, "Remove duplicates"),
|
|
44
|
+
React.createElement(DialogDescription, null, "Select the columns that contain duplicate values. Rows that match in every selected column are removed, keeping the first one."),
|
|
45
|
+
React.createElement(DialogCloseButton, { onClick: () => onOpenChange(false) },
|
|
46
|
+
React.createElement(Cross1Icon, null)),
|
|
47
|
+
React.createElement(DialogBody, { className: "flex flex-col gap-3" },
|
|
48
|
+
React.createElement("label", { className: "font-system text-sm flex items-center gap-1" },
|
|
49
|
+
React.createElement("input", { type: "checkbox", name: "hasHeader", checked: hasHeader, onChange: (e) => setHasHeader(e.target.checked) }),
|
|
50
|
+
"My data has headers"),
|
|
51
|
+
React.createElement("div", { className: "flex gap-2" },
|
|
52
|
+
React.createElement(Button, { variant: "outline", size: "xs", type: "button", onClick: () => setChecked(new Set(allColumnIndexes)) }, "Select all"),
|
|
53
|
+
React.createElement(Button, { variant: "outline", size: "xs", type: "button", onClick: () => setChecked(new Set()) }, "Unselect all")),
|
|
54
|
+
React.createElement("div", { className: "flex flex-col gap-1 max-h-[240px] overflow-y-auto border border-rnc-border rounded p-2", role: "group", "aria-label": "Columns" }, columns.map((column) => {
|
|
55
|
+
const text = hasHeader && column.header ? column.header : column.label;
|
|
56
|
+
return (React.createElement("label", { key: column.columnIndex, className: "font-system text-sm flex items-center gap-1" },
|
|
57
|
+
React.createElement("input", { type: "checkbox", name: `column-${column.columnIndex}`, checked: checked.has(column.columnIndex), onChange: () => toggleColumn(column.columnIndex) }),
|
|
58
|
+
React.createElement("span", { className: "truncate", title: text }, text)));
|
|
59
|
+
})),
|
|
60
|
+
React.createElement(Separator, null),
|
|
61
|
+
React.createElement("div", { className: "flex justify-end gap-2" },
|
|
62
|
+
React.createElement(Button, { variant: "outline", size: "sm", type: "button", onClick: () => onOpenChange(false) }, "Cancel"),
|
|
63
|
+
React.createElement(Button, { size: "sm", type: "button", onClick: commit, disabled: checked.size === 0 }, "OK"))))));
|
|
64
|
+
});
|
|
65
|
+
RemoveDuplicatesDialog.displayName = "RemoveDuplicatesDialog";
|
|
66
|
+
/**
|
|
67
|
+
* Host-mounted, atom-driven wrapper around {@link RemoveDuplicatesDialog}.
|
|
68
|
+
* Mount this once in the host (alongside the other editor dialogs); the
|
|
69
|
+
* context menu opens it via `useOpenRemoveDuplicatesDialog`. While no host is
|
|
70
|
+
* mounted the context menu keeps its one-click de-dupe, so adopting the
|
|
71
|
+
* dialog is opt-in per host.
|
|
72
|
+
*/
|
|
73
|
+
export const RemoveDuplicatesDialogHost = () => {
|
|
74
|
+
useRegisterRemoveDuplicatesDialogHost();
|
|
75
|
+
const [request, setRequest] = useRemoveDuplicatesDialogState();
|
|
76
|
+
const close = useCallback(() => setRequest(null), [setRequest]);
|
|
77
|
+
return (React.createElement(RemoveDuplicatesDialog, { open: request != null, onOpenChange: (open) => {
|
|
78
|
+
if (!open)
|
|
79
|
+
close();
|
|
80
|
+
}, columns: request?.columns ?? [], initialHasHeader: request?.hasHeader, onApply: (options) => request?.onApply(options) }));
|
|
81
|
+
};
|
|
82
|
+
RemoveDuplicatesDialogHost.displayName = "RemoveDuplicatesDialogHost";
|
|
@@ -10,6 +10,7 @@ export * from "./font-family-selector";
|
|
|
10
10
|
export * from "./text-horizontal-align-selector";
|
|
11
11
|
export * from "./text-vertical-align-selector";
|
|
12
12
|
export * from "./text-wrap-selector";
|
|
13
|
+
export * from "./text-rotation-selector";
|
|
13
14
|
export * from "./text-format-selector";
|
|
14
15
|
export * from "./theme-selector";
|
|
15
16
|
export * from "./table-actions";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/toolbar/index.tsx"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kCAAkC,CAAC;AACjD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,kBAAkB,CAAC;AACjC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/toolbar/index.tsx"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kCAAkC,CAAC;AACjD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,kBAAkB,CAAC;AACjC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC"}
|
|
@@ -10,6 +10,7 @@ export * from "./font-family-selector";
|
|
|
10
10
|
export * from "./text-horizontal-align-selector";
|
|
11
11
|
export * from "./text-vertical-align-selector";
|
|
12
12
|
export * from "./text-wrap-selector";
|
|
13
|
+
export * from "./text-rotation-selector";
|
|
13
14
|
export * from "./text-format-selector";
|
|
14
15
|
export * from "./theme-selector";
|
|
15
16
|
export * from "./table-actions";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { CellFormat } from "../../types";
|
|
3
|
+
export type TextRotationValue = NonNullable<CellFormat["textRotation"]>;
|
|
4
|
+
type TextRotationPreset = {
|
|
5
|
+
label: string;
|
|
6
|
+
value: TextRotationValue;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Excel's Orientation menu. Degrees are counter-clockwise positive (Excel's
|
|
10
|
+
* convention, what the renderer and the xlsx round-trip use), so "clockwise"
|
|
11
|
+
* presets are negative. `0` is horizontal — used instead of clearing the
|
|
12
|
+
* property so both engines apply it as a plain format write.
|
|
13
|
+
*/
|
|
14
|
+
export declare const TEXT_ROTATION_PRESETS: TextRotationPreset[];
|
|
15
|
+
export declare const MIN_TEXT_ROTATION = -90;
|
|
16
|
+
export declare const MAX_TEXT_ROTATION = 90;
|
|
17
|
+
/**
|
|
18
|
+
* Coerce free-form degree input into the supported -90…90 range. Returns
|
|
19
|
+
* `undefined` for input that is not a finite number.
|
|
20
|
+
*/
|
|
21
|
+
export declare const clampTextRotation: (input: string | number) => number | undefined;
|
|
22
|
+
type TextRotationSelectorProps = {
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
value?: CellFormat["textRotation"];
|
|
25
|
+
onChange(value: TextRotationValue): void;
|
|
26
|
+
};
|
|
27
|
+
export declare const TextRotationSelector: ({ value, onChange, disabled, }: TextRotationSelectorProps) => React.JSX.Element;
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=text-rotation-selector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-rotation-selector.d.ts","sourceRoot":"","sources":["../../../../components/toolbar/text-rotation-selector.tsx"],"names":[],"mappings":"AAaA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAGnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,MAAM,MAAM,iBAAiB,GAAG,WAAW,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC;AAExE,KAAK,kBAAkB,GAAG;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,iBAAiB,CAAC;CAC1B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,kBAAkB,EAOrD,CAAC;AAEF,eAAO,MAAM,iBAAiB,MAAM,CAAC;AACrC,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAI,OAAO,MAAM,GAAG,MAAM,uBAMvD,CAAC;AAEF,KAAK,yBAAyB,GAAG;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,UAAU,CAAC,cAAc,CAAC,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAC;CAC1C,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,gCAIlC,yBAAyB,sBAoF3B,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { MdOutlineArrowDropDown, RotateCounterClockwiseIcon, } from "@rowsncolumns/icons";
|
|
2
|
+
import { Button, IconButton, Input, Popover, PopoverContent, PopoverTrigger, Stack, } from "@rowsncolumns/ui";
|
|
3
|
+
import React, { useEffect, useState } from "react";
|
|
4
|
+
import { useFocusSheet } from "../../atoms";
|
|
5
|
+
/**
|
|
6
|
+
* Excel's Orientation menu. Degrees are counter-clockwise positive (Excel's
|
|
7
|
+
* convention, what the renderer and the xlsx round-trip use), so "clockwise"
|
|
8
|
+
* presets are negative. `0` is horizontal — used instead of clearing the
|
|
9
|
+
* property so both engines apply it as a plain format write.
|
|
10
|
+
*/
|
|
11
|
+
export const TEXT_ROTATION_PRESETS = [
|
|
12
|
+
{ label: "Horizontal", value: 0 },
|
|
13
|
+
{ label: "Angle counterclockwise", value: 45 },
|
|
14
|
+
{ label: "Angle clockwise", value: -45 },
|
|
15
|
+
{ label: "Vertical text", value: "vertical" },
|
|
16
|
+
{ label: "Rotate text up", value: 90 },
|
|
17
|
+
{ label: "Rotate text down", value: -90 },
|
|
18
|
+
];
|
|
19
|
+
export const MIN_TEXT_ROTATION = -90;
|
|
20
|
+
export const MAX_TEXT_ROTATION = 90;
|
|
21
|
+
/**
|
|
22
|
+
* Coerce free-form degree input into the supported -90…90 range. Returns
|
|
23
|
+
* `undefined` for input that is not a finite number.
|
|
24
|
+
*/
|
|
25
|
+
export const clampTextRotation = (input) => {
|
|
26
|
+
const value = typeof input === "number" ? input : Number(input);
|
|
27
|
+
if (!Number.isFinite(value)) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
return Math.max(MIN_TEXT_ROTATION, Math.min(MAX_TEXT_ROTATION, value));
|
|
31
|
+
};
|
|
32
|
+
export const TextRotationSelector = ({ value, onChange, disabled, }) => {
|
|
33
|
+
const focusSheet = useFocusSheet();
|
|
34
|
+
const [isOpen, onOpenChange] = useState(false);
|
|
35
|
+
const current = value ?? 0;
|
|
36
|
+
const [degrees, setDegrees] = useState(typeof current === "number" ? String(current) : "");
|
|
37
|
+
// Reflect the active cell's rotation in the custom-degrees field whenever
|
|
38
|
+
// the popover opens on a different value.
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (isOpen) {
|
|
41
|
+
setDegrees(typeof current === "number" ? String(current) : "");
|
|
42
|
+
}
|
|
43
|
+
}, [isOpen, current]);
|
|
44
|
+
const close = () => onOpenChange(false);
|
|
45
|
+
const apply = (next) => {
|
|
46
|
+
onChange(next);
|
|
47
|
+
focusSheet?.();
|
|
48
|
+
close();
|
|
49
|
+
};
|
|
50
|
+
const applyDegrees = () => {
|
|
51
|
+
const next = clampTextRotation(degrees);
|
|
52
|
+
if (next !== undefined) {
|
|
53
|
+
apply(next);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
return (React.createElement(Popover, { open: isOpen, onOpenChange: onOpenChange },
|
|
57
|
+
React.createElement(PopoverTrigger, { asChild: true },
|
|
58
|
+
React.createElement(IconButton, { size: "lg", tooltip: "Text orientation", "aria-label": "Text orientation", disabled: disabled },
|
|
59
|
+
React.createElement(RotateCounterClockwiseIcon, null),
|
|
60
|
+
React.createElement(MdOutlineArrowDropDown, null))),
|
|
61
|
+
React.createElement(PopoverContent, { align: "start" },
|
|
62
|
+
React.createElement(Stack, { direction: "vertical", className: "gap-1 min-w-[200px]" },
|
|
63
|
+
TEXT_ROTATION_PRESETS.map((preset) => (React.createElement(Button, { key: preset.label, size: "xs", variant: current === preset.value ? "primary" : "ghost", className: "justify-start", "aria-pressed": current === preset.value, onClick: () => apply(preset.value) }, preset.label))),
|
|
64
|
+
React.createElement("form", { className: "flex items-center gap-1 pt-1 m-0", onSubmit: (e) => {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
applyDegrees();
|
|
67
|
+
} },
|
|
68
|
+
React.createElement("label", { className: "font-system text-xs shrink-0", htmlFor: "rnc-text-rotation-degrees" }, "Degrees"),
|
|
69
|
+
React.createElement(Input, { id: "rnc-text-rotation-degrees", type: "number", min: MIN_TEXT_ROTATION, max: MAX_TEXT_ROTATION, step: 1, value: degrees, onChange: (e) => setDegrees(e.target.value), className: "w-[72px]" }),
|
|
70
|
+
React.createElement(Button, { size: "xs", type: "submit" }, "Apply"))))));
|
|
71
|
+
};
|
|
@@ -3,7 +3,13 @@ import type { WrapStrategy } from "../../types";
|
|
|
3
3
|
type TextWrapSelectorProps = {
|
|
4
4
|
value?: WrapStrategy | null;
|
|
5
5
|
onChange(value: WrapStrategy): void;
|
|
6
|
+
/**
|
|
7
|
+
* Excel's "Shrink to fit": scale the font down so the text fits the cell.
|
|
8
|
+
* The toggle only renders when `onChangeShrinkToFit` is provided.
|
|
9
|
+
*/
|
|
10
|
+
shrinkToFit?: boolean | null;
|
|
11
|
+
onChangeShrinkToFit?(value: boolean): void;
|
|
6
12
|
};
|
|
7
|
-
export declare const TextWrapSelector: ({ value, onChange, }: TextWrapSelectorProps) => React.JSX.Element;
|
|
13
|
+
export declare const TextWrapSelector: ({ value, onChange, shrinkToFit, onChangeShrinkToFit, }: TextWrapSelectorProps) => React.JSX.Element;
|
|
8
14
|
export {};
|
|
9
15
|
//# sourceMappingURL=text-wrap-selector.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-wrap-selector.d.ts","sourceRoot":"","sources":["../../../../components/toolbar/text-wrap-selector.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAGxC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,KAAK,qBAAqB,GAAG;IAC3B,KAAK,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"text-wrap-selector.d.ts","sourceRoot":"","sources":["../../../../components/toolbar/text-wrap-selector.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAGxC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,KAAK,qBAAqB,GAAG;IAC3B,KAAK,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;IACpC;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC7B,mBAAmB,CAAC,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CAC5C,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,wDAK9B,qBAAqB,sBAoFvB,CAAC"}
|
|
@@ -3,13 +3,13 @@ import { MdOutlineArrowDropDown } from "@rowsncolumns/icons";
|
|
|
3
3
|
import { IconButton, Popover, PopoverContent, PopoverTrigger, Stack, } from "@rowsncolumns/ui";
|
|
4
4
|
import React, { useState } from "react";
|
|
5
5
|
import { useFocusSheet } from "../../atoms";
|
|
6
|
-
export const TextWrapSelector = ({ value = "overflow", onChange, }) => {
|
|
6
|
+
export const TextWrapSelector = ({ value = "overflow", onChange, shrinkToFit, onChangeShrinkToFit, }) => {
|
|
7
7
|
const focusSheet = useFocusSheet();
|
|
8
8
|
const [isOpen, onOpenChange] = useState(false);
|
|
9
9
|
const close = () => onOpenChange(false);
|
|
10
10
|
return (React.createElement(Popover, { open: isOpen, onOpenChange: onOpenChange },
|
|
11
11
|
React.createElement(PopoverTrigger, { asChild: true },
|
|
12
|
-
React.createElement(IconButton, { size: "lg", tooltip: "Text wrapping" },
|
|
12
|
+
React.createElement(IconButton, { size: "lg", tooltip: "Text wrapping", "aria-label": "Text wrapping" },
|
|
13
13
|
value === "overflow" ? (React.createElement(TextOverflowIcon, null)) : value === "wrap" ? (React.createElement(TextOverflowWrapIcon, null)) : (React.createElement(TextoverflowClipIcon, null)),
|
|
14
14
|
React.createElement(MdOutlineArrowDropDown, null))),
|
|
15
15
|
React.createElement(PopoverContent, { align: "start" },
|
|
@@ -31,5 +31,12 @@ export const TextWrapSelector = ({ value = "overflow", onChange, }) => {
|
|
|
31
31
|
focusSheet?.();
|
|
32
32
|
close();
|
|
33
33
|
} },
|
|
34
|
-
React.createElement(TextoverflowClipIcon, null))
|
|
34
|
+
React.createElement(TextoverflowClipIcon, null)),
|
|
35
|
+
onChangeShrinkToFit ? (React.createElement(IconButton, { size: "lg", isActive: !!shrinkToFit, "aria-pressed": !!shrinkToFit, tooltip: "Shrink text to fit the cell", "aria-label": "Shrink text to fit the cell", className: "w-full justify-start gap-1", onClick: () => {
|
|
36
|
+
onChangeShrinkToFit(!shrinkToFit);
|
|
37
|
+
focusSheet?.();
|
|
38
|
+
close();
|
|
39
|
+
} },
|
|
40
|
+
React.createElement(TextoverflowClipIcon, null),
|
|
41
|
+
React.createElement("span", { className: "text-xs" }, "Shrink to fit"))) : null))));
|
|
35
42
|
};
|
|
@@ -31,11 +31,30 @@ export type UseCopyPasteCutProps = {
|
|
|
31
31
|
onCut: CanvasGridProps["onCut"];
|
|
32
32
|
};
|
|
33
33
|
export type PasteSpecialType = "Transposed" | "Formatting" | "Value" | "Formula" | "Link";
|
|
34
|
+
/** Excel Paste Special → Operation: combine each pasted cell with the cell it lands on. */
|
|
35
|
+
export type PasteSpecialOperation = "add" | "subtract" | "multiply" | "divide";
|
|
36
|
+
/**
|
|
37
|
+
* Excel's Paste Special dialog as one request. `paste` picks what is
|
|
38
|
+
* pasted (all / values / formulas / formats), `operation` combines the pasted
|
|
39
|
+
* value with the destination (`=dest+source`, formulas stay formulas),
|
|
40
|
+
* `skipBlanks` leaves the destination untouched where the clipboard cell is
|
|
41
|
+
* empty and `transpose` swaps rows and columns. Every flag composes with the
|
|
42
|
+
* others; the legacy single-mode {@link PasteSpecialType} strings map onto
|
|
43
|
+
* the same fields.
|
|
44
|
+
*/
|
|
45
|
+
export type PasteSpecialOptions = {
|
|
46
|
+
paste?: "all" | "values" | "formulas" | "formats";
|
|
47
|
+
operation?: PasteSpecialOperation;
|
|
48
|
+
skipBlanks?: boolean;
|
|
49
|
+
transpose?: boolean;
|
|
50
|
+
};
|
|
51
|
+
/** Argument accepted by `paste()` — a legacy single mode or the full dialog request. */
|
|
52
|
+
export type PasteSpecialRequest = PasteSpecialType | PasteSpecialOptions;
|
|
34
53
|
export declare const useCopyPasteCut: ({ selections, activeCell, sheetId, onPaste, onCopy, onCut, focusSheet, }: UseCopyPasteCutProps) => {
|
|
35
54
|
selections: SelectionArea[];
|
|
36
55
|
copy: () => void;
|
|
37
56
|
cut: () => void;
|
|
38
|
-
paste: (
|
|
57
|
+
paste: (pasteSpecial?: PasteSpecialRequest) => Promise<void>;
|
|
39
58
|
copiedSelections: any[];
|
|
40
59
|
clearCopiedSelections: () => void;
|
|
41
60
|
onInternalCopy: (e: ClipboardEvent, isCutOperation?: boolean) => boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-copy-paste-cut.d.ts","sourceRoot":"","sources":["../../../hooks/use-copy-paste-cut.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"use-copy-paste-cut.d.ts","sourceRoot":"","sources":["../../../hooks/use-copy-paste-cut.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAYvE,OAAO,KAAK,EACV,eAAe,EAEhB,MAAM,mCAAmC,CAAC;AAK3C,MAAM,MAAM,oBAAoB,GAAG;IACjC;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,UAAU,EAAE,aAAa,CAAC;IAC1B;;OAEG;IACH,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B;;OAEG;IACH,UAAU,IAAI,IAAI,CAAC;IACnB;;OAEG;IACH,OAAO,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;IACpC;;OAEG;IACH,MAAM,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAC;IAClC;;OAEG;IACH,KAAK,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,gBAAgB,GACxB,YAAY,GACZ,YAAY,GACZ,OAAO,GACP,SAAS,GACT,MAAM,CAAC;AAEX,2FAA2F;AAC3F,MAAM,MAAM,qBAAqB,GAAG,KAAK,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC;AAE/E;;;;;;;;GAQG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;IAClD,SAAS,CAAC,EAAE,qBAAqB,CAAC;IAClC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,wFAAwF;AACxF,MAAM,MAAM,mBAAmB,GAAG,gBAAgB,GAAG,mBAAmB,CAAC;AAEzE,eAAO,MAAM,eAAe,GAAI,0EAQ7B,oBAAoB;;;;2BAoRG,mBAAmB;;;wBA1KrC,cAAc;yBAuDd,cAAc;uBAtBd,cAAc;CA0MrB,CAAC"}
|