@ricsam/react-spreadsheets 0.0.0 → 0.0.1

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.
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "@ricsam/react-spreadsheets",
3
+ "version": "0.0.1",
4
+ "type": "module"
5
+ }
@@ -0,0 +1,483 @@
1
+ /**
2
+ * @ricsam/react-spreadsheets — base stylesheet
3
+ *
4
+ * Import once in your app:
5
+ * import "@ricsam/react-spreadsheets/styles.css";
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:
9
+ *
10
+ * .rsp-root { --rsp-accent: #7c3aed; --rsp-cell-font-size: 13px; }
11
+ *
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`.
14
+ */
15
+
16
+ .rsp-root {
17
+ color-scheme: light dark;
18
+
19
+ /* 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);
24
+
25
+ /* Lines */
26
+ --rsp-border: light-dark(#e1e5e9, #4b5563);
27
+ --rsp-corner-border: light-dark(#d0d0d0, #374151);
28
+ --rsp-gridline: light-dark(#e1e5e9, #4b5563);
29
+
30
+ /* 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);
37
+ --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));
42
+
43
+ /* Danger (delete buttons, invalid input) */
44
+ --rsp-danger: light-dark(#dc2626, #f87171);
45
+ --rsp-danger-bg: light-dark(#fef2f2, #7f1d1d);
46
+
47
+ /* Typography */
48
+ --rsp-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
49
+ "Helvetica Neue", Arial, sans-serif;
50
+ --rsp-mono-font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo,
51
+ Consolas, "Liberation Mono", monospace;
52
+ --rsp-cell-font-size: 12px;
53
+ --rsp-header-font-size: 12px;
54
+
55
+ /* Shape */
56
+ --rsp-radius: 6px;
57
+
58
+ /* Fill handle */
59
+ --rsp-fill-handle-size: 8px;
60
+ }
61
+
62
+ /* ------------------------------------------------------------------ *
63
+ * Spreadsheet grid
64
+ * ------------------------------------------------------------------ */
65
+
66
+ .rsp-container {
67
+ position: relative;
68
+ overflow: hidden;
69
+ user-select: none;
70
+ background: var(--rsp-bg);
71
+ color: var(--rsp-text);
72
+ font-family: var(--rsp-font-family);
73
+ }
74
+
75
+ .rsp-container:focus {
76
+ outline: none;
77
+ }
78
+
79
+ .rsp-container.rsp-focused {
80
+ box-shadow: 0 0 0 2px var(--rsp-accent);
81
+ }
82
+
83
+ .rsp-canvas,
84
+ .rsp-cells {
85
+ position: absolute;
86
+ top: 0;
87
+ left: 0;
88
+ pointer-events: none;
89
+ }
90
+
91
+ .rsp-cell {
92
+ position: absolute;
93
+ border: 1px solid transparent;
94
+ background: transparent;
95
+ padding: 4px;
96
+ font-size: var(--rsp-cell-font-size);
97
+ font-family: var(--rsp-font-family);
98
+ overflow: hidden;
99
+ white-space: pre;
100
+ text-overflow: ellipsis;
101
+ pointer-events: auto;
102
+ cursor: cell;
103
+ display: flex;
104
+ align-items: center;
105
+ color: var(--rsp-text);
106
+ box-sizing: border-box;
107
+ }
108
+
109
+ .rsp-cell.rsp-cell-multiline {
110
+ align-items: flex-start;
111
+ }
112
+
113
+ .rsp-cell:hover {
114
+ background: var(--rsp-hover-bg);
115
+ }
116
+
117
+ .rsp-cell.rsp-cell-selected {
118
+ border-color: var(--rsp-accent);
119
+ }
120
+
121
+ /* `rsp-cell-plain` marks cells whose background is not overridden by a
122
+ consumer-supplied custom style, so selection tinting is safe to apply. */
123
+ .rsp-cell.rsp-cell-selected.rsp-cell-plain {
124
+ background: var(--rsp-selection-bg);
125
+ }
126
+
127
+ .rsp-cell.rsp-cell-in-selection.rsp-cell-plain {
128
+ background: var(--rsp-selection-range-bg);
129
+ }
130
+
131
+ .rsp-cell.rsp-cell-editing {
132
+ background: var(--rsp-editor-bg);
133
+ border: 2px solid var(--rsp-accent);
134
+ box-shadow: 0 0 0 2px var(--rsp-focus-ring);
135
+ z-index: 999;
136
+ pointer-events: none;
137
+ }
138
+
139
+ .rsp-cell-editor {
140
+ position: fixed;
141
+ width: 100vw;
142
+ height: 100vh;
143
+ border: none;
144
+ outline: none;
145
+ padding: 0;
146
+ margin: 0;
147
+ font-size: var(--rsp-cell-font-size);
148
+ font-family: var(--rsp-font-family);
149
+ background: transparent;
150
+ color: var(--rsp-text);
151
+ resize: none;
152
+ overflow: hidden;
153
+ z-index: 1000;
154
+ white-space: pre-wrap;
155
+ word-wrap: break-word;
156
+ }
157
+
158
+ .rsp-fill-handle {
159
+ position: absolute;
160
+ bottom: 0;
161
+ right: 0;
162
+ width: var(--rsp-fill-handle-size);
163
+ height: var(--rsp-fill-handle-size);
164
+ background-color: var(--rsp-accent);
165
+ cursor: crosshair;
166
+ }
167
+
168
+ /* Headers ---------------------------------------------------------- */
169
+
170
+ .rsp-header {
171
+ position: absolute;
172
+ border: 1px solid var(--rsp-border);
173
+ background-color: var(--rsp-header-bg);
174
+ display: flex;
175
+ align-items: center;
176
+ justify-content: center;
177
+ font-size: var(--rsp-header-font-size);
178
+ cursor: pointer;
179
+ user-select: none;
180
+ color: var(--rsp-header-text);
181
+ box-sizing: border-box;
182
+ }
183
+
184
+ .rsp-header.rsp-header-selected {
185
+ background-color: var(--rsp-selection-bg);
186
+ }
187
+
188
+ .rsp-column-headers,
189
+ .rsp-row-headers {
190
+ position: absolute;
191
+ overflow: hidden;
192
+ background-color: var(--rsp-header-bg);
193
+ z-index: 1;
194
+ }
195
+
196
+ .rsp-column-headers {
197
+ top: 0;
198
+ border-bottom: 1px solid var(--rsp-border);
199
+ }
200
+
201
+ .rsp-row-headers {
202
+ border-right: 1px solid var(--rsp-border);
203
+ }
204
+
205
+ .rsp-corner-header {
206
+ position: absolute;
207
+ top: 0;
208
+ left: 0;
209
+ background-color: var(--rsp-corner-bg);
210
+ border: 1px solid var(--rsp-corner-border);
211
+ z-index: 20;
212
+ }
213
+
214
+ .rsp-col-resize-handle {
215
+ position: absolute;
216
+ right: -2px;
217
+ top: 0;
218
+ bottom: 0;
219
+ width: 5px;
220
+ cursor: col-resize;
221
+ z-index: 10;
222
+ }
223
+
224
+ .rsp-row-resize-handle {
225
+ position: absolute;
226
+ left: 0;
227
+ right: 0;
228
+ bottom: -2px;
229
+ height: 5px;
230
+ cursor: row-resize;
231
+ z-index: 10;
232
+ }
233
+
234
+ /* Overlays --------------------------------------------------------- */
235
+
236
+ .rsp-overlays {
237
+ position: absolute;
238
+ inset: 0;
239
+ pointer-events: none;
240
+ z-index: 100;
241
+ }
242
+
243
+ .rsp-overlays > * {
244
+ pointer-events: auto;
245
+ }
246
+
247
+ .rsp-overlay-placeholder {
248
+ position: absolute;
249
+ opacity: 0.5;
250
+ pointer-events: none;
251
+ border: 2px dashed var(--rsp-accent);
252
+ border-radius: 4px;
253
+ background-color: var(--rsp-focus-ring);
254
+ }
255
+
256
+ .rsp-overlay-placeholder-label {
257
+ position: absolute;
258
+ top: 50%;
259
+ left: 50%;
260
+ transform: translate(-50%, -50%);
261
+ color: var(--rsp-accent);
262
+ font-weight: 600;
263
+ font-size: 14px;
264
+ white-space: nowrap;
265
+ pointer-events: none;
266
+ }
267
+
268
+ /* ------------------------------------------------------------------ *
269
+ * Workbook chrome (sheet tabs, toolbar)
270
+ * ------------------------------------------------------------------ */
271
+
272
+ .rsp-workbook {
273
+ display: flex;
274
+ flex-direction: column;
275
+ height: 100%;
276
+ width: 100%;
277
+ background: var(--rsp-bg);
278
+ color: var(--rsp-text);
279
+ font-family: var(--rsp-font-family);
280
+ }
281
+
282
+ .rsp-workbook-header {
283
+ display: flex;
284
+ align-items: center;
285
+ gap: 8px;
286
+ padding: 6px 8px;
287
+ border-bottom: 1px solid var(--rsp-border);
288
+ background: var(--rsp-header-bg);
289
+ flex: none;
290
+ }
291
+
292
+ .rsp-workbook-name {
293
+ font-weight: 600;
294
+ font-size: 13px;
295
+ color: var(--rsp-header-text);
296
+ white-space: nowrap;
297
+ }
298
+
299
+ .rsp-workbook-body {
300
+ position: relative;
301
+ flex: 1;
302
+ min-height: 0;
303
+ }
304
+
305
+ .rsp-sheet-tabs {
306
+ display: flex;
307
+ align-items: center;
308
+ gap: 4px;
309
+ padding: 4px 8px;
310
+ border-top: 1px solid var(--rsp-border);
311
+ background: var(--rsp-header-bg);
312
+ flex: none;
313
+ }
314
+
315
+ .rsp-sheet-tabs-scroll {
316
+ display: flex;
317
+ align-items: center;
318
+ gap: 4px;
319
+ overflow-x: auto;
320
+ scrollbar-width: thin;
321
+ }
322
+
323
+ .rsp-sheet-tab {
324
+ display: inline-flex;
325
+ align-items: center;
326
+ gap: 6px;
327
+ padding: 4px 10px;
328
+ border: 1px solid transparent;
329
+ border-radius: var(--rsp-radius);
330
+ font-size: 12px;
331
+ line-height: 1.4;
332
+ cursor: pointer;
333
+ white-space: nowrap;
334
+ color: var(--rsp-header-text);
335
+ background: transparent;
336
+ }
337
+
338
+ .rsp-sheet-tab:hover {
339
+ background: var(--rsp-hover-bg);
340
+ }
341
+
342
+ .rsp-sheet-tab.rsp-sheet-tab-active {
343
+ background: var(--rsp-bg);
344
+ border-color: var(--rsp-border);
345
+ color: var(--rsp-text);
346
+ font-weight: 600;
347
+ box-shadow: inset 0 -2px 0 var(--rsp-accent);
348
+ }
349
+
350
+ /* Per-tab action buttons reveal on hover / when the tab is active. */
351
+ .rsp-sheet-tab .sheet-tab-actions {
352
+ display: flex;
353
+ align-items: center;
354
+ gap: 2px;
355
+ opacity: 0;
356
+ transition: opacity 0.15s;
357
+ }
358
+
359
+ .rsp-sheet-tab:hover .sheet-tab-actions,
360
+ .rsp-sheet-tab.rsp-sheet-tab-active .sheet-tab-actions,
361
+ .rsp-sheet-tab:focus-within .sheet-tab-actions {
362
+ opacity: 1;
363
+ }
364
+
365
+ .rsp-sheet-tab-input {
366
+ font: inherit;
367
+ color: inherit;
368
+ background: var(--rsp-editor-bg);
369
+ border: 1px solid var(--rsp-accent);
370
+ border-radius: 4px;
371
+ padding: 1px 4px;
372
+ min-width: 80px;
373
+ }
374
+
375
+ .rsp-icon-button {
376
+ display: inline-flex;
377
+ align-items: center;
378
+ justify-content: center;
379
+ width: 22px;
380
+ height: 22px;
381
+ padding: 0;
382
+ border: none;
383
+ border-radius: 4px;
384
+ background: transparent;
385
+ color: var(--rsp-muted-text);
386
+ cursor: pointer;
387
+ font-size: 14px;
388
+ line-height: 1;
389
+ }
390
+
391
+ .rsp-icon-button:hover {
392
+ background: var(--rsp-hover-bg);
393
+ color: var(--rsp-text);
394
+ }
395
+
396
+ .rsp-icon-button.rsp-icon-button-delete:hover {
397
+ background: var(--rsp-danger-bg);
398
+ color: var(--rsp-danger);
399
+ }
400
+
401
+ .rsp-add-sheet-button {
402
+ display: inline-flex;
403
+ align-items: center;
404
+ justify-content: center;
405
+ gap: 4px;
406
+ height: 24px;
407
+ padding: 0 10px;
408
+ border: 1px dashed var(--rsp-border);
409
+ border-radius: var(--rsp-radius);
410
+ background: transparent;
411
+ color: var(--rsp-header-text);
412
+ font-size: 12px;
413
+ cursor: pointer;
414
+ white-space: nowrap;
415
+ }
416
+
417
+ .rsp-add-sheet-button:hover {
418
+ border-color: var(--rsp-accent);
419
+ color: var(--rsp-accent);
420
+ }
421
+
422
+ .rsp-zoom-controls {
423
+ display: inline-flex;
424
+ align-items: center;
425
+ gap: 2px;
426
+ margin-left: auto;
427
+ padding-left: 8px;
428
+ }
429
+
430
+ .rsp-zoom-value {
431
+ min-width: 44px;
432
+ text-align: center;
433
+ font-size: 12px;
434
+ color: var(--rsp-muted-text);
435
+ font-variant-numeric: tabular-nums;
436
+ }
437
+
438
+ /* Formula bar ------------------------------------------------------ */
439
+
440
+ .rsp-formula-bar {
441
+ display: flex;
442
+ align-items: center;
443
+ gap: 8px;
444
+ padding: 4px 8px;
445
+ border-bottom: 1px solid var(--rsp-border);
446
+ background: var(--rsp-bg);
447
+ flex: none;
448
+ }
449
+
450
+ .rsp-name-box {
451
+ min-width: 84px;
452
+ padding: 3px 8px;
453
+ border: 1px solid var(--rsp-border);
454
+ border-radius: 4px;
455
+ background: var(--rsp-header-bg);
456
+ font-family: var(--rsp-mono-font-family);
457
+ font-size: 12px;
458
+ text-align: center;
459
+ color: var(--rsp-text);
460
+ }
461
+
462
+ .rsp-formula-input {
463
+ flex: 1;
464
+ min-width: 0;
465
+ padding: 3px 8px;
466
+ border: 1px solid var(--rsp-border);
467
+ border-radius: 4px;
468
+ background: var(--rsp-editor-bg);
469
+ color: var(--rsp-text);
470
+ font-family: var(--rsp-mono-font-family);
471
+ font-size: 12px;
472
+ }
473
+
474
+ .rsp-formula-input:focus {
475
+ outline: none;
476
+ border-color: var(--rsp-accent);
477
+ box-shadow: 0 0 0 2px var(--rsp-focus-ring);
478
+ }
479
+
480
+ .rsp-formula-input:disabled {
481
+ opacity: 0.6;
482
+ cursor: not-allowed;
483
+ }
@@ -0,0 +1,51 @@
1
+ import type { SelectionManager } from "@ricsam/selection-manager";
2
+ import type { SerializedCellValue } from "../types";
3
+ export type CellDataUpdate = {
4
+ rowIndex: number;
5
+ colIndex: number;
6
+ value: string;
7
+ };
8
+ export interface ExtractedCells {
9
+ width: number;
10
+ height: number;
11
+ cells: {
12
+ relative: {
13
+ rowIndex: number;
14
+ columnIndex: number;
15
+ };
16
+ absolute: {
17
+ rowIndex: number;
18
+ columnIndex: number;
19
+ };
20
+ value: SerializedCellValue;
21
+ key: string;
22
+ }[];
23
+ }
24
+ /**
25
+ * Shared clipboard helpers for grid selections.
26
+ *
27
+ * Subclasses decide *what* is copied (raw values, formulas, styles) while this
28
+ * base class owns the geometry of the selection and the TSV serialization that
29
+ * external apps such as Excel and Google Sheets expect.
30
+ */
31
+ export declare class ClipboardUtils {
32
+ /**
33
+ * Flattens the current selection into a rectangular block plus per-cell
34
+ * coordinates relative to the top-left of that block.
35
+ */
36
+ extractCellsFromSelection(selectionManager: SelectionManager, cellData: Map<string, SerializedCellValue>): ExtractedCells | undefined;
37
+ createExportGrid(width: number, height: number): SerializedCellValue[][];
38
+ /**
39
+ * Serializes a grid into the TSV form used on the clipboard.
40
+ *
41
+ * Exposed separately from {@link writeToOsClipboard} so callers can compute a
42
+ * signature for round-trip detection ("did this paste originate here?")
43
+ * without touching the system clipboard.
44
+ */
45
+ getTsvString(grid: SerializedCellValue[][]): string;
46
+ /**
47
+ * Writes a TSV representation to the OS clipboard and returns the string so
48
+ * callers can assert on it in tests or reuse it for an internal buffer.
49
+ */
50
+ writeToOsClipboard(grid: SerializedCellValue[][]): string;
51
+ }
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import { ViewportStream } from './types';
3
+ import type { GridChild, ResizeHandle } from './types';
4
+ type FrameChild = GridChild & {
5
+ snapToCells?: boolean;
6
+ };
7
+ interface FrameProps {
8
+ child: FrameChild;
9
+ isSelected: boolean;
10
+ zoom: number;
11
+ viewport$: ViewportStream;
12
+ onMove: (deltaX: number, deltaY: number) => void;
13
+ onResize: (deltaWidth: number, deltaHeight: number, shiftKey: boolean, handle: ResizeHandle) => void;
14
+ onSnapToCellsToggle?: (enabled: boolean) => void;
15
+ onReset: () => void;
16
+ onMinimize: () => void;
17
+ onMaximize?: () => void;
18
+ onClose: () => void;
19
+ onInteractionEnd?: () => void;
20
+ }
21
+ export declare function Frame({ child, isSelected, zoom, viewport$, onMove, onResize, onSnapToCellsToggle, onReset, onMinimize, onMaximize, onClose, onInteractionEnd }: FrameProps): React.JSX.Element | null;
22
+ export {};
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Inline icons.
3
+ *
4
+ * The library intentionally ships no icon dependency, so the handful of glyphs
5
+ * used by overlay frames are defined here as plain SVG. Paths are from the
6
+ * Material Design icon set (Apache-2.0).
7
+ */
8
+ import type { SVGProps } from "react";
9
+ type IconProps = Omit<SVGProps<SVGSVGElement>, "size"> & {
10
+ size?: number;
11
+ };
12
+ export declare function DeleteIcon({ size, ...props }: IconProps): import("react").JSX.Element;
13
+ export declare function GridOnIcon({ size, ...props }: IconProps): import("react").JSX.Element;
14
+ export declare function GridOffIcon({ size, ...props }: IconProps): import("react").JSX.Element;
15
+ export {};