@ricsam/react-spreadsheets 0.0.2 → 0.0.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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/react-spreadsheets",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "type": "module"
5
5
  }
package/dist/styles.css CHANGED
@@ -4,45 +4,138 @@
4
4
  * Import once in your app:
5
5
  * import "@ricsam/react-spreadsheets/styles.css";
6
6
  *
7
- * Everything is themed through CSS custom properties declared on `.rsp-root`,
8
- * so you can restyle the whole grid without overriding individual rules:
7
+ * ---------------------------------------------------------------------------
8
+ * Theming
9
+ * ---------------------------------------------------------------------------
10
+ * Every visual is driven by `--rsp-*` custom properties. Because the tokens are
11
+ * declared on `:root` (not on `.rsp-root`), you can override them from *any*
12
+ * ancestor and the grid will pick them up:
9
13
  *
10
- * .rsp-root { --rsp-accent: #7c3aed; --rsp-cell-font-size: 13px; }
14
+ * .my-panel { --rsp-accent: #7c3aed; --rsp-cell-font-size: 13px; }
11
15
  *
12
- * Light/dark are handled automatically via `light-dark()` and `color-scheme`.
13
- * Force one mode by setting `color-scheme: light` (or `dark`) on `.rsp-root`.
16
+ * ---------------------------------------------------------------------------
17
+ * Light / dark
18
+ * ---------------------------------------------------------------------------
19
+ * By default the grid follows the user's OS preference. To pin a subtree, set
20
+ * `color-scheme` on the grid root or on any ancestor:
21
+ *
22
+ * .workbook-stage { color-scheme: light; } // always light
23
+ * .workbook-stage { color-scheme: dark; } // always dark
24
+ *
25
+ * Or use the helper classes shipped below:
26
+ *
27
+ * <FormulaWorkbook className="rsp-theme-light" />
28
+ * <FormulaWorkbook className="rsp-theme-dark" />
29
+ *
30
+ * Implementation note — why not `light-dark()`?
31
+ * ---------------------------------------------------------------------------
32
+ * `light-dark()` would be the natural choice, but bundlers that target
33
+ * browsers without native support (Vite's Lightning CSS, for example) lower it
34
+ * to a pair of switch custom properties that are only emitted in rules which
35
+ * *also* declare `color-scheme`. Tokens declared once on a shared root then
36
+ * compute to an invalid value like `#ffffff #0f1523`, and every themed surface
37
+ * silently falls back to `transparent` / inherited text — a failure that only
38
+ * appears in a production build.
39
+ *
40
+ * Instead the same space-toggle trick is applied directly, using our own
41
+ * `--rsp-light` / `--rsp-dark` switches. Exactly one of the two is set to
42
+ * `initial` (making its `var()` fall back to the color) while the other is set
43
+ * to a single space (contributing nothing), so each token resolves to one
44
+ * color. This is plain custom-property substitution that no bundler rewrites,
45
+ * and it inherits, so an ancestor's pin still wins.
46
+ *
47
+ * A plain stylesheet cannot read an ancestor's `color-scheme`, so the component
48
+ * resolves it from the mounted element and reflects the result onto the grid
49
+ * root as `data-rsp-scheme`. That covers a pin declared in CSS, inline, or via
50
+ * the helper classes. The media query below is the pre-resolution default.
14
51
  */
15
52
 
53
+ /* Default: follow the OS preference. */
54
+ :root,
16
55
  .rsp-root {
17
- color-scheme: light dark;
56
+ --rsp-light: initial;
57
+ --rsp-dark: ;
58
+ }
59
+
60
+ @media (prefers-color-scheme: dark) {
61
+ :root,
62
+ .rsp-root {
63
+ --rsp-light: ;
64
+ --rsp-dark: initial;
65
+ }
66
+ }
67
+
68
+ /* Resolved by the component from the inherited `color-scheme`, so a pin set in
69
+ CSS, inline, or via the helper classes below is all honoured. */
70
+ [data-rsp-scheme="light"] {
71
+ --rsp-light: initial;
72
+ --rsp-dark: ;
73
+ }
74
+
75
+ [data-rsp-scheme="dark"] {
76
+ --rsp-light: ;
77
+ --rsp-dark: initial;
78
+ }
18
79
 
80
+ /* Explicit opt-in pins. `color-scheme` is set too so native scrollbars and
81
+ form controls agree with the token switch. */
82
+ .rsp-theme-light {
83
+ color-scheme: light;
84
+ --rsp-light: initial;
85
+ --rsp-dark: ;
86
+ }
87
+
88
+ .rsp-theme-dark {
89
+ color-scheme: dark;
90
+ --rsp-light: ;
91
+ --rsp-dark: initial;
92
+ }
93
+
94
+ :root,
95
+ .rsp-root {
19
96
  /* Surfaces */
20
- --rsp-bg: light-dark(#ffffff, #111827);
21
- --rsp-header-bg: light-dark(#f8f9fa, #374151);
22
- --rsp-corner-bg: light-dark(#e0e0e0, #4b5563);
23
- --rsp-editor-bg: light-dark(#ffffff, #1f2937);
97
+ --rsp-bg: var(--rsp-light, #ffffff) var(--rsp-dark, #0f1523);
98
+ --rsp-surface: var(--rsp-light, #ffffff) var(--rsp-dark, #151c2c);
99
+ --rsp-header-bg: var(--rsp-light, #f7f8fa) var(--rsp-dark, #1a2233);
100
+ --rsp-header-active-bg: var(--rsp-light, #e9edf5) var(--rsp-dark, #24304a);
101
+ --rsp-corner-bg: var(--rsp-light, #eef1f6) var(--rsp-dark, #1a2233);
102
+ --rsp-editor-bg: var(--rsp-light, #ffffff) var(--rsp-dark, #1a2233);
24
103
 
25
104
  /* Lines */
26
- --rsp-border: light-dark(#e1e5e9, #4b5563);
27
- --rsp-corner-border: light-dark(#d0d0d0, #374151);
28
- --rsp-gridline: light-dark(#e1e5e9, #4b5563);
105
+ --rsp-border: var(--rsp-light, #e3e7ee) var(--rsp-dark, #2a3346);
106
+ --rsp-corner-border: var(--rsp-light, #d7dce6) var(--rsp-dark, #2a3346);
107
+ --rsp-gridline: var(--rsp-light, #e8ebf1) var(--rsp-dark, #232c3d);
29
108
 
30
109
  /* Text */
31
- --rsp-text: light-dark(#000000, #f9fafb);
32
- --rsp-header-text: light-dark(#374151, #d1d5db);
33
- --rsp-muted-text: light-dark(#6b7280, #9ca3af);
34
-
35
- /* Accent / selection */
36
- --rsp-accent: light-dark(#2196f3, #60a5fa);
110
+ --rsp-text: var(--rsp-light, #101828) var(--rsp-dark, #e8ecf4);
111
+ --rsp-header-text: var(--rsp-light, #5a6478) var(--rsp-dark, #99a4bb);
112
+ --rsp-muted-text: var(--rsp-light, #6b7280) var(--rsp-dark, #8c97ad);
113
+
114
+ /* Accent / selection.
115
+ NOTE: `@ricsam/selection-manager` paints the selection *perimeter* as an
116
+ inline box-shadow using a hardcoded `#2196F3`, which CSS cannot recolor.
117
+ The default accent stays in that blue family so the perimeter agrees with
118
+ the rest of the chrome out of the box. Overriding `--rsp-accent` with a
119
+ distant hue will make the perimeter read slightly differently. */
120
+ --rsp-accent: var(--rsp-light, #2196f3) var(--rsp-dark, #58a6ff);
37
121
  --rsp-accent-contrast: #ffffff;
38
- --rsp-selection-bg: light-dark(#e3f2fd, #1e3a5f);
39
- --rsp-selection-range-bg: light-dark(#e8f4fd, #1e3a5f);
40
- --rsp-hover-bg: light-dark(#f8f9fa, #374151);
41
- --rsp-focus-ring: light-dark(rgba(33, 150, 243, 0.2), rgba(96, 165, 250, 0.2));
122
+ --rsp-selection-bg: var(--rsp-light, #e8f2fe) var(--rsp-dark, #17304f);
123
+ --rsp-selection-range-bg: var(--rsp-light, #f1f7fe) var(--rsp-dark, #14263d);
124
+ --rsp-hover-bg: var(--rsp-light, #f5f7fa) var(--rsp-dark, #1c2436);
125
+ --rsp-focus-ring: var(--rsp-light, rgba(33, 150, 243, 0.22)) var(--rsp-dark, rgba(88, 166, 255, 0.28));
42
126
 
43
127
  /* Danger (delete buttons, invalid input) */
44
- --rsp-danger: light-dark(#dc2626, #f87171);
45
- --rsp-danger-bg: light-dark(#fef2f2, #7f1d1d);
128
+ --rsp-danger: var(--rsp-light, #d92d20) var(--rsp-dark, #ff8a80);
129
+ --rsp-danger-bg: var(--rsp-light, #fef3f2) var(--rsp-dark, #3a1512);
130
+
131
+ /* Tables (consumed by the workbook's inline cell styles) */
132
+ --rsp-table-header-bg: var(--rsp-light, #2196f3) var(--rsp-dark, #1c3a5e);
133
+ --rsp-table-header-color: var(--rsp-light, #ffffff) var(--rsp-dark, #e8ecf4);
134
+ --rsp-table-header-border: var(--rsp-light, #1877c9) var(--rsp-dark, #2a5384);
135
+ --rsp-table-even-row-bg: var(--rsp-light, #f7f9fc) var(--rsp-dark, #161e2e);
136
+ --rsp-table-odd-row-bg: var(--rsp-light, #ffffff) var(--rsp-dark, #131a28);
137
+ --rsp-table-border-color: var(--rsp-light, #2196f3) var(--rsp-dark, #34405c);
138
+ --rsp-table-border-light: var(--rsp-light, #dfe4ec) var(--rsp-dark, #2a3346);
46
139
 
47
140
  /* Typography */
48
141
  --rsp-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
@@ -50,15 +143,42 @@
50
143
  --rsp-mono-font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo,
51
144
  Consolas, "Liberation Mono", monospace;
52
145
  --rsp-cell-font-size: 12px;
53
- --rsp-header-font-size: 12px;
146
+ --rsp-header-font-size: 11px;
54
147
 
55
- /* Shape */
56
- --rsp-radius: 6px;
148
+ /* Shape + motion */
149
+ --rsp-radius: 8px;
150
+ --rsp-radius-sm: 5px;
151
+ --rsp-cell-padding: 0 7px;
152
+ --rsp-transition: 120ms cubic-bezier(0.4, 0, 0.2, 1);
57
153
 
58
154
  /* Fill handle */
59
155
  --rsp-fill-handle-size: 8px;
60
156
  }
61
157
 
158
+ /* Opt-in scheme pins. Applied to `.rsp-root` (or any ancestor) these force one
159
+ mode regardless of the OS preference, and every `light-dark()` token below
160
+ follows because `color-scheme` inherits. */
161
+ .rsp-theme-light {
162
+ color-scheme: light;
163
+ }
164
+
165
+ .rsp-theme-dark {
166
+ color-scheme: dark;
167
+ }
168
+
169
+ .rsp-root {
170
+ /* No `color-scheme` here on purpose: declaring it would override a pin set by
171
+ an ancestor. See the file header. */
172
+ color: var(--rsp-text);
173
+ font-family: var(--rsp-font-family);
174
+ }
175
+
176
+ .rsp-root *,
177
+ .rsp-root *::before,
178
+ .rsp-root *::after {
179
+ box-sizing: border-box;
180
+ }
181
+
62
182
  /* ------------------------------------------------------------------ *
63
183
  * Spreadsheet grid
64
184
  * ------------------------------------------------------------------ */
@@ -77,7 +197,7 @@
77
197
  }
78
198
 
79
199
  .rsp-container.rsp-focused {
80
- box-shadow: 0 0 0 2px var(--rsp-accent);
200
+ box-shadow: inset 0 0 0 1px var(--rsp-accent);
81
201
  }
82
202
 
83
203
  .rsp-canvas,
@@ -92,9 +212,10 @@
92
212
  position: absolute;
93
213
  border: 1px solid transparent;
94
214
  background: transparent;
95
- padding: 4px;
215
+ padding: var(--rsp-cell-padding);
96
216
  font-size: var(--rsp-cell-font-size);
97
217
  font-family: var(--rsp-font-family);
218
+ font-variant-numeric: tabular-nums;
98
219
  overflow: hidden;
99
220
  white-space: pre;
100
221
  text-overflow: ellipsis;
@@ -108,30 +229,47 @@
108
229
 
109
230
  .rsp-cell.rsp-cell-multiline {
110
231
  align-items: flex-start;
232
+ padding-top: 4px;
111
233
  }
112
234
 
113
- .rsp-cell:hover {
235
+ /* Hover tint must never repaint a cell the consumer has colored, so it is
236
+ restricted to "plain" cells (no custom background). */
237
+ .rsp-cell.rsp-cell-plain:hover {
114
238
  background: var(--rsp-hover-bg);
115
239
  }
116
240
 
117
- .rsp-cell.rsp-cell-selected {
118
- border-color: var(--rsp-accent);
119
- }
120
-
121
241
  /* `rsp-cell-plain` marks cells whose background is not overridden by a
122
242
  consumer-supplied custom style, so selection tinting is safe to apply. */
243
+ .rsp-cell.rsp-cell-in-selection.rsp-cell-plain {
244
+ background: var(--rsp-selection-range-bg);
245
+ }
246
+
123
247
  .rsp-cell.rsp-cell-selected.rsp-cell-plain {
124
248
  background: var(--rsp-selection-bg);
125
249
  }
126
250
 
127
- .rsp-cell.rsp-cell-in-selection.rsp-cell-plain {
128
- background: var(--rsp-selection-range-bg);
251
+ /* The selection perimeter is painted by the selection manager as an inline
252
+ `box-shadow` on the individual edge cells, so this rule must NOT declare a
253
+ box-shadow of its own: `rsp-cell-selected` is present on every cell of a
254
+ multi-cell range, and a full outline here would box each interior cell.
255
+ Only the stacking order is raised so the perimeter is not clipped by
256
+ neighbouring cells. */
257
+ .rsp-cell.rsp-cell-selected {
258
+ z-index: 2;
129
259
  }
130
260
 
131
- .rsp-cell.rsp-cell-editing {
261
+ /* The editor surface must win over the selection tint above (equal specificity
262
+ would otherwise leave the selected-cell background showing through), and over
263
+ a consumer background so the text being typed stays legible. */
264
+ .rsp-cell.rsp-cell-editing,
265
+ .rsp-cell.rsp-cell-editing.rsp-cell-plain,
266
+ .rsp-cell.rsp-cell-selected.rsp-cell-editing,
267
+ .rsp-cell.rsp-cell-selected.rsp-cell-editing.rsp-cell-plain {
132
268
  background: var(--rsp-editor-bg);
133
- border: 2px solid var(--rsp-accent);
134
- box-shadow: 0 0 0 2px var(--rsp-focus-ring);
269
+ color: var(--rsp-text);
270
+ box-shadow:
271
+ inset 0 0 0 2px var(--rsp-accent),
272
+ 0 6px 16px -4px var(--rsp-focus-ring);
135
273
  z-index: 999;
136
274
  pointer-events: none;
137
275
  }
@@ -148,6 +286,7 @@
148
286
  font-family: var(--rsp-font-family);
149
287
  background: transparent;
150
288
  color: var(--rsp-text);
289
+ caret-color: var(--rsp-accent);
151
290
  resize: none;
152
291
  overflow: hidden;
153
292
  z-index: 1000;
@@ -157,12 +296,15 @@
157
296
 
158
297
  .rsp-fill-handle {
159
298
  position: absolute;
160
- bottom: 0;
161
- right: 0;
299
+ bottom: -1px;
300
+ right: -1px;
162
301
  width: var(--rsp-fill-handle-size);
163
302
  height: var(--rsp-fill-handle-size);
164
303
  background-color: var(--rsp-accent);
304
+ border: 1px solid var(--rsp-bg);
305
+ border-radius: 1px;
165
306
  cursor: crosshair;
307
+ z-index: 3;
166
308
  }
167
309
 
168
310
  /* Headers ---------------------------------------------------------- */
@@ -175,14 +317,34 @@
175
317
  align-items: center;
176
318
  justify-content: center;
177
319
  font-size: var(--rsp-header-font-size);
320
+ font-weight: 500;
321
+ letter-spacing: 0.02em;
178
322
  cursor: pointer;
179
323
  user-select: none;
180
324
  color: var(--rsp-header-text);
181
325
  box-sizing: border-box;
326
+ transition: background-color var(--rsp-transition), color var(--rsp-transition);
327
+ }
328
+
329
+ .rsp-header:hover {
330
+ background-color: var(--rsp-header-active-bg);
331
+ color: var(--rsp-text);
182
332
  }
183
333
 
184
334
  .rsp-header.rsp-header-selected {
185
- background-color: var(--rsp-selection-bg);
335
+ background-color: var(--rsp-header-active-bg);
336
+ color: var(--rsp-accent);
337
+ font-weight: 600;
338
+ }
339
+
340
+ /* A crisp accent edge on the side that faces the grid marks the active
341
+ row/column without changing the header's box metrics. */
342
+ .rsp-header.rsp-header-col.rsp-header-selected {
343
+ box-shadow: inset 0 -2px 0 var(--rsp-accent);
344
+ }
345
+
346
+ .rsp-header.rsp-header-row.rsp-header-selected {
347
+ box-shadow: inset -2px 0 0 var(--rsp-accent);
186
348
  }
187
349
 
188
350
  .rsp-column-headers,
@@ -231,6 +393,11 @@
231
393
  z-index: 10;
232
394
  }
233
395
 
396
+ .rsp-col-resize-handle:hover,
397
+ .rsp-row-resize-handle:hover {
398
+ background: var(--rsp-accent);
399
+ }
400
+
234
401
  /* Overlays --------------------------------------------------------- */
235
402
 
236
403
  .rsp-overlays {
@@ -249,7 +416,7 @@
249
416
  opacity: 0.5;
250
417
  pointer-events: none;
251
418
  border: 2px dashed var(--rsp-accent);
252
- border-radius: 4px;
419
+ border-radius: var(--rsp-radius-sm);
253
420
  background-color: var(--rsp-focus-ring);
254
421
  }
255
422
 
@@ -283,7 +450,7 @@
283
450
  display: flex;
284
451
  align-items: center;
285
452
  gap: 8px;
286
- padding: 6px 8px;
453
+ padding: 7px 10px;
287
454
  border-bottom: 1px solid var(--rsp-border);
288
455
  background: var(--rsp-header-bg);
289
456
  flex: none;
@@ -291,8 +458,9 @@
291
458
 
292
459
  .rsp-workbook-name {
293
460
  font-weight: 600;
294
- font-size: 13px;
295
- color: var(--rsp-header-text);
461
+ font-size: 12px;
462
+ letter-spacing: -0.01em;
463
+ color: var(--rsp-text);
296
464
  white-space: nowrap;
297
465
  }
298
466
 
@@ -305,8 +473,8 @@
305
473
  .rsp-sheet-tabs {
306
474
  display: flex;
307
475
  align-items: center;
308
- gap: 4px;
309
- padding: 4px 8px;
476
+ gap: 6px;
477
+ padding: 6px 10px;
310
478
  border-top: 1px solid var(--rsp-border);
311
479
  background: var(--rsp-header-bg);
312
480
  flex: none;
@@ -324,27 +492,38 @@
324
492
  display: inline-flex;
325
493
  align-items: center;
326
494
  gap: 6px;
327
- padding: 4px 10px;
495
+ padding: 5px 11px;
328
496
  border: 1px solid transparent;
329
- border-radius: var(--rsp-radius);
497
+ border-radius: var(--rsp-radius-sm);
330
498
  font-size: 12px;
331
499
  line-height: 1.4;
332
500
  cursor: pointer;
333
501
  white-space: nowrap;
334
502
  color: var(--rsp-header-text);
335
503
  background: transparent;
504
+ transition: background-color var(--rsp-transition), color var(--rsp-transition);
336
505
  }
337
506
 
338
507
  .rsp-sheet-tab:hover {
339
508
  background: var(--rsp-hover-bg);
509
+ color: var(--rsp-text);
340
510
  }
341
511
 
342
512
  .rsp-sheet-tab.rsp-sheet-tab-active {
343
- background: var(--rsp-bg);
344
- border-color: var(--rsp-border);
345
- color: var(--rsp-text);
513
+ background: var(--rsp-accent);
514
+ border-color: transparent;
515
+ color: var(--rsp-accent-contrast);
346
516
  font-weight: 600;
347
- box-shadow: inset 0 -2px 0 var(--rsp-accent);
517
+ }
518
+
519
+ /* Icon buttons inside the active tab must read against the accent fill. */
520
+ .rsp-sheet-tab.rsp-sheet-tab-active .rsp-icon-button {
521
+ color: var(--rsp-accent-contrast);
522
+ }
523
+
524
+ .rsp-sheet-tab.rsp-sheet-tab-active .rsp-icon-button:hover {
525
+ background: rgba(255, 255, 255, 0.22);
526
+ color: var(--rsp-accent-contrast);
348
527
  }
349
528
 
350
529
  /* Per-tab action buttons reveal on hover / when the tab is active. */
@@ -353,7 +532,7 @@
353
532
  align-items: center;
354
533
  gap: 2px;
355
534
  opacity: 0;
356
- transition: opacity 0.15s;
535
+ transition: opacity var(--rsp-transition);
357
536
  }
358
537
 
359
538
  .rsp-sheet-tab:hover .sheet-tab-actions,
@@ -364,12 +543,14 @@
364
543
 
365
544
  .rsp-sheet-tab-input {
366
545
  font: inherit;
367
- color: inherit;
546
+ color: var(--rsp-text);
368
547
  background: var(--rsp-editor-bg);
369
548
  border: 1px solid var(--rsp-accent);
370
- border-radius: 4px;
371
- padding: 1px 4px;
549
+ border-radius: var(--rsp-radius-sm);
550
+ padding: 2px 6px;
372
551
  min-width: 80px;
552
+ outline: none;
553
+ box-shadow: 0 0 0 3px var(--rsp-focus-ring);
373
554
  }
374
555
 
375
556
  .rsp-icon-button {
@@ -380,12 +561,13 @@
380
561
  height: 22px;
381
562
  padding: 0;
382
563
  border: none;
383
- border-radius: 4px;
564
+ border-radius: var(--rsp-radius-sm);
384
565
  background: transparent;
385
566
  color: var(--rsp-muted-text);
386
567
  cursor: pointer;
387
568
  font-size: 14px;
388
569
  line-height: 1;
570
+ transition: background-color var(--rsp-transition), color var(--rsp-transition);
389
571
  }
390
572
 
391
573
  .rsp-icon-button:hover {
@@ -393,6 +575,11 @@
393
575
  color: var(--rsp-text);
394
576
  }
395
577
 
578
+ .rsp-icon-button:disabled {
579
+ opacity: 0.4;
580
+ cursor: not-allowed;
581
+ }
582
+
396
583
  .rsp-icon-button.rsp-icon-button-delete:hover {
397
584
  background: var(--rsp-danger-bg);
398
585
  color: var(--rsp-danger);
@@ -403,19 +590,21 @@
403
590
  align-items: center;
404
591
  justify-content: center;
405
592
  gap: 4px;
406
- height: 24px;
407
- padding: 0 10px;
593
+ height: 26px;
594
+ padding: 0 11px;
408
595
  border: 1px dashed var(--rsp-border);
409
- border-radius: var(--rsp-radius);
596
+ border-radius: var(--rsp-radius-sm);
410
597
  background: transparent;
411
598
  color: var(--rsp-header-text);
412
599
  font-size: 12px;
413
600
  cursor: pointer;
414
601
  white-space: nowrap;
602
+ transition: border-color var(--rsp-transition), color var(--rsp-transition);
415
603
  }
416
604
 
417
605
  .rsp-add-sheet-button:hover {
418
606
  border-color: var(--rsp-accent);
607
+ border-style: solid;
419
608
  color: var(--rsp-accent);
420
609
  }
421
610
 
@@ -430,7 +619,7 @@
430
619
  .rsp-zoom-value {
431
620
  min-width: 44px;
432
621
  text-align: center;
433
- font-size: 12px;
622
+ font-size: 11px;
434
623
  color: var(--rsp-muted-text);
435
624
  font-variant-numeric: tabular-nums;
436
625
  }
@@ -441,7 +630,7 @@
441
630
  display: flex;
442
631
  align-items: center;
443
632
  gap: 8px;
444
- padding: 4px 8px;
633
+ padding: 6px 10px;
445
634
  border-bottom: 1px solid var(--rsp-border);
446
635
  background: var(--rsp-bg);
447
636
  flex: none;
@@ -449,12 +638,12 @@
449
638
 
450
639
  .rsp-name-box {
451
640
  min-width: 84px;
452
- padding: 3px 8px;
641
+ padding: 4px 8px;
453
642
  border: 1px solid var(--rsp-border);
454
- border-radius: 4px;
643
+ border-radius: var(--rsp-radius-sm);
455
644
  background: var(--rsp-header-bg);
456
645
  font-family: var(--rsp-mono-font-family);
457
- font-size: 12px;
646
+ font-size: 11px;
458
647
  text-align: center;
459
648
  color: var(--rsp-text);
460
649
  }
@@ -462,22 +651,52 @@
462
651
  .rsp-formula-input {
463
652
  flex: 1;
464
653
  min-width: 0;
465
- padding: 3px 8px;
654
+ padding: 4px 9px;
466
655
  border: 1px solid var(--rsp-border);
467
- border-radius: 4px;
656
+ border-radius: var(--rsp-radius-sm);
468
657
  background: var(--rsp-editor-bg);
469
658
  color: var(--rsp-text);
470
659
  font-family: var(--rsp-mono-font-family);
471
660
  font-size: 12px;
661
+ transition: border-color var(--rsp-transition), box-shadow var(--rsp-transition);
662
+ }
663
+
664
+ .rsp-formula-input::placeholder {
665
+ color: var(--rsp-muted-text);
472
666
  }
473
667
 
474
668
  .rsp-formula-input:focus {
475
669
  outline: none;
476
670
  border-color: var(--rsp-accent);
477
- box-shadow: 0 0 0 2px var(--rsp-focus-ring);
671
+ box-shadow: 0 0 0 3px var(--rsp-focus-ring);
478
672
  }
479
673
 
480
674
  .rsp-formula-input:disabled {
481
675
  opacity: 0.6;
482
676
  cursor: not-allowed;
483
677
  }
678
+
679
+ /* Scrollbars ------------------------------------------------------- */
680
+
681
+ .rsp-root {
682
+ scrollbar-color: var(--rsp-border) transparent;
683
+ }
684
+
685
+ .rsp-root ::-webkit-scrollbar {
686
+ width: 10px;
687
+ height: 10px;
688
+ }
689
+
690
+ .rsp-root ::-webkit-scrollbar-track {
691
+ background: transparent;
692
+ }
693
+
694
+ .rsp-root ::-webkit-scrollbar-thumb {
695
+ background: var(--rsp-border);
696
+ border: 2px solid var(--rsp-bg);
697
+ border-radius: 99px;
698
+ }
699
+
700
+ .rsp-root ::-webkit-scrollbar-thumb:hover {
701
+ background: var(--rsp-muted-text);
702
+ }
@@ -23,4 +23,5 @@ export { cn } from "./utils/cn";
23
23
  export { useResizeObserver } from "./utils/use-resize-observer";
24
24
  export { useColorScheme, type ColorScheme } from "./utils/use-color-scheme";
25
25
  export { useClickAway } from "./utils/use-click-away";
26
+ export { getContrastingTextColor, parseCssColor, relativeLuminance, } from "./utils/contrast-text";
26
27
  export { ViewportStream, type ViewportState, type ResizeHandle } from "./grid/types";
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Contrast helpers for consumer-supplied cell backgrounds.
3
+ *
4
+ * The grid's default text color is theme-aware (`--rsp-text` resolves through
5
+ * `light-dark()`), but a consumer-supplied `backgroundColor` usually is not —
6
+ * spreadsheet fills typically come from a document model that stores one
7
+ * literal color, e.g. `#f1f5f9`.
8
+ *
9
+ * Without help, a light literal fill combined with the dark theme's near-white
10
+ * text renders as white-on-white and the cell becomes unreadable. These helpers
11
+ * pick a readable ink for a known background instead.
12
+ *
13
+ * Only values we can actually parse are considered. Anything dynamic
14
+ * (`var(...)`, `color-mix(...)`, gradients, `transparent`, unknown keywords)
15
+ * returns `undefined` so the caller keeps the inherited themed color.
16
+ */
17
+ type Rgba = {
18
+ r: number;
19
+ g: number;
20
+ b: number;
21
+ a: number;
22
+ };
23
+ /**
24
+ * Parses hex (#rgb, #rgba, #rrggbb, #rrggbbaa), rgb()/rgba() and a small set of
25
+ * common keywords. Returns `undefined` for anything else.
26
+ */
27
+ export declare function parseCssColor(input: string): Rgba | undefined;
28
+ /** Relative luminance per WCAG 2.1. */
29
+ export declare function relativeLuminance({ r, g, b }: Pick<Rgba, "r" | "g" | "b">): number;
30
+ /**
31
+ * Returns a readable ink for `background`, or `undefined` when the background
32
+ * is unparseable or too translucent to classify (in which case the themed
33
+ * default should keep applying).
34
+ */
35
+ export declare function getContrastingTextColor(background: string): string | undefined;
36
+ export {};
@@ -0,0 +1,16 @@
1
+ import { type ColorScheme } from "./use-color-scheme";
2
+ /**
3
+ * Props for a `.rsp-root` element so its light/dark tokens match the inherited
4
+ * `color-scheme`.
5
+ *
6
+ * A stylesheet cannot branch on an *ancestor's* `color-scheme`, and the
7
+ * `light-dark()` function is rewritten by some bundlers into switch variables
8
+ * that only exist next to a `color-scheme` declaration (see `styles.css`).
9
+ * Resolving the scheme from the mounted element and reflecting it as
10
+ * `data-rsp-scheme` keeps the tokens correct without the component declaring
11
+ * `color-scheme` itself, which would override a consumer's pin.
12
+ */
13
+ export declare function useSchemeRoot(): {
14
+ ref: (element: HTMLElement | null) => void;
15
+ "data-rsp-scheme": ColorScheme;
16
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/react-spreadsheets",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Headless-friendly React spreadsheet and workbook components: infinite virtualized grid, zoom, resizing, clipboard, fill handles and formula-engine bindings.",
5
5
  "type": "module",
6
6
  "module": "./dist/mjs/lib.mjs",
@@ -21,7 +21,7 @@
21
21
  "build": "NODE_ENV=production bun build-lib.ts",
22
22
  "test": "bun test",
23
23
  "typecheck": "tsc --noEmit",
24
- "demo": "bun --hot demo/index.tsx"
24
+ "demo": "bun demo/serve.ts"
25
25
  },
26
26
  "keywords": [
27
27
  "react",