@rowsncolumns/spreadsheet 10.4.1 → 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.
Files changed (40) hide show
  1. package/dist/cjs/components/filter-box/date-tree.d.ts +23 -5
  2. package/dist/cjs/components/filter-box/date-tree.d.ts.map +1 -1
  3. package/dist/cjs/components/filter-box/date-tree.js +76 -11
  4. package/dist/cjs/components/filter-box/filter-by-condition.d.ts +26 -1
  5. package/dist/cjs/components/filter-box/filter-by-condition.d.ts.map +1 -1
  6. package/dist/cjs/components/filter-box/filter-by-condition.js +108 -9
  7. package/dist/cjs/components/filter-box/index.d.ts.map +1 -1
  8. package/dist/cjs/components/filter-box/index.js +7 -2
  9. package/dist/cjs/components/formula-input/index.d.ts.map +1 -1
  10. package/dist/cjs/components/formula-input/index.js +26 -4
  11. package/dist/cjs/components/toolbar/table-style-editor.d.ts +85 -0
  12. package/dist/cjs/components/toolbar/table-style-editor.d.ts.map +1 -0
  13. package/dist/cjs/components/toolbar/table-style-editor.js +235 -0
  14. package/dist/cjs/components/toolbar/table-style-selector.d.ts +7 -7
  15. package/dist/cjs/components/toolbar/table-style-selector.d.ts.map +1 -1
  16. package/dist/cjs/components/toolbar/table-style-selector.js +77 -87
  17. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  18. package/dist/cjs/types.d.ts +10 -0
  19. package/dist/cjs/types.d.ts.map +1 -1
  20. package/dist/esm/components/filter-box/date-tree.d.ts +23 -5
  21. package/dist/esm/components/filter-box/date-tree.d.ts.map +1 -1
  22. package/dist/esm/components/filter-box/date-tree.js +62 -10
  23. package/dist/esm/components/filter-box/filter-by-condition.d.ts +26 -1
  24. package/dist/esm/components/filter-box/filter-by-condition.d.ts.map +1 -1
  25. package/dist/esm/components/filter-box/filter-by-condition.js +78 -12
  26. package/dist/esm/components/filter-box/index.d.ts.map +1 -1
  27. package/dist/esm/components/filter-box/index.js +7 -2
  28. package/dist/esm/components/formula-input/index.d.ts.map +1 -1
  29. package/dist/esm/components/formula-input/index.js +26 -4
  30. package/dist/esm/components/toolbar/table-style-editor.d.ts +85 -0
  31. package/dist/esm/components/toolbar/table-style-editor.d.ts.map +1 -0
  32. package/dist/esm/components/toolbar/table-style-editor.js +189 -0
  33. package/dist/esm/components/toolbar/table-style-selector.d.ts +7 -7
  34. package/dist/esm/components/toolbar/table-style-selector.d.ts.map +1 -1
  35. package/dist/esm/components/toolbar/table-style-selector.js +74 -81
  36. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  37. package/dist/esm/types.d.ts +10 -0
  38. package/dist/esm/types.d.ts.map +1 -1
  39. package/dist/spreadsheet.min.css +1 -1
  40. package/package.json +1 -1
@@ -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 ConditionSelect = ({ value, labels, onChange, ariaLabel, }) => (React.createElement(Select, { value: value ?? CONDITION_NONE, onValueChange: (next) => onChange(next === CONDITION_NONE ? undefined : next) },
27
- React.createElement(SelectTrigger, { className: "mb-1", "aria-label": ariaLabel },
28
- React.createElement(SelectValue, { placeholder: "None" })),
29
- React.createElement(SelectPortal, null,
30
- React.createElement(SelectContent, null,
31
- React.createElement(SelectItem, { value: CONDITION_NONE }, "None"),
32
- labels.map(({ condition, label }) => (React.createElement(SelectItem, { key: condition, value: condition }, label)))))));
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;AAI3D,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,uBA4kBlB,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"}
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;AAatC,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,sGAiIxB,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
- setFormulaMode(isAFormula(value));
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
- setFormulaMode,
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
- setFormulaMode(isAFormula(value));
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
- setFormulaMode,
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;AAkBvE,OAAO,EACL,wBAAwB,EAGzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAuD,MAAM,OAAO,CAAC;AAG5E,OAAO,EAAE,wBAAwB,EAAE,CAAC;AAKpC,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;AAEhC,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;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;AAsBF,mGAAmG;AACnG,eAAO,MAAM,qBAAqB,GAChC,MAAM,MAAM,EACZ,QAAQ;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,KACnE,gBASD,CAAC;AAEH,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,wMAa1B,uBAAuB,uBAoS3B,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"}