@rowsncolumns/spreadsheet 10.6.0 → 10.6.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 (182) hide show
  1. package/dist/cjs/api/cf-data-bar.d.ts +73 -0
  2. package/dist/cjs/api/cf-data-bar.d.ts.map +1 -0
  3. package/dist/cjs/api/cf-data-bar.js +182 -0
  4. package/dist/cjs/api/cf-icons.d.ts +73 -0
  5. package/dist/cjs/api/cf-icons.d.ts.map +1 -0
  6. package/dist/cjs/api/cf-icons.js +322 -0
  7. package/dist/cjs/api/cf-overlays.d.ts +89 -3
  8. package/dist/cjs/api/cf-overlays.d.ts.map +1 -1
  9. package/dist/cjs/api/cf-overlays.js +256 -80
  10. package/dist/cjs/api/cf-priority.d.ts +34 -0
  11. package/dist/cjs/api/cf-priority.d.ts.map +1 -0
  12. package/dist/cjs/api/cf-priority.js +94 -0
  13. package/dist/cjs/api/heatmap.d.ts +7 -1
  14. package/dist/cjs/api/heatmap.d.ts.map +1 -1
  15. package/dist/cjs/api/heatmap.js +27 -11
  16. package/dist/cjs/api/selections.d.ts +2 -2
  17. package/dist/cjs/api/validation.d.ts +6 -0
  18. package/dist/cjs/api/validation.d.ts.map +1 -1
  19. package/dist/cjs/api/validation.js +10 -1
  20. package/dist/cjs/components/canvas-grid/index.d.ts.map +1 -1
  21. package/dist/cjs/components/canvas-grid/index.js +194 -51
  22. package/dist/cjs/components/canvas-grid/overflow.d.ts +37 -0
  23. package/dist/cjs/components/canvas-grid/overflow.d.ts.map +1 -0
  24. package/dist/cjs/components/canvas-grid/overflow.js +49 -0
  25. package/dist/cjs/components/canvas-grid/page-breaks.d.ts +15 -0
  26. package/dist/cjs/components/canvas-grid/page-breaks.d.ts.map +1 -0
  27. package/dist/cjs/components/canvas-grid/page-breaks.js +77 -0
  28. package/dist/cjs/components/canvas-grid/typings.d.ts +25 -0
  29. package/dist/cjs/components/canvas-grid/typings.d.ts.map +1 -1
  30. package/dist/cjs/components/cell/alignment.d.ts +58 -1
  31. package/dist/cjs/components/cell/alignment.d.ts.map +1 -1
  32. package/dist/cjs/components/cell/alignment.js +144 -1
  33. package/dist/cjs/components/cell/cell-background.d.ts +2 -0
  34. package/dist/cjs/components/cell/cell-background.d.ts.map +1 -1
  35. package/dist/cjs/components/cell/cell-background.js +5 -6
  36. package/dist/cjs/components/cell/cell-backgrounds.d.ts.map +1 -1
  37. package/dist/cjs/components/cell/cell-backgrounds.js +5 -1
  38. package/dist/cjs/components/cell/cell-text-runs.js +1 -1
  39. package/dist/cjs/components/cell/index.d.ts.map +1 -1
  40. package/dist/cjs/components/cell/index.js +378 -37
  41. package/dist/cjs/components/cell/number-format-layout.d.ts +35 -0
  42. package/dist/cjs/components/cell/number-format-layout.d.ts.map +1 -0
  43. package/dist/cjs/components/cell/number-format-layout.js +106 -0
  44. package/dist/cjs/components/cell/typings.d.ts +22 -0
  45. package/dist/cjs/components/cell/typings.d.ts.map +1 -1
  46. package/dist/cjs/components/cell-editor/index.js +2 -2
  47. package/dist/cjs/components/cell-filter/index.d.ts.map +1 -1
  48. package/dist/cjs/components/cell-filter/index.js +3 -3
  49. package/dist/cjs/components/cell-overlay/index.d.ts +20 -0
  50. package/dist/cjs/components/cell-overlay/index.d.ts.map +1 -1
  51. package/dist/cjs/components/cell-overlay/index.js +155 -28
  52. package/dist/cjs/components/cf-icon-preview/index.d.ts +16 -0
  53. package/dist/cjs/components/cf-icon-preview/index.d.ts.map +1 -0
  54. package/dist/cjs/components/cf-icon-preview/index.js +48 -0
  55. package/dist/cjs/components/embeds/embed-component.d.ts.map +1 -1
  56. package/dist/cjs/components/embeds/embed-component.js +3 -2
  57. package/dist/cjs/components/hidden-markers/index.d.ts.map +1 -1
  58. package/dist/cjs/components/hidden-markers/index.js +8 -7
  59. package/dist/cjs/components/hidden-markers/style.d.ts +7 -0
  60. package/dist/cjs/components/hidden-markers/style.d.ts.map +1 -1
  61. package/dist/cjs/components/hidden-markers/style.js +13 -2
  62. package/dist/cjs/components/protect-sheet-dialog/index.d.ts +67 -0
  63. package/dist/cjs/components/protect-sheet-dialog/index.d.ts.map +1 -0
  64. package/dist/cjs/components/protect-sheet-dialog/index.js +196 -0
  65. package/dist/cjs/components/toolbar/types.d.ts +1 -1
  66. package/dist/cjs/components/toolbar/types.d.ts.map +1 -1
  67. package/dist/cjs/config.d.ts +13 -1
  68. package/dist/cjs/config.d.ts.map +1 -1
  69. package/dist/cjs/config.js +15 -3
  70. package/dist/cjs/helpers.d.ts +10 -5
  71. package/dist/cjs/helpers.d.ts.map +1 -1
  72. package/dist/cjs/helpers.js +43 -31
  73. package/dist/cjs/hooks/use-border-styles.d.ts.map +1 -1
  74. package/dist/cjs/hooks/use-border-styles.js +5 -28
  75. package/dist/cjs/hooks/use-cell-renderer.d.ts +35 -3
  76. package/dist/cjs/hooks/use-cell-renderer.d.ts.map +1 -1
  77. package/dist/cjs/hooks/use-cell-renderer.js +85 -47
  78. package/dist/cjs/hooks/use-conditional-formatting.d.ts +13 -1
  79. package/dist/cjs/hooks/use-conditional-formatting.d.ts.map +1 -1
  80. package/dist/cjs/hooks/use-conditional-formatting.js +281 -230
  81. package/dist/cjs/index.d.ts +5 -0
  82. package/dist/cjs/index.d.ts.map +1 -1
  83. package/dist/cjs/index.js +5 -0
  84. package/dist/cjs/release-date.js +1 -1
  85. package/dist/cjs/themes.d.ts +8 -0
  86. package/dist/cjs/themes.d.ts.map +1 -1
  87. package/dist/cjs/themes.js +21 -3
  88. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  89. package/dist/cjs/types.d.ts +81 -4
  90. package/dist/cjs/types.d.ts.map +1 -1
  91. package/dist/esm/api/cf-data-bar.d.ts +73 -0
  92. package/dist/esm/api/cf-data-bar.d.ts.map +1 -0
  93. package/dist/esm/api/cf-data-bar.js +178 -0
  94. package/dist/esm/api/cf-icons.d.ts +73 -0
  95. package/dist/esm/api/cf-icons.d.ts.map +1 -0
  96. package/dist/esm/api/cf-icons.js +306 -0
  97. package/dist/esm/api/cf-overlays.d.ts +89 -3
  98. package/dist/esm/api/cf-overlays.d.ts.map +1 -1
  99. package/dist/esm/api/cf-overlays.js +223 -67
  100. package/dist/esm/api/cf-priority.d.ts +34 -0
  101. package/dist/esm/api/cf-priority.d.ts.map +1 -0
  102. package/dist/esm/api/cf-priority.js +61 -0
  103. package/dist/esm/api/heatmap.d.ts +7 -1
  104. package/dist/esm/api/heatmap.d.ts.map +1 -1
  105. package/dist/esm/api/heatmap.js +24 -9
  106. package/dist/esm/api/selections.d.ts +2 -2
  107. package/dist/esm/api/validation.d.ts +6 -0
  108. package/dist/esm/api/validation.d.ts.map +1 -1
  109. package/dist/esm/api/validation.js +6 -0
  110. package/dist/esm/components/canvas-grid/index.d.ts.map +1 -1
  111. package/dist/esm/components/canvas-grid/index.js +163 -47
  112. package/dist/esm/components/canvas-grid/overflow.d.ts +37 -0
  113. package/dist/esm/components/canvas-grid/overflow.d.ts.map +1 -0
  114. package/dist/esm/components/canvas-grid/overflow.js +39 -0
  115. package/dist/esm/components/canvas-grid/page-breaks.d.ts +15 -0
  116. package/dist/esm/components/canvas-grid/page-breaks.d.ts.map +1 -0
  117. package/dist/esm/components/canvas-grid/page-breaks.js +50 -0
  118. package/dist/esm/components/canvas-grid/typings.d.ts +25 -0
  119. package/dist/esm/components/canvas-grid/typings.d.ts.map +1 -1
  120. package/dist/esm/components/cell/alignment.d.ts +58 -1
  121. package/dist/esm/components/cell/alignment.d.ts.map +1 -1
  122. package/dist/esm/components/cell/alignment.js +110 -1
  123. package/dist/esm/components/cell/cell-background.d.ts +2 -0
  124. package/dist/esm/components/cell/cell-background.d.ts.map +1 -1
  125. package/dist/esm/components/cell/cell-background.js +4 -5
  126. package/dist/esm/components/cell/cell-backgrounds.d.ts.map +1 -1
  127. package/dist/esm/components/cell/cell-backgrounds.js +5 -1
  128. package/dist/esm/components/cell/cell-text-runs.js +2 -2
  129. package/dist/esm/components/cell/index.d.ts.map +1 -1
  130. package/dist/esm/components/cell/index.js +339 -39
  131. package/dist/esm/components/cell/number-format-layout.d.ts +35 -0
  132. package/dist/esm/components/cell/number-format-layout.d.ts.map +1 -0
  133. package/dist/esm/components/cell/number-format-layout.js +99 -0
  134. package/dist/esm/components/cell/typings.d.ts +22 -0
  135. package/dist/esm/components/cell/typings.d.ts.map +1 -1
  136. package/dist/esm/components/cell-editor/index.js +2 -2
  137. package/dist/esm/components/cell-filter/index.d.ts.map +1 -1
  138. package/dist/esm/components/cell-filter/index.js +4 -4
  139. package/dist/esm/components/cell-overlay/index.d.ts +20 -0
  140. package/dist/esm/components/cell-overlay/index.d.ts.map +1 -1
  141. package/dist/esm/components/cell-overlay/index.js +111 -25
  142. package/dist/esm/components/cf-icon-preview/index.d.ts +16 -0
  143. package/dist/esm/components/cf-icon-preview/index.d.ts.map +1 -0
  144. package/dist/esm/components/cf-icon-preview/index.js +41 -0
  145. package/dist/esm/components/embeds/embed-component.d.ts.map +1 -1
  146. package/dist/esm/components/embeds/embed-component.js +4 -3
  147. package/dist/esm/components/hidden-markers/index.d.ts.map +1 -1
  148. package/dist/esm/components/hidden-markers/index.js +9 -8
  149. package/dist/esm/components/hidden-markers/style.d.ts +7 -0
  150. package/dist/esm/components/hidden-markers/style.d.ts.map +1 -1
  151. package/dist/esm/components/hidden-markers/style.js +8 -1
  152. package/dist/esm/components/protect-sheet-dialog/index.d.ts +67 -0
  153. package/dist/esm/components/protect-sheet-dialog/index.d.ts.map +1 -0
  154. package/dist/esm/components/protect-sheet-dialog/index.js +154 -0
  155. package/dist/esm/components/toolbar/types.d.ts +1 -1
  156. package/dist/esm/components/toolbar/types.d.ts.map +1 -1
  157. package/dist/esm/config.d.ts +13 -1
  158. package/dist/esm/config.d.ts.map +1 -1
  159. package/dist/esm/config.js +13 -1
  160. package/dist/esm/helpers.d.ts +10 -5
  161. package/dist/esm/helpers.d.ts.map +1 -1
  162. package/dist/esm/helpers.js +40 -28
  163. package/dist/esm/hooks/use-border-styles.d.ts.map +1 -1
  164. package/dist/esm/hooks/use-border-styles.js +5 -26
  165. package/dist/esm/hooks/use-cell-renderer.d.ts +35 -3
  166. package/dist/esm/hooks/use-cell-renderer.d.ts.map +1 -1
  167. package/dist/esm/hooks/use-cell-renderer.js +83 -42
  168. package/dist/esm/hooks/use-conditional-formatting.d.ts +13 -1
  169. package/dist/esm/hooks/use-conditional-formatting.d.ts.map +1 -1
  170. package/dist/esm/hooks/use-conditional-formatting.js +215 -120
  171. package/dist/esm/index.d.ts +5 -0
  172. package/dist/esm/index.d.ts.map +1 -1
  173. package/dist/esm/index.js +5 -0
  174. package/dist/esm/release-date.js +1 -1
  175. package/dist/esm/themes.d.ts +8 -0
  176. package/dist/esm/themes.d.ts.map +1 -1
  177. package/dist/esm/themes.js +20 -3
  178. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  179. package/dist/esm/types.d.ts +81 -4
  180. package/dist/esm/types.d.ts.map +1 -1
  181. package/dist/spreadsheet.min.css +1 -1
  182. package/package.json +4 -4
@@ -1,10 +1,12 @@
1
1
  import { CELL_TEXT_PADDING, getTextNodeBounds, Image, } from "@rowsncolumns/grid";
2
2
  import { getContrastColor, isNil } from "@rowsncolumns/utils";
3
3
  import React, { memo, useCallback } from "react";
4
- import { Group, Label, Rect, Shape, Tag, Text } from "react-konva";
5
- import { CELL_HORIZONTAL_ALIGNMENT, CELL_VERTICAL_ALIGNMENT, CF_ICON_SPACING, CF_ICON_WIDTH, CHIP_HEIGHT, CHIP_RADIUS, CHIP_SPACING, COMMENT_COLOR, DEFAULT_CELL_SPACING, ERROR_COLOR, INDENT_SPACING, INVALID_COLOR, NOTE_COLOR, } from "../../config";
4
+ import { Group, Label, Shape, Tag, Text } from "react-konva";
5
+ import { paintDataBar } from "../../api/cf-data-bar";
6
+ import { getCfIcon, paintCfIcon } from "../../api/cf-icons";
7
+ import { ARROW_DOWN_WIDTH, CELL_HORIZONTAL_ALIGNMENT, CELL_VERTICAL_ALIGNMENT, CF_ICON_SPACING, CF_ICON_WIDTH, CHIP_HEIGHT, CHIP_RADIUS, CHIP_SPACING, COMMENT_COLOR, DEFAULT_CELL_SPACING, ERROR_COLOR, INDENT_SPACING, INVALID_COLOR, NOTE_COLOR, } from "../../config";
6
8
  import { getChipVerticalPosition, normalizeDisplayText, pointToPixel, } from "../../helpers";
7
- import { getStringifiedColor } from "../../themes";
9
+ import { getStringifiedColor, resolveThemeFontFamily } from "../../themes";
8
10
  import { CellArrow } from "../cell-arrow";
9
11
  import { CellCalculated } from "../cell-calculated";
10
12
  import { CellCheckbox } from "../cell-checkbox";
@@ -12,18 +14,17 @@ import { CellError, ERROR_TAG_WIDTH } from "../cell-error";
12
14
  import { CellExpand } from "../cell-expand";
13
15
  import { CellFilter } from "../cell-filter";
14
16
  import { CellGroup } from "../cell-group";
17
+ import { alignmentImpliesWrap, computeShrinkToFitFontSize, fillCellText, getKonvaRotationDegrees, getRotatedTextLayout, getUnderlineStrokes, getVerticalSpreadLineHeight, layoutDistributedLines, resolveCanvasTextAlign, resolveCanvasVerticalAlign, resolveReadingOrderAlignment, stackTextVertically, verticalAlignmentImpliesWrap, } from "./alignment";
15
18
  import { CellTextRuns } from "./cell-text-runs";
19
+ import { getLeftSpacing, getRightSpacing } from "./helpers";
16
20
  import { getLineMetrics } from "./line-height";
21
+ import { layoutNumberFormatParts } from "./number-format-layout";
17
22
  import { hasRenderableRuns } from "./rich-text";
18
- import { alignmentImpliesWrap, computeShrinkToFitFontSize, fillCellText, getKonvaRotationDegrees, getRotatedTextLayout, resolveCanvasTextAlign, stackTextVertically, } from "./alignment";
19
- import { getLeftSpacing, getRightSpacing } from "./helpers";
20
23
  const LOADING_VALUE = "Loading...";
21
24
  // CSS-equivalent scale for sup/sub baseline offset (matches browser default).
22
25
  const BASELINE_OFFSET_FONT_SCALE = 0.58;
23
- // Vertical inset of data-bar rect from cell edges (top/bottom).
24
- const CF_DATABAR_INSET_Y = 2;
25
26
  export const Cell = memo((props) => {
26
- const { x, y, width, height, value, padding = CELL_TEXT_PADDING, scale = 1, lineHeight = 1.2, lineHeightMode = "fontMetrics", imageUrl, cellFormat, chipBackgroundColor, defaultColor, defaultBackgroundColor, sheetId, rowIndex, columnIndex, onRequestEdit, onArrowClick, isChecked, isInvalid, isError, showCheckbox, showArrow, showFilter, showChip, showNote, showComment, isFilterActive, filterIconColor, onFilterClick, onCheckboxClick, enableOverflow, cellWidth, textWidth: _textWidth, theme, showCalculatedColumnIcon, isCalculatedColumn, expandable, expanded, columnSpans, overflowRight, showGridLines, structuredValue, sparklineGroupAxis, defaultFontSizePoint, sortOrder, textFormatRuns, StructuredResult, readonly = false, onExpandCollapse, getDefaultHorizontalAlignment, } = props;
27
+ const { x, y, width, height, value, padding = CELL_TEXT_PADDING, scale = 1, lineHeight = 1.2, lineHeightMode = "fontMetrics", imageUrl, cellFormat, chipBackgroundColor, defaultColor, defaultBackgroundColor, sheetId, rowIndex, columnIndex, onRequestEdit, onArrowClick, isChecked, isInvalid, isError, showCheckbox, showArrow, showFilter, showChip, showNote, showComment, isFilterActive, filterIconColor, onFilterClick, onCheckboxClick, enableOverflow, cellWidth, textWidth: _textWidth, theme, showCalculatedColumnIcon, isCalculatedColumn, expandable, expanded, columnSpans, overflowRight, overflowLeft, centerOnSpan = false, arrowOutside = false, showGridLines, structuredValue, sparklineGroupAxis, defaultFontSizePoint, sortOrder, textFormatRuns, numberFormatLayout, StructuredResult, readonly = false, onExpandCollapse, getDefaultHorizontalAlignment, } = props;
27
28
  const { textFormat, wrapStrategy } = cellFormat ?? {};
28
29
  // openpyxl's `relativeIndent` is an additive delta on top of `indent`.
29
30
  const indent = (cellFormat?.indent ?? 0) + (cellFormat?.relativeIndent ?? 0);
@@ -33,25 +34,37 @@ export const Cell = memo((props) => {
33
34
  // we cross-package-duplicate the sentinel so apps/spreadsheet doesn't
34
35
  // depend on spreadsheet-state.
35
36
  const isLoading = value === LOADING_VALUE;
37
+ // Format Cells → Alignment → Text direction: a right-to-left reading
38
+ // order starts General-aligned text at the right edge.
39
+ const readingOrder = cellFormat?.readingOrder;
36
40
  const horizontalAlignment = isLoading
37
41
  ? "center"
38
42
  : cellFormat?.horizontalAlignment ??
39
- (getDefaultHorizontalAlignment(sheetId, rowIndex, columnIndex) ||
43
+ (resolveReadingOrderAlignment(readingOrder, getDefaultHorizontalAlignment(sheetId, rowIndex, columnIndex)) ||
40
44
  CELL_HORIZONTAL_ALIGNMENT);
41
- const verticalAlignment = cellFormat?.verticalAlignment || CELL_VERTICAL_ALIGNMENT;
45
+ // Excel's vertical justify / distributed paint as top / middle for one
46
+ // line and spread wrapped lines over the cell height (below).
47
+ const cellVerticalAlignment = cellFormat?.verticalAlignment || CELL_VERTICAL_ALIGNMENT;
48
+ const verticalAlignment = resolveCanvasVerticalAlign(cellVerticalAlignment);
49
+ const isVerticalSpread = verticalAlignmentImpliesWrap(cellVerticalAlignment);
42
50
  // Excel-only modes (justify / distributed / fill / centerContinuous)
43
51
  // collapse onto what the canvas text node can paint.
44
52
  const canvasTextAlign = resolveCanvasTextAlign(horizontalAlignment);
45
53
  const isFillAlignment = horizontalAlignment === "fill";
46
54
  // Text format
47
- const { color: colorObject, bold, italic, strikethrough, underline, baselineOffset, } = textFormat ?? {};
55
+ const { color: colorObject, bold, italic, strikethrough, underline, underlineStyle, baselineOffset, } = textFormat ?? {};
48
56
  const fontSizePt = textFormat?.fontSize ?? defaultFontSizePoint;
49
57
  // Fall back to the theme's body font so the painter agrees with the
50
58
  // sizer (AutoSizerCanvas measures with theme.primaryFontFamily).
51
- const fontFamily = textFormat?.fontFamily ?? theme?.primaryFontFamily;
59
+ const fontFamily = resolveThemeFontFamily(textFormat?.fontFamily, theme) ??
60
+ theme?.primaryFontFamily;
52
61
  const color = getStringifiedColor(colorObject, theme);
53
- // Text decoration
54
- const textDecoration = `${underline ? "underline" : ""} ${strikethrough ? "line-through" : ""}`;
62
+ // Text decoration. Konva draws only a single underline; the double and
63
+ // accounting underlines (Format Cells → Font → Underline) are stroked by
64
+ // `underlineSceneFunc` below, where the text is one unrotated line.
65
+ const hasStyledUnderline = !!underline && !!underlineStyle && underlineStyle !== "single";
66
+ const textDecorationFor = (konvaUnderline) => `${konvaUnderline ? "underline" : ""} ${strikethrough ? "line-through" : ""}`;
67
+ const textDecoration = textDecorationFor(!!underline);
55
68
  /* Because of 1px + 0.5px (gridline width + spacing )*/
56
69
  const borderWidth = 1;
57
70
  const cellSpacingY = borderWidth;
@@ -68,9 +81,14 @@ export const Cell = memo((props) => {
68
81
  const showText = !isNil(structuredValue) ? false : !isNil(value);
69
82
  // Overflow line
70
83
  const overflowFillFunc = useCallback((context, shape) => {
71
- for (const [_, columnOffset, columnWidth, cellBackground,] of columnSpans) {
84
+ for (const [spanColumnIndex, columnOffset, columnWidth, cellBackground,] of columnSpans) {
72
85
  context.beginPath();
73
- const x = columnOffset - shape.x() + (overflowRight ? columnWidth : 0) + 0.5;
86
+ // A column left of the cell hides the gridline on its right edge,
87
+ // one to the right the gridline on its left edge.
88
+ const x = columnOffset -
89
+ shape.x() +
90
+ (spanColumnIndex < columnIndex ? columnWidth : 0) +
91
+ 0.5;
74
92
  const strokeColor = cellBackground
75
93
  ? getStringifiedColor(cellBackground, theme)
76
94
  : defaultBackgroundColor;
@@ -81,7 +99,7 @@ export const Cell = memo((props) => {
81
99
  context.stroke();
82
100
  context.closePath();
83
101
  }
84
- }, [defaultBackgroundColor, columnSpans, theme, overflowRight, scale]);
102
+ }, [defaultBackgroundColor, columnSpans, theme, columnIndex, scale]);
85
103
  // Show error tag
86
104
  const showError = isError || isInvalid;
87
105
  // Calculated
@@ -89,13 +107,26 @@ export const Cell = memo((props) => {
89
107
  // Alignment
90
108
  const isRightAligned = horizontalAlignment === "right";
91
109
  // Spacing
92
- const indentSpacing = indent ? indent * INDENT_SPACING : 0;
110
+ const indentSpacing = indent ? indent * INDENT_SPACING * scale : 0;
93
111
  const iconSpacing = DEFAULT_CELL_SPACING;
94
112
  const showExpand = false;
95
113
  const leftSpacing = getLeftSpacing(isCalculatedCell, expandable, isRightAligned ? showExpand : false, iconSpacing, scale);
96
- const rightSpacing = getRightSpacing(showFilter, showArrow, isRightAligned ? false : showExpand, iconSpacing, scale);
114
+ const rightSpacing = getRightSpacing(showFilter, showArrow && !arrowOutside, isRightAligned ? false : showExpand, iconSpacing, scale);
97
115
  // final cell width
98
116
  const finalCellWidth = cellWidth ?? 0;
117
+ // Centred overflow: the spill area is `[x - overflowLeft, x - overflowLeft
118
+ // + cellWidth]`. Centred text stays centred on its own cell (a blocked
119
+ // side clips it); Center Across Selection centres on the whole span.
120
+ const centredSpill = enableOverflow && overflowLeft !== undefined
121
+ ? (() => {
122
+ const spanLeft = x - overflowLeft;
123
+ if (centerOnSpan) {
124
+ return { spanLeft, boxLeft: spanLeft, boxWidth: finalCellWidth };
125
+ }
126
+ const reach = Math.max(overflowLeft, finalCellWidth - width - overflowLeft);
127
+ return { spanLeft, boxLeft: x - reach, boxWidth: width + 2 * reach };
128
+ })()
129
+ : undefined;
99
130
  // text
100
131
  const textWidth = _textWidth + indentSpacing;
101
132
  // Reserve room for the CF icon on the left side of the cell. Only adds
@@ -107,7 +138,11 @@ export const Cell = memo((props) => {
107
138
  // doesn't render past the cell's right edge when an iconSet is present.
108
139
  const finalTextWidth = (overflowRight
109
140
  ? Math.max(textWidth, width)
110
- : (enableOverflow ? finalCellWidth : width) -
141
+ : (centredSpill
142
+ ? centredSpill.boxWidth
143
+ : enableOverflow
144
+ ? finalCellWidth
145
+ : width) -
111
146
  rightSpacing -
112
147
  leftSpacing -
113
148
  indentSpacing) - cfIconReserve;
@@ -116,8 +151,10 @@ export const Cell = memo((props) => {
116
151
  ? textWidth > width
117
152
  ? x - (textWidth - width)
118
153
  : x
119
- : x +
120
- (isRightAligned ? -indentSpacing - Math.floor((padding * scale) / 2) : 0);
154
+ : centredSpill
155
+ ? centredSpill.boxLeft
156
+ : x +
157
+ (isRightAligned ? -indentSpacing - Math.floor((padding * scale) / 2) : 0);
121
158
  // Text left padding
122
159
  const textLeftPadding = textX +
123
160
  leftSpacing +
@@ -131,7 +168,9 @@ export const Cell = memo((props) => {
131
168
  ? enableOverflow
132
169
  ? textX + (textWidth - finalCellWidth)
133
170
  : textX
134
- : textX;
171
+ : centredSpill
172
+ ? centredSpill.spanLeft
173
+ : textX;
135
174
  const overflowWidth = finalCellWidth;
136
175
  // Clip
137
176
  const clipX = (enableOverflow ? overflowX : x) +
@@ -158,7 +197,9 @@ export const Cell = memo((props) => {
158
197
  : fontSize;
159
198
  // Wrap — disabled in stacked, shrink, or rotated modes. Justify /
160
199
  // distributed alignment wraps implicitly, as in Excel.
161
- const wrapRequested = wrapStrategy === "wrap" || alignmentImpliesWrap(horizontalAlignment);
200
+ const wrapRequested = wrapStrategy === "wrap" ||
201
+ alignmentImpliesWrap(horizontalAlignment) ||
202
+ isVerticalSpread;
162
203
  const textWrapStrategy = isStackedVertical || shrinkEnabled || hasNumericRotation
163
204
  ? "none"
164
205
  : wrapRequested
@@ -178,7 +219,7 @@ export const Cell = memo((props) => {
178
219
  // the same numbers (#1141). Konva's `lineHeight` is a multiple of the
179
220
  // font size.
180
221
  const textLineMetrics = getLineMetrics({ fontFamily, fontSize: effectiveFontSize, bold, italic }, lineHeightMode, lineHeight, scale);
181
- const textLineHeight = effectiveFontSize > 0
222
+ const baseTextLineHeight = effectiveFontSize > 0
182
223
  ? textLineMetrics.advance / effectiveFontSize
183
224
  : lineHeight;
184
225
  // The sizer fitted the row to Konva's text block exactly — one advance
@@ -186,6 +227,16 @@ export const Cell = memo((props) => {
186
227
  // by the padding on both sides, its padding box is the cell, and every
187
228
  // fitted line shows (#1182, #1199).
188
229
  const textNode = getTextNodeBounds(y + cellSpacingY, height, padding);
230
+ // Vertical justify / distributed: spread the wrapped lines over the
231
+ // cell's inner height. The line count is estimated from the natural
232
+ // width against the wrap width (and any hard line breaks).
233
+ const wrapWidth = Math.max(1, finalTextWidth - 2 * padding);
234
+ const estimatedLineCount = shouldWrap
235
+ ? Math.max((displayValue ?? "").split("\n").length, Math.ceil(_textWidth / wrapWidth))
236
+ : 1;
237
+ const textLineHeight = isVerticalSpread
238
+ ? getVerticalSpreadLineHeight(estimatedLineCount, textNode.height - 2 * padding, effectiveFontSize, baseTextLineHeight)
239
+ : baseTextLineHeight;
189
240
  // For rotated text, anchor the rotation pivot to an alignment-aware
190
241
  // corner of the cell so the text stays inside instead of sliding off
191
242
  // (Konva otherwise rotates around the Text's top-left at x/y).
@@ -208,16 +259,37 @@ export const Cell = memo((props) => {
208
259
  })
209
260
  : undefined;
210
261
  const rotationDegrees = rotatedLayout?.rotation ?? getKonvaRotationDegrees(textRotation);
211
- // CF data-bar geometry. Painted behind the text Group so the cell value
212
- // remains readable on top of the bar. We inset 2px vertically so the bar
213
- // doesn't touch the borders.
214
- const dataBarHeight = Math.max(0, height - 2 * CF_DATABAR_INSET_Y * scale);
215
- const dataBarMaxWidth = Math.max(0, width - 2);
216
- const dataBarFill = conditionalDataBar
217
- ? Math.max(0, Math.min(1, conditionalDataBar.fillFraction))
218
- : 0;
219
- const dataBarWidth = dataBarMaxWidth * dataBarFill;
220
- const dataBarFromRight = conditionalDataBar?.direction === "rightToLeft";
262
+ // CF data bar — painted behind the text Group so the cell value stays
263
+ // readable on top of the bar (`paintDataBar` owns the geometry).
264
+ const dataBarSceneFunc = useCallback((context, shape) => {
265
+ if (!conditionalDataBar)
266
+ return;
267
+ const barStart = conditionalDataBar.barStart ??
268
+ (conditionalDataBar.direction === "rightToLeft"
269
+ ? 1 - conditionalDataBar.fillFraction
270
+ : 0);
271
+ const barEnd = conditionalDataBar.barEnd ??
272
+ barStart + Math.max(0, Math.min(1, conditionalDataBar.fillFraction));
273
+ paintDataBar(context, { width: shape.width(), height: shape.height(), scale }, { ...conditionalDataBar, barStart, barEnd });
274
+ }, [conditionalDataBar, scale]);
275
+ // CF icon — Excel's vector icon at the cell's left edge, aligned with the
276
+ // cell's vertical alignment.
277
+ const cfIconShape = conditionalIcon
278
+ ? getCfIcon(conditionalIcon.iconSet, conditionalIcon.iconId)
279
+ : undefined;
280
+ const cfIconSceneFunc = useCallback((context, shape) => {
281
+ if (!cfIconShape)
282
+ return;
283
+ const size = Math.min(CF_ICON_WIDTH * scale, shape.height() - 2 * scale);
284
+ if (size <= 0)
285
+ return;
286
+ const iconY = verticalAlignment === "top"
287
+ ? scale
288
+ : verticalAlignment === "middle"
289
+ ? (shape.height() - size) / 2
290
+ : shape.height() - size - 2 * scale;
291
+ paintCfIcon(context, cfIconShape, 2 * scale, iconY, size);
292
+ }, [cfIconShape, scale, verticalAlignment]);
221
293
  // Use the segment-based renderer only when per-segment formatting
222
294
  // exists AND none of the unsupported display modes are active.
223
295
  // Rotation, stacked-vertical, shrink-to-fit, and chips stay on the
@@ -234,10 +306,236 @@ export const Cell = memo((props) => {
234
306
  !isStackedVertical &&
235
307
  !hasNumericRotation &&
236
308
  hasRenderableRuns(textFormatRuns ?? undefined);
309
+ // Double / accounting underlines on a single unrotated line (the default
310
+ // Text path); wrapped, rotated or rich-text cells keep Konva's single line.
311
+ const paintStyledUnderline = hasStyledUnderline &&
312
+ !useRichTextRunPath &&
313
+ !showChip &&
314
+ !shouldWrap &&
315
+ !isStackedVertical &&
316
+ !hasNumericRotation;
317
+ const textDirection = readingOrder === "rtl" ? "rtl" : readingOrder === "ltr" ? "ltr" : "inherit";
318
+ const underlineSceneFunc = useCallback((context) => {
319
+ if (!paintStyledUnderline || !underlineStyle)
320
+ return;
321
+ const advance = textLineMetrics.advance;
322
+ const innerLeft = textLeftPadding + padding;
323
+ const innerWidth = Math.max(0, finalTextWidth - 2 * padding);
324
+ const glyphWidth = Math.min(_textWidth, innerWidth);
325
+ const textStart = canvasTextAlign === "right"
326
+ ? innerLeft + innerWidth - glyphWidth
327
+ : canvasTextAlign === "center"
328
+ ? innerLeft + (innerWidth - glyphWidth) / 2
329
+ : innerLeft;
330
+ const innerTop = textNode.y + padding;
331
+ const innerHeight = textNode.height - 2 * padding;
332
+ const lineTop = effectiveVerticalAlignment === "top"
333
+ ? innerTop
334
+ : effectiveVerticalAlignment === "middle"
335
+ ? innerTop + (innerHeight - advance) / 2
336
+ : innerTop + innerHeight - advance;
337
+ const strokes = getUnderlineStrokes({
338
+ style: underlineStyle,
339
+ textX: textStart,
340
+ textWidth: glyphWidth,
341
+ cellX: x,
342
+ cellWidth: width,
343
+ baselineY: lineTop + advance / 2 + effectiveFontSize * 0.35,
344
+ fontSize: effectiveFontSize,
345
+ scale,
346
+ });
347
+ context.beginPath();
348
+ for (const { x1, x2, y: strokeY } of strokes) {
349
+ const yy = Math.round(strokeY) + 0.5;
350
+ context.moveTo(x1, yy);
351
+ context.lineTo(x2, yy);
352
+ }
353
+ context.setAttr("strokeStyle", color ?? defaultColor);
354
+ context.setAttr("lineWidth", Math.max(1, scale));
355
+ context.stroke();
356
+ }, [
357
+ paintStyledUnderline,
358
+ underlineStyle,
359
+ textLineMetrics.advance,
360
+ textLeftPadding,
361
+ padding,
362
+ finalTextWidth,
363
+ _textWidth,
364
+ canvasTextAlign,
365
+ textNode.y,
366
+ textNode.height,
367
+ effectiveVerticalAlignment,
368
+ x,
369
+ width,
370
+ effectiveFontSize,
371
+ scale,
372
+ color,
373
+ defaultColor,
374
+ ]);
375
+ // Excel's `*x` fill and `_x` / `?` reserves (accounting `$` left, number
376
+ // right): one unrotated, unwrapped line whose parts are placed with
377
+ // measured glyph widths. Every other display mode keeps the plain string.
378
+ const paintNumberFormatLayout = !!numberFormatLayout &&
379
+ !useRichTextRunPath &&
380
+ !showChip &&
381
+ !shouldWrap &&
382
+ !isStackedVertical &&
383
+ !hasNumericRotation &&
384
+ !shrinkEnabled &&
385
+ !isFillAlignment &&
386
+ !enableOverflow &&
387
+ textDirection !== "rtl";
388
+ const numberFormatSceneFunc = useCallback((context, shape) => {
389
+ if (!numberFormatLayout)
390
+ return;
391
+ const ctx = context;
392
+ ctx.setAttr("font", `${textStyle} normal ${effectiveFontSize}px ${fontFamily ?? "sans-serif"}`);
393
+ ctx.setAttr("textBaseline", "alphabetic");
394
+ ctx.setAttr("textAlign", "left");
395
+ ctx.setAttr("fillStyle", color ?? defaultColor);
396
+ const innerWidth = Math.max(0, shape.width() - 2 * padding);
397
+ const measure = (text) => ctx.measureText(text).width;
398
+ const layout = layoutNumberFormatParts({
399
+ parts: numberFormatLayout,
400
+ measure,
401
+ availableWidth: innerWidth,
402
+ align: canvasTextAlign,
403
+ }) ?? {
404
+ // The fixed parts alone overflow the cell: paint the plain string
405
+ // aligned the way the text node would (the group clips it).
406
+ runs: [
407
+ {
408
+ text: value ?? "",
409
+ x: canvasTextAlign === "right"
410
+ ? innerWidth - measure(value ?? "")
411
+ : canvasTextAlign === "center"
412
+ ? (innerWidth - measure(value ?? "")) / 2
413
+ : 0,
414
+ width: measure(value ?? ""),
415
+ },
416
+ ],
417
+ start: 0,
418
+ end: innerWidth,
419
+ };
420
+ // Konva's text baseline: the line box is `lineHeight × fontSize` tall,
421
+ // aligned in the node, glyphs centred on it by the font's M box.
422
+ const lineHeightPx = textLineHeight * effectiveFontSize;
423
+ const metrics = ctx.measureText("M");
424
+ const ascent = metrics.fontBoundingBoxAscent ?? metrics.actualBoundingBoxAscent ?? 0;
425
+ const descent = metrics.fontBoundingBoxDescent ??
426
+ metrics.actualBoundingBoxDescent ??
427
+ 0;
428
+ const alignY = effectiveVerticalAlignment === "top"
429
+ ? 0
430
+ : effectiveVerticalAlignment === "middle"
431
+ ? (shape.height() - lineHeightPx - 2 * padding) / 2
432
+ : shape.height() - lineHeightPx - 2 * padding;
433
+ const baselineY = padding + alignY + (ascent - descent) / 2 + lineHeightPx / 2;
434
+ for (const run of layout.runs) {
435
+ ctx.fillText(run.text, padding + run.x, baselineY);
436
+ }
437
+ if (!underline && !strikethrough)
438
+ return;
439
+ context.beginPath();
440
+ const textStart = padding + layout.start;
441
+ const textWidthPx = layout.end - layout.start;
442
+ if (underline) {
443
+ for (const { x1, x2, y: strokeY } of getUnderlineStrokes({
444
+ style: underlineStyle ?? "single",
445
+ textX: textStart,
446
+ textWidth: textWidthPx,
447
+ cellX: x - shape.x(),
448
+ cellWidth: width,
449
+ baselineY,
450
+ fontSize: effectiveFontSize,
451
+ scale,
452
+ })) {
453
+ const yy = Math.round(strokeY) + 0.5;
454
+ context.moveTo(x1, yy);
455
+ context.lineTo(x2, yy);
456
+ }
457
+ }
458
+ if (strikethrough) {
459
+ const yy = Math.round(baselineY - effectiveFontSize * 0.3) + 0.5;
460
+ context.moveTo(textStart, yy);
461
+ context.lineTo(textStart + textWidthPx, yy);
462
+ }
463
+ ctx.setAttr("strokeStyle", color ?? defaultColor);
464
+ ctx.setAttr("lineWidth", Math.max(1, scale));
465
+ context.stroke();
466
+ }, [
467
+ numberFormatLayout,
468
+ textStyle,
469
+ effectiveFontSize,
470
+ fontFamily,
471
+ color,
472
+ defaultColor,
473
+ padding,
474
+ canvasTextAlign,
475
+ value,
476
+ textLineHeight,
477
+ effectiveVerticalAlignment,
478
+ underline,
479
+ strikethrough,
480
+ underlineStyle,
481
+ x,
482
+ width,
483
+ scale,
484
+ ]);
485
+ // Excel's horizontal `distributed`: wrapped lines spread edge to edge
486
+ // (the last one too), a lone word centred, the indent kept on both sides.
487
+ const paintDistributed = horizontalAlignment === "distributed" &&
488
+ !useRichTextRunPath &&
489
+ !showChip &&
490
+ !isStackedVertical &&
491
+ !hasNumericRotation &&
492
+ !shrinkEnabled;
493
+ const distributedSceneFunc = useCallback((context, shape) => {
494
+ const ctx = context;
495
+ ctx.setAttr("font", `${textStyle} normal ${effectiveFontSize}px ${fontFamily ?? "sans-serif"}`);
496
+ ctx.setAttr("textBaseline", "alphabetic");
497
+ ctx.setAttr("textAlign", "left");
498
+ ctx.setAttr("fillStyle", color ?? defaultColor);
499
+ const innerWidth = Math.max(0, shape.width() - 2 * padding - indentSpacing);
500
+ const lines = layoutDistributedLines(displayValue ?? "", innerWidth, (text) => ctx.measureText(text).width);
501
+ const lineHeightPx = textLineHeight * effectiveFontSize;
502
+ const metrics = ctx.measureText("M");
503
+ const ascent = metrics.fontBoundingBoxAscent ?? metrics.actualBoundingBoxAscent ?? 0;
504
+ const descent = metrics.fontBoundingBoxDescent ??
505
+ metrics.actualBoundingBoxDescent ??
506
+ 0;
507
+ const blockHeight = lines.length * lineHeightPx;
508
+ const alignY = effectiveVerticalAlignment === "top"
509
+ ? 0
510
+ : effectiveVerticalAlignment === "middle"
511
+ ? (shape.height() - blockHeight - 2 * padding) / 2
512
+ : shape.height() - blockHeight - 2 * padding;
513
+ lines.forEach((words, lineIndex) => {
514
+ const baselineY = padding +
515
+ alignY +
516
+ lineIndex * lineHeightPx +
517
+ (ascent - descent) / 2 +
518
+ lineHeightPx / 2;
519
+ for (const word of words) {
520
+ ctx.fillText(word.text, padding + word.x, baselineY);
521
+ }
522
+ });
523
+ }, [
524
+ textStyle,
525
+ effectiveFontSize,
526
+ fontFamily,
527
+ color,
528
+ defaultColor,
529
+ padding,
530
+ indentSpacing,
531
+ displayValue,
532
+ textLineHeight,
533
+ effectiveVerticalAlignment,
534
+ ]);
237
535
  return (React.createElement(React.Fragment, null,
238
536
  enableOverflow && showGridLines ? (React.createElement(Shape, { x: x, y: y, width: width, height: height, sceneFunc: overflowFillFunc, listening: false })) : null,
239
- conditionalDataBar && dataBarWidth > 0 ? (React.createElement(Rect, { x: dataBarFromRight ? x + width - 1 - dataBarWidth : x + 1, y: y + CF_DATABAR_INSET_Y * scale, width: dataBarWidth, height: dataBarHeight, fill: conditionalDataBar.color, stroke: conditionalDataBar.borderColor, strokeWidth: conditionalDataBar.borderColor ? 1 : 0, listening: false, perfectDrawEnabled: false })) : null,
240
- conditionalIcon ? (React.createElement(Text, { x: x + 2, y: y, width: CF_ICON_WIDTH * scale, height: height, text: conditionalIcon.glyph, fill: conditionalIcon.color ?? color ?? defaultColor, verticalAlign: "middle", align: "center", fontSize: fontSize, listening: false, perfectDrawEnabled: false })) : null,
537
+ conditionalDataBar ? (React.createElement(Shape, { x: x, y: y, width: width, height: height, sceneFunc: dataBarSceneFunc, listening: false, perfectDrawEnabled: false })) : null,
538
+ conditionalIcon && cfIconShape ? (React.createElement(Shape, { x: x, y: y, width: width, height: height, sceneFunc: cfIconSceneFunc, listening: false, perfectDrawEnabled: false })) : null,
241
539
  showCheckbox ? (React.createElement(CellCheckbox, { x: x, y: y, scale: scale, width: width, height: height, checked: isChecked, onChange: onCheckboxClick, rowIndex: props.rowIndex, columnIndex: props.columnIndex, verticalAlignment: verticalAlignment, horizontalAlignment: horizontalAlignment })) : showText ? (React.createElement(Group, { clipY: y + borderWidth, clipHeight: height, clipX: clipX, clipWidth: clipWidth }, showChip ? (React.createElement(Label, { width: width - CHIP_SPACING * scale, height: CHIP_HEIGHT * scale, x: textLeftPadding, y: getChipVerticalPosition(y, height, scale, verticalAlignment), offsetX: -CHIP_SPACING * scale, offsetY: -CHIP_SPACING * scale },
242
540
  React.createElement(Tag, { height: CHIP_HEIGHT * scale, fill: chipBackgroundColor, cornerRadius: CHIP_RADIUS * scale, perfectDrawEnabled: false, hitStrokeWidth: 0 }),
243
541
  React.createElement(Text, { text: displayValue, fill: color ?? defaultColor, verticalAlign: effectiveVerticalAlignment, align: canvasTextAlign, fontFamily: fontFamily, fontStyle: textStyle, fontSize: fontSize, textDecoration: textDecoration, padding: padding, lineHeight: 1, offsetY: -1, perfectDrawEnabled: false, wrap: "none" }))) : useRichTextRunPath ? (
@@ -257,8 +555,10 @@ export const Cell = memo((props) => {
257
555
  // sup/sub font scale itself (exactly once). Shrink-to-
258
556
  // fit never reaches this path, so `baseFontSize` equals
259
557
  // `effectiveFontSize` for every other cell.
260
- baseFontSize: baseFontSize, baseBold: !!bold, baseItalic: !!italic, baseUnderline: !!underline, baseStrikethrough: !!strikethrough, baseBaselineOffset: baselineOffset ?? undefined, horizontalAlign: canvasTextAlign, verticalAlign: effectiveVerticalAlignment, lineHeight: lineHeight, lineHeightMode: lineHeightMode, scale: scale, wrap: shouldWrap, theme: theme })) : (React.createElement(Text, { x: rotatedLayout?.x ?? textLeftPadding, y: rotatedLayout?.y ?? textNode.y, offsetX: rotatedLayout?.offsetX, offsetY: rotatedLayout?.offsetY, height: rotatedLayout ? approxTextHeight : textNode.height, width: rotatedLayout?.width ?? finalTextWidth, text: displayValue, fill: color ?? defaultColor, verticalAlign: rotatedLayout ? "top" : effectiveVerticalAlignment, align: rotatedLayout ? "left" : canvasTextAlign, fontFamily: fontFamily, fontStyle: textStyle, fontSize: effectiveFontSize, textDecoration: textDecoration, padding: padding, wrap: textWrapStrategy, lineHeight: textLineHeight, rotation: rotationDegrees, hitStrokeWidth: 0, perfectDrawEnabled: false })))) : imageUrl ? (React.createElement(Image, { x: x, y: y, spacing: 1, width: width, height: height, url: imageUrl, perfectDrawEnabled: false, horizontalAlignment: canvasTextAlign === "justify" ? "left" : canvasTextAlign, verticalAlignment: verticalAlignment })) : null,
261
- showFilter ? (React.createElement(CellFilter, { scale: scale, isActive: isFilterActive, onClick: readonly ? undefined : onFilterClick, rowIndex: props.rowIndex, columnIndex: props.columnIndex, x: x, y: y, height: height, width: width, filterIconColor: filterIconColor, verticalAlignment: verticalAlignment, backgroundColor: defaultBackgroundColor, sortOrder: sortOrder })) : showArrow && !readonly ? (React.createElement(CellArrow, { scale: scale, onClick: onArrowClick ?? onRequestEdit, rowIndex: rowIndex, columnIndex: columnIndex, x: x, y: y, height: height, width: width, verticalAlignment: verticalAlignment })) : null,
558
+ baseFontSize: baseFontSize, baseBold: !!bold, baseItalic: !!italic, baseUnderline: !!underline, baseStrikethrough: !!strikethrough, baseBaselineOffset: baselineOffset ?? undefined, horizontalAlign: canvasTextAlign, verticalAlign: effectiveVerticalAlignment, lineHeight: lineHeight, lineHeightMode: lineHeightMode, scale: scale, wrap: shouldWrap, theme: theme })) : paintDistributed ? (React.createElement(Shape, { x: textLeftPadding, y: textNode.y, width: finalTextWidth, height: textNode.height, sceneFunc: distributedSceneFunc, listening: false, perfectDrawEnabled: false })) : paintNumberFormatLayout ? (React.createElement(Shape, { x: textLeftPadding, y: textNode.y, width: finalTextWidth, height: textNode.height, sceneFunc: numberFormatSceneFunc, listening: false, perfectDrawEnabled: false })) : (React.createElement(React.Fragment, null,
559
+ React.createElement(Text, { x: rotatedLayout?.x ?? textLeftPadding, y: rotatedLayout?.y ?? textNode.y, offsetX: rotatedLayout?.offsetX, offsetY: rotatedLayout?.offsetY, height: rotatedLayout ? approxTextHeight : textNode.height, width: rotatedLayout?.width ?? finalTextWidth, text: displayValue, fill: color ?? defaultColor, verticalAlign: rotatedLayout ? "top" : effectiveVerticalAlignment, align: rotatedLayout ? "left" : canvasTextAlign, fontFamily: fontFamily, fontStyle: textStyle, fontSize: effectiveFontSize, textDecoration: paintStyledUnderline ? textDecorationFor(false) : textDecoration, padding: padding, wrap: textWrapStrategy, lineHeight: textLineHeight, rotation: rotationDegrees, direction: textDirection, hitStrokeWidth: 0, perfectDrawEnabled: false }),
560
+ paintStyledUnderline ? (React.createElement(Shape, { sceneFunc: underlineSceneFunc, listening: false, perfectDrawEnabled: false })) : null)))) : imageUrl ? (React.createElement(Image, { x: x, y: y, spacing: 1, width: width, height: height, url: imageUrl, perfectDrawEnabled: false, horizontalAlignment: canvasTextAlign === "justify" ? "left" : canvasTextAlign, verticalAlignment: verticalAlignment })) : null,
561
+ showFilter ? (React.createElement(CellFilter, { scale: scale, isActive: isFilterActive, onClick: readonly ? undefined : onFilterClick, rowIndex: props.rowIndex, columnIndex: props.columnIndex, x: x, y: y, height: height, width: width, filterIconColor: filterIconColor, verticalAlignment: verticalAlignment, backgroundColor: defaultBackgroundColor, sortOrder: sortOrder })) : showArrow && !readonly ? (React.createElement(CellArrow, { scale: scale, onClick: onArrowClick ?? onRequestEdit, rowIndex: rowIndex, columnIndex: columnIndex, x: arrowOutside ? x + (ARROW_DOWN_WIDTH + 4) * scale : x, y: y, height: height, width: width, verticalAlignment: verticalAlignment })) : null,
262
562
  expandable ? (React.createElement(CellGroup, { scale: scale, onClick: (cell) => {
263
563
  onExpandCollapse?.(cell, !expanded);
264
564
  }, rowIndex: rowIndex, columnIndex: columnIndex, x: x + indentSpacing, y: y, height: height, width: width, verticalAlignment: verticalAlignment, expanded: expanded, theme: theme })) : null,
@@ -0,0 +1,35 @@
1
+ import { type NumberFormatLayoutPart } from "@rowsncolumns/utils";
2
+ export type NumberFormatRun = {
3
+ text: string;
4
+ x: number;
5
+ width: number;
6
+ };
7
+ export type NumberFormatRunLayout = {
8
+ runs: NumberFormatRun[];
9
+ /** Left edge of the first painted glyph (or reserve). */
10
+ start: number;
11
+ /** Right edge of the last one. */
12
+ end: number;
13
+ };
14
+ /**
15
+ * Position the parts inside `availableWidth`. Parts before the (first) fill
16
+ * start at the left edge and parts after it end at the right edge, with the
17
+ * fill character repeated in between as many whole times as it fits —
18
+ * Excel's accounting format (`$` left, number right). Without a fill the
19
+ * block is aligned by `align`. Returns `undefined` when the fixed parts
20
+ * alone do not fit, so the caller keeps its plain-text paint.
21
+ */
22
+ export declare const layoutNumberFormatParts: ({ parts, measure, availableWidth, align, }: {
23
+ parts: readonly NumberFormatLayoutPart[];
24
+ measure: (text: string) => number;
25
+ availableWidth: number;
26
+ align: "left" | "center" | "right" | "justify";
27
+ }) => NumberFormatRunLayout | undefined;
28
+ /**
29
+ * The layout parts for a cell, or `undefined` when the pattern has no
30
+ * width-aware token or when the parts would paint a different string than
31
+ * the cell's formatted value (another engine, locale or conditional number
32
+ * format produced it) — the plain string then stays authoritative.
33
+ */
34
+ export declare const getCellNumberFormatLayout: (pattern: string | null | undefined, value: number | string | null | undefined, formattedValue: string | null | undefined, locale?: string) => NumberFormatLayoutPart[] | undefined;
35
+ //# sourceMappingURL=number-format-layout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"number-format-layout.d.ts","sourceRoot":"","sources":["../../../../components/cell/number-format-layout.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,KAAK,sBAAsB,EAG5B,MAAM,qBAAqB,CAAC;AAE7B,MAAM,MAAM,eAAe,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzE,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,eAAe,EAAE,CAAC;IACxB,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GAAI,4CAKrC;IACD,KAAK,EAAE,SAAS,sBAAsB,EAAE,CAAC;IACzC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAClC,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;CAChD,KAAG,qBAAqB,GAAG,SA+D3B,CAAC;AAKF;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,GACpC,SAAS,MAAM,GAAG,IAAI,GAAG,SAAS,EAClC,OAAO,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,EACzC,gBAAgB,MAAM,GAAG,IAAI,GAAG,SAAS,EACzC,SAAS,MAAM,KACd,sBAAsB,EAAE,GAAG,SAa7B,CAAC"}