@ricsam/react-spreadsheets 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -88,11 +88,11 @@ top of `FormulaSheet`.
88
88
 
89
89
  ## Theming
90
90
 
91
- All colors, fonts and sizes are CSS custom properties declared on `.rsp-root`.
92
- Override them anywhere in your cascade:
91
+ All colors, fonts and sizes are CSS custom properties declared on `:root`, so
92
+ you can override them from **any** ancestor of the grid:
93
93
 
94
94
  ```css
95
- .rsp-root {
95
+ .my-panel {
96
96
  --rsp-accent: #7c3aed;
97
97
  --rsp-bg: #ffffff;
98
98
  --rsp-header-bg: #f4f4f5;
@@ -100,9 +100,34 @@ Override them anywhere in your cascade:
100
100
  }
101
101
  ```
102
102
 
103
- Light and dark are resolved automatically with `light-dark()`. To pin a subtree
104
- to one mode, set `color-scheme: light` (or `dark`) on `.rsp-root`; canvas
105
- gridlines follow the same setting.
103
+ ### Light and dark
104
+
105
+ Colors resolve through `light-dark()`, which follows the inherited
106
+ `color-scheme`. By default the grid follows the user's OS preference.
107
+
108
+ The grid deliberately does **not** declare `color-scheme` on `.rsp-root`, so a
109
+ pin you set on an ancestor is respected. Pin a subtree with plain CSS:
110
+
111
+ ```css
112
+ .workbook-stage { color-scheme: light; } /* always light */
113
+ .workbook-stage { color-scheme: dark; } /* always dark */
114
+ ```
115
+
116
+ or with the bundled helper classes:
117
+
118
+ ```tsx
119
+ <FormulaWorkbook className="rsp-theme-dark" ... />
120
+ ```
121
+
122
+ Canvas gridlines and every DOM surface follow the same resolved scheme.
123
+
124
+ ### Cell fills stay readable
125
+
126
+ A `backgroundColor` coming from your document model is usually a single literal
127
+ color with no dark-mode variant. To stop light fills from pairing with the dark
128
+ theme's near-white text, the grid derives a readable ink for any cell that sets
129
+ a background but no explicit `color`. Set `color` yourself to opt out, and use
130
+ `getContrastingTextColor` if you want the same behaviour elsewhere.
106
131
 
107
132
  Per-cell styling is done in JS:
108
133
 
package/dist/cjs/lib.cjs CHANGED
@@ -92,6 +92,7 @@ __export(exports_lib, {
92
92
  getCellReference: () => getCellReference,
93
93
  getCellSnapAnchorFromRect: () => getCellSnapAnchorFromRect,
94
94
  getColumnEdgePosition: () => getColumnEdgePosition,
95
+ getContrastingTextColor: () => getContrastingTextColor,
95
96
  getNearestColumnEdgeIndex: () => getNearestColumnEdgeIndex,
96
97
  getNearestRowEdgeIndex: () => getNearestRowEdgeIndex,
97
98
  getRectFromCellSnapAnchor: () => getRectFromCellSnapAnchor,
@@ -105,7 +106,9 @@ __export(exports_lib, {
105
106
  normalizeCellSnapAnchor: () => normalizeCellSnapAnchor,
106
107
  onCellEditError: () => onCellEditError,
107
108
  parseCellReference: () => parseCellReference,
109
+ parseCssColor: () => parseCssColor,
108
110
  queueCellEditError: () => queueCellEditError,
111
+ relativeLuminance: () => relativeLuminance,
109
112
  resizeCellSnapAnchorToNearestEdges: () => resizeCellSnapAnchorToNearestEdges,
110
113
  rowToLetter: () => rowToLetter,
111
114
  useClickAway: () => useClickAway,
@@ -174,6 +177,85 @@ function useColorScheme(element) {
174
177
  return scheme;
175
178
  }
176
179
 
180
+ // src/utils/contrast-text.ts
181
+ var DARK_INK = "#101828";
182
+ var LIGHT_INK = "#ffffff";
183
+ var NAMED_COLORS = {
184
+ white: [255, 255, 255],
185
+ black: [0, 0, 0],
186
+ red: [255, 0, 0],
187
+ green: [0, 128, 0],
188
+ blue: [0, 0, 255],
189
+ yellow: [255, 255, 0],
190
+ orange: [255, 165, 0],
191
+ purple: [128, 0, 128],
192
+ gray: [128, 128, 128],
193
+ grey: [128, 128, 128],
194
+ silver: [192, 192, 192]
195
+ };
196
+ var clamp255 = (value) => Math.min(255, Math.max(0, value));
197
+ function parseCssColor(input) {
198
+ const value = input.trim().toLowerCase();
199
+ if (!value || value === "transparent" || value === "currentcolor" || value === "inherit") {
200
+ return;
201
+ }
202
+ if (value.includes("var(") || value.includes("color-mix(") || value.includes("gradient")) {
203
+ return;
204
+ }
205
+ const named = NAMED_COLORS[value];
206
+ if (named) {
207
+ return { r: named[0], g: named[1], b: named[2], a: 1 };
208
+ }
209
+ if (value.startsWith("#")) {
210
+ const hex = value.slice(1);
211
+ const expand = (part) => parseInt(part + part, 16);
212
+ if (hex.length === 3 || hex.length === 4) {
213
+ const r = expand(hex[0]);
214
+ const g = expand(hex[1]);
215
+ const b = expand(hex[2]);
216
+ const a = hex.length === 4 ? expand(hex[3]) / 255 : 1;
217
+ return Number.isNaN(r + g + b) ? undefined : { r, g, b, a };
218
+ }
219
+ if (hex.length === 6 || hex.length === 8) {
220
+ const r = parseInt(hex.slice(0, 2), 16);
221
+ const g = parseInt(hex.slice(2, 4), 16);
222
+ const b = parseInt(hex.slice(4, 6), 16);
223
+ const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1;
224
+ return Number.isNaN(r + g + b) ? undefined : { r, g, b, a };
225
+ }
226
+ return;
227
+ }
228
+ const rgbMatch = value.match(/^rgba?\(([^)]+)\)$/);
229
+ if (rgbMatch) {
230
+ const parts = rgbMatch[1].split(/[\s,/]+/).filter(Boolean);
231
+ if (parts.length < 3)
232
+ return;
233
+ const toChannel = (part) => part.endsWith("%") ? parseFloat(part) / 100 * 255 : parseFloat(part);
234
+ const r = toChannel(parts[0]);
235
+ const g = toChannel(parts[1]);
236
+ const b = toChannel(parts[2]);
237
+ const alphaPart = parts[3];
238
+ const a = alphaPart === undefined ? 1 : alphaPart.endsWith("%") ? parseFloat(alphaPart) / 100 : parseFloat(alphaPart);
239
+ if ([r, g, b, a].some((channel) => Number.isNaN(channel)))
240
+ return;
241
+ return { r: clamp255(r), g: clamp255(g), b: clamp255(b), a };
242
+ }
243
+ return;
244
+ }
245
+ function relativeLuminance({ r, g, b }) {
246
+ const channel = (value) => {
247
+ const srgb = value / 255;
248
+ return srgb <= 0.03928 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4);
249
+ };
250
+ return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
251
+ }
252
+ function getContrastingTextColor(background) {
253
+ const color = parseCssColor(background);
254
+ if (!color || color.a < 0.5)
255
+ return;
256
+ return relativeLuminance(color) > 0.45 ? DARK_INK : LIGHT_INK;
257
+ }
258
+
177
259
  // src/utils/cn.ts
178
260
  function cn(...values) {
179
261
  const out = [];
@@ -1352,16 +1434,27 @@ var resizeCellSnapAnchorToNearestEdges = (anchor, rect, handle, columnWidths, ro
1352
1434
 
1353
1435
  // src/spreadsheet/spreadsheet.tsx
1354
1436
  var jsx_runtime3 = require("react/jsx-runtime");
1355
- var resolveGridLineColor = (element, colorScheme) => {
1356
- const fallback = colorScheme === "dark" ? "#4b5563" : "#e1e5e9";
1357
- if (!element || typeof getComputedStyle !== "function") {
1437
+ var resolveGridLineColor = (probe, colorScheme) => {
1438
+ const fallback = colorScheme === "dark" ? "#232c3d" : "#e8ebf1";
1439
+ if (!probe || typeof getComputedStyle !== "function") {
1358
1440
  return fallback;
1359
1441
  }
1360
- const declared = getComputedStyle(element).getPropertyValue("--rsp-gridline").trim();
1361
- if (!declared || declared.includes("light-dark(")) {
1442
+ const resolved = getComputedStyle(probe).color?.trim();
1443
+ if (!resolved || resolved.includes("light-dark(") || resolved === "rgba(0, 0, 0, 0)") {
1362
1444
  return fallback;
1363
1445
  }
1364
- return declared;
1446
+ return resolved;
1447
+ };
1448
+ var withReadableTextColor = (style) => {
1449
+ if (!style?.backgroundColor || style.color)
1450
+ return style;
1451
+ const background = style.backgroundColor;
1452
+ if (typeof background !== "string")
1453
+ return style;
1454
+ const color = getContrastingTextColor(background);
1455
+ if (!color)
1456
+ return style;
1457
+ return { ...style, color };
1365
1458
  };
1366
1459
  var hasBorderStyle = (style) => Boolean(style?.borderColor || style?.borderTopColor || style?.borderRightColor || style?.borderBottomColor || style?.borderLeftColor);
1367
1460
  var CellComponent = import_react4.default.memo(({
@@ -1404,7 +1497,7 @@ var CellComponent = import_react4.default.memo(({
1404
1497
  isSelected,
1405
1498
  isBeingEdited: isBeingEdited || false
1406
1499
  };
1407
- const customStyle = customCellStyle?.(cellStyleCallbackData);
1500
+ const customStyle = withReadableTextColor(customCellStyle?.(cellStyleCallbackData));
1408
1501
  const canHaveFillHandle = import_selection_manager.useSelectionManager(selectionManager, () => {
1409
1502
  return selectionManager.canCellHaveFillHandle({
1410
1503
  row: rowIndex,
@@ -1436,7 +1529,8 @@ var CellComponent = import_react4.default.memo(({
1436
1529
  top: y,
1437
1530
  width: getColumnWidth(col) - 1,
1438
1531
  height: getRowHeight(row) - 1,
1439
- ...customStyle
1532
+ ...customStyle,
1533
+ ...isBeingEdited ? { backgroundColor: undefined, color: undefined } : null
1440
1534
  },
1441
1535
  children: [
1442
1536
  isBeingEdited ? /* @__PURE__ */ jsx_runtime3.jsx("textarea", {
@@ -1686,6 +1780,7 @@ var Spreadsheet = import_react4.forwardRef(({
1686
1780
  const [{ width, height }, containerRef] = useResizeObserver();
1687
1781
  const [containerDivRef, setContainerDivRef] = import_react4.useState(null);
1688
1782
  const colorScheme = useColorScheme(containerDivRef);
1783
+ const gridLineProbeRef = import_react4.useRef(null);
1689
1784
  const selectionManager = import_selection_manager.useInitializeSelectionManager({
1690
1785
  getNumCols: () => ({ type: "infinity" }),
1691
1786
  getNumRows: () => ({ type: "infinity" }),
@@ -1879,7 +1974,7 @@ var Spreadsheet = import_react4.forwardRef(({
1879
1974
  if (!ctx)
1880
1975
  return;
1881
1976
  ctx.clearRect(0, 0, width, height);
1882
- ctx.strokeStyle = resolveGridLineColor(canvasRef.current, colorScheme);
1977
+ ctx.strokeStyle = resolveGridLineColor(gridLineProbeRef.current, colorScheme);
1883
1978
  ctx.lineWidth = 1;
1884
1979
  let x = gridDims.headerWidth;
1885
1980
  let colIndex = 0;
@@ -2642,6 +2737,18 @@ var Spreadsheet = import_react4.forwardRef(({
2642
2737
  },
2643
2738
  tabIndex: 0,
2644
2739
  children: [
2740
+ /* @__PURE__ */ jsx_runtime3.jsx("span", {
2741
+ ref: gridLineProbeRef,
2742
+ "aria-hidden": "true",
2743
+ style: {
2744
+ position: "absolute",
2745
+ width: 0,
2746
+ height: 0,
2747
+ visibility: "hidden",
2748
+ pointerEvents: "none",
2749
+ color: "var(--rsp-gridline)"
2750
+ }
2751
+ }),
2645
2752
  /* @__PURE__ */ jsx_runtime3.jsx("canvas", {
2646
2753
  ref: canvasRef,
2647
2754
  className: "rsp-canvas",
@@ -3554,32 +3661,32 @@ function FormulaSheet({
3554
3661
  const isLastColumn = cell.colIndex === tableInfo.start.colIndex + tableInfo.headers.size - 1;
3555
3662
  const isLastRow = tableInfo.endRow.type === "number" ? cell.rowIndex === tableInfo.endRow.value : false;
3556
3663
  const style = {
3557
- border: "1px solid var(--table-border-light)"
3664
+ border: "1px solid var(--rsp-table-border-light)"
3558
3665
  };
3559
3666
  if (isHeaderRow) {
3560
- style.backgroundColor = "var(--table-header-bg)";
3561
- style.color = "var(--table-header-color)";
3667
+ style.backgroundColor = "var(--rsp-table-header-bg)";
3668
+ style.color = "var(--rsp-table-header-color)";
3562
3669
  style.fontWeight = "bold";
3563
- style.borderBottom = "2px solid var(--table-header-border)";
3670
+ style.borderBottom = "2px solid var(--rsp-table-header-border)";
3564
3671
  } else {
3565
3672
  const dataRowIndex = cell.rowIndex - tableInfo.start.rowIndex - 1;
3566
3673
  if (dataRowIndex % 2 === 0) {
3567
- style.backgroundColor = "var(--table-even-row-bg)";
3674
+ style.backgroundColor = "var(--rsp-table-even-row-bg)";
3568
3675
  } else {
3569
- style.backgroundColor = "var(--table-odd-row-bg)";
3676
+ style.backgroundColor = "var(--rsp-table-odd-row-bg)";
3570
3677
  }
3571
3678
  }
3572
3679
  if (isFirstColumn) {
3573
- style.borderLeft = "2px solid var(--table-border-color)";
3680
+ style.borderLeft = "2px solid var(--rsp-table-border-color)";
3574
3681
  }
3575
3682
  if (isLastColumn) {
3576
- style.borderRight = "2px solid var(--table-border-color)";
3683
+ style.borderRight = "2px solid var(--rsp-table-border-color)";
3577
3684
  }
3578
3685
  if (isHeaderRow) {
3579
- style.borderTop = "2px solid var(--table-border-color)";
3686
+ style.borderTop = "2px solid var(--rsp-table-border-color)";
3580
3687
  }
3581
3688
  if (isLastRow) {
3582
- style.borderBottom = "2px solid var(--table-border-color)";
3689
+ style.borderBottom = "2px solid var(--rsp-table-border-color)";
3583
3690
  }
3584
3691
  if (conditionalStyle?.backgroundColor) {
3585
3692
  style.backgroundColor = conditionalStyle.backgroundColor;
@@ -4042,4 +4149,4 @@ var useClickAway = (ref, callback) => {
4042
4149
  }, [ref, callback]);
4043
4150
  };
4044
4151
 
4045
- //# debugId=2D3DCCBBF4E8DCA364756E2164756E21
4152
+ //# debugId=203CB6768D786E8764756E2164756E21