@rowsncolumns/spreadsheet 10.4.1 → 10.4.4

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 (104) hide show
  1. package/dist/cjs/atoms/sheet-context.atom.d.ts +2 -0
  2. package/dist/cjs/atoms/sheet-context.atom.d.ts.map +1 -1
  3. package/dist/cjs/atoms/sheet-context.atom.js +8 -2
  4. package/dist/cjs/atoms/sort-dialog.atom.d.ts +6 -2
  5. package/dist/cjs/atoms/sort-dialog.atom.d.ts.map +1 -1
  6. package/dist/cjs/components/canvas-grid/index.d.ts.map +1 -1
  7. package/dist/cjs/components/canvas-grid/index.js +4 -2
  8. package/dist/cjs/components/canvas-grid/typings.d.ts +10 -0
  9. package/dist/cjs/components/canvas-grid/typings.d.ts.map +1 -1
  10. package/dist/cjs/components/filter-box/date-tree.d.ts +23 -5
  11. package/dist/cjs/components/filter-box/date-tree.d.ts.map +1 -1
  12. package/dist/cjs/components/filter-box/date-tree.js +76 -11
  13. package/dist/cjs/components/filter-box/filter-by-condition.d.ts +26 -1
  14. package/dist/cjs/components/filter-box/filter-by-condition.d.ts.map +1 -1
  15. package/dist/cjs/components/filter-box/filter-by-condition.js +108 -9
  16. package/dist/cjs/components/filter-box/index.d.ts.map +1 -1
  17. package/dist/cjs/components/filter-box/index.js +21 -5
  18. package/dist/cjs/components/formula-input/index.d.ts.map +1 -1
  19. package/dist/cjs/components/formula-input/index.js +26 -4
  20. package/dist/cjs/components/sheet-tabs/index.d.ts.map +1 -1
  21. package/dist/cjs/components/sheet-tabs/index.js +19 -5
  22. package/dist/cjs/components/sort-range-dialog/index.d.ts +20 -3
  23. package/dist/cjs/components/sort-range-dialog/index.d.ts.map +1 -1
  24. package/dist/cjs/components/sort-range-dialog/index.js +20 -6
  25. package/dist/cjs/components/toolbar/cell-style-selector.d.ts +9 -0
  26. package/dist/cjs/components/toolbar/cell-style-selector.d.ts.map +1 -1
  27. package/dist/cjs/components/toolbar/cell-style-selector.js +12 -3
  28. package/dist/cjs/components/toolbar/merge-cells-selector.d.ts +27 -1
  29. package/dist/cjs/components/toolbar/merge-cells-selector.d.ts.map +1 -1
  30. package/dist/cjs/components/toolbar/merge-cells-selector.js +106 -8
  31. package/dist/cjs/components/toolbar/table-style-editor.d.ts +85 -0
  32. package/dist/cjs/components/toolbar/table-style-editor.d.ts.map +1 -0
  33. package/dist/cjs/components/toolbar/table-style-editor.js +235 -0
  34. package/dist/cjs/components/toolbar/table-style-selector.d.ts +7 -7
  35. package/dist/cjs/components/toolbar/table-style-selector.d.ts.map +1 -1
  36. package/dist/cjs/components/toolbar/table-style-selector.js +77 -87
  37. package/dist/cjs/config.d.ts +6 -0
  38. package/dist/cjs/config.d.ts.map +1 -1
  39. package/dist/cjs/config.js +7 -1
  40. package/dist/cjs/hooks/use-conditional-formatting.d.ts +8 -0
  41. package/dist/cjs/hooks/use-conditional-formatting.d.ts.map +1 -1
  42. package/dist/cjs/hooks/use-conditional-formatting.js +128 -77
  43. package/dist/cjs/hooks/use-editable.d.ts.map +1 -1
  44. package/dist/cjs/hooks/use-editable.js +59 -1
  45. package/dist/cjs/hooks/use-selection.d.ts +6 -1
  46. package/dist/cjs/hooks/use-selection.d.ts.map +1 -1
  47. package/dist/cjs/hooks/use-selection.js +37 -1
  48. package/dist/cjs/release-date.js +1 -1
  49. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  50. package/dist/cjs/types.d.ts +64 -0
  51. package/dist/cjs/types.d.ts.map +1 -1
  52. package/dist/esm/atoms/sheet-context.atom.d.ts +2 -0
  53. package/dist/esm/atoms/sheet-context.atom.d.ts.map +1 -1
  54. package/dist/esm/atoms/sheet-context.atom.js +3 -0
  55. package/dist/esm/atoms/sort-dialog.atom.d.ts +6 -2
  56. package/dist/esm/atoms/sort-dialog.atom.d.ts.map +1 -1
  57. package/dist/esm/components/canvas-grid/index.d.ts.map +1 -1
  58. package/dist/esm/components/canvas-grid/index.js +3 -1
  59. package/dist/esm/components/canvas-grid/typings.d.ts +10 -0
  60. package/dist/esm/components/canvas-grid/typings.d.ts.map +1 -1
  61. package/dist/esm/components/filter-box/date-tree.d.ts +23 -5
  62. package/dist/esm/components/filter-box/date-tree.d.ts.map +1 -1
  63. package/dist/esm/components/filter-box/date-tree.js +62 -10
  64. package/dist/esm/components/filter-box/filter-by-condition.d.ts +26 -1
  65. package/dist/esm/components/filter-box/filter-by-condition.d.ts.map +1 -1
  66. package/dist/esm/components/filter-box/filter-by-condition.js +78 -12
  67. package/dist/esm/components/filter-box/index.d.ts.map +1 -1
  68. package/dist/esm/components/filter-box/index.js +21 -5
  69. package/dist/esm/components/formula-input/index.d.ts.map +1 -1
  70. package/dist/esm/components/formula-input/index.js +26 -4
  71. package/dist/esm/components/sheet-tabs/index.d.ts.map +1 -1
  72. package/dist/esm/components/sheet-tabs/index.js +20 -6
  73. package/dist/esm/components/sort-range-dialog/index.d.ts +20 -3
  74. package/dist/esm/components/sort-range-dialog/index.d.ts.map +1 -1
  75. package/dist/esm/components/sort-range-dialog/index.js +17 -5
  76. package/dist/esm/components/toolbar/cell-style-selector.d.ts +9 -0
  77. package/dist/esm/components/toolbar/cell-style-selector.d.ts.map +1 -1
  78. package/dist/esm/components/toolbar/cell-style-selector.js +11 -2
  79. package/dist/esm/components/toolbar/merge-cells-selector.d.ts +27 -1
  80. package/dist/esm/components/toolbar/merge-cells-selector.d.ts.map +1 -1
  81. package/dist/esm/components/toolbar/merge-cells-selector.js +94 -10
  82. package/dist/esm/components/toolbar/table-style-editor.d.ts +85 -0
  83. package/dist/esm/components/toolbar/table-style-editor.d.ts.map +1 -0
  84. package/dist/esm/components/toolbar/table-style-editor.js +189 -0
  85. package/dist/esm/components/toolbar/table-style-selector.d.ts +7 -7
  86. package/dist/esm/components/toolbar/table-style-selector.d.ts.map +1 -1
  87. package/dist/esm/components/toolbar/table-style-selector.js +74 -81
  88. package/dist/esm/config.d.ts +6 -0
  89. package/dist/esm/config.d.ts.map +1 -1
  90. package/dist/esm/config.js +6 -0
  91. package/dist/esm/hooks/use-conditional-formatting.d.ts +8 -0
  92. package/dist/esm/hooks/use-conditional-formatting.d.ts.map +1 -1
  93. package/dist/esm/hooks/use-conditional-formatting.js +50 -13
  94. package/dist/esm/hooks/use-editable.d.ts.map +1 -1
  95. package/dist/esm/hooks/use-editable.js +48 -2
  96. package/dist/esm/hooks/use-selection.d.ts +6 -1
  97. package/dist/esm/hooks/use-selection.d.ts.map +1 -1
  98. package/dist/esm/hooks/use-selection.js +37 -1
  99. package/dist/esm/release-date.js +1 -1
  100. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  101. package/dist/esm/types.d.ts +64 -0
  102. package/dist/esm/types.d.ts.map +1 -1
  103. package/dist/spreadsheet.min.css +1 -1
  104. package/package.json +5 -5
@@ -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,uBA8lBlB,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"}
@@ -4,9 +4,11 @@ import { Button, cn, DropdownLeftSlot, DropdownMenuSeparator, Popover, PopoverCo
4
4
  import { isNil, number2Alpha } from "@rowsncolumns/utils";
5
5
  import React, { useEffect, useRef, useState } from "react";
6
6
  import { memo, useCallback, useMemo } from "react";
7
- import { useFocusSheet } from "../../atoms";
8
- import { StyledFakeTriggerButton } from "../context-menu/style";
7
+ import { useFocusSheet, useGetFormattedValue } from "../../atoms";
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.
@@ -122,15 +127,23 @@ export const FilterBoxWrapper = memo(({ dimensionIndex, onOpenChange, onSort, on
122
127
  // filter box is currently anchored on (that's the column we computed
123
128
  // distinct colors/icons for). Other columns expose value + custom-list
124
129
  // sort only.
130
+ // Excel labels sort levels by the header row — a table's column names, or
131
+ // the first row of a filtered range (an AutoFilter range always starts on
132
+ // its header row). A blank header falls back to the column letter.
133
+ const getFormattedValue = useGetFormattedValue();
125
134
  const sortDialogColumns = useMemo(() => {
126
- const { startColumnIndex, endColumnIndex } = filter.range;
135
+ const { startColumnIndex, endColumnIndex, startRowIndex } = filter.range;
127
136
  const cols = [];
128
137
  for (let c = startColumnIndex; c <= endColumnIndex; c++) {
129
138
  const dim = c - startColumnIndex;
130
139
  const isActive = dim === dimensionIndex;
140
+ const header = isTableView(filter)
141
+ ? filter.columns?.[dim]?.name
142
+ : getFormattedValue?.(sheetId, startRowIndex, c);
131
143
  cols.push({
132
144
  dimensionIndex: dim,
133
145
  label: `Column ${number2Alpha(c)}`,
146
+ header: header ?? undefined,
134
147
  ...(isActive
135
148
  ? {
136
149
  availableCellColors,
@@ -142,7 +155,9 @@ export const FilterBoxWrapper = memo(({ dimensionIndex, onOpenChange, onSort, on
142
155
  }
143
156
  return cols;
144
157
  }, [
145
- filter.range,
158
+ filter,
159
+ sheetId,
160
+ getFormattedValue,
146
161
  dimensionIndex,
147
162
  availableCellColors,
148
163
  availableFontColors,
@@ -347,6 +362,7 @@ export const FilterBoxWrapper = memo(({ dimensionIndex, onOpenChange, onSort, on
347
362
  openSortDialog({
348
363
  columns: sortDialogColumns,
349
364
  customLists,
365
+ initialHasHeader: true,
350
366
  onApply: onApplySort,
351
367
  });
352
368
  onClose();
@@ -377,7 +393,7 @@ export const FilterBoxWrapper = memo(({ dimensionIndex, onOpenChange, onSort, on
377
393
  React.createElement(Cross2Icon, null)),
378
394
  clearLabel),
379
395
  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 }),
396
+ 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
397
  React.createElement(Separator, { className: "mt-2 mb-2" }),
382
398
  React.createElement(FilterByColor, { colorFilter: colorFilter === undefined
383
399
  ? 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
  ]);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/sheet-tabs/index.tsx"],"names":[],"mappings":"AAqBA,OAAO,KAAyB,MAAM,OAAO,CAAC;AAI9C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAIzC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,GAAI,CAAC,SAAS,KAAK,EAAE,QAAQ,CAAC,EAAE,KAClB,MACjD,CAAC;AAEF;;;;GAIG;AACH,iBAAS,kBAAkB,CAAC,CAAC,SAAS,KAAK,EAAE,EAC3C,MAAM,EACN,aAAa,EACb,aAAa,EACb,eAAe,EACf,WAAiC,EACjC,aAAa,EACb,kBAAkB,EAClB,WAAW,EACX,WAAW,EACX,WAAW,EACX,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,EACrB,WAAW,EACX,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EACjB,KAAK,EACL,QAAQ,EACR,sBAA8B,GAC/B,EAAE,cAAc,CAAC,CAAC,CAAC,qBAqUnB;AAED,eAAO,MAAM,SAAS,EAA+B,OAAO,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/sheet-tabs/index.tsx"],"names":[],"mappings":"AAqBA,OAAO,KAAyB,MAAM,OAAO,CAAC;AAK9C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAIzC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,GAAI,CAAC,SAAS,KAAK,EAAE,QAAQ,CAAC,EAAE,KAClB,MACjD,CAAC;AAEF;;;;GAIG;AACH,iBAAS,kBAAkB,CAAC,CAAC,SAAS,KAAK,EAAE,EAC3C,MAAM,EACN,aAAa,EACb,aAAa,EACb,eAAe,EACf,WAAiC,EACjC,aAAa,EACb,kBAAkB,EAClB,WAAW,EACX,WAAW,EACX,WAAW,EACX,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,EACrB,WAAW,EACX,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EACjB,KAAK,EACL,QAAQ,EACR,sBAA8B,GAC/B,EAAE,cAAc,CAAC,CAAC,CAAC,qBAkVnB;AAED,eAAO,MAAM,SAAS,EAA+B,OAAO,kBAAkB,CAAC"}
@@ -3,10 +3,11 @@ import { restrictToHorizontalAxis, restrictToParentElement, } from "@dnd-kit/mod
3
3
  import { horizontalListSortingStrategy, SortableContext, } from "@dnd-kit/sortable";
4
4
  import { ChevronLeftIcon, ChevronRightIcon } from "@rowsncolumns/icons";
5
5
  import { IconButton, Separator } from "@rowsncolumns/ui";
6
- import { isNil, sortSheetsByIndex } from "@rowsncolumns/utils";
6
+ import { isNil, sortSheetsByIndex, validateSheetName } from "@rowsncolumns/utils";
7
7
  import { useCallback, useId, useMemo, useRef, useState } from "react";
8
8
  import React, { Fragment, memo } from "react";
9
9
  import { isProtectedSheet } from "../../api/validation";
10
+ import { useShowValidationAlert } from "../../atoms/validation-alert.atom";
10
11
  import { useIsomorphicLayoutEffect } from "../../hooks/use-isomorphic-layout-effect";
11
12
  import { SheetTabContextMenu } from "./context-menu";
12
13
  import { SheetTab } from "./sheet-tab";
@@ -45,16 +46,29 @@ function SheetTabsComponent({ sheets, activeSheetId, onRenameSheet, protectedRan
45
46
  }
46
47
  return !isProtectedSheet(sheetId, protectedRanges);
47
48
  }, [protectedRanges, readonly]);
49
+ const showValidationAlert = useShowValidationAlert();
48
50
  const canRename = useCallback((sheetId, newName) => {
49
- // Worksheet names cannot be empty or whitespace-only
50
- if (isNil(newName) || newName.trim().length === 0) {
51
+ if (isNil(newName) || !canEditSheet(sheetId)) {
51
52
  return false;
52
53
  }
53
- if (!canEditSheet(sheetId)) {
54
+ // Excel's worksheet-name rules (rowsncolumns/spreadsheet#939 U-01): at most 31 characters,
55
+ // none of `\ / ? * [ ] :`, no leading / trailing apostrophe, not `History`, and unique
56
+ // case-insensitively (a sheet may re-case its own title). Every refusal shows Excel's own
57
+ // dialog; a blank name is the editor's "nothing typed" and just restores the title.
58
+ const validation = validateSheetName(newName, sheets, sheetId);
59
+ if (!validation.ok) {
60
+ if (validation.reason !== "blank") {
61
+ void showValidationAlert({
62
+ style: "stop",
63
+ title: "Error",
64
+ message: validation.message,
65
+ confirm: false,
66
+ });
67
+ }
54
68
  return false;
55
69
  }
56
- return !sheets.find((sheet) => sheet.title === newName);
57
- }, [canEditSheet, sheets]);
70
+ return true;
71
+ }, [canEditSheet, sheets, showValidationAlert]);
58
72
  const canDeleteSheet = useCallback((sheetId) => {
59
73
  if (!canEditSheet(sheetId)) {
60
74
  return false;
@@ -10,7 +10,13 @@ import type { SortSpecs } from "../../types";
10
10
  */
11
11
  export type SortDialogColumn = {
12
12
  dimensionIndex: number;
13
+ /** Positional label ("Column B") — the fallback when the header cell is blank. */
13
14
  label: string;
15
+ /**
16
+ * Text of the column's first-row cell. Shown instead of `label` while
17
+ * "My data has headers" is on (Excel labels sort levels by header).
18
+ */
19
+ header?: string | null;
14
20
  availableCellColors?: string[];
15
21
  availableFontColors?: string[];
16
22
  availableIcons?: AvailableIcon[];
@@ -33,13 +39,24 @@ export type SortDialogProps = {
33
39
  * first column.
34
40
  */
35
41
  initialSpecs?: SortSpecs[];
42
+ /**
43
+ * Initial state of Excel's "My data has headers". The toggle renders when
44
+ * this is set or any column carries a `header`; a host that omits both keeps
45
+ * the header-less dialog.
46
+ */
47
+ initialHasHeader?: boolean;
36
48
  /**
37
49
  * Emits the assembled ordered `SortSpecs[]` (primary first, then
38
- * tiebreakers). The host routes these to `onSortTable` / `onSortRange`.
50
+ * tiebreakers) plus the header choice. The host routes these to
51
+ * `onSortTable` / `onSortRange` (trimming the header row when `hasHeader`).
39
52
  * The dialog never touches state directly.
40
53
  */
41
- onApply(specs: SortSpecs[]): void;
54
+ onApply(specs: SortSpecs[], options: {
55
+ hasHeader: boolean;
56
+ }): void;
42
57
  };
58
+ /** The label a sort level shows for a column: its header while the data has headers, else its letter. */
59
+ export declare const sortColumnLabel: (column: Pick<SortDialogColumn, "label" | "header">, hasHeader: boolean) => string;
43
60
  /**
44
61
  * Custom multi-level sort dialog. Mirrors Excel's "Sort…" dialog and
45
62
  * Google Sheets' "Sort range — advanced". Each row is a sort level:
@@ -51,7 +68,7 @@ export type SortDialogProps = {
51
68
  * `SortSpecs[]` via `onApply`; the host disambiguates table vs range and
52
69
  * routes through its existing sort callbacks.
53
70
  */
54
- export declare const SortRangeDialog: React.MemoExoticComponent<({ open, onOpenChange, columns, customLists, initialSpecs, onApply, }: SortDialogProps) => React.JSX.Element>;
71
+ export declare const SortRangeDialog: React.MemoExoticComponent<({ open, onOpenChange, columns, customLists, initialSpecs, initialHasHeader, onApply, }: SortDialogProps) => React.JSX.Element>;
55
72
  /**
56
73
  * Host-mounted, atom-driven wrapper around {@link SortRangeDialog}. Mount this
57
74
  * once in the host (alongside the other editor dialogs); the filter box opens
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/sort-range-dialog/index.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAA0D,MAAM,OAAO,CAAC;AAE/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,KAAK,EAAa,SAAS,EAAE,MAAM,aAAa,CAAC;AAQxD;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,cAAc,CAAC,EAAE,aAAa,EAAE,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,2BAA2B;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IAClC,oCAAoC;IACpC,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,SAAS,EAAE,CAAC;IAC3B;;;;OAIG;IACH,OAAO,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;CACnC,CAAC;AA2DF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,mGAQvB,eAAe,uBAiVnB,CAAC;AAIF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB;;;CAe/B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/sort-range-dialog/index.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAA0D,MAAM,OAAO,CAAC;AAE/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,KAAK,EAAa,SAAS,EAAE,MAAM,aAAa,CAAC;AAQxD;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,kFAAkF;IAClF,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,cAAc,CAAC,EAAE,aAAa,EAAE,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,2BAA2B;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IAClC,oCAAoC;IACpC,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,SAAS,EAAE,CAAC;IAC3B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;CACpE,CAAC;AAEF,yGAAyG;AACzG,eAAO,MAAM,eAAe,GAC1B,QAAQ,IAAI,CAAC,gBAAgB,EAAE,OAAO,GAAG,QAAQ,CAAC,EAClD,WAAW,OAAO,KACjB,MAGF,CAAC;AA2DF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,qHASvB,eAAe,uBAiWnB,CAAC;AAIF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB;;;CAgB/B,CAAC"}
@@ -2,6 +2,11 @@ import { ArrowDownIcon, ArrowUpIcon, Cross1Icon, PlusIcon, SortDownIcon, SortUpI
2
2
  import { Button, Dialog, DialogBody, DialogCloseButton, DialogContent, DialogTitle, IconButton, Select, SelectContent, SelectItem, SelectPortal, SelectTrigger, SelectValue, Separator, } from "@rowsncolumns/ui";
3
3
  import React, { memo, useCallback, useEffect, useMemo, useState } from "react";
4
4
  import { useSortDialogState } from "../../atoms/sort-dialog.atom";
5
+ /** The label a sort level shows for a column: its header while the data has headers, else its letter. */
6
+ export const sortColumnLabel = (column, hasHeader) => {
7
+ const header = column.header?.trim();
8
+ return hasHeader && header ? header : column.label;
9
+ };
5
10
  const CUSTOM_LIST_PREFIX = "custom:";
6
11
  const ColorSwatch = ({ color }) => (React.createElement("span", { className: "inline-block h-3.5 w-3.5 rounded border border-gray-300 shrink-0", style: { backgroundColor: color } }));
7
12
  const IconSwatch = ({ glyph, color }) => (React.createElement("span", { className: "inline-block h-3.5 w-3.5 leading-none text-center text-sm shrink-0", style: color ? { color } : undefined }, glyph));
@@ -36,17 +41,21 @@ const specToLevel = (spec, customLists) => {
36
41
  * `SortSpecs[]` via `onApply`; the host disambiguates table vs range and
37
42
  * routes through its existing sort callbacks.
38
43
  */
39
- export const SortRangeDialog = memo(({ open, onOpenChange, columns, customLists, initialSpecs, onApply, }) => {
44
+ export const SortRangeDialog = memo(({ open, onOpenChange, columns, customLists, initialSpecs, initialHasHeader, onApply, }) => {
40
45
  const lists = useMemo(() => customLists ?? [], [customLists]);
41
46
  const [levels, setLevels] = useState(() => initialSpecs?.length
42
47
  ? initialSpecs.map((spec) => specToLevel(spec, lists))
43
48
  : [makeDefaultLevel(columns)]);
49
+ const showHeaderToggle = initialHasHeader !== undefined ||
50
+ columns.some((column) => column.header !== undefined);
51
+ const [hasHeader, setHasHeader] = useState(initialHasHeader ?? false);
44
52
  // Re-seed when reopened with different columns / specs.
45
53
  useEffect(() => {
46
54
  if (open) {
47
55
  setLevels(initialSpecs?.length
48
56
  ? initialSpecs.map((spec) => specToLevel(spec, lists))
49
57
  : [makeDefaultLevel(columns)]);
58
+ setHasHeader(initialHasHeader ?? false);
50
59
  }
51
60
  // Only reset on open transitions.
52
61
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -98,15 +107,18 @@ export const SortRangeDialog = memo(({ open, onOpenChange, columns, customLists,
98
107
  }
99
108
  return spec;
100
109
  });
101
- onApply(specs);
110
+ onApply(specs, { hasHeader });
102
111
  onOpenChange(false);
103
- }, [levels, lists, onApply, onOpenChange]);
112
+ }, [levels, lists, hasHeader, onApply, onOpenChange]);
104
113
  return (React.createElement(Dialog, { open: open, onOpenChange: onOpenChange },
105
114
  React.createElement(DialogContent, { className: "w-[640px] max-w-[92vw] pointer-events-auto" },
106
115
  React.createElement(DialogTitle, null, "Sort"),
107
116
  React.createElement(DialogCloseButton, { onClick: () => onOpenChange(false) },
108
117
  React.createElement(Cross1Icon, null)),
109
118
  React.createElement(DialogBody, { className: "flex flex-col gap-3" },
119
+ showHeaderToggle ? (React.createElement("label", { className: "font-system text-sm flex items-center gap-1" },
120
+ React.createElement("input", { type: "checkbox", name: "hasHeader", checked: hasHeader, onChange: (e) => setHasHeader(e.target.checked) }),
121
+ "My data has headers")) : null,
110
122
  React.createElement("div", { className: "flex flex-col gap-2" }, levels.map((lvl, rowIndex) => {
111
123
  const col = columnByIndex.get(lvl.dimensionIndex);
112
124
  const cellColors = col?.availableCellColors ?? [];
@@ -124,7 +136,7 @@ export const SortRangeDialog = memo(({ open, onOpenChange, columns, customLists,
124
136
  React.createElement(SelectTrigger, { className: "w-[160px]" },
125
137
  React.createElement(SelectValue, { placeholder: "Column" })),
126
138
  React.createElement(SelectPortal, null,
127
- React.createElement(SelectContent, null, columns.map((c) => (React.createElement(SelectItem, { key: c.dimensionIndex, value: String(c.dimensionIndex) }, c.label)))))),
139
+ React.createElement(SelectContent, null, columns.map((c) => (React.createElement(SelectItem, { key: c.dimensionIndex, value: String(c.dimensionIndex) }, sortColumnLabel(c, hasHeader))))))),
128
140
  React.createElement(Select, { value: lvl.customListIndex != null && lvl.sortBy === "value"
129
141
  ? `${CUSTOM_LIST_PREFIX}${lvl.customListIndex}`
130
142
  : lvl.sortBy, onValueChange: (v) => {
@@ -210,6 +222,6 @@ export const SortRangeDialogHost = () => {
210
222
  return (React.createElement(SortRangeDialog, { open: request != null, onOpenChange: (open) => {
211
223
  if (!open)
212
224
  close();
213
- }, columns: request?.columns ?? [], customLists: request?.customLists, initialSpecs: request?.initialSpecs, onApply: (specs) => request?.onApply(specs) }));
225
+ }, columns: request?.columns ?? [], customLists: request?.customLists, initialSpecs: request?.initialSpecs, initialHasHeader: request?.initialHasHeader, onApply: (specs, options) => request?.onApply(specs, options) }));
214
226
  };
215
227
  SortRangeDialogHost.displayName = "SortRangeDialogHost";
@@ -33,6 +33,15 @@ export declare const getGalleryNamedCellStyles: (namedCellStyles: NamedCellStyle
33
33
  /** CSS preview of a named style's format for the gallery swatch. */
34
34
  export declare const getNamedCellStylePreview: (format: CellFormat | undefined, theme: SpreadsheetTheme) => React.CSSProperties;
35
35
  export declare const getCellStyles: (theme: SpreadsheetTheme, currencySymbol?: string) => Map<string, StyleList[]>;
36
+ /**
37
+ * Excel's built-in "Comma Style" / "Comma [0]" (numFmtId 43 / 41): accounting
38
+ * layout without a currency symbol — negatives in parentheses, zero as `-`,
39
+ * `_(`/`_)` padding so digits align. `-15` shows `(15.00)` in the cell's own
40
+ * colour; the `[Red]` parenthesised variants are separate built-ins (38/40)
41
+ * that the Format Cells dialog offers under Custom, not the toolbar style.
42
+ */
43
+ export declare const PATTERN_COMMA_STYLE = "_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)";
44
+ export declare const PATTERN_COMMA_STYLE_ROUNDED = "_(* #,##0_);_(* (#,##0);_(* \"-\"??_);_(@_)";
36
45
  export declare const CellStyleSelector: React.MemoExoticComponent<({ currency, locale, theme, selectedFormat, namedCellStyles, canAddCustomStyle, onChangeFormatting, onClearFormatting, }: CellStyleSelectorProps) => React.JSX.Element>;
37
46
  export {};
38
47
  //# sourceMappingURL=cell-style-selector.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cell-style-selector.d.ts","sourceRoot":"","sources":["../../../../components/toolbar/cell-style-selector.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAgC,MAAM,OAAO,CAAC;AAMrD,OAAO,KAAK,EACV,UAAU,EAEV,cAAc,EAEf,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,KAAK,cAAc,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;AAClD,KAAK,aAAa,CAAC,CAAC,SAAS,MAAM,UAAU,IAAI,CAC/C,IAAI,EAAE,CAAC,EACP,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,KACjB,IAAI,CAAC;AAEV,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,gBAAgB,CAAC;IACxB,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC;IAC3C;;;;;OAKG;IACH,eAAe,CAAC,EAAE,cAAc,EAAE,GAAG,IAAI,CAAC;IAC1C,cAAc,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACnC,qBAAqB,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,kBAAkB,EAAE,CAClB,GAAG,IAAI,EAAE,UAAU,CAAC,cAAc,GAAG,aAAa,CAAC,MAAM,UAAU,CAAC,CAAC,KAClE,IAAI,CAAC;IACV,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,iBAAiB,EAAE,MAAM,IAAI,CAAC;CAC/B,CAAC;AAEF,KAAK,SAAS,GAAG;IACf,QAAQ,EAAE,MAAM;IAChB,MAAM,EAAE,UAAU;IAClB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa;CAC5B,CAAC;AAEF,8EAA8E;AAC9E,eAAO,MAAM,yBAAyB,GACpC,iBAAiB,cAAc,EAAE,GAAG,IAAI,GAAG,SAAS,KACnD,KAAK,CAAC,cAAc,GAAG;IAAE,MAAM,EAAE,UAAU,CAAA;CAAE,CAQ7C,CAAC;AAcJ,oEAAoE;AACpE,eAAO,MAAM,wBAAwB,GACnC,QAAQ,UAAU,GAAG,SAAS,EAC9B,OAAO,gBAAgB,KACtB,KAAK,CAAC,aAqBR,CAAC;AAEF,eAAO,MAAM,aAAa,GACxB,OAAO,gBAAgB,EACvB,iBAAiB,MAAM,6BAgYxB,CAAC;AAEF,eAAO,MAAM,iBAAiB,sJAUzB,sBAAsB,uBAsO1B,CAAC"}
1
+ {"version":3,"file":"cell-style-selector.d.ts","sourceRoot":"","sources":["../../../../components/toolbar/cell-style-selector.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAgC,MAAM,OAAO,CAAC;AAMrD,OAAO,KAAK,EACV,UAAU,EAEV,cAAc,EAEf,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,KAAK,cAAc,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;AAClD,KAAK,aAAa,CAAC,CAAC,SAAS,MAAM,UAAU,IAAI,CAC/C,IAAI,EAAE,CAAC,EACP,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,KACjB,IAAI,CAAC;AAEV,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,gBAAgB,CAAC;IACxB,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC;IAC3C;;;;;OAKG;IACH,eAAe,CAAC,EAAE,cAAc,EAAE,GAAG,IAAI,CAAC;IAC1C,cAAc,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACnC,qBAAqB,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,kBAAkB,EAAE,CAClB,GAAG,IAAI,EAAE,UAAU,CAAC,cAAc,GAAG,aAAa,CAAC,MAAM,UAAU,CAAC,CAAC,KAClE,IAAI,CAAC;IACV,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,iBAAiB,EAAE,MAAM,IAAI,CAAC;CAC/B,CAAC;AAEF,KAAK,SAAS,GAAG;IACf,QAAQ,EAAE,MAAM;IAChB,MAAM,EAAE,UAAU;IAClB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa;CAC5B,CAAC;AAEF,8EAA8E;AAC9E,eAAO,MAAM,yBAAyB,GACpC,iBAAiB,cAAc,EAAE,GAAG,IAAI,GAAG,SAAS,KACnD,KAAK,CAAC,cAAc,GAAG;IAAE,MAAM,EAAE,UAAU,CAAA;CAAE,CAQ7C,CAAC;AAcJ,oEAAoE;AACpE,eAAO,MAAM,wBAAwB,GACnC,QAAQ,UAAU,GAAG,SAAS,EAC9B,OAAO,gBAAgB,KACtB,KAAK,CAAC,aAqBR,CAAC;AAEF,eAAO,MAAM,aAAa,GACxB,OAAO,gBAAgB,EACvB,iBAAiB,MAAM,6BAgYxB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,sDAAoD,CAAC;AACrF,eAAO,MAAM,2BAA2B,gDAA8C,CAAC;AAEvF,eAAO,MAAM,iBAAiB,sJAUzB,sBAAsB,uBAsO1B,CAAC"}
@@ -422,6 +422,15 @@ export const getCellStyles = (theme, currencySymbol) => {
422
422
  ],
423
423
  ]);
424
424
  };
425
+ /**
426
+ * Excel's built-in "Comma Style" / "Comma [0]" (numFmtId 43 / 41): accounting
427
+ * layout without a currency symbol — negatives in parentheses, zero as `-`,
428
+ * `_(`/`_)` padding so digits align. `-15` shows `(15.00)` in the cell's own
429
+ * colour; the `[Red]` parenthesised variants are separate built-ins (38/40)
430
+ * that the Format Cells dialog offers under Custom, not the toolbar style.
431
+ */
432
+ export const PATTERN_COMMA_STYLE = '_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(@_)';
433
+ export const PATTERN_COMMA_STYLE_ROUNDED = '_(* #,##0_);_(* (#,##0);_(* "-"??_);_(@_)';
425
434
  export const CellStyleSelector = memo(({ currency, locale, theme, selectedFormat, namedCellStyles, canAddCustomStyle = false, onChangeFormatting, onClearFormatting, }) => {
426
435
  const colorIndexes = [4, 5, 6, 7, 8, 9];
427
436
  const colorTints = [0.8, 0.6, 0.4, 0];
@@ -436,14 +445,14 @@ export const CellStyleSelector = memo(({ currency, locale, theme, selectedFormat
436
445
  "Comma",
437
446
  {
438
447
  type: "NUMBER",
439
- pattern: "#,##0.00",
448
+ pattern: PATTERN_COMMA_STYLE,
440
449
  },
441
450
  ],
442
451
  [
443
452
  "Comma[0]",
444
453
  {
445
454
  type: "NUMBER",
446
- pattern: "#,##0",
455
+ pattern: PATTERN_COMMA_STYLE_ROUNDED,
447
456
  },
448
457
  ],
449
458
  [
@@ -1,5 +1,6 @@
1
1
  import type { CellInterface, GridRange, SelectionArea } from "@rowsncolumns/grid";
2
2
  import { Direction } from "@rowsncolumns/grid";
3
+ import type { Maybe } from "@rowsncolumns/utils";
3
4
  import React from "react";
4
5
  import type { TableView } from "../../types";
5
6
  import type { SelectionAttributes } from "../canvas-grid/typings";
@@ -12,7 +13,32 @@ type MergeCellsSelectorProps = {
12
13
  merges?: GridRange[] | null;
13
14
  /** Tables of the workbook — a selection touching one on `sheetId` cannot be merged (Excel). */
14
15
  tables?: TableView[] | null;
16
+ /**
17
+ * Cell value reader behind Excel's "Merging cells only keeps the upper-left value" warning.
18
+ * Defaults to the reader the grid publishes; the warning is skipped when neither is available.
19
+ */
20
+ getFormattedValue?: (sheetId: number, rowIndex: number, columnIndex: number) => Maybe<string>;
15
21
  };
16
- export declare const MergeCellsSelector: ({ activeCell, selections, sheetId, merges, tables, onMerge, onUnMerge, }: MergeCellsSelectorProps) => React.JSX.Element;
22
+ /** Excel's wording for a merge that would drop cell values. */
23
+ export declare const MERGE_DISCARDS_VALUES_TITLE = "Merge cells";
24
+ export declare const MERGE_DISCARDS_VALUES_MESSAGE = "Merging cells only keeps the upper-left value and discards other values.";
25
+ /**
26
+ * The regions `onMergeCells` merges for a toolbar request: each row (merge
27
+ * horizontally), each column (merge vertically), every area of a multi-area
28
+ * selection, or the last selection as one block — the store action's own rules.
29
+ */
30
+ export declare const mergeRegionsForSelection: (selections: SelectionArea[], direction?: Direction) => GridRange[];
31
+ /**
32
+ * Would this merge discard values? Excel warns when more than one cell of a
33
+ * merged block holds a value (the merge keeps only the upper-left one). A
34
+ * headless / programmatic merge never asks — this gate lives in the toolbar.
35
+ */
36
+ export declare const mergeDiscardsValues: ({ sheetId, selections, direction, getFormattedValue, }: {
37
+ sheetId: number;
38
+ selections: SelectionArea[];
39
+ direction?: Direction;
40
+ getFormattedValue: (sheetId: number, rowIndex: number, columnIndex: number) => Maybe<string>;
41
+ }) => boolean;
42
+ export declare const MergeCellsSelector: ({ activeCell, selections, sheetId, merges, tables, onMerge, onUnMerge, getFormattedValue: getFormattedValueProp, }: MergeCellsSelectorProps) => React.JSX.Element;
17
43
  export {};
18
44
  //# sourceMappingURL=merge-cells-selector.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"merge-cells-selector.d.ts","sourceRoot":"","sources":["../../../../components/toolbar/merge-cells-selector.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,SAAS,EACT,aAAa,EACd,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAkB,SAAS,EAAgB,MAAM,oBAAoB,CAAC;AAY7E,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAElE,KAAK,uBAAuB,GAAG;IAC7B,UAAU,EAAE,aAAa,CAAC;IAC1B,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CACL,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,aAAa,EACzB,UAAU,EAAE,aAAa,CAAC,mBAAmB,CAAC,EAAE,EAChD,SAAS,CAAC,EAAE,SAAS,GACpB,IAAI,CAAC;IACR,SAAS,CACP,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,aAAa,EACzB,UAAU,EAAE,aAAa,CAAC,mBAAmB,CAAC,EAAE,GAC/C,IAAI,CAAC;IACR,MAAM,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAC5B,+FAA+F;IAC/F,MAAM,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;CAC7B,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,0EAQhC,uBAAuB,sBAgGzB,CAAC"}
1
+ {"version":3,"file":"merge-cells-selector.d.ts","sourceRoot":"","sources":["../../../../components/toolbar/merge-cells-selector.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,SAAS,EACT,aAAa,EACd,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAkB,SAAS,EAAgB,MAAM,oBAAoB,CAAC;AAyB7E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,KAAmB,MAAM,OAAO,CAAC;AAIxC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAElE,KAAK,uBAAuB,GAAG;IAC7B,UAAU,EAAE,aAAa,CAAC;IAC1B,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CACL,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,aAAa,EACzB,UAAU,EAAE,aAAa,CAAC,mBAAmB,CAAC,EAAE,EAChD,SAAS,CAAC,EAAE,SAAS,GACpB,IAAI,CAAC;IACR,SAAS,CACP,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,aAAa,EACzB,UAAU,EAAE,aAAa,CAAC,mBAAmB,CAAC,EAAE,GAC/C,IAAI,CAAC;IACR,MAAM,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAC5B,+FAA+F;IAC/F,MAAM,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAC5B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,CAClB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,KAChB,KAAK,CAAC,MAAM,CAAC,CAAC;CACpB,CAAC;AAEF,+DAA+D;AAC/D,eAAO,MAAM,2BAA2B,gBAAgB,CAAC;AACzD,eAAO,MAAM,6BAA6B,6EACkC,CAAC;AAE7E;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GACnC,YAAY,aAAa,EAAE,EAC3B,YAAY,SAAS,KACpB,SAAS,EAmBX,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAAI,wDAKjC;IACD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,iBAAiB,EAAE,CACjB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,KAChB,KAAK,CAAC,MAAM,CAAC,CAAC;CACpB,KAAG,OAcH,CAAC;AAIF,eAAO,MAAM,kBAAkB,GAAI,oHAShC,uBAAuB,sBA8IzB,CAAC"}