@rowsncolumns/spreadsheet 10.4.0 → 10.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/components/filter-box/date-tree.d.ts +23 -5
- package/dist/cjs/components/filter-box/date-tree.d.ts.map +1 -1
- package/dist/cjs/components/filter-box/date-tree.js +76 -11
- package/dist/cjs/components/filter-box/filter-by-condition.d.ts +26 -1
- package/dist/cjs/components/filter-box/filter-by-condition.d.ts.map +1 -1
- package/dist/cjs/components/filter-box/filter-by-condition.js +108 -9
- package/dist/cjs/components/filter-box/index.d.ts.map +1 -1
- package/dist/cjs/components/filter-box/index.js +7 -2
- package/dist/cjs/components/formula-input/index.d.ts.map +1 -1
- package/dist/cjs/components/formula-input/index.js +26 -4
- package/dist/cjs/components/toolbar/table-style-editor.d.ts +85 -0
- package/dist/cjs/components/toolbar/table-style-editor.d.ts.map +1 -0
- package/dist/cjs/components/toolbar/table-style-editor.js +235 -0
- package/dist/cjs/components/toolbar/table-style-selector.d.ts +7 -7
- package/dist/cjs/components/toolbar/table-style-selector.d.ts.map +1 -1
- package/dist/cjs/components/toolbar/table-style-selector.js +77 -87
- package/dist/cjs/release-date.js +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/cjs/types.d.ts +10 -0
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/esm/components/filter-box/date-tree.d.ts +23 -5
- package/dist/esm/components/filter-box/date-tree.d.ts.map +1 -1
- package/dist/esm/components/filter-box/date-tree.js +62 -10
- package/dist/esm/components/filter-box/filter-by-condition.d.ts +26 -1
- package/dist/esm/components/filter-box/filter-by-condition.d.ts.map +1 -1
- package/dist/esm/components/filter-box/filter-by-condition.js +78 -12
- package/dist/esm/components/filter-box/index.d.ts.map +1 -1
- package/dist/esm/components/filter-box/index.js +7 -2
- package/dist/esm/components/formula-input/index.d.ts.map +1 -1
- package/dist/esm/components/formula-input/index.js +26 -4
- package/dist/esm/components/toolbar/table-style-editor.d.ts +85 -0
- package/dist/esm/components/toolbar/table-style-editor.d.ts.map +1 -0
- package/dist/esm/components/toolbar/table-style-editor.js +189 -0
- package/dist/esm/components/toolbar/table-style-selector.d.ts +7 -7
- package/dist/esm/components/toolbar/table-style-selector.d.ts.map +1 -1
- package/dist/esm/components/toolbar/table-style-selector.js +74 -81
- package/dist/esm/release-date.js +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/esm/types.d.ts +10 -0
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/spreadsheet.min.css +1 -1
- package/package.json +10 -10
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Button, Input, Select, SelectContent, SelectItem, SelectPortal, SelectTrigger, SelectValue, Stack, } from "@rowsncolumns/ui";
|
|
2
|
-
import React, { useState } from "react";
|
|
1
|
+
import { Button, Input, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectPortal, SelectSeparator, SelectTrigger, SelectValue, Stack, } from "@rowsncolumns/ui";
|
|
2
|
+
import React, { useMemo, useState } from "react";
|
|
3
3
|
import { CONDITION_LABELS, CONDITION_NONE } from "../../config";
|
|
4
4
|
import { shouldShowFromValue, shouldShowToValue } from "../../helpers";
|
|
5
5
|
import { useIsomorphicLayoutEffect } from "../../hooks/use-isomorphic-layout-effect";
|
|
@@ -23,13 +23,79 @@ export const top10ConditionType = (direction, unit) => direction === "top"
|
|
|
23
23
|
const SECOND_CLAUSE_LABELS = CONDITION_LABELS.filter(({ condition }) => !isTop10ConditionType(condition) &&
|
|
24
24
|
condition !== "NUMBER_ABOVE_AVERAGE" &&
|
|
25
25
|
condition !== "NUMBER_BELOW_AVERAGE");
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
export const DATE_PERIOD_GROUP_LABEL = "All dates in the period";
|
|
27
|
+
const FOREIGN_GROUP_LABEL = "Applied filter";
|
|
28
|
+
const isDatePeriodCondition = (type) => /^DATE_IN_[QM]\d+$/.test(type);
|
|
29
|
+
/**
|
|
30
|
+
* Which Excel AutoFilter family a condition belongs to. Anything outside the
|
|
31
|
+
* text / number / date prefixes ("Is empty", "Errors", "Custom formula") is
|
|
32
|
+
* offered on every column.
|
|
33
|
+
*/
|
|
34
|
+
const conditionFamily = (type) => {
|
|
35
|
+
if (type.startsWith("TEXT_"))
|
|
36
|
+
return "text";
|
|
37
|
+
if (type.startsWith("NUMBER_"))
|
|
38
|
+
return "number";
|
|
39
|
+
if (type.startsWith("DATE_"))
|
|
40
|
+
return isDatePeriodCondition(type) ? "period" : "date";
|
|
41
|
+
return "generic";
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* The condition menu Excel shows for a column: one family — "Text Filters",
|
|
45
|
+
* "Number Filters" or "Date Filters" (with the sixteen period conditions
|
|
46
|
+
* grouped under "All Dates in the Period") — plus the generic items. With no
|
|
47
|
+
* `columnType` (an unclassified column) every family is offered, grouped.
|
|
48
|
+
* A `current` condition from another family (a date filter stored on a column
|
|
49
|
+
* that now holds text) stays listed and selected so the user can see and
|
|
50
|
+
* clear it instead of it silently vanishing from the menu.
|
|
51
|
+
*/
|
|
52
|
+
export const conditionOptionsFor = (labels, columnType, current) => {
|
|
53
|
+
const families = columnType
|
|
54
|
+
? [columnType]
|
|
55
|
+
: ["text", "number", "date"];
|
|
56
|
+
const groups = [];
|
|
57
|
+
for (const family of families) {
|
|
58
|
+
groups.push({
|
|
59
|
+
key: family,
|
|
60
|
+
options: labels.filter(({ condition }) => conditionFamily(condition) === family),
|
|
61
|
+
});
|
|
62
|
+
if (family === "date") {
|
|
63
|
+
groups.push({
|
|
64
|
+
key: "period",
|
|
65
|
+
label: DATE_PERIOD_GROUP_LABEL,
|
|
66
|
+
options: labels.filter(({ condition }) => conditionFamily(condition) === "period"),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
groups.push({
|
|
71
|
+
key: "generic",
|
|
72
|
+
options: labels.filter(({ condition }) => conditionFamily(condition) === "generic"),
|
|
73
|
+
});
|
|
74
|
+
const offered = groups.filter((group) => group.options.length > 0);
|
|
75
|
+
if (current &&
|
|
76
|
+
!offered.some((group) => group.options.some(({ condition }) => condition === current))) {
|
|
77
|
+
const foreign = labels.find(({ condition }) => condition === current) ??
|
|
78
|
+
CONDITION_LABELS.find(({ condition }) => condition === current);
|
|
79
|
+
if (foreign) {
|
|
80
|
+
offered.unshift({ key: "current", label: FOREIGN_GROUP_LABEL, options: [foreign] });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return offered;
|
|
84
|
+
};
|
|
85
|
+
const ConditionSelect = ({ value, labels, columnType, onChange, ariaLabel, }) => {
|
|
86
|
+
const groups = useMemo(() => conditionOptionsFor(labels, columnType, value), [labels, columnType, value]);
|
|
87
|
+
return (React.createElement(Select, { value: value ?? CONDITION_NONE, onValueChange: (next) => onChange(next === CONDITION_NONE ? undefined : next) },
|
|
88
|
+
React.createElement(SelectTrigger, { className: "mb-1", "aria-label": ariaLabel },
|
|
89
|
+
React.createElement(SelectValue, { placeholder: "None" })),
|
|
90
|
+
React.createElement(SelectPortal, null,
|
|
91
|
+
React.createElement(SelectContent, null,
|
|
92
|
+
React.createElement(SelectItem, { value: CONDITION_NONE }, "None"),
|
|
93
|
+
groups.map((group) => (React.createElement(React.Fragment, { key: group.key },
|
|
94
|
+
React.createElement(SelectSeparator, null),
|
|
95
|
+
React.createElement(SelectGroup, null,
|
|
96
|
+
group.label ? React.createElement(SelectLabel, null, group.label) : null,
|
|
97
|
+
group.options.map(({ condition, label }) => (React.createElement(SelectItem, { key: condition, value: condition }, label)))))))))));
|
|
98
|
+
};
|
|
33
99
|
const ValueInputs = ({ conditionType, fromValue, toValue, onChangeFromValue, onChangeToValue, namePrefix, }) => {
|
|
34
100
|
const [showToValue, setShowToValue] = useState(() => shouldShowToValue(conditionType));
|
|
35
101
|
const [showFromValue, setShowFromValue] = useState(() => shouldShowFromValue(conditionType));
|
|
@@ -72,19 +138,19 @@ const Top10Fields = ({ conditionType, count, onChangeConditionType, onChangeCoun
|
|
|
72
138
|
React.createElement(SelectItem, { value: "items" }, "Items"),
|
|
73
139
|
React.createElement(SelectItem, { value: "percent" }, "Percent"))))));
|
|
74
140
|
};
|
|
75
|
-
export const FilterByCondition = ({ conditionType, fromValue, toValue, onChangeToValue, onChangeFromValue, onChangeConditionType, condition2Type, condition2From, condition2To, conditionJoin, onChangeCondition2Type, onChangeCondition2From, onChangeCondition2To, onChangeConditionJoin, }) => {
|
|
141
|
+
export const FilterByCondition = ({ columnType, conditionType, fromValue, toValue, onChangeToValue, onChangeFromValue, onChangeConditionType, condition2Type, condition2From, condition2To, conditionJoin, onChangeCondition2Type, onChangeCondition2From, onChangeCondition2To, onChangeConditionJoin, }) => {
|
|
76
142
|
// Excel's Custom AutoFilter shows the second clause row only on request;
|
|
77
143
|
// it stays open while a second clause is stored on the column.
|
|
78
144
|
const [showSecondClause, setShowSecondClause] = useState(() => condition2Type !== undefined);
|
|
79
145
|
const isTop10 = isTop10ConditionType(conditionType);
|
|
80
146
|
return (React.createElement(React.Fragment, null,
|
|
81
147
|
React.createElement("label", { className: "text-sm font-semibold pb-1 block" }, "Filter by condition"),
|
|
82
|
-
React.createElement(ConditionSelect, { value: conditionType, labels: CONDITION_LABELS, onChange: (type) => onChangeConditionType?.(type), ariaLabel: "Condition" }),
|
|
148
|
+
React.createElement(ConditionSelect, { value: conditionType, labels: CONDITION_LABELS, columnType: columnType, onChange: (type) => onChangeConditionType?.(type), ariaLabel: "Condition" }),
|
|
83
149
|
isTop10 && conditionType ? (React.createElement(Top10Fields, { conditionType: conditionType, count: fromValue, onChangeConditionType: (type) => onChangeConditionType?.(type), onChangeCount: onChangeFromValue })) : (React.createElement(ValueInputs, { conditionType: conditionType, fromValue: fromValue, toValue: toValue, onChangeFromValue: onChangeFromValue, onChangeToValue: onChangeToValue, namePrefix: "" })),
|
|
84
150
|
conditionType && !isTop10 ? (showSecondClause ? (React.createElement("div", { className: "mt-1 flex flex-col gap-1" },
|
|
85
151
|
React.createElement("div", { role: "radiogroup", "aria-label": "Combine conditions", className: "flex gap-3 text-xs px-1" }, ["AND", "OR"].map((join) => (React.createElement("label", { key: join, className: "flex items-center gap-1 cursor-pointer" },
|
|
86
152
|
React.createElement("input", { type: "radio", name: "conditionJoin", value: join, checked: conditionJoin === join, onChange: () => onChangeConditionJoin(join) }),
|
|
87
153
|
join === "AND" ? "And" : "Or")))),
|
|
88
|
-
React.createElement(ConditionSelect, { value: condition2Type, labels: SECOND_CLAUSE_LABELS, onChange: onChangeCondition2Type, ariaLabel: "Second condition" }),
|
|
154
|
+
React.createElement(ConditionSelect, { value: condition2Type, labels: SECOND_CLAUSE_LABELS, columnType: columnType, onChange: onChangeCondition2Type, ariaLabel: "Second condition" }),
|
|
89
155
|
React.createElement(ValueInputs, { conditionType: condition2Type, fromValue: condition2From, toValue: condition2To, onChangeFromValue: onChangeCondition2From, onChangeToValue: onChangeCondition2To, namePrefix: "second" }))) : (React.createElement(Button, { type: "button", variant: "link", className: "text-xs px-1 mt-1 self-start", onClick: () => setShowSecondClause(true) }, "Custom filter (And / Or)\u2026"))) : null));
|
|
90
156
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/filter-box/index.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAsC,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/filter-box/index.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAM3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAMV,cAAc,EACd,UAAU,EAGV,SAAS,EACV,MAAM,aAAa,CAAC;AAKrB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,YAAY,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAKvD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IACnC,MAAM,EAAE,cAAc,CAAC,aAAa,CAAC,CAAC;IACtC,QAAQ,EAAE,cAAc,CAAC,eAAe,CAAC,CAAC;IAC1C,mBAAmB,EAAE,cAAc,CAAC,qBAAqB,CAAC,CAAC;IAC3D,mBAAmB,EAAE,cAAc,CAAC,qBAAqB,CAAC,CAAC;IAC3D,MAAM,EAAE,UAAU,GAAG,SAAS,CAAC;IAC/B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,+DAA+D;IAC/D,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,sEAAsE;IACtE,cAAc,CAAC,EAAE,aAAa,EAAE,CAAC;IACjC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC;CAC1B,CAAC;AAGF,eAAO,MAAM,WAAW,GACtB,QAAQ,UAAU,GAAG,SAAS,KAC7B,MAAM,IAAI,SAEZ,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,GAClC,UAAU,cAAc,GAAG,SAAS,KACnC,OAQA,CAAC;AA0BJ,eAAO,MAAM,gBAAgB,4NAexB,cAAc,uBAmlBlB,CAAC;AAIF,MAAM,MAAM,cAAc,GAAG,kBAAkB,GAAG;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IAChC,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,SAAS;YAAW,cAAc;;CAuC9C,CAAC"}
|
|
@@ -5,8 +5,10 @@ import { isNil, number2Alpha } from "@rowsncolumns/utils";
|
|
|
5
5
|
import React, { useEffect, useRef, useState } from "react";
|
|
6
6
|
import { memo, useCallback, useMemo } from "react";
|
|
7
7
|
import { useFocusSheet } from "../../atoms";
|
|
8
|
-
import { StyledFakeTriggerButton } from "../context-menu/style";
|
|
9
8
|
import { useOpenSortDialog } from "../../atoms/sort-dialog.atom";
|
|
9
|
+
import { BLANK_LABEL } from "../../config";
|
|
10
|
+
import { StyledFakeTriggerButton } from "../context-menu/style";
|
|
11
|
+
import { classifyFilterColumn } from "./date-tree";
|
|
10
12
|
import { FilterByColor } from "./filter-by-color";
|
|
11
13
|
import { FilterByCondition } from "./filter-by-condition";
|
|
12
14
|
import { FilterByValue } from "./filter-by-value";
|
|
@@ -76,6 +78,9 @@ export const FilterBoxWrapper = memo(({ dimensionIndex, onOpenChange, onSort, on
|
|
|
76
78
|
const [conditionJoin, onChangeConditionJoin] = useState(filterCriteria?.conditionJoin ?? "AND");
|
|
77
79
|
// "Sort by Color" is a collapsed sub-menu of the dropdown, like Excel's.
|
|
78
80
|
const [showSortByColor, setShowSortByColor] = useState(false);
|
|
81
|
+
// Excel offers one condition family per column (Text / Number / Date
|
|
82
|
+
// Filters), read from the same displayed values the checklist shows.
|
|
83
|
+
const columnType = useMemo(() => classifyFilterColumn(values, BLANK_LABEL), [values]);
|
|
79
84
|
// Auto apply (Excel parity). `pendingAutoApplyTick` counts user edits made
|
|
80
85
|
// while auto apply is on; the debounced effect below applies the newest
|
|
81
86
|
// state once per burst.
|
|
@@ -377,7 +382,7 @@ export const FilterBoxWrapper = memo(({ dimensionIndex, onOpenChange, onSort, on
|
|
|
377
382
|
React.createElement(Cross2Icon, null)),
|
|
378
383
|
clearLabel),
|
|
379
384
|
React.createElement("div", { className: "py-2 px-2" },
|
|
380
|
-
React.createElement(FilterByCondition, { conditionType: conditionType, fromValue: fromValue, toValue: toValue, onChangeConditionType: onUserChangeConditionType, onChangeFromValue: onUserChangeFromValue, onChangeToValue: onUserChangeToValue, condition2Type: condition2Type, condition2From: condition2From, condition2To: condition2To, conditionJoin: conditionJoin, onChangeCondition2Type: onUserChangeCondition2Type, onChangeCondition2From: onUserChangeCondition2From, onChangeCondition2To: onUserChangeCondition2To, onChangeConditionJoin: onUserChangeConditionJoin }),
|
|
385
|
+
React.createElement(FilterByCondition, { columnType: columnType, conditionType: conditionType, fromValue: fromValue, toValue: toValue, onChangeConditionType: onUserChangeConditionType, onChangeFromValue: onUserChangeFromValue, onChangeToValue: onUserChangeToValue, condition2Type: condition2Type, condition2From: condition2From, condition2To: condition2To, conditionJoin: conditionJoin, onChangeCondition2Type: onUserChangeCondition2Type, onChangeCondition2From: onUserChangeCondition2From, onChangeCondition2To: onUserChangeCondition2To, onChangeConditionJoin: onUserChangeConditionJoin }),
|
|
381
386
|
React.createElement(Separator, { className: "mt-2 mb-2" }),
|
|
382
387
|
React.createElement(FilterByColor, { colorFilter: colorFilter === undefined
|
|
383
388
|
? filterCriteria?.colorFilter
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/formula-input/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAiB,MAAM,kBAAkB,CAAC;AAGrE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAE5C,OAAO,KAAiB,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/formula-input/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAiB,MAAM,kBAAkB,CAAC;AAGrE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAE5C,OAAO,KAAiB,MAAM,OAAO,CAAC;AActC,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE1C,KAAK,iBAAiB,GAAG,cAAc,CAAC,OAAO,SAAS,CAAC,GAAG;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,YAAY,sGA8JxB,CAAC"}
|
|
@@ -4,6 +4,7 @@ import { forwardRef, useCallback } from "react";
|
|
|
4
4
|
import React, { useRef } from "react";
|
|
5
5
|
import { useActiveEditor, useFormulaMode, useGetGridRef, useGetSelectionsFromFormula, useOnEditorKeyDown, useSetActiveEditor, useSetFormulaSelections, } from "../../atoms";
|
|
6
6
|
import { useCellInputContext } from "../../atoms/cell-input-context.atom";
|
|
7
|
+
import { useIsomorphicLayoutEffect } from "../../hooks/use-isomorphic-layout-effect";
|
|
7
8
|
import { useMergeRefs } from "../../hooks/use-merge.refs";
|
|
8
9
|
import { CellInput } from "../cell-input";
|
|
9
10
|
/**
|
|
@@ -21,14 +22,35 @@ export const FormulaInput = forwardRef(({ value = "", onChange, onCancel, onBlur
|
|
|
21
22
|
const getSelectionsFromFormula = useGetSelectionsFromFormula();
|
|
22
23
|
const onEditorKeyDown = useOnEditorKeyDown();
|
|
23
24
|
const { getTableNames, getTableColumnNames, getNamedRanges } = useCellInputContext();
|
|
25
|
+
// Whether THIS input is the one holding formula mode on. `handleBlur` keeps the mode
|
|
26
|
+
// alive when focus moves to the grid — that is how a range gets picked — so an input
|
|
27
|
+
// that unmounts while the grid holds focus (Cancel / Save / the dialog's X / Esc) has
|
|
28
|
+
// to be the one to turn it off; otherwise the picked range keeps rendering and the
|
|
29
|
+
// grid stays in formula-selection mode with no editor left to receive the picks.
|
|
30
|
+
const ownsFormulaModeRef = useRef(false);
|
|
31
|
+
const enterFormulaMode = useCallback((nextValue) => {
|
|
32
|
+
const formulaMode = isAFormula(nextValue);
|
|
33
|
+
setFormulaMode(formulaMode);
|
|
34
|
+
ownsFormulaModeRef.current = formulaMode;
|
|
35
|
+
}, [setFormulaMode]);
|
|
24
36
|
const closeFormulaMode = useCallback(() => {
|
|
25
37
|
setFormulaMode(false);
|
|
26
38
|
setFormulaSelections([]);
|
|
39
|
+
ownsFormulaModeRef.current = false;
|
|
27
40
|
onCancel?.();
|
|
28
41
|
}, [onCancel, setFormulaMode, setFormulaSelections]);
|
|
42
|
+
// Layout (not passive) cleanup: when a host re-keys its range inputs ("Add range"),
|
|
43
|
+
// the departing input must release the mode before the new one's autoFocus claims it.
|
|
44
|
+
useIsomorphicLayoutEffect(() => () => {
|
|
45
|
+
if (ownsFormulaModeRef.current) {
|
|
46
|
+
ownsFormulaModeRef.current = false;
|
|
47
|
+
setFormulaMode(false);
|
|
48
|
+
setFormulaSelections([]);
|
|
49
|
+
}
|
|
50
|
+
}, [setFormulaMode, setFormulaSelections]);
|
|
29
51
|
const handleFocus = useCallback((e) => {
|
|
30
52
|
// Set formula mode
|
|
31
|
-
|
|
53
|
+
enterFormulaMode(value);
|
|
32
54
|
// Get selections from formula
|
|
33
55
|
const selections = getSelectionsFromFormula?.(value);
|
|
34
56
|
if (selections) {
|
|
@@ -40,7 +62,7 @@ export const FormulaInput = forwardRef(({ value = "", onChange, onCancel, onBlur
|
|
|
40
62
|
onChange?.(value);
|
|
41
63
|
onFocus?.(e);
|
|
42
64
|
}, [
|
|
43
|
-
|
|
65
|
+
enterFormulaMode,
|
|
44
66
|
setFormulaSelections,
|
|
45
67
|
getSelectionsFromFormula,
|
|
46
68
|
value,
|
|
@@ -66,7 +88,7 @@ export const FormulaInput = forwardRef(({ value = "", onChange, onCancel, onBlur
|
|
|
66
88
|
return;
|
|
67
89
|
}
|
|
68
90
|
// Set formula mode
|
|
69
|
-
|
|
91
|
+
enterFormulaMode(value);
|
|
70
92
|
// Get selections from formula
|
|
71
93
|
const selections = getSelectionsFromFormula?.(value);
|
|
72
94
|
if (selections) {
|
|
@@ -75,7 +97,7 @@ export const FormulaInput = forwardRef(({ value = "", onChange, onCancel, onBlur
|
|
|
75
97
|
}, [
|
|
76
98
|
activeEditor,
|
|
77
99
|
onChange,
|
|
78
|
-
|
|
100
|
+
enterFormulaMode,
|
|
79
101
|
setFormulaSelections,
|
|
80
102
|
getSelectionsFromFormula,
|
|
81
103
|
]);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { BandingProperties, CustomTableStyle, Maybe } from "../../types";
|
|
3
|
+
import type { SpreadsheetTheme } from "../canvas-grid/typings";
|
|
4
|
+
/**
|
|
5
|
+
* The style elements a custom table style can colour — the `rowProperties` fields the grid's
|
|
6
|
+
* banding resolves (`api/banded-ranges.ts`), in the order of Excel's New Table Style dialog.
|
|
7
|
+
* The first three are what the gallery swatches show and what every created style carries; the
|
|
8
|
+
* rest are only written when the user picks a colour for them.
|
|
9
|
+
*/
|
|
10
|
+
export declare const TABLE_STYLE_ELEMENTS: readonly [{
|
|
11
|
+
readonly key: "header";
|
|
12
|
+
readonly label: "Header row";
|
|
13
|
+
readonly property: "headerColor";
|
|
14
|
+
}, {
|
|
15
|
+
readonly key: "firstBand";
|
|
16
|
+
readonly label: "First row stripe";
|
|
17
|
+
readonly property: "firstBandColor";
|
|
18
|
+
}, {
|
|
19
|
+
readonly key: "secondBand";
|
|
20
|
+
readonly label: "Second row stripe";
|
|
21
|
+
readonly property: "secondBandColor";
|
|
22
|
+
}, {
|
|
23
|
+
readonly key: "firstColumn";
|
|
24
|
+
readonly label: "First column";
|
|
25
|
+
readonly property: "firstColumnColor";
|
|
26
|
+
}, {
|
|
27
|
+
readonly key: "lastColumn";
|
|
28
|
+
readonly label: "Last column";
|
|
29
|
+
readonly property: "lastColumnColor";
|
|
30
|
+
}, {
|
|
31
|
+
readonly key: "totalRow";
|
|
32
|
+
readonly label: "Total row";
|
|
33
|
+
readonly property: "totalRowColor";
|
|
34
|
+
}];
|
|
35
|
+
export type TableStyleElementKey = (typeof TABLE_STYLE_ELEMENTS)[number]["key"];
|
|
36
|
+
/** The colour picked for each element; `undefined` leaves the element unpainted. */
|
|
37
|
+
export type TableStyleColors = Partial<Record<TableStyleElementKey, string>>;
|
|
38
|
+
/** The colours a fresh style starts from (the default palette's accent 1 and its light tint). */
|
|
39
|
+
export declare const DEFAULT_TABLE_STYLE_COLORS: TableStyleColors;
|
|
40
|
+
/**
|
|
41
|
+
* The banding a custom table style paints from its element colours. Header and the two stripes
|
|
42
|
+
* are always present (unset ones stay unpainted); the optional elements are written only when
|
|
43
|
+
* coloured, so a style that only picks the first three keeps its historical shape.
|
|
44
|
+
*/
|
|
45
|
+
export declare const buildCustomTableStyle: (name: string, colors: TableStyleColors) => CustomTableStyle;
|
|
46
|
+
/** The element colours of an existing custom style (theme colours resolved to hex). */
|
|
47
|
+
export declare const getTableStyleColors: (style: CustomTableStyle, theme: SpreadsheetTheme) => TableStyleColors;
|
|
48
|
+
/**
|
|
49
|
+
* Why a style name cannot be used, or `undefined` when it can: empty, a built-in style's name
|
|
50
|
+
* (the gallery tells the two apart by name), or another custom style's name (case-insensitive,
|
|
51
|
+
* like Excel). `currentName` is the style being edited, which may keep its own name.
|
|
52
|
+
*/
|
|
53
|
+
export declare const getTableStyleNameError: (name: string, existingNames: readonly string[], currentName?: string) => string | undefined;
|
|
54
|
+
export type TableStyleEditorProps = {
|
|
55
|
+
/** The custom style being modified; omit to create a new one. */
|
|
56
|
+
style?: CustomTableStyle;
|
|
57
|
+
/** The name a new style starts with (ignored when `style` is given). */
|
|
58
|
+
defaultName?: string;
|
|
59
|
+
theme: SpreadsheetTheme;
|
|
60
|
+
/** Names of the workbook's custom styles, for the duplicate-name check. */
|
|
61
|
+
existingNames?: readonly string[];
|
|
62
|
+
/** Create (or, when `style` is given, save) — receives the style the picks produce. */
|
|
63
|
+
onSubmit(style: CustomTableStyle): void;
|
|
64
|
+
onCancel(): void;
|
|
65
|
+
/** Offered as "Delete style" while editing; the footer omits it when absent. */
|
|
66
|
+
onDelete?(style: CustomTableStyle): void;
|
|
67
|
+
className?: string;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Excel's New Table Style / Modify Table Style dialog, laid out for the Format as Table gallery:
|
|
71
|
+
* name row, the table elements with a colour button each (the toolbar's colour popover), a live
|
|
72
|
+
* preview of the table the picks produce, and an Excel-ordered footer.
|
|
73
|
+
*/
|
|
74
|
+
export declare const TableStyleEditor: ({ style, defaultName, theme, existingNames, onSubmit, onCancel, onDelete, className, }: TableStyleEditorProps) => React.JSX.Element;
|
|
75
|
+
/**
|
|
76
|
+
* The table a style paints: a header row over four banded rows, with the first / last column and
|
|
77
|
+
* total row emphasis when the style colours them. Uses the grid's banding rules
|
|
78
|
+
* (`api/banded-ranges.ts`): the first body row is the first stripe, the last body row is the
|
|
79
|
+
* total row, and an edge column's colour outranks the stripe under it.
|
|
80
|
+
*/
|
|
81
|
+
export declare const TableStylePreviewCard: ({ rowProperties, theme, }: {
|
|
82
|
+
rowProperties: Maybe<BandingProperties>;
|
|
83
|
+
theme: SpreadsheetTheme;
|
|
84
|
+
}) => React.JSX.Element;
|
|
85
|
+
//# sourceMappingURL=table-style-editor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table-style-editor.d.ts","sourceRoot":"","sources":["../../../../components/toolbar/table-style-editor.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAmC,MAAM,OAAO,CAAC;AAIxD,OAAO,KAAK,EACV,iBAAiB,EAEjB,gBAAgB,EAChB,KAAK,EACN,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG/D;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;EAW/B,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC;AAEhF,oFAAoF;AACpF,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,oBAAoB,EAAE,MAAM,CAAC,CAAC,CAAC;AAE7E,iGAAiG;AACjG,eAAO,MAAM,0BAA0B,EAAE,gBAIxC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,GAChC,MAAM,MAAM,EACZ,QAAQ,gBAAgB,KACvB,gBAUF,CAAC;AAEF,uFAAuF;AACvF,eAAO,MAAM,mBAAmB,GAC9B,OAAO,gBAAgB,EACvB,OAAO,gBAAgB,KACtB,gBAWF,CAAC;AAIF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,GACjC,MAAM,MAAM,EACZ,eAAe,SAAS,MAAM,EAAE,EAChC,cAAc,MAAM,KACnB,MAAM,GAAG,SAaX,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,iEAAiE;IACjE,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,gBAAgB,CAAC;IACxB,2EAA2E;IAC3E,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,uFAAuF;IACvF,QAAQ,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACxC,QAAQ,IAAI,IAAI,CAAC;IACjB,gFAAgF;IAChF,QAAQ,CAAC,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAAI,wFAS9B,qBAAqB,sBAuIvB,CAAC;AAkEF;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAAI,2BAGnC;IACD,aAAa,EAAE,KAAK,CAAC,iBAAiB,CAAC,CAAC;IACxC,KAAK,EAAE,gBAAgB,CAAC;CACzB,sBA2EA,CAAC"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { ChevronDownIcon } from "@rowsncolumns/icons";
|
|
2
|
+
import { Button, cn, Input, Popover, PopoverContent, PopoverTrigger, Separator, } from "@rowsncolumns/ui";
|
|
3
|
+
import { getContrastColor } from "@rowsncolumns/utils";
|
|
4
|
+
import React, { useId, useMemo, useState } from "react";
|
|
5
|
+
import { parseTintedColor } from "../../api/banded-ranges";
|
|
6
|
+
import { getStringifiedColor } from "../../themes";
|
|
7
|
+
import { ColorSelector } from "./color-selector";
|
|
8
|
+
/**
|
|
9
|
+
* The style elements a custom table style can colour — the `rowProperties` fields the grid's
|
|
10
|
+
* banding resolves (`api/banded-ranges.ts`), in the order of Excel's New Table Style dialog.
|
|
11
|
+
* The first three are what the gallery swatches show and what every created style carries; the
|
|
12
|
+
* rest are only written when the user picks a colour for them.
|
|
13
|
+
*/
|
|
14
|
+
export const TABLE_STYLE_ELEMENTS = [
|
|
15
|
+
{ key: "header", label: "Header row", property: "headerColor" },
|
|
16
|
+
{ key: "firstBand", label: "First row stripe", property: "firstBandColor" },
|
|
17
|
+
{ key: "secondBand", label: "Second row stripe", property: "secondBandColor" },
|
|
18
|
+
{ key: "firstColumn", label: "First column", property: "firstColumnColor" },
|
|
19
|
+
{ key: "lastColumn", label: "Last column", property: "lastColumnColor" },
|
|
20
|
+
{ key: "totalRow", label: "Total row", property: "totalRowColor" },
|
|
21
|
+
];
|
|
22
|
+
/** The colours a fresh style starts from (the default palette's accent 1 and its light tint). */
|
|
23
|
+
export const DEFAULT_TABLE_STYLE_COLORS = {
|
|
24
|
+
header: "#4472c4",
|
|
25
|
+
firstBand: "#b4c6e7",
|
|
26
|
+
secondBand: "#ffffff",
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* The banding a custom table style paints from its element colours. Header and the two stripes
|
|
30
|
+
* are always present (unset ones stay unpainted); the optional elements are written only when
|
|
31
|
+
* coloured, so a style that only picks the first three keeps its historical shape.
|
|
32
|
+
*/
|
|
33
|
+
export const buildCustomTableStyle = (name, colors) => {
|
|
34
|
+
const rowProperties = {
|
|
35
|
+
headerColor: colors.header,
|
|
36
|
+
firstBandColor: colors.firstBand,
|
|
37
|
+
secondBandColor: colors.secondBand,
|
|
38
|
+
};
|
|
39
|
+
if (colors.firstColumn)
|
|
40
|
+
rowProperties.firstColumnColor = colors.firstColumn;
|
|
41
|
+
if (colors.lastColumn)
|
|
42
|
+
rowProperties.lastColumnColor = colors.lastColumn;
|
|
43
|
+
if (colors.totalRow)
|
|
44
|
+
rowProperties.totalRowColor = colors.totalRow;
|
|
45
|
+
return { name, bandedRange: { rowProperties } };
|
|
46
|
+
};
|
|
47
|
+
/** The element colours of an existing custom style (theme colours resolved to hex). */
|
|
48
|
+
export const getTableStyleColors = (style, theme) => {
|
|
49
|
+
const rowProperties = style.bandedRange.rowProperties ?? {};
|
|
50
|
+
const colors = {};
|
|
51
|
+
for (const { key, property } of TABLE_STYLE_ELEMENTS) {
|
|
52
|
+
const color = getStringifiedColor(parseTintedColor(rowProperties[property]), theme);
|
|
53
|
+
if (color)
|
|
54
|
+
colors[key] = color;
|
|
55
|
+
}
|
|
56
|
+
return colors;
|
|
57
|
+
};
|
|
58
|
+
const BUILT_IN_STYLE_NAME = /^TableStyle(Light|Medium|Dark)\d+$/i;
|
|
59
|
+
/**
|
|
60
|
+
* Why a style name cannot be used, or `undefined` when it can: empty, a built-in style's name
|
|
61
|
+
* (the gallery tells the two apart by name), or another custom style's name (case-insensitive,
|
|
62
|
+
* like Excel). `currentName` is the style being edited, which may keep its own name.
|
|
63
|
+
*/
|
|
64
|
+
export const getTableStyleNameError = (name, existingNames, currentName) => {
|
|
65
|
+
const trimmed = name.trim();
|
|
66
|
+
if (!trimmed)
|
|
67
|
+
return "Enter a name for the style.";
|
|
68
|
+
if (BUILT_IN_STYLE_NAME.test(trimmed)) {
|
|
69
|
+
return "That name belongs to a built-in style.";
|
|
70
|
+
}
|
|
71
|
+
const lower = trimmed.toLowerCase();
|
|
72
|
+
const taken = existingNames.some((existing) => existing.toLowerCase() === lower &&
|
|
73
|
+
existing.toLowerCase() !== currentName?.toLowerCase());
|
|
74
|
+
return taken ? `A table style named “${trimmed}” already exists.` : undefined;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Excel's New Table Style / Modify Table Style dialog, laid out for the Format as Table gallery:
|
|
78
|
+
* name row, the table elements with a colour button each (the toolbar's colour popover), a live
|
|
79
|
+
* preview of the table the picks produce, and an Excel-ordered footer.
|
|
80
|
+
*/
|
|
81
|
+
export const TableStyleEditor = ({ style, defaultName = "", theme, existingNames = [], onSubmit, onCancel, onDelete, className, }) => {
|
|
82
|
+
const isEditing = !!style;
|
|
83
|
+
const nameId = useId();
|
|
84
|
+
const [name, setName] = useState(style?.name ?? defaultName);
|
|
85
|
+
const [colors, setColors] = useState(() => style ? getTableStyleColors(style, theme) : DEFAULT_TABLE_STYLE_COLORS);
|
|
86
|
+
const nameError = getTableStyleNameError(name, existingNames, style?.name);
|
|
87
|
+
const preview = useMemo(() => buildCustomTableStyle(name.trim(), colors), [name, colors]);
|
|
88
|
+
const setColor = (key, color) => setColors((prev) => ({ ...prev, [key]: color }));
|
|
89
|
+
const hasAnyColor = TABLE_STYLE_ELEMENTS.some(({ key }) => colors[key]);
|
|
90
|
+
return (React.createElement("form", { "data-testid": "table-style-editor", "aria-label": isEditing ? "Edit table style" : "New table style", className: cn("m-0 p-2 flex flex-col gap-3 font-system text-xs", className),
|
|
91
|
+
// The gallery is a Radix menu: keep typing in the fields from driving its typeahead.
|
|
92
|
+
onKeyDown: (event) => event.stopPropagation(), onSubmit: (event) => {
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
if (!nameError) {
|
|
95
|
+
onSubmit(preview);
|
|
96
|
+
}
|
|
97
|
+
} },
|
|
98
|
+
React.createElement("div", { className: "font-medium text-sm" }, isEditing ? "Edit table style" : "New table style"),
|
|
99
|
+
React.createElement("div", { className: "grid grid-cols-[6rem_minmax(0,1fr)] items-center gap-x-3 gap-y-1" },
|
|
100
|
+
React.createElement("label", { htmlFor: nameId, className: "truncate whitespace-nowrap" }, "Name"),
|
|
101
|
+
React.createElement(Input, { id: nameId, value: name, onChange: (event) => setName(event.target.value), autoComplete: "off", "aria-invalid": nameError ? true : undefined, "aria-describedby": nameError ? `${nameId}-error` : undefined, className: "text-xs" }),
|
|
102
|
+
nameError ? (React.createElement(React.Fragment, null,
|
|
103
|
+
React.createElement("span", { "aria-hidden": "true" }),
|
|
104
|
+
React.createElement("p", { id: `${nameId}-error`, role: "alert", className: "m-0 text-rnc-destructive" }, nameError))) : null),
|
|
105
|
+
React.createElement("div", { className: "flex items-start gap-4" },
|
|
106
|
+
React.createElement("div", { role: "group", "aria-label": "Table elements", className: "flex-1 min-w-0 flex flex-col gap-1.5" },
|
|
107
|
+
React.createElement("div", { className: "text-rnc-muted-foreground" }, "Table element"),
|
|
108
|
+
TABLE_STYLE_ELEMENTS.map(({ key, label }) => (React.createElement("div", { key: key, className: "grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3 h-7" },
|
|
109
|
+
React.createElement("span", { className: "truncate whitespace-nowrap", title: label }, label),
|
|
110
|
+
React.createElement(ElementColorButton, { label: label, color: colors[key], theme: theme, onChange: (color) => setColor(key, color) }))))),
|
|
111
|
+
React.createElement("div", { className: "w-[180px] shrink-0 flex flex-col gap-1.5" },
|
|
112
|
+
React.createElement("div", { className: "text-rnc-muted-foreground" }, "Preview"),
|
|
113
|
+
React.createElement(TableStylePreviewCard, { rowProperties: preview.bandedRange.rowProperties, theme: theme }),
|
|
114
|
+
React.createElement(Button, { type: "button", variant: "link", size: "xs", className: "self-start no-underline hover:underline", disabled: !hasAnyColor, onClick: () => setColors({}) }, "Clear all colors"))),
|
|
115
|
+
React.createElement(Separator, null),
|
|
116
|
+
React.createElement("div", { className: "flex items-center gap-2" },
|
|
117
|
+
isEditing && onDelete ? (React.createElement(Button, { type: "button", variant: "ghost", size: "xs", className: "text-rnc-destructive hover:text-rnc-destructive", onClick: () => onDelete(style) }, "Delete style")) : null,
|
|
118
|
+
React.createElement("div", { className: "ml-auto flex items-center gap-2" },
|
|
119
|
+
React.createElement(Button, { type: "button", variant: "ghost", size: "xs", onClick: onCancel }, "Cancel"),
|
|
120
|
+
React.createElement(Button, { type: "submit", variant: "primary", size: "xs", disabled: !!nameError }, isEditing ? "Save" : "Create")))));
|
|
121
|
+
};
|
|
122
|
+
/** "No colour" swatch: a red diagonal on the popover background, as the toolbar's fill button. */
|
|
123
|
+
const NO_COLOR_BACKGROUND = "linear-gradient(to top right, transparent 44%, #d32f2f 44%, #d32f2f 56%, transparent 56%)";
|
|
124
|
+
/**
|
|
125
|
+
* One element's colour: a swatch button (current colour + chevron) opening the shared colour
|
|
126
|
+
* popover the toolbar's fill / text colour buttons use (theme colours, standard colours, custom,
|
|
127
|
+
* and "No color").
|
|
128
|
+
*/
|
|
129
|
+
const ElementColorButton = ({ label, color, theme, onChange, }) => {
|
|
130
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
131
|
+
return (React.createElement(Popover, { modal: false, open: isOpen, onOpenChange: setIsOpen },
|
|
132
|
+
React.createElement(PopoverTrigger, { asChild: true },
|
|
133
|
+
React.createElement(Button, { type: "button", variant: "outline", size: "xs", "aria-label": `${label} color: ${color ? color.toUpperCase() : "No color"}`, "aria-haspopup": "dialog", "aria-expanded": isOpen, className: "h-7 w-14 px-1 py-0 gap-1 justify-between rounded-sm" },
|
|
134
|
+
React.createElement("span", { "data-testid": "element-color-swatch", "aria-hidden": "true", className: "block w-5 h-5 rounded-sm border border-solid border-rnc-border", style: color
|
|
135
|
+
? { backgroundColor: color }
|
|
136
|
+
: { backgroundImage: NO_COLOR_BACKGROUND } }),
|
|
137
|
+
React.createElement(ChevronDownIcon, { className: "w-3 h-3 text-rnc-muted-foreground" }))),
|
|
138
|
+
React.createElement(PopoverContent, { align: "end" },
|
|
139
|
+
React.createElement(ColorSelector, { color: color, theme: theme, resetButtonTitle: "No color", onChange: (next) => {
|
|
140
|
+
onChange(next ? getStringifiedColor(next, theme) : undefined);
|
|
141
|
+
setIsOpen(false);
|
|
142
|
+
} }))));
|
|
143
|
+
};
|
|
144
|
+
const PREVIEW_COLUMNS = 5;
|
|
145
|
+
const PREVIEW_BODY_ROWS = 4;
|
|
146
|
+
/**
|
|
147
|
+
* The table a style paints: a header row over four banded rows, with the first / last column and
|
|
148
|
+
* total row emphasis when the style colours them. Uses the grid's banding rules
|
|
149
|
+
* (`api/banded-ranges.ts`): the first body row is the first stripe, the last body row is the
|
|
150
|
+
* total row, and an edge column's colour outranks the stripe under it.
|
|
151
|
+
*/
|
|
152
|
+
export const TableStylePreviewCard = ({ rowProperties, theme, }) => {
|
|
153
|
+
const resolve = (color) => getStringifiedColor(parseTintedColor(color), theme) ?? undefined;
|
|
154
|
+
const headerColor = resolve(rowProperties?.headerColor);
|
|
155
|
+
const firstBand = resolve(rowProperties?.firstBandColor);
|
|
156
|
+
const secondBand = resolve(rowProperties?.secondBandColor);
|
|
157
|
+
const firstColumn = resolve(rowProperties?.firstColumnColor);
|
|
158
|
+
const lastColumn = resolve(rowProperties?.lastColumnColor);
|
|
159
|
+
const totalRow = resolve(rowProperties?.totalRowColor);
|
|
160
|
+
const dark = getStringifiedColor({ theme: 1 }, theme);
|
|
161
|
+
const light = getStringifiedColor({ theme: 0 }, theme);
|
|
162
|
+
const barColor = (background) => background ? getContrastColor(background, dark, light) : undefined;
|
|
163
|
+
const cellClass = "table-cell border border-solid border-rnc-border/70 align-middle px-1.5";
|
|
164
|
+
return (React.createElement("div", { "data-testid": "table-style-preview", className: "table border-collapse table-fixed w-full h-[108px] rounded-sm bg-rnc-popover" },
|
|
165
|
+
React.createElement("div", { className: "table-row", "data-testid": "preview-header-row" }, Array.from({ length: PREVIEW_COLUMNS }, (_, column) => (React.createElement("div", { key: column, className: cellClass, style: { backgroundColor: headerColor } },
|
|
166
|
+
React.createElement("span", { className: "block h-1.5 w-3/4 rounded-full", style: {
|
|
167
|
+
backgroundColor: barColor(headerColor) ?? dark,
|
|
168
|
+
opacity: headerColor ? 0.9 : 0.6,
|
|
169
|
+
} }))))),
|
|
170
|
+
Array.from({ length: PREVIEW_BODY_ROWS }, (_, row) => {
|
|
171
|
+
const isTotalRow = row === PREVIEW_BODY_ROWS - 1;
|
|
172
|
+
const stripe = row % 2 === 0 ? firstBand : secondBand;
|
|
173
|
+
const rowBackground = isTotalRow ? totalRow ?? stripe : stripe;
|
|
174
|
+
return (React.createElement("div", { className: "table-row", key: row, "data-testid": "preview-body-row" }, Array.from({ length: PREVIEW_COLUMNS }, (_, column) => {
|
|
175
|
+
const edge = column === 0
|
|
176
|
+
? firstColumn
|
|
177
|
+
: column === PREVIEW_COLUMNS - 1
|
|
178
|
+
? lastColumn
|
|
179
|
+
: undefined;
|
|
180
|
+
const background = edge ?? rowBackground;
|
|
181
|
+
return (React.createElement("div", { key: column, className: cellClass, style: { backgroundColor: background } },
|
|
182
|
+
React.createElement("span", { className: "block h-1 rounded-full", style: {
|
|
183
|
+
width: `${55 + ((row * 3 + column * 5) % 4) * 10}%`,
|
|
184
|
+
backgroundColor: barColor(background) ?? dark,
|
|
185
|
+
opacity: 0.35,
|
|
186
|
+
} })));
|
|
187
|
+
})));
|
|
188
|
+
})));
|
|
189
|
+
};
|
|
@@ -2,6 +2,7 @@ import type { CellInterface, SelectionArea } from "@rowsncolumns/grid";
|
|
|
2
2
|
import { findMatchingBandProperty } from "@rowsncolumns/utils";
|
|
3
3
|
import React from "react";
|
|
4
4
|
export { findMatchingBandProperty };
|
|
5
|
+
export { buildCustomTableStyle } from "./table-style-editor";
|
|
5
6
|
import type { BandedDefinition, CustomTableStyle, TableStylePreview, TableTheme, TableView } from "../../types";
|
|
6
7
|
import type { CanvasGridProps, SelectionAttributes, SpreadsheetTheme } from "../canvas-grid/typings";
|
|
7
8
|
export type TableStyleSelectorProps = {
|
|
@@ -26,6 +27,11 @@ export type TableStyleSelectorProps = {
|
|
|
26
27
|
* without it the selector keeps the styles it created for the session.
|
|
27
28
|
*/
|
|
28
29
|
onCreateCustomTableStyle?(style: CustomTableStyle): void;
|
|
30
|
+
/**
|
|
31
|
+
* Called with a host-kept custom style the user deletes from the style editor (right-click a
|
|
32
|
+
* Custom swatch → Delete style); without it host-kept styles cannot be deleted from the gallery.
|
|
33
|
+
*/
|
|
34
|
+
onDeleteCustomTableStyle?(style: CustomTableStyle): void;
|
|
29
35
|
};
|
|
30
36
|
/** Excel's Table Style Options (Table Design tab), in ribbon order. */
|
|
31
37
|
declare const STYLE_OPTIONS: Array<{
|
|
@@ -39,12 +45,6 @@ declare const STYLE_OPTIONS: Array<{
|
|
|
39
45
|
* does); a custom style has no palette to re-resolve from and keeps its banding.
|
|
40
46
|
*/
|
|
41
47
|
export declare const getStyleOptionUpdate: (table: TableView, key: (typeof STYLE_OPTIONS)[number]["key"], checked: boolean) => Partial<TableView>;
|
|
42
|
-
/** The banding a New Table Style paints from its three colours (unset elements stay unpainted). */
|
|
43
|
-
export declare const buildCustomTableStyle: (name: string, colors: {
|
|
44
|
-
header?: string;
|
|
45
|
-
firstBand?: string;
|
|
46
|
-
secondBand?: string;
|
|
47
|
-
}) => CustomTableStyle;
|
|
48
48
|
type TableStyleList = [
|
|
49
49
|
theme: TableTheme,
|
|
50
50
|
firtBandStripeTint: number | undefined,
|
|
@@ -57,5 +57,5 @@ type TableStyleList = [
|
|
|
57
57
|
* importers and both engines resolve a theme name into) plus a per-swatch preview style.
|
|
58
58
|
*/
|
|
59
59
|
export declare const getTableBorderStyles: (showRowStripes: boolean | undefined, showColumnStripes: boolean | undefined, theme?: SpreadsheetTheme) => Map<string, TableStyleList[]>;
|
|
60
|
-
export declare const TableStyleSelector: React.MemoExoticComponent<({ tables, sheetId, theme, activeCell, selections, onUpdateTable, onCreateTable, onRequestCreateTable, onPreviewTableStyle, customTableStyles, onCreateCustomTableStyle, }: TableStyleSelectorProps) => React.JSX.Element>;
|
|
60
|
+
export declare const TableStyleSelector: React.MemoExoticComponent<({ tables, sheetId, theme, activeCell, selections, onUpdateTable, onCreateTable, onRequestCreateTable, onPreviewTableStyle, customTableStyles, onCreateCustomTableStyle, onDeleteCustomTableStyle, }: TableStyleSelectorProps) => React.JSX.Element>;
|
|
61
61
|
//# sourceMappingURL=table-style-selector.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"table-style-selector.d.ts","sourceRoot":"","sources":["../../../../components/toolbar/table-style-selector.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"table-style-selector.d.ts","sourceRoot":"","sources":["../../../../components/toolbar/table-style-selector.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAevE,OAAO,EACL,wBAAwB,EAGzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAuD,MAAM,OAAO,CAAC;AAG5E,OAAO,EAAE,wBAAwB,EAAE,CAAC;AACpC,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAK7D,OAAO,KAAK,EACV,gBAAgB,EAEhB,gBAAgB,EAEhB,iBAAiB,EACjB,UAAU,EACV,SAAS,EACV,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACnB,gBAAgB,EACjB,MAAM,wBAAwB,CAAC;AAGhC,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,gBAAgB,CAAC;IACxB,UAAU,EAAE,aAAa,CAAC,mBAAmB,CAAC,EAAE,CAAC;IACjD,UAAU,EAAE,aAAa,CAAC;IAC1B,aAAa,CAAC,CACZ,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,EACxB,YAAY,EAAE,OAAO,CAAC,SAAS,CAAC,GAC/B,IAAI,CAAC;IACR,aAAa,EAAE,eAAe,CAAC,eAAe,CAAC,CAAC;IAChD,4FAA4F;IAC5F,oBAAoB,CAAC,EAAE,eAAe,CAAC,sBAAsB,CAAC,CAAC;IAC/D;;;OAGG;IACH,mBAAmB,CAAC,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,GAAG,IAAI,CAAC;IAC9D,uFAAuF;IACvF,iBAAiB,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACvC;;;OAGG;IACH,wBAAwB,CAAC,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACzD;;;OAGG;IACH,wBAAwB,CAAC,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAC1D,CAAC;AAEF,uEAAuE;AACvE,QAAA,MAAM,aAAa,EAAE,KAAK,CAAC;IACzB,GAAG,EACC,WAAW,GACX,UAAU,GACV,gBAAgB,GAChB,mBAAmB,GACnB,iBAAiB,GACjB,gBAAgB,GAChB,cAAc,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,OAAO,CAAC;CACvB,CAQA,CAAC;AAKF;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAC/B,OAAO,SAAS,EAChB,KAAK,CAAC,OAAO,aAAa,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,EAC1C,SAAS,OAAO,KACf,OAAO,CAAC,SAAS,CAqBnB,CAAC;AAEF,KAAK,cAAc,GAAG;IACpB,KAAK,EAAE,UAAU;IACjB,kBAAkB,EAAE,MAAM,GAAG,SAAS;IACtC,cAAc,EAAE,MAAM,GAAG,SAAS;IAClC,cAAc,EAAE,gBAAgB,GAAG,SAAS;IAC5C,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa;CAC5B,CAAC;AAKF;;;GAGG;AACH,eAAO,MAAM,oBAAoB,GAC/B,gBAAgB,OAAO,GAAG,SAAS,EACnC,mBAAmB,OAAO,GAAG,SAAS,EACtC,QAAQ,gBAAgB,kCAoBzB,CAAC;AAEF,eAAO,MAAM,kBAAkB,kOAc1B,uBAAuB,uBA+W3B,CAAC"}
|