@rowsncolumns/spreadsheet 10.2.74 → 10.2.75

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.
Files changed (194) hide show
  1. package/dist/cjs/api/excel-validation-kinds.d.ts +71 -0
  2. package/dist/cjs/api/excel-validation-kinds.d.ts.map +1 -0
  3. package/dist/cjs/api/excel-validation-kinds.js +304 -0
  4. package/dist/cjs/api/go-to-special.d.ts +32 -0
  5. package/dist/cjs/api/go-to-special.d.ts.map +1 -0
  6. package/dist/cjs/api/go-to-special.js +111 -0
  7. package/dist/cjs/api/validation-tooltip.d.ts +17 -0
  8. package/dist/cjs/api/validation-tooltip.d.ts.map +1 -0
  9. package/dist/cjs/api/validation-tooltip.js +42 -0
  10. package/dist/cjs/atoms/advanced-filter-dialog.atom.d.ts +28 -0
  11. package/dist/cjs/atoms/advanced-filter-dialog.atom.d.ts.map +1 -0
  12. package/dist/cjs/atoms/advanced-filter-dialog.atom.js +32 -0
  13. package/dist/cjs/atoms/go-to-dialog.atom.d.ts +11 -1
  14. package/dist/cjs/atoms/go-to-dialog.atom.d.ts.map +1 -1
  15. package/dist/cjs/atoms/goal-seek-dialog.atom.d.ts +29 -0
  16. package/dist/cjs/atoms/goal-seek-dialog.atom.d.ts.map +1 -0
  17. package/dist/cjs/atoms/goal-seek-dialog.atom.js +30 -0
  18. package/dist/cjs/atoms/index.d.ts +1 -0
  19. package/dist/cjs/atoms/index.d.ts.map +1 -1
  20. package/dist/cjs/atoms/index.js +1 -0
  21. package/dist/cjs/atoms/subtotal-dialog.atom.d.ts +39 -0
  22. package/dist/cjs/atoms/subtotal-dialog.atom.d.ts.map +1 -0
  23. package/dist/cjs/atoms/subtotal-dialog.atom.js +30 -0
  24. package/dist/cjs/atoms/text-to-columns-dialog.atom.d.ts +34 -0
  25. package/dist/cjs/atoms/text-to-columns-dialog.atom.d.ts.map +1 -0
  26. package/dist/cjs/atoms/text-to-columns-dialog.atom.js +36 -0
  27. package/dist/cjs/atoms/watch-window.atom.d.ts +37 -0
  28. package/dist/cjs/atoms/watch-window.atom.d.ts.map +1 -0
  29. package/dist/cjs/atoms/watch-window.atom.js +61 -0
  30. package/dist/cjs/components/advanced-filter-dialog/index.d.ts +28 -0
  31. package/dist/cjs/components/advanced-filter-dialog/index.d.ts.map +1 -0
  32. package/dist/cjs/components/advanced-filter-dialog/index.js +134 -0
  33. package/dist/cjs/components/calculation-options-dialog/index.d.ts +33 -0
  34. package/dist/cjs/components/calculation-options-dialog/index.d.ts.map +1 -0
  35. package/dist/cjs/components/calculation-options-dialog/index.js +69 -0
  36. package/dist/cjs/components/canvas-grid/index.d.ts.map +1 -1
  37. package/dist/cjs/components/canvas-grid/index.js +104 -21
  38. package/dist/cjs/components/canvas-grid/typings.d.ts +19 -2
  39. package/dist/cjs/components/canvas-grid/typings.d.ts.map +1 -1
  40. package/dist/cjs/components/cell/alignment.d.ts +30 -0
  41. package/dist/cjs/components/cell/alignment.d.ts.map +1 -1
  42. package/dist/cjs/components/cell/alignment.js +56 -1
  43. package/dist/cjs/components/cell/index.d.ts.map +1 -1
  44. package/dist/cjs/components/cell/index.js +15 -7
  45. package/dist/cjs/components/cell-diff/index.d.ts +3 -2
  46. package/dist/cjs/components/cell-diff/index.d.ts.map +1 -1
  47. package/dist/cjs/components/cell-editor/index.d.ts.map +1 -1
  48. package/dist/cjs/components/cell-editor/index.js +2 -1
  49. package/dist/cjs/components/context-menu/index.d.ts +6 -1
  50. package/dist/cjs/components/context-menu/index.d.ts.map +1 -1
  51. package/dist/cjs/components/context-menu/index.js +146 -3
  52. package/dist/cjs/components/embeds/embed-component.d.ts.map +1 -1
  53. package/dist/cjs/components/embeds/embed-component.js +16 -1
  54. package/dist/cjs/components/go-to-dialog/index.d.ts +11 -1
  55. package/dist/cjs/components/go-to-dialog/index.d.ts.map +1 -1
  56. package/dist/cjs/components/go-to-dialog/index.js +67 -24
  57. package/dist/cjs/components/goal-seek-dialog/index.d.ts +32 -0
  58. package/dist/cjs/components/goal-seek-dialog/index.d.ts.map +1 -0
  59. package/dist/cjs/components/goal-seek-dialog/index.js +149 -0
  60. package/dist/cjs/components/subtotal-dialog/index.d.ts +30 -0
  61. package/dist/cjs/components/subtotal-dialog/index.d.ts.map +1 -0
  62. package/dist/cjs/components/subtotal-dialog/index.js +126 -0
  63. package/dist/cjs/components/text-to-columns-dialog/index.d.ts +37 -0
  64. package/dist/cjs/components/text-to-columns-dialog/index.d.ts.map +1 -0
  65. package/dist/cjs/components/text-to-columns-dialog/index.js +166 -0
  66. package/dist/cjs/components/toolbar/background-color-selector.d.ts +13 -1
  67. package/dist/cjs/components/toolbar/background-color-selector.d.ts.map +1 -1
  68. package/dist/cjs/components/toolbar/background-color-selector.js +57 -11
  69. package/dist/cjs/components/toolbar/cell-style-selector.d.ts +15 -2
  70. package/dist/cjs/components/toolbar/cell-style-selector.d.ts.map +1 -1
  71. package/dist/cjs/components/toolbar/cell-style-selector.js +56 -3
  72. package/dist/cjs/components/toolbar/fill-selectors.d.ts +49 -0
  73. package/dist/cjs/components/toolbar/fill-selectors.d.ts.map +1 -0
  74. package/dist/cjs/components/toolbar/fill-selectors.js +196 -0
  75. package/dist/cjs/components/toolbar/index.d.ts +1 -0
  76. package/dist/cjs/components/toolbar/index.d.ts.map +1 -1
  77. package/dist/cjs/components/toolbar/index.js +1 -0
  78. package/dist/cjs/components/toolbar/text-horizontal-align-selector.d.ts +8 -0
  79. package/dist/cjs/components/toolbar/text-horizontal-align-selector.d.ts.map +1 -1
  80. package/dist/cjs/components/toolbar/text-horizontal-align-selector.js +23 -4
  81. package/dist/cjs/components/watch-window/index.d.ts +25 -0
  82. package/dist/cjs/components/watch-window/index.d.ts.map +1 -0
  83. package/dist/cjs/components/watch-window/index.js +63 -0
  84. package/dist/cjs/hooks/use-cell-renderer.d.ts.map +1 -1
  85. package/dist/cjs/hooks/use-cell-renderer.js +17 -6
  86. package/dist/cjs/hooks/use-get-tooltip.d.ts.map +1 -1
  87. package/dist/cjs/hooks/use-get-tooltip.js +7 -2
  88. package/dist/cjs/hooks/use-keyboard-handler.d.ts.map +1 -1
  89. package/dist/cjs/hooks/use-keyboard-handler.js +24 -12
  90. package/dist/cjs/index.d.ts +8 -0
  91. package/dist/cjs/index.d.ts.map +1 -1
  92. package/dist/cjs/index.js +8 -0
  93. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  94. package/dist/cjs/types.d.ts +117 -4
  95. package/dist/cjs/types.d.ts.map +1 -1
  96. package/dist/cjs/types.js +19 -1
  97. package/dist/esm/api/excel-validation-kinds.d.ts +71 -0
  98. package/dist/esm/api/excel-validation-kinds.d.ts.map +1 -0
  99. package/dist/esm/api/excel-validation-kinds.js +287 -0
  100. package/dist/esm/api/go-to-special.d.ts +32 -0
  101. package/dist/esm/api/go-to-special.d.ts.map +1 -0
  102. package/dist/esm/api/go-to-special.js +96 -0
  103. package/dist/esm/api/validation-tooltip.d.ts +17 -0
  104. package/dist/esm/api/validation-tooltip.d.ts.map +1 -0
  105. package/dist/esm/api/validation-tooltip.js +34 -0
  106. package/dist/esm/atoms/advanced-filter-dialog.atom.d.ts +28 -0
  107. package/dist/esm/atoms/advanced-filter-dialog.atom.d.ts.map +1 -0
  108. package/dist/esm/atoms/advanced-filter-dialog.atom.js +21 -0
  109. package/dist/esm/atoms/go-to-dialog.atom.d.ts +11 -1
  110. package/dist/esm/atoms/go-to-dialog.atom.d.ts.map +1 -1
  111. package/dist/esm/atoms/goal-seek-dialog.atom.d.ts +29 -0
  112. package/dist/esm/atoms/goal-seek-dialog.atom.d.ts.map +1 -0
  113. package/dist/esm/atoms/goal-seek-dialog.atom.js +21 -0
  114. package/dist/esm/atoms/index.d.ts +1 -0
  115. package/dist/esm/atoms/index.d.ts.map +1 -1
  116. package/dist/esm/atoms/index.js +1 -0
  117. package/dist/esm/atoms/subtotal-dialog.atom.d.ts +39 -0
  118. package/dist/esm/atoms/subtotal-dialog.atom.d.ts.map +1 -0
  119. package/dist/esm/atoms/subtotal-dialog.atom.js +21 -0
  120. package/dist/esm/atoms/text-to-columns-dialog.atom.d.ts +34 -0
  121. package/dist/esm/atoms/text-to-columns-dialog.atom.d.ts.map +1 -0
  122. package/dist/esm/atoms/text-to-columns-dialog.atom.js +25 -0
  123. package/dist/esm/atoms/watch-window.atom.d.ts +37 -0
  124. package/dist/esm/atoms/watch-window.atom.d.ts.map +1 -0
  125. package/dist/esm/atoms/watch-window.atom.js +37 -0
  126. package/dist/esm/components/advanced-filter-dialog/index.d.ts +28 -0
  127. package/dist/esm/components/advanced-filter-dialog/index.d.ts.map +1 -0
  128. package/dist/esm/components/advanced-filter-dialog/index.js +125 -0
  129. package/dist/esm/components/calculation-options-dialog/index.d.ts +33 -0
  130. package/dist/esm/components/calculation-options-dialog/index.d.ts.map +1 -0
  131. package/dist/esm/components/calculation-options-dialog/index.js +61 -0
  132. package/dist/esm/components/canvas-grid/index.d.ts.map +1 -1
  133. package/dist/esm/components/canvas-grid/index.js +99 -20
  134. package/dist/esm/components/canvas-grid/typings.d.ts +19 -2
  135. package/dist/esm/components/canvas-grid/typings.d.ts.map +1 -1
  136. package/dist/esm/components/cell/alignment.d.ts +30 -0
  137. package/dist/esm/components/cell/alignment.d.ts.map +1 -1
  138. package/dist/esm/components/cell/alignment.js +51 -0
  139. package/dist/esm/components/cell/index.d.ts.map +1 -1
  140. package/dist/esm/components/cell/index.js +16 -8
  141. package/dist/esm/components/cell-diff/index.d.ts +3 -2
  142. package/dist/esm/components/cell-diff/index.d.ts.map +1 -1
  143. package/dist/esm/components/cell-editor/index.d.ts.map +1 -1
  144. package/dist/esm/components/cell-editor/index.js +2 -1
  145. package/dist/esm/components/context-menu/index.d.ts +6 -1
  146. package/dist/esm/components/context-menu/index.d.ts.map +1 -1
  147. package/dist/esm/components/context-menu/index.js +139 -4
  148. package/dist/esm/components/embeds/embed-component.d.ts.map +1 -1
  149. package/dist/esm/components/embeds/embed-component.js +17 -1
  150. package/dist/esm/components/go-to-dialog/index.d.ts +11 -1
  151. package/dist/esm/components/go-to-dialog/index.d.ts.map +1 -1
  152. package/dist/esm/components/go-to-dialog/index.js +63 -23
  153. package/dist/esm/components/goal-seek-dialog/index.d.ts +32 -0
  154. package/dist/esm/components/goal-seek-dialog/index.d.ts.map +1 -0
  155. package/dist/esm/components/goal-seek-dialog/index.js +125 -0
  156. package/dist/esm/components/subtotal-dialog/index.d.ts +30 -0
  157. package/dist/esm/components/subtotal-dialog/index.d.ts.map +1 -0
  158. package/dist/esm/components/subtotal-dialog/index.js +115 -0
  159. package/dist/esm/components/text-to-columns-dialog/index.d.ts +37 -0
  160. package/dist/esm/components/text-to-columns-dialog/index.d.ts.map +1 -0
  161. package/dist/esm/components/text-to-columns-dialog/index.js +147 -0
  162. package/dist/esm/components/toolbar/background-color-selector.d.ts +13 -1
  163. package/dist/esm/components/toolbar/background-color-selector.d.ts.map +1 -1
  164. package/dist/esm/components/toolbar/background-color-selector.js +61 -11
  165. package/dist/esm/components/toolbar/cell-style-selector.d.ts +15 -2
  166. package/dist/esm/components/toolbar/cell-style-selector.d.ts.map +1 -1
  167. package/dist/esm/components/toolbar/cell-style-selector.js +46 -2
  168. package/dist/esm/components/toolbar/fill-selectors.d.ts +49 -0
  169. package/dist/esm/components/toolbar/fill-selectors.d.ts.map +1 -0
  170. package/dist/esm/components/toolbar/fill-selectors.js +162 -0
  171. package/dist/esm/components/toolbar/index.d.ts +1 -0
  172. package/dist/esm/components/toolbar/index.d.ts.map +1 -1
  173. package/dist/esm/components/toolbar/index.js +1 -0
  174. package/dist/esm/components/toolbar/text-horizontal-align-selector.d.ts +8 -0
  175. package/dist/esm/components/toolbar/text-horizontal-align-selector.d.ts.map +1 -1
  176. package/dist/esm/components/toolbar/text-horizontal-align-selector.js +21 -5
  177. package/dist/esm/components/watch-window/index.d.ts +25 -0
  178. package/dist/esm/components/watch-window/index.d.ts.map +1 -0
  179. package/dist/esm/components/watch-window/index.js +54 -0
  180. package/dist/esm/hooks/use-cell-renderer.d.ts.map +1 -1
  181. package/dist/esm/hooks/use-cell-renderer.js +17 -6
  182. package/dist/esm/hooks/use-get-tooltip.d.ts.map +1 -1
  183. package/dist/esm/hooks/use-get-tooltip.js +7 -2
  184. package/dist/esm/hooks/use-keyboard-handler.d.ts.map +1 -1
  185. package/dist/esm/hooks/use-keyboard-handler.js +24 -12
  186. package/dist/esm/index.d.ts +8 -0
  187. package/dist/esm/index.d.ts.map +1 -1
  188. package/dist/esm/index.js +8 -0
  189. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  190. package/dist/esm/types.d.ts +117 -4
  191. package/dist/esm/types.d.ts.map +1 -1
  192. package/dist/esm/types.js +18 -0
  193. package/dist/spreadsheet.min.css +1 -1
  194. package/package.json +6 -6
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/goal-seek-dialog/index.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAiD,MAAM,OAAO,CAAC;AAMtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,MAAM,MAAM,mBAAmB,GAAG;IAChC,2BAA2B;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IAClC,4DAA4D;IAC5D,cAAc,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,OAAO,CACL,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;CACxC,CAAC;AAKF,qDAAqD;AACrD,eAAO,MAAM,kBAAkB,GAC7B,SAAS,MAAM,EACf,QAAQ,cAAc,GAAG,SAAS,KACjC,MAKF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,8EACyB,mBAAmB,uBAyJtE,CAAC;AAIF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB;;;CAkB9B,CAAC"}
@@ -0,0 +1,125 @@
1
+ import { Cross1Icon } from "@rowsncolumns/icons";
2
+ import { Button, Dialog, DialogBody, DialogCloseButton, DialogContent, DialogDescription, DialogTitle, Input, Separator, } from "@rowsncolumns/ui";
3
+ import { addressToCell } from "@rowsncolumns/utils";
4
+ import React, { memo, useCallback, useEffect, useState } from "react";
5
+ import { useGoalSeekDialogState, useRegisterGoalSeekDialogHost, } from "../../atoms/goal-seek-dialog.atom";
6
+ const isCellReference = (text) => /^\$?[A-Za-z]{1,3}\$?\d+$/.test(text.trim()) && addressToCell(text.trim()) != null;
7
+ /** Excel's status sentence for a finished search. */
8
+ export const goalSeekStatusText = (setCell, result) => {
9
+ if (!result)
10
+ return `Goal Seeking with Cell ${setCell} finished.`;
11
+ return result.status === "found"
12
+ ? `Goal Seeking with Cell ${setCell} found a solution.`
13
+ : `Goal Seeking with Cell ${setCell} may not have found a solution.`;
14
+ };
15
+ /**
16
+ * Excel's Goal Seek dialog: Set cell / To value / By changing cell, then the
17
+ * Goal Seek Status sentence with target and current value.
18
+ */
19
+ export const GoalSeekDialog = memo(({ open, onOpenChange, initialSetCell, onApply }) => {
20
+ const [setCell, setSetCell] = useState(initialSetCell);
21
+ const [target, setTarget] = useState("");
22
+ const [changingCell, setChangingCell] = useState("");
23
+ const [error, setError] = useState(undefined);
24
+ const [busy, setBusy] = useState(false);
25
+ const [result, setResult] = useState(undefined);
26
+ useEffect(() => {
27
+ if (open) {
28
+ setSetCell(initialSetCell);
29
+ setTarget("");
30
+ setChangingCell("");
31
+ setError(undefined);
32
+ setBusy(false);
33
+ setResult(undefined);
34
+ }
35
+ // Only reset on open transitions.
36
+ // eslint-disable-next-line react-hooks/exhaustive-deps
37
+ }, [open]);
38
+ const submit = useCallback(async (event) => {
39
+ event.preventDefault();
40
+ const targetValue = Number(target.trim());
41
+ if (!isCellReference(setCell)) {
42
+ setError("Set cell must be a single cell reference, e.g. B4.");
43
+ return;
44
+ }
45
+ if (target.trim() === "" || !Number.isFinite(targetValue)) {
46
+ setError("To value must be a number.");
47
+ return;
48
+ }
49
+ if (!isCellReference(changingCell)) {
50
+ setError("By changing cell must be a single cell reference, e.g. A1.");
51
+ return;
52
+ }
53
+ setError(undefined);
54
+ setBusy(true);
55
+ try {
56
+ const outcome = await onApply(setCell.trim().toUpperCase(), targetValue, changingCell.trim().toUpperCase());
57
+ setResult({ setCell: setCell.trim().toUpperCase(), result: outcome });
58
+ }
59
+ catch (err) {
60
+ setError(err instanceof Error ? err.message : "Goal Seek failed.");
61
+ }
62
+ finally {
63
+ setBusy(false);
64
+ }
65
+ }, [setCell, target, changingCell, onApply]);
66
+ return (React.createElement(Dialog, { open: open, onOpenChange: onOpenChange },
67
+ React.createElement(DialogContent, { className: "w-[360px] max-w-[92vw] pointer-events-auto" },
68
+ React.createElement(DialogTitle, null, result ? "Goal Seek Status" : "Goal Seek"),
69
+ React.createElement(DialogDescription, null, "Find the input value that makes a formula reach a target."),
70
+ React.createElement(DialogCloseButton, { onClick: () => onOpenChange(false) },
71
+ React.createElement(Cross1Icon, null)),
72
+ React.createElement(DialogBody, { className: "flex flex-col gap-3 font-system text-sm" }, result ? (React.createElement("div", { className: "flex flex-col gap-2", role: "status" },
73
+ React.createElement("div", null, goalSeekStatusText(result.setCell, result.result)),
74
+ result.result ? (React.createElement("dl", { className: "grid grid-cols-2 gap-1 text-xs" },
75
+ React.createElement("dt", { className: "text-rnc-foreground/70" }, "Target value:"),
76
+ React.createElement("dd", null, result.result.targetValue),
77
+ React.createElement("dt", { className: "text-rnc-foreground/70" }, "Current value:"),
78
+ React.createElement("dd", null, result.result.currentValue === undefined
79
+ ? "—"
80
+ : String(result.result.currentValue)))) : null,
81
+ React.createElement(Separator, null),
82
+ React.createElement("div", { className: "flex justify-end gap-2" },
83
+ React.createElement(Button, { size: "sm", type: "button", onClick: () => onOpenChange(false) }, "OK")))) : (React.createElement("form", { className: "flex flex-col gap-3", onSubmit: submit },
84
+ React.createElement("label", { className: "flex flex-col gap-1 text-xs font-medium" },
85
+ "Set cell:",
86
+ React.createElement(Input, { autoFocus: true, value: setCell, "aria-label": "Set cell", onChange: (e) => {
87
+ setSetCell(e.target.value);
88
+ setError(undefined);
89
+ } })),
90
+ React.createElement("label", { className: "flex flex-col gap-1 text-xs font-medium" },
91
+ "To value:",
92
+ React.createElement(Input, { value: target, "aria-label": "To value", inputMode: "decimal", onChange: (e) => {
93
+ setTarget(e.target.value);
94
+ setError(undefined);
95
+ } })),
96
+ React.createElement("label", { className: "flex flex-col gap-1 text-xs font-medium" },
97
+ "By changing cell:",
98
+ React.createElement(Input, { value: changingCell, "aria-label": "By changing cell", onChange: (e) => {
99
+ setChangingCell(e.target.value);
100
+ setError(undefined);
101
+ } })),
102
+ error ? (React.createElement("div", { role: "alert", className: "text-xs text-rnc-destructive" }, error)) : null,
103
+ React.createElement(Separator, null),
104
+ React.createElement("div", { className: "flex justify-end gap-2" },
105
+ React.createElement(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onOpenChange(false) }, "Cancel"),
106
+ React.createElement(Button, { type: "submit", size: "sm", disabled: busy }, busy ? "Seeking…" : "OK"))))))));
107
+ });
108
+ GoalSeekDialog.displayName = "GoalSeekDialog";
109
+ /**
110
+ * Host-mounted, atom-driven wrapper around {@link GoalSeekDialog}. Mount
111
+ * once in the host; the context menu opens it via `useOpenGoalSeekDialog`
112
+ * and only offers the item while a host is mounted.
113
+ */
114
+ export const GoalSeekDialogHost = () => {
115
+ useRegisterGoalSeekDialogHost();
116
+ const [request, setRequest] = useGoalSeekDialogState();
117
+ const close = useCallback(() => setRequest(null), [setRequest]);
118
+ return (React.createElement(GoalSeekDialog, { open: request != null, onOpenChange: (open) => {
119
+ if (!open)
120
+ close();
121
+ }, initialSetCell: request?.setCell ?? "", onApply: (setCell, targetValue, changingCell) => request
122
+ ? request.onApply(setCell, targetValue, changingCell)
123
+ : Promise.resolve(undefined) }));
124
+ };
125
+ GoalSeekDialogHost.displayName = "GoalSeekDialogHost";
@@ -0,0 +1,30 @@
1
+ import React from "react";
2
+ import { type SubtotalDialogChoices, type SubtotalDialogColumn } from "../../atoms/subtotal-dialog.atom";
3
+ export type { SubtotalDialogChoices, SubtotalDialogColumn };
4
+ export type SubtotalDialogProps = {
5
+ /** Controls visibility. */
6
+ open: boolean;
7
+ onOpenChange(open: boolean): void;
8
+ /** Columns of the range, in sheet order. */
9
+ columns: SubtotalDialogColumn[];
10
+ /** Initial state of "My data has headers". */
11
+ initialHasHeader?: boolean;
12
+ /** Emits the choices; the host adds the range and routes to `onSubtotal`. */
13
+ onApply(choices: SubtotalDialogChoices): void;
14
+ };
15
+ /**
16
+ * Excel's Subtotal dialog: "At each change in" (group column), "Use
17
+ * function", "Add subtotal to" (aggregate columns), "Replace current
18
+ * subtotals". Summary rows are always placed below the data.
19
+ */
20
+ export declare const SubtotalDialog: React.MemoExoticComponent<({ open, onOpenChange, columns, initialHasHeader, onApply, }: SubtotalDialogProps) => React.JSX.Element>;
21
+ /**
22
+ * Host-mounted, atom-driven wrapper around {@link SubtotalDialog}. Mount
23
+ * once in the host; the context menu opens it via `useOpenSubtotalDialog`
24
+ * and only offers the item while a host is mounted.
25
+ */
26
+ export declare const SubtotalDialogHost: {
27
+ (): React.JSX.Element;
28
+ displayName: string;
29
+ };
30
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/subtotal-dialog/index.tsx"],"names":[],"mappings":"AAWA,OAAO,KAA0D,MAAM,OAAO,CAAC;AAE/E,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EAG1B,MAAM,kCAAkC,CAAC;AAG1C,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,CAAC;AAoB5D,MAAM,MAAM,mBAAmB,GAAG;IAChC,2BAA2B;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IAClC,4CAA4C;IAC5C,OAAO,EAAE,oBAAoB,EAAE,CAAC;IAChC,8CAA8C;IAC9C,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,6EAA6E;IAC7E,OAAO,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI,CAAC;CAC/C,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,cAAc,0FAOtB,mBAAmB,uBAoKvB,CAAC;AAIF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB;;;CAe9B,CAAC"}
@@ -0,0 +1,115 @@
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 { useRegisterSubtotalDialogHost, useSubtotalDialogState, } from "../../atoms/subtotal-dialog.atom";
5
+ import { SUBTOTAL_FUNCTIONS } from "../../types";
6
+ /**
7
+ * Excel's "Use function" labels. Excel's "Count" is COUNTA (non-empty cells,
8
+ * `SUBTOTAL(3,…)`) and "Count Numbers" is COUNT (`SUBTOTAL(2,…)`).
9
+ */
10
+ const FUNCTION_LABELS = {
11
+ SUM: "Sum",
12
+ COUNTA: "Count",
13
+ AVERAGE: "Average",
14
+ MAX: "Max",
15
+ MIN: "Min",
16
+ PRODUCT: "Product",
17
+ COUNT: "Count Numbers",
18
+ STDEV: "StdDev",
19
+ STDEVP: "StdDevp",
20
+ VAR: "Var",
21
+ VARP: "Varp",
22
+ };
23
+ /**
24
+ * Excel's Subtotal dialog: "At each change in" (group column), "Use
25
+ * function", "Add subtotal to" (aggregate columns), "Replace current
26
+ * subtotals". Summary rows are always placed below the data.
27
+ */
28
+ export const SubtotalDialog = memo(({ open, onOpenChange, columns, initialHasHeader = true, onApply, }) => {
29
+ const [hasHeader, setHasHeader] = useState(initialHasHeader);
30
+ const [groupBy, setGroupBy] = useState(columns[0]?.columnIndex);
31
+ const [fn, setFn] = useState("SUM");
32
+ const [aggregates, setAggregates] = useState(() => new Set(columns.length ? [columns[columns.length - 1].columnIndex] : []));
33
+ const [replaceCurrent, setReplaceCurrent] = useState(true);
34
+ // Re-seed when reopened for a different range.
35
+ useEffect(() => {
36
+ if (open) {
37
+ setHasHeader(initialHasHeader);
38
+ setGroupBy(columns[0]?.columnIndex);
39
+ setFn("SUM");
40
+ setAggregates(new Set(columns.length ? [columns[columns.length - 1].columnIndex] : []));
41
+ setReplaceCurrent(true);
42
+ }
43
+ // Only reset on open transitions.
44
+ // eslint-disable-next-line react-hooks/exhaustive-deps
45
+ }, [open]);
46
+ const labelOf = useCallback((column) => hasHeader && column.header ? column.header : column.label, [hasHeader]);
47
+ const toggleAggregate = useCallback((columnIndex) => {
48
+ setAggregates((prev) => {
49
+ const next = new Set(prev);
50
+ if (next.has(columnIndex))
51
+ next.delete(columnIndex);
52
+ else
53
+ next.add(columnIndex);
54
+ return next;
55
+ });
56
+ }, []);
57
+ const canApply = groupBy !== undefined && aggregates.size > 0;
58
+ const orderedAggregates = useMemo(() => columns.map((c) => c.columnIndex).filter((c) => aggregates.has(c)), [columns, aggregates]);
59
+ const commit = useCallback(() => {
60
+ if (groupBy === undefined)
61
+ return;
62
+ onApply({
63
+ groupByColumnIndex: groupBy,
64
+ aggregateColumnIndices: orderedAggregates,
65
+ fn,
66
+ hasHeader,
67
+ replaceCurrent,
68
+ });
69
+ onOpenChange(false);
70
+ }, [groupBy, orderedAggregates, fn, hasHeader, replaceCurrent, onApply, onOpenChange]);
71
+ return (React.createElement(Dialog, { open: open, onOpenChange: onOpenChange },
72
+ React.createElement(DialogContent, { className: "w-[420px] max-w-[92vw] pointer-events-auto" },
73
+ React.createElement(DialogTitle, null, "Subtotal"),
74
+ React.createElement(DialogDescription, null, "Insert a total row after every change in a column, plus a grand total, and outline the groups."),
75
+ React.createElement(DialogCloseButton, { onClick: () => onOpenChange(false) },
76
+ React.createElement(Cross1Icon, null)),
77
+ React.createElement(DialogBody, { className: "flex flex-col gap-3 font-system text-sm" },
78
+ React.createElement("label", { className: "flex items-center gap-1" },
79
+ React.createElement("input", { type: "checkbox", name: "hasHeader", checked: hasHeader, onChange: (e) => setHasHeader(e.target.checked) }),
80
+ "My data has headers"),
81
+ React.createElement("label", { className: "flex flex-col gap-1 text-xs font-medium" },
82
+ "At each change in:",
83
+ React.createElement("select", { className: "h-8 rounded border border-rnc-border bg-transparent px-2 text-sm", "aria-label": "At each change in", value: groupBy ?? "", onChange: (e) => setGroupBy(Number(e.target.value)) }, columns.map((column) => (React.createElement("option", { key: column.columnIndex, value: column.columnIndex }, labelOf(column)))))),
84
+ React.createElement("label", { className: "flex flex-col gap-1 text-xs font-medium" },
85
+ "Use function:",
86
+ React.createElement("select", { className: "h-8 rounded border border-rnc-border bg-transparent px-2 text-sm", "aria-label": "Use function", value: fn, onChange: (e) => setFn(e.target.value) }, SUBTOTAL_FUNCTIONS.map((name) => (React.createElement("option", { key: name, value: name }, FUNCTION_LABELS[name]))))),
87
+ React.createElement("div", { className: "flex flex-col gap-1" },
88
+ React.createElement("span", { className: "text-xs font-medium" }, "Add subtotal to:"),
89
+ React.createElement("div", { className: "flex flex-col gap-1 max-h-[180px] overflow-y-auto border border-rnc-border rounded p-2", role: "group", "aria-label": "Add subtotal to" }, columns.map((column) => (React.createElement("label", { key: column.columnIndex, className: "flex items-center gap-1" },
90
+ React.createElement("input", { type: "checkbox", name: `subtotal-column-${column.columnIndex}`, checked: aggregates.has(column.columnIndex), onChange: () => toggleAggregate(column.columnIndex) }),
91
+ React.createElement("span", { className: "truncate", title: labelOf(column) }, labelOf(column))))))),
92
+ React.createElement("label", { className: "flex items-center gap-1" },
93
+ React.createElement("input", { type: "checkbox", name: "replaceCurrent", checked: replaceCurrent, onChange: (e) => setReplaceCurrent(e.target.checked) }),
94
+ "Replace current subtotals"),
95
+ React.createElement(Separator, null),
96
+ React.createElement("div", { className: "flex justify-end gap-2" },
97
+ React.createElement(Button, { variant: "outline", size: "sm", type: "button", onClick: () => onOpenChange(false) }, "Cancel"),
98
+ React.createElement(Button, { size: "sm", type: "button", onClick: commit, disabled: !canApply }, "OK"))))));
99
+ });
100
+ SubtotalDialog.displayName = "SubtotalDialog";
101
+ /**
102
+ * Host-mounted, atom-driven wrapper around {@link SubtotalDialog}. Mount
103
+ * once in the host; the context menu opens it via `useOpenSubtotalDialog`
104
+ * and only offers the item while a host is mounted.
105
+ */
106
+ export const SubtotalDialogHost = () => {
107
+ useRegisterSubtotalDialogHost();
108
+ const [request, setRequest] = useSubtotalDialogState();
109
+ const close = useCallback(() => setRequest(null), [setRequest]);
110
+ return (React.createElement(SubtotalDialog, { open: request != null, onOpenChange: (open) => {
111
+ if (!open)
112
+ close();
113
+ }, columns: request?.columns ?? [], initialHasHeader: request?.hasHeader, onApply: (choices) => request?.onApply(choices) }));
114
+ };
115
+ SubtotalDialogHost.displayName = "SubtotalDialogHost";
@@ -0,0 +1,37 @@
1
+ import React from "react";
2
+ import type { SplitTextOptions } from "../../types";
3
+ export type TextToColumnsMode = "delimited" | "fixed";
4
+ export type TextToColumnsDialogProps = {
5
+ /** Controls visibility. */
6
+ open: boolean;
7
+ onOpenChange(open: boolean): void;
8
+ /** Texts of the first cells of the selected column — the preview / ruler. */
9
+ preview: string[];
10
+ /** Initial wizard step-1 choice (default Delimited). */
11
+ initialMode?: TextToColumnsMode;
12
+ /**
13
+ * Emits the chosen delimiter (Delimited) or column breaks (Fixed width).
14
+ * The host routes it to `onSplitTextToColumns`.
15
+ */
16
+ onApply(options: {
17
+ delimiter: string;
18
+ } & SplitTextOptions): void;
19
+ };
20
+ /**
21
+ * Excel's Convert Text to Columns wizard, condensed: choose Delimited (pick
22
+ * a delimiter or type another) or Fixed width (click the ruler to add or
23
+ * remove break lines, or type the positions), with a live preview of the
24
+ * first rows.
25
+ */
26
+ export declare const TextToColumnsDialog: React.MemoExoticComponent<({ open, onOpenChange, preview, initialMode, onApply, }: TextToColumnsDialogProps) => React.JSX.Element>;
27
+ /**
28
+ * Host-mounted, atom-driven wrapper around {@link TextToColumnsDialog}.
29
+ * Mount once in the host (alongside the other editor dialogs); the context
30
+ * menu opens it via `useOpenTextToColumnsDialog`. Without a host the menu
31
+ * keeps its one-click delimiter items only.
32
+ */
33
+ export declare const TextToColumnsDialogHost: {
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/text-to-columns-dialog/index.tsx"],"names":[],"mappings":"AAYA,OAAO,KAA0D,MAAM,OAAO,CAAC;AAM/E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,OAAO,CAAC;AAEtD,MAAM,MAAM,wBAAwB,GAAG;IACrC,2BAA2B;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IAClC,6EAA6E;IAC7E,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,wDAAwD;IACxD,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC;;;OAGG;IACH,OAAO,CAAC,OAAO,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,gBAAgB,GAAG,IAAI,CAAC;CAClE,CAAC;AAiCF;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,qFAO3B,wBAAwB,uBAgO5B,CAAC;AAIF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB;;;CAenC,CAAC"}
@@ -0,0 +1,147 @@
1
+ import { Cross1Icon } from "@rowsncolumns/icons";
2
+ import { Button, Dialog, DialogBody, DialogCloseButton, DialogContent, DialogDescription, DialogTitle, Input, Separator, } from "@rowsncolumns/ui";
3
+ import React, { memo, useCallback, useEffect, useMemo, useState } from "react";
4
+ import { useRegisterTextToColumnsDialogHost, useTextToColumnsDialogState, } from "../../atoms/text-to-columns-dialog.atom";
5
+ const DELIMITERS = [
6
+ { label: "Comma", value: "," },
7
+ { label: "Semicolon", value: ";" },
8
+ { label: "Tab", value: "\t" },
9
+ { label: "Space", value: " " },
10
+ { label: "Period", value: "." },
11
+ { label: "Pipe", value: "|" },
12
+ ];
13
+ /** Cuts `text` at ascending character positions (the dialog's own preview). */
14
+ const cutAt = (text, breaks) => {
15
+ const parts = [];
16
+ let start = 0;
17
+ for (const position of breaks) {
18
+ parts.push(text.slice(start, position));
19
+ start = position;
20
+ }
21
+ parts.push(text.slice(start));
22
+ return parts;
23
+ };
24
+ const parseBreaks = (text) => [
25
+ ...new Set(text
26
+ .split(/[\s,;]+/)
27
+ .map((part) => Number.parseInt(part, 10))
28
+ .filter((n) => Number.isInteger(n) && n > 0)),
29
+ ].sort((a, b) => a - b);
30
+ /**
31
+ * Excel's Convert Text to Columns wizard, condensed: choose Delimited (pick
32
+ * a delimiter or type another) or Fixed width (click the ruler to add or
33
+ * remove break lines, or type the positions), with a live preview of the
34
+ * first rows.
35
+ */
36
+ export const TextToColumnsDialog = memo(({ open, onOpenChange, preview, initialMode = "delimited", onApply, }) => {
37
+ const [mode, setMode] = useState(initialMode);
38
+ const [delimiter, setDelimiter] = useState(",");
39
+ const [customDelimiter, setCustomDelimiter] = useState("");
40
+ const [breaks, setBreaks] = useState([]);
41
+ const [breaksText, setBreaksText] = useState("");
42
+ useEffect(() => {
43
+ if (open) {
44
+ setMode(initialMode);
45
+ setDelimiter(",");
46
+ setCustomDelimiter("");
47
+ setBreaks([]);
48
+ setBreaksText("");
49
+ }
50
+ // Only reset on open transitions.
51
+ // eslint-disable-next-line react-hooks/exhaustive-deps
52
+ }, [open]);
53
+ const sample = useMemo(() => preview.slice(0, 6), [preview]);
54
+ const rulerLength = useMemo(() => Math.min(120, Math.max(10, ...sample.map((text) => text.length))), [sample]);
55
+ const toggleBreak = useCallback((position) => {
56
+ setBreaks((prev) => {
57
+ const next = prev.includes(position)
58
+ ? prev.filter((p) => p !== position)
59
+ : [...prev, position].sort((a, b) => a - b);
60
+ setBreaksText(next.join(", "));
61
+ return next;
62
+ });
63
+ }, []);
64
+ const effectiveDelimiter = delimiter === "custom" ? customDelimiter : delimiter;
65
+ const canApply = mode === "delimited" ? effectiveDelimiter.length > 0 : breaks.length > 0;
66
+ const commit = useCallback(() => {
67
+ if (mode === "fixed") {
68
+ onApply({ delimiter: ",", columnBreaks: breaks });
69
+ }
70
+ else {
71
+ onApply({ delimiter: effectiveDelimiter });
72
+ }
73
+ onOpenChange(false);
74
+ }, [mode, breaks, effectiveDelimiter, onApply, onOpenChange]);
75
+ return (React.createElement(Dialog, { open: open, onOpenChange: onOpenChange },
76
+ React.createElement(DialogContent, { className: "w-[520px] max-w-[92vw] pointer-events-auto" },
77
+ React.createElement(DialogTitle, null, "Text to columns"),
78
+ React.createElement(DialogDescription, null, "Split the selected column into several columns."),
79
+ React.createElement(DialogCloseButton, { onClick: () => onOpenChange(false) },
80
+ React.createElement(Cross1Icon, null)),
81
+ React.createElement(DialogBody, { className: "flex flex-col gap-3 font-system text-sm" },
82
+ React.createElement("div", { className: "flex gap-4", role: "radiogroup", "aria-label": "Original data type" },
83
+ React.createElement("label", { className: "flex items-center gap-1" },
84
+ React.createElement("input", { type: "radio", name: "text-to-columns-mode", value: "delimited", checked: mode === "delimited", onChange: () => setMode("delimited") }),
85
+ "Delimited"),
86
+ React.createElement("label", { className: "flex items-center gap-1" },
87
+ React.createElement("input", { type: "radio", name: "text-to-columns-mode", value: "fixed", checked: mode === "fixed", onChange: () => setMode("fixed") }),
88
+ "Fixed width")),
89
+ mode === "delimited" ? (React.createElement("div", { className: "flex flex-col gap-2" },
90
+ React.createElement("label", { className: "flex flex-col gap-1 text-xs font-medium" },
91
+ "Delimiter",
92
+ React.createElement("select", { className: "h-8 rounded border border-rnc-border bg-transparent px-2 text-sm", value: delimiter, onChange: (e) => setDelimiter(e.target.value), "aria-label": "Delimiter" },
93
+ DELIMITERS.map((option) => (React.createElement("option", { key: option.value, value: option.value }, option.label))),
94
+ React.createElement("option", { value: "custom" }, "Other\u2026"))),
95
+ delimiter === "custom" ? (React.createElement("label", { className: "flex flex-col gap-1 text-xs font-medium" },
96
+ "Other delimiter",
97
+ React.createElement(Input, { value: customDelimiter, maxLength: 5, "aria-label": "Other delimiter", onChange: (e) => setCustomDelimiter(e.target.value) }))) : null)) : (React.createElement("div", { className: "flex flex-col gap-2" },
98
+ React.createElement("div", { className: "text-xs text-rnc-foreground/70" }, "Click the ruler to create a break line; click it again to remove it."),
99
+ React.createElement("div", { className: "overflow-x-auto rounded border border-rnc-border p-2 font-mono text-xs leading-5", "data-testid": "fixed-width-ruler" },
100
+ React.createElement("div", { className: "flex select-none" }, Array.from({ length: rulerLength }, (_, index) => {
101
+ const position = index + 1;
102
+ const active = breaks.includes(position);
103
+ return (React.createElement("button", { key: position, type: "button", "aria-label": `Break after character ${position}`, "aria-pressed": active, className: `w-[1ch] shrink-0 border-r ${active ? "border-rnc-primary bg-rnc-primary/20" : "border-transparent"} hover:bg-rnc-accent`, onClick: () => toggleBreak(position) }, position % 10 === 0 ? String(position / 10) : "·"));
104
+ })),
105
+ sample.map((text, rowIndex) => (React.createElement("div", { key: rowIndex, className: "flex whitespace-pre" }, Array.from({ length: rulerLength }, (_, index) => {
106
+ const position = index + 1;
107
+ const active = breaks.includes(position);
108
+ return (React.createElement("span", { key: position, className: `w-[1ch] shrink-0 border-r ${active ? "border-rnc-primary" : "border-transparent"}` }, text[index] ?? " "));
109
+ }))))),
110
+ React.createElement("label", { className: "flex flex-col gap-1 text-xs font-medium" },
111
+ "Break positions (characters before each break)",
112
+ React.createElement(Input, { value: breaksText, placeholder: "e.g. 4, 10", "aria-label": "Break positions", onChange: (e) => {
113
+ setBreaksText(e.target.value);
114
+ setBreaks(parseBreaks(e.target.value));
115
+ } })))),
116
+ React.createElement("div", { className: "flex flex-col gap-1" },
117
+ React.createElement("span", { className: "text-xs font-medium" }, "Data preview"),
118
+ React.createElement("div", { className: "max-h-[120px] overflow-auto rounded border border-rnc-border p-2 font-mono text-xs", "data-testid": "text-to-columns-preview" }, sample.map((text, rowIndex) => {
119
+ const parts = mode === "fixed"
120
+ ? cutAt(text, breaks)
121
+ : effectiveDelimiter
122
+ ? text.split(effectiveDelimiter)
123
+ : [text];
124
+ return (React.createElement("div", { key: rowIndex, className: "flex gap-1 whitespace-pre" }, parts.map((part, partIndex) => (React.createElement("span", { key: partIndex, className: "border-r border-rnc-border pr-1 last:border-r-0" }, part.trim() || " ")))));
125
+ }))),
126
+ React.createElement(Separator, null),
127
+ React.createElement("div", { className: "flex justify-end gap-2" },
128
+ React.createElement(Button, { variant: "outline", size: "sm", type: "button", onClick: () => onOpenChange(false) }, "Cancel"),
129
+ React.createElement(Button, { size: "sm", type: "button", onClick: commit, disabled: !canApply }, "Finish"))))));
130
+ });
131
+ TextToColumnsDialog.displayName = "TextToColumnsDialog";
132
+ /**
133
+ * Host-mounted, atom-driven wrapper around {@link TextToColumnsDialog}.
134
+ * Mount once in the host (alongside the other editor dialogs); the context
135
+ * menu opens it via `useOpenTextToColumnsDialog`. Without a host the menu
136
+ * keeps its one-click delimiter items only.
137
+ */
138
+ export const TextToColumnsDialogHost = () => {
139
+ useRegisterTextToColumnsDialogHost();
140
+ const [request, setRequest] = useTextToColumnsDialogState();
141
+ const close = useCallback(() => setRequest(null), [setRequest]);
142
+ return (React.createElement(TextToColumnsDialog, { open: request != null, onOpenChange: (open) => {
143
+ if (!open)
144
+ close();
145
+ }, preview: request?.preview ?? [], initialMode: "fixed", onApply: (options) => request?.onApply(options) }));
146
+ };
147
+ TextToColumnsDialogHost.displayName = "TextToColumnsDialogHost";
@@ -4,16 +4,28 @@ import type { Color } from "../../types";
4
4
  import type { SpreadsheetTheme } from "../canvas-grid/typings";
5
5
  import type { ColorSelectorProps } from "./color-selector";
6
6
  import { ColorSelector } from "./color-selector";
7
+ import { type CellFill } from "./fill-selectors";
7
8
  type BackgroundColorSelectorProps = {
8
9
  disabled?: boolean;
9
10
  color?: Color | null;
10
11
  theme?: SpreadsheetTheme;
11
12
  onChange?(color: Color | undefined): void;
13
+ /**
14
+ * The active cell's fill (pattern / gradient fields) — enables the
15
+ * Pattern and Gradient tabs together with `onChangeFill`. Without
16
+ * `onChangeFill` the popover is the solid colour picker only.
17
+ */
18
+ fill?: CellFill | null;
19
+ /**
20
+ * Apply a pattern or gradient fill (Excel: Format Cells → Fill → Pattern /
21
+ * Fill Effects). Receives the full fill slice so cleared fields reset.
22
+ */
23
+ onChangeFill?(fill: CellFill): void;
12
24
  /**
13
25
  * Custom color selector component
14
26
  */
15
27
  ColorSelector?: ComponentType<ColorSelectorProps>;
16
28
  } & Pick<ComponentProps<typeof ColorSelector>, "userDefinedColors" | "onAddUserDefinedColor">;
17
- export declare const BackgroundColorSelector: ({ color, theme, onChange, onAddUserDefinedColor, userDefinedColors, disabled, ColorSelector: ColorSelectorComponent, }: BackgroundColorSelectorProps) => React.JSX.Element;
29
+ export declare const BackgroundColorSelector: ({ color, theme, onChange, fill, onChangeFill, onAddUserDefinedColor, userDefinedColors, disabled, ColorSelector: ColorSelectorComponent, }: BackgroundColorSelectorProps) => React.JSX.Element;
18
30
  export {};
19
31
  //# sourceMappingURL=background-color-selector.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"background-color-selector.d.ts","sourceRoot":"","sources":["../../../../components/toolbar/background-color-selector.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3D,OAAO,KAAK,MAAM,OAAO,CAAC;AAM1B,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,KAAK,4BAA4B,GAAG;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;IACrB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,QAAQ,CAAC,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,IAAI,CAAC;IAC1C;;OAEG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAC;CACnD,GAAG,IAAI,CACN,cAAc,CAAC,OAAO,aAAa,CAAC,EACpC,mBAAmB,GAAG,uBAAuB,CAC9C,CAAC;AACF,eAAO,MAAM,uBAAuB,GAAI,wHAQrC,4BAA4B,sBAqC9B,CAAC"}
1
+ {"version":3,"file":"background-color-selector.d.ts","sourceRoot":"","sources":["../../../../components/toolbar/background-color-selector.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3D,OAAO,KAAK,MAAM,OAAO,CAAC;AAM1B,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EACL,KAAK,QAAQ,EAId,MAAM,kBAAkB,CAAC;AAE1B,KAAK,4BAA4B,GAAG;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;IACrB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,QAAQ,CAAC,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,IAAI,CAAC;IAC1C;;;;OAIG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IACvB;;;OAGG;IACH,YAAY,CAAC,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC;IACpC;;OAEG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAC;CACnD,GAAG,IAAI,CACN,cAAc,CAAC,OAAO,aAAa,CAAC,EACpC,mBAAmB,GAAG,uBAAuB,CAC9C,CAAC;AACF,eAAO,MAAM,uBAAuB,GAAI,4IAUrC,4BAA4B,sBA+G9B,CAAC"}
@@ -1,28 +1,78 @@
1
1
  import { IoMdColorFill } from "@rowsncolumns/icons";
2
2
  import { Popover, PopoverContent, PopoverTrigger } from "@rowsncolumns/ui";
3
- import { IconButton } from "@rowsncolumns/ui";
3
+ import { IconButton, Tabs, TabsContent, TabsList, TabsTrigger, } from "@rowsncolumns/ui";
4
4
  import React from "react";
5
5
  import { useState } from "react";
6
6
  import { useFocusSheet } from "../../atoms";
7
7
  import { defaultSpreadsheetTheme } from "../../config";
8
8
  import { getStringifiedColor } from "../../themes";
9
9
  import { ColorSelector } from "./color-selector";
10
- export const BackgroundColorSelector = ({ color = "transparent", theme = defaultSpreadsheetTheme, onChange, onAddUserDefinedColor, userDefinedColors, disabled, ColorSelector: ColorSelectorComponent = ColorSelector, }) => {
10
+ import { gradientToCss, GradientFillPicker, PatternFillPicker, } from "./fill-selectors";
11
+ export const BackgroundColorSelector = ({ color = "transparent", theme = defaultSpreadsheetTheme, onChange, fill, onChangeFill, onAddUserDefinedColor, userDefinedColors, disabled, ColorSelector: ColorSelectorComponent = ColorSelector, }) => {
11
12
  const [isOpen, setIsOpen] = useState(false);
12
13
  const close = () => setIsOpen(false);
13
14
  const open = () => setIsOpen(true);
14
15
  const colorString = getStringifiedColor(color, theme);
15
16
  const focusSheet = useFocusSheet();
17
+ // The trigger underline previews the effective fill: gradient, then the
18
+ // pattern colour, then the solid colour.
19
+ const swatchStyle = fill?.gradient
20
+ ? {
21
+ borderBottom: "4px solid transparent",
22
+ borderImage: `${gradientToCss(fill.gradient)} 1`,
23
+ }
24
+ : {
25
+ borderBottom: `4px ${fill?.fillPattern &&
26
+ fill.fillPattern !== "none" &&
27
+ fill.foregroundColor
28
+ ? getStringifiedColor(fill.foregroundColor, theme)
29
+ : colorString} solid`,
30
+ };
31
+ const solidPicker = (React.createElement(ColorSelectorComponent, { color: color, onChange: (color) => {
32
+ // A solid pick replaces a gradient (Excel keeps them exclusive) and,
33
+ // with "No fill", clears the pattern too.
34
+ if (onChangeFill && (fill?.gradient || (!color && fill?.fillPattern))) {
35
+ onChangeFill({
36
+ backgroundColor: color,
37
+ fillPattern: color ? fill?.fillPattern ?? undefined : undefined,
38
+ foregroundColor: color
39
+ ? fill?.foregroundColor ?? undefined
40
+ : undefined,
41
+ gradient: undefined,
42
+ });
43
+ }
44
+ else {
45
+ onChange?.(color);
46
+ }
47
+ focusSheet?.();
48
+ close();
49
+ }, theme: theme, resetButtonTitle: "No fill", userDefinedColors: userDefinedColors, onAddUserDefinedColor: onAddUserDefinedColor }));
50
+ const applyFill = (next) => {
51
+ onChangeFill?.(next);
52
+ focusSheet?.();
53
+ };
16
54
  return (React.createElement(Popover, { modal: false, open: isOpen, onOpenChange: setIsOpen },
17
55
  React.createElement(PopoverTrigger, { asChild: true },
18
- React.createElement(IconButton, { className: "text-base flex-row", style: {
19
- borderBottom: `4px ${colorString} solid`,
20
- }, tooltip: "Fill color", onClick: open, disabled: disabled },
56
+ React.createElement(IconButton, { className: "text-base flex-row", style: swatchStyle, tooltip: "Fill color", "aria-label": "Fill color", onClick: open, disabled: disabled },
21
57
  React.createElement(IoMdColorFill, null))),
22
- React.createElement(PopoverContent, { align: "start" },
23
- React.createElement(ColorSelectorComponent, { color: color, onChange: (color) => {
24
- onChange?.(color);
25
- focusSheet?.();
26
- close();
27
- }, theme: theme, resetButtonTitle: "No fill", userDefinedColors: userDefinedColors, onAddUserDefinedColor: onAddUserDefinedColor }))));
58
+ React.createElement(PopoverContent, { align: "start" }, onChangeFill ? (React.createElement(Tabs, { defaultValue: fill?.gradient
59
+ ? "gradient"
60
+ : fill?.fillPattern && fill.fillPattern !== "none"
61
+ ? "pattern"
62
+ : "solid" },
63
+ React.createElement(TabsList, { className: "grid-cols-3 mb-2" },
64
+ React.createElement(TabsTrigger, { value: "solid" }, "Solid"),
65
+ React.createElement(TabsTrigger, { value: "pattern" }, "Pattern"),
66
+ React.createElement(TabsTrigger, { value: "gradient" }, "Gradient")),
67
+ React.createElement(TabsContent, { value: "solid" }, solidPicker),
68
+ React.createElement(TabsContent, { value: "pattern" },
69
+ React.createElement(PatternFillPicker, { fill: {
70
+ ...fill,
71
+ backgroundColor: fill?.backgroundColor ?? color,
72
+ }, theme: theme, onChangeFill: applyFill })),
73
+ React.createElement(TabsContent, { value: "gradient" },
74
+ React.createElement(GradientFillPicker, { fill: {
75
+ ...fill,
76
+ backgroundColor: fill?.backgroundColor ?? color,
77
+ }, theme: theme, onChangeFill: applyFill })))) : (solidPicker))));
28
78
  };
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import type { CellFormat } from "../../types";
2
+ import type { CellFormat, NamedCellStyle } from "../../types";
3
3
  import type { SpreadsheetTheme } from "../canvas-grid/typings";
4
4
  type WithCellFormat = (value: CellFormat) => void;
5
5
  type WithTypeValue<T extends keyof CellFormat> = (type: T, value: CellFormat[T]) => void;
@@ -8,6 +8,13 @@ export type CellStyleSelectorProps = {
8
8
  locale?: string;
9
9
  theme: SpreadsheetTheme;
10
10
  userDefinedStyles?: [string, CellFormat][];
11
+ /**
12
+ * Workbook named cell styles imported from `<cellStyles>` ("Heading 1",
13
+ * "Good", "Accent 1", …). Shown as a "Workbook styles" section ahead of
14
+ * the built-in gallery; hidden entries and the "Normal" base style are
15
+ * skipped, as is any entry whose xf resolved to no formatting.
16
+ */
17
+ namedCellStyles?: NamedCellStyle[] | null;
11
18
  selectedFormat?: CellFormat | null;
12
19
  onAddUserDefinedStyle?(name: string): void;
13
20
  onChangeFormatting: (...args: Parameters<WithCellFormat | WithTypeValue<keyof CellFormat>>) => void;
@@ -19,7 +26,13 @@ type StyleList = [
19
26
  format: CellFormat,
20
27
  style?: React.CSSProperties
21
28
  ];
29
+ /** Named styles worth showing in the gallery (see `namedCellStyles` prop). */
30
+ export declare const getGalleryNamedCellStyles: (namedCellStyles: NamedCellStyle[] | null | undefined) => Array<NamedCellStyle & {
31
+ format: CellFormat;
32
+ }>;
33
+ /** CSS preview of a named style's format for the gallery swatch. */
34
+ export declare const getNamedCellStylePreview: (format: CellFormat | undefined, theme: SpreadsheetTheme) => React.CSSProperties;
22
35
  export declare const getCellStyles: (theme: SpreadsheetTheme, currencySymbol?: string) => Map<string, StyleList[]>;
23
- export declare const CellStyleSelector: React.MemoExoticComponent<({ currency, locale, theme, selectedFormat, canAddCustomStyle, onChangeFormatting, onClearFormatting, }: CellStyleSelectorProps) => React.JSX.Element>;
36
+ export declare const CellStyleSelector: React.MemoExoticComponent<({ currency, locale, theme, selectedFormat, namedCellStyles, canAddCustomStyle, onChangeFormatting, onClearFormatting, }: CellStyleSelectorProps) => React.JSX.Element>;
24
37
  export {};
25
38
  //# sourceMappingURL=cell-style-selector.d.ts.map