socialcalc-ai 1.0.0

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 (41) hide show
  1. package/README.md +268 -0
  2. package/components/CellEditModal/CellEditModal.css +584 -0
  3. package/components/CellEditModal/CellEditModal.tsx +817 -0
  4. package/components/DemoVideosModal/DemoVideosModal.css +307 -0
  5. package/components/DemoVideosModal/DemoVideosModal.tsx +289 -0
  6. package/components/EditableCellsModal/EditableCellsModal.css +346 -0
  7. package/components/EditableCellsModal/EditableCellsModal.tsx +555 -0
  8. package/components/HorizontalScrollBar.css +127 -0
  9. package/components/HorizontalScrollBar.tsx +140 -0
  10. package/components/RowActionPopover/RowActionPopover.css +95 -0
  11. package/components/RowActionPopover/RowActionPopover.tsx +82 -0
  12. package/core/constants.js +1039 -0
  13. package/core/core.js +7172 -0
  14. package/core/format-number.js +7034 -0
  15. package/core/formula.js +1675 -0
  16. package/core/index.js +28 -0
  17. package/core/spreadsheet-control.js +7646 -0
  18. package/core/table-editor.js +8156 -0
  19. package/index.d.ts +68 -0
  20. package/index.js +43 -0
  21. package/modules/device.js +22 -0
  22. package/modules/editable-cells.js +230 -0
  23. package/modules/exporters.js +22 -0
  24. package/modules/formatting.js +258 -0
  25. package/modules/grid-lines.js +127 -0
  26. package/modules/history.js +26 -0
  27. package/modules/horizontal-scroll.js +335 -0
  28. package/modules/init.js +94 -0
  29. package/modules/invoice.js +558 -0
  30. package/modules/listeners.js +531 -0
  31. package/modules/logos.js +191 -0
  32. package/modules/plugin-manager.js +208 -0
  33. package/modules/prompts.js +170 -0
  34. package/modules/row-col-headers.js +514 -0
  35. package/modules/sheets.js +151 -0
  36. package/modules/touch-scroll.js +474 -0
  37. package/modules/utils.js +317 -0
  38. package/modules/weight.js +287 -0
  39. package/package.json +67 -0
  40. package/utils/imageCompressor.ts +118 -0
  41. package/utils/scriptLoader.ts +39 -0
@@ -0,0 +1,817 @@
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import * as AppGeneral from '../../index.js';
3
+ import { compressImage } from '../../utils/imageCompressor';
4
+ import './CellEditModal.css';
5
+
6
+ export interface CellData {
7
+ coord: string;
8
+ text: string;
9
+ okfn: (value: string) => void;
10
+ cleanup?: () => void;
11
+ }
12
+
13
+ export interface CellEditModalProps {
14
+ isOpen: boolean;
15
+ onClose: () => void;
16
+ cellData: CellData | null;
17
+ /** Optional custom title */
18
+ title?: string;
19
+ }
20
+
21
+ export const FONT_COLORS = [
22
+ { name: 'Default', color: null, value: null },
23
+ { name: 'Black', color: '#000000', value: 'black' },
24
+ { name: 'White', color: '#ffffff', value: 'white' },
25
+ { name: 'Red', color: '#ef4444', value: 'red' },
26
+ { name: 'Blue', color: '#3b82f6', value: 'blue' },
27
+ { name: 'Green', color: '#22c55e', value: 'green' },
28
+ { name: 'Yellow', color: '#eab308', value: 'yellow' },
29
+ { name: 'Purple', color: '#a855f7', value: 'purple' },
30
+ ];
31
+
32
+ export const BG_COLORS = [
33
+ { name: 'Default', color: null, value: null },
34
+ { name: 'White', color: '#ffffff', value: 'white' },
35
+ { name: 'Light Gray', color: '#d1d5db', value: 'lightgray' },
36
+ { name: 'Light Blue', color: '#bfdbfe', value: 'lightblue' },
37
+ { name: 'Light Green', color: '#bbf7d0', value: 'lightgreen' },
38
+ { name: 'Light Yellow', color: '#fef08a', value: 'lightyellow' },
39
+ { name: 'Light Pink', color: '#fbcfe8', value: 'lightpink' },
40
+ ];
41
+
42
+ export const BORDER_POSITIONS = [
43
+ { id: 'all', label: 'All' },
44
+ { id: 'top', label: 'Top' },
45
+ { id: 'bottom', label: 'Bottom' },
46
+ { id: 'left', label: 'Left' },
47
+ { id: 'right', label: 'Right' },
48
+ { id: 'none', label: 'None' },
49
+ ];
50
+
51
+ export const BORDER_STYLES = ['solid', 'dashed', 'dotted', 'double'];
52
+ export const BORDER_WIDTHS = ['1px', '2px', '3px', '4px'];
53
+ export const BORDER_COLORS = [
54
+ { name: 'Black', color: '#000000' },
55
+ { name: 'Slate', color: '#334155' },
56
+ { name: 'Light Gray', color: '#cbd5e1' },
57
+ { name: 'Blue', color: '#2563eb' },
58
+ { name: 'Red', color: '#dc2626' },
59
+ { name: 'Green', color: '#16a34a' },
60
+ ];
61
+
62
+ export interface CellFormatOption {
63
+ id: string;
64
+ name: string;
65
+ formatString: string;
66
+ category: 'general' | 'number' | 'currency' | 'percent' | 'date' | 'time';
67
+ preview: string;
68
+ }
69
+
70
+ export const CELL_FORMAT_CATEGORIES = [
71
+ { id: 'all', label: 'All' },
72
+ { id: 'number', label: 'Number' },
73
+ { id: 'currency', label: 'Currency' },
74
+ { id: 'percent', label: 'Percent' },
75
+ { id: 'date', label: 'Date' },
76
+ { id: 'time', label: 'Time' },
77
+ ];
78
+
79
+ export const POPULAR_CELL_FORMATS: CellFormatOption[] = [
80
+ // General
81
+ { id: 'default', name: 'Default', formatString: '', category: 'general', preview: 'Automatic' },
82
+ { id: 'auto_commas', name: 'Auto w/ commas', formatString: '[,]General', category: 'general', preview: '1,234.56' },
83
+
84
+ // Numbers
85
+ { id: 'num_2dec', name: '1,234.56', formatString: '#,##0.00', category: 'number', preview: '1,234.56' },
86
+ { id: 'num_int', name: '1,234', formatString: '#,##0', category: 'number', preview: '1,234' },
87
+ { id: 'num_1dec', name: '1,234.5', formatString: '#,##0.0', category: 'number', preview: '1,234.5' },
88
+ { id: 'num_plain', name: '1234', formatString: '0', category: 'number', preview: '1234' },
89
+ { id: 'num_parens', name: '(1,234.56)', formatString: '#,##0.00_);(#,##0.00)', category: 'number', preview: '(1,234.56)' },
90
+
91
+ // Currency
92
+ { id: 'curr_2dec', name: '$1,234.56', formatString: '$#,##0.00', category: 'currency', preview: '$1,234.56' },
93
+ { id: 'curr_whole', name: '$1,234', formatString: '$#,##0', category: 'currency', preview: '$1,234' },
94
+ { id: 'curr_1dec', name: '$1,234.5', formatString: '$#,##0.0', category: 'currency', preview: '$1,234.5' },
95
+ { id: 'curr_parens', name: '($1,234.56)', formatString: '$#,##0.00_);($#,##0.00)', category: 'currency', preview: '($1,234.56)' },
96
+
97
+ // Percent
98
+ { id: 'pct_whole', name: '1,234%', formatString: '#,##0%', category: 'percent', preview: '12%' },
99
+ { id: 'pct_1dec', name: '1,234.5%', formatString: '#,##0.0%', category: 'percent', preview: '12.5%' },
100
+ { id: 'pct_2dec', name: '1,234.56%', formatString: '#,##0.00%', category: 'percent', preview: '12.34%' },
101
+
102
+ // Date
103
+ { id: 'date_us', name: '01/04/2006', formatString: 'mm/dd/yyyy', category: 'date', preview: '01/04/2026' },
104
+ { id: 'date_short', name: '1/4/06', formatString: 'm/d/yy', category: 'date', preview: '1/4/26' },
105
+ { id: 'date_iso', name: '2006-01-04', formatString: 'yyyy-mm-dd', category: 'date', preview: '2026-01-04' },
106
+ { id: 'date_med', name: '04-Jan-2006', formatString: 'dd-mmm-yyyy', category: 'date', preview: '04-Jan-2026' },
107
+ { id: 'date_long', name: 'January 4, 2006', formatString: 'mmmm d, yyyy', category: 'date', preview: 'January 4, 2026' },
108
+
109
+ // Time
110
+ { id: 'time_12h', name: '1:23 PM', formatString: 'h:mm AM/PM', category: 'time', preview: '1:23 PM' },
111
+ { id: 'time_24h_sec', name: '01:23:45', formatString: 'hh:mm:ss', category: 'time', preview: '01:23:45' },
112
+ { id: 'time_24h', name: '1:23', formatString: 'h:mm', category: 'time', preview: '1:23' },
113
+ ];
114
+
115
+ export const CellEditModal: React.FC<CellEditModalProps> = ({
116
+ isOpen,
117
+ onClose,
118
+ cellData,
119
+ title = 'Edit Cell'
120
+ }) => {
121
+ const [inputValue, setInputValue] = useState('');
122
+ const [keyboardHeight, setKeyboardHeight] = useState(0);
123
+
124
+ // Formatting state
125
+ const [selectedFontColor, setSelectedFontColor] = useState<any>(FONT_COLORS[0]);
126
+ const [selectedBgColor, setSelectedBgColor] = useState<any>(BG_COLORS[0]);
127
+ const [showOptions, setShowOptions] = useState(false);
128
+ const [showFontColors, setShowFontColors] = useState(false);
129
+ const [showBgColors, setShowBgColors] = useState(false);
130
+
131
+ // Cell format state
132
+ const [showFormatOptions, setShowFormatOptions] = useState(false);
133
+ const [selectedValueFormat, setSelectedValueFormat] = useState<CellFormatOption>(POPULAR_CELL_FORMATS[0]);
134
+ const [selectedFormatCategory, setSelectedFormatCategory] = useState<string>('all');
135
+ const [isValueFormatChanged, setIsValueFormatChanged] = useState(false);
136
+
137
+ // Border state
138
+ const [showBorderOptions, setShowBorderOptions] = useState(false);
139
+ const [borderPlacement, setBorderPlacement] = useState<string>('none');
140
+ const [borderStyle, setBorderStyle] = useState<string>('solid');
141
+ const [borderWidth, setBorderWidth] = useState<string>('1px');
142
+ const [borderColor, setBorderColor] = useState<string>('#000000');
143
+
144
+ // Change tracking flags - ensure editing one property does not alter or disrupt others
145
+ const [isFontColorChanged, setIsFontColorChanged] = useState(false);
146
+ const [isBgColorChanged, setIsBgColorChanged] = useState(false);
147
+ const [isBorderChanged, setIsBorderChanged] = useState(false);
148
+
149
+ // Logo / Image state
150
+ const [imagePreview, setImagePreview] = useState<string | null>(null);
151
+ const [isProcessingImage, setIsProcessingImage] = useState<boolean>(false);
152
+ const fileInputRef = useRef<HTMLInputElement>(null);
153
+
154
+ const inputRef = useRef<HTMLInputElement>(null);
155
+
156
+ // Keyboard offset detection via visualViewport
157
+ useEffect(() => {
158
+ if (!isOpen) {
159
+ setKeyboardHeight(0);
160
+ return;
161
+ }
162
+
163
+ const viewport = window.visualViewport;
164
+ if (!viewport) return;
165
+
166
+ const onViewportResize = () => {
167
+ const kbHeight = Math.max(
168
+ 0,
169
+ window.innerHeight - viewport.height - viewport.offsetTop
170
+ );
171
+ setKeyboardHeight(kbHeight);
172
+ };
173
+
174
+ viewport.addEventListener('resize', onViewportResize);
175
+ viewport.addEventListener('scroll', onViewportResize);
176
+ onViewportResize();
177
+
178
+ return () => {
179
+ viewport.removeEventListener('resize', onViewportResize);
180
+ viewport.removeEventListener('scroll', onViewportResize);
181
+ setKeyboardHeight(0);
182
+ };
183
+ }, [isOpen]);
184
+
185
+ // Sync cell data when modal opens
186
+ useEffect(() => {
187
+ if (isOpen && cellData) {
188
+ setInputValue(cellData.text || '');
189
+ setIsFontColorChanged(false);
190
+ setIsBgColorChanged(false);
191
+ setIsBorderChanged(false);
192
+ setIsValueFormatChanged(false);
193
+ setShowFormatOptions(false);
194
+ setSelectedFormatCategory('all');
195
+
196
+ let matchedFont = FONT_COLORS[0];
197
+ let matchedBg = BG_COLORS[0];
198
+ let matchedFormat = POPULAR_CELL_FORMATS[0];
199
+
200
+ try {
201
+ const formatting = AppGeneral.getCellFormatting
202
+ ? AppGeneral.getCellFormatting(cellData.coord)
203
+ : null;
204
+ if (formatting) {
205
+ if (formatting.color) {
206
+ const fColorLower = formatting.color.toLowerCase().trim();
207
+ const found = FONT_COLORS.find(
208
+ (c) =>
209
+ (c.value && c.value.toLowerCase() === fColorLower) ||
210
+ (c.color && c.color.toLowerCase() === fColorLower)
211
+ );
212
+ matchedFont = found || { name: 'Current', color: formatting.color, value: formatting.color };
213
+ }
214
+ if (formatting.bgcolor) {
215
+ const bColorLower = formatting.bgcolor.toLowerCase().trim();
216
+ const found = BG_COLORS.find(
217
+ (c) =>
218
+ (c.value && c.value.toLowerCase() === bColorLower) ||
219
+ (c.color && c.color.toLowerCase() === bColorLower)
220
+ );
221
+ matchedBg = found || { name: 'Current', color: formatting.bgcolor, value: formatting.bgcolor };
222
+ }
223
+
224
+ if (formatting.valueFormat) {
225
+ const rawFmt = formatting.valueFormat.trim();
226
+ const foundFmt = POPULAR_CELL_FORMATS.find(
227
+ (f) => f.formatString === rawFmt || f.formatString.toLowerCase() === rawFmt.toLowerCase()
228
+ );
229
+ matchedFormat = foundFmt || {
230
+ id: 'custom',
231
+ name: rawFmt,
232
+ formatString: rawFmt,
233
+ category: 'number',
234
+ preview: rawFmt,
235
+ };
236
+ }
237
+
238
+ // Inspect cell borders
239
+ const b = formatting.borders;
240
+ const topB = b?.top;
241
+ const bottomB = b?.bottom;
242
+ const leftB = b?.left;
243
+ const rightB = b?.right;
244
+
245
+ let detectedPlacement = 'none';
246
+ if (topB && bottomB && leftB && rightB) {
247
+ detectedPlacement = 'all';
248
+ } else if (bottomB && !topB && !leftB && !rightB) {
249
+ detectedPlacement = 'bottom';
250
+ } else if (topB && !bottomB && !leftB && !rightB) {
251
+ detectedPlacement = 'top';
252
+ } else if (leftB && !topB && !bottomB && !rightB) {
253
+ detectedPlacement = 'left';
254
+ } else if (rightB && !topB && !bottomB && !leftB) {
255
+ detectedPlacement = 'right';
256
+ } else if (bottomB) {
257
+ detectedPlacement = 'bottom';
258
+ } else if (topB) {
259
+ detectedPlacement = 'top';
260
+ }
261
+
262
+ setBorderPlacement(detectedPlacement);
263
+
264
+ const existingBorderStr = bottomB || topB || leftB || rightB;
265
+ if (existingBorderStr) {
266
+ const match = existingBorderStr.match(/(\S+)\s+(\S+)\s+(\S.+)/);
267
+ if (match) {
268
+ const w = match[1];
269
+ const s = match[2];
270
+ const c = match[3];
271
+ if (BORDER_WIDTHS.includes(w)) {
272
+ setBorderWidth(w);
273
+ } else if (w === 'thin') {
274
+ setBorderWidth('1px');
275
+ } else if (w === 'medium') {
276
+ setBorderWidth('2px');
277
+ } else if (w === 'thick') {
278
+ setBorderWidth('3px');
279
+ }
280
+ if (BORDER_STYLES.includes(s.toLowerCase())) {
281
+ setBorderStyle(s.toLowerCase());
282
+ }
283
+ if (c) {
284
+ setBorderColor(c);
285
+ }
286
+ }
287
+ } else {
288
+ setBorderWidth('1px');
289
+ setBorderStyle('solid');
290
+ setBorderColor('#000000');
291
+ }
292
+ } else {
293
+ setBorderPlacement('none');
294
+ setBorderWidth('1px');
295
+ setBorderStyle('solid');
296
+ setBorderColor('#000000');
297
+ }
298
+ } catch (e) {
299
+ console.warn('Could not read cell formatting:', e);
300
+ setBorderPlacement('none');
301
+ }
302
+ setSelectedFontColor(matchedFont);
303
+ setSelectedBgColor(matchedBg);
304
+ setSelectedValueFormat(matchedFormat);
305
+ setShowFontColors(false);
306
+ setShowBgColors(false);
307
+ setShowBorderOptions(false);
308
+ setShowFormatOptions(false);
309
+ setImagePreview(null);
310
+
311
+ // Check if current cell already contains an embedded image
312
+ if (cellData.text && cellData.text.includes('<img')) {
313
+ const match = cellData.text.match(/src=["']([^"']+)["']/i);
314
+ if (match && match[1]) {
315
+ setImagePreview(match[1]);
316
+ }
317
+ }
318
+
319
+ // Auto-focus input timed with iOS keyboard
320
+ setTimeout(() => {
321
+ if (inputRef.current) {
322
+ inputRef.current.focus();
323
+ }
324
+ }, 240);
325
+ }
326
+ }, [isOpen, cellData]);
327
+
328
+ if (!isOpen || !cellData) return null;
329
+
330
+ const handleClear = () => {
331
+ setInputValue('');
332
+ setImagePreview(null);
333
+ inputRef.current?.focus();
334
+ };
335
+
336
+ const handleImageSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
337
+ const file = e.target.files?.[0];
338
+ if (!file) return;
339
+
340
+ try {
341
+ setIsProcessingImage(true);
342
+ // Downscale & compress image to ~30-40 KB target
343
+ const compressedDataUrl = await compressImage(file, 40 * 1024);
344
+ setImagePreview(compressedDataUrl);
345
+ // Construct img tag for the cell
346
+ const imgTag = `<img src="${compressedDataUrl}" style="max-height:80px;max-width:100%;object-fit:contain;" alt="Logo" />`;
347
+ setInputValue(imgTag);
348
+ } catch (error) {
349
+ console.error('Error compressing image:', error);
350
+ } finally {
351
+ setIsProcessingImage(false);
352
+ if (fileInputRef.current) {
353
+ fileInputRef.current.value = '';
354
+ }
355
+ }
356
+ };
357
+
358
+ const handleRemoveImage = () => {
359
+ setImagePreview(null);
360
+ setInputValue('');
361
+ };
362
+
363
+ const handleApply = () => {
364
+ if (!cellData) return;
365
+
366
+ // Only construct formatting for properties that were explicitly modified by the user
367
+ const formatting: any = {};
368
+
369
+ if (isFontColorChanged) {
370
+ // If user selected 'Default', value is null -> pass "" to clear/reset color
371
+ formatting.fontColor = selectedFontColor?.value ?? '';
372
+ }
373
+
374
+ if (isBgColorChanged) {
375
+ // If user selected 'Default', value is null -> pass "" to clear/reset bgcolor
376
+ formatting.bgColor = selectedBgColor?.value ?? '';
377
+ }
378
+
379
+ if (isBorderChanged) {
380
+ let bordersConfig: { top?: string; bottom?: string; left?: string; right?: string } = {};
381
+ if (borderPlacement === 'none') {
382
+ // Clear all borders on cell
383
+ bordersConfig = { top: '', bottom: '', left: '', right: '' };
384
+ } else if (borderPlacement === 'all') {
385
+ const borderDef = `${borderWidth} ${borderStyle} ${borderColor}`;
386
+ bordersConfig = { top: borderDef, bottom: borderDef, left: borderDef, right: borderDef };
387
+ } else if (borderPlacement === 'top') {
388
+ const borderDef = `${borderWidth} ${borderStyle} ${borderColor}`;
389
+ bordersConfig = { top: borderDef, bottom: '', left: '', right: '' };
390
+ } else if (borderPlacement === 'bottom') {
391
+ const borderDef = `${borderWidth} ${borderStyle} ${borderColor}`;
392
+ bordersConfig = { top: '', bottom: borderDef, left: '', right: '' };
393
+ } else if (borderPlacement === 'left') {
394
+ const borderDef = `${borderWidth} ${borderStyle} ${borderColor}`;
395
+ bordersConfig = { top: '', bottom: '', left: borderDef, right: '' };
396
+ } else if (borderPlacement === 'right') {
397
+ const borderDef = `${borderWidth} ${borderStyle} ${borderColor}`;
398
+ bordersConfig = { top: '', bottom: '', left: '', right: borderDef };
399
+ }
400
+ formatting.borders = bordersConfig;
401
+ }
402
+
403
+ if (isValueFormatChanged) {
404
+ // If default/empty, SocialCalc clears nontextvalueformat
405
+ formatting.valueFormat = selectedValueFormat ? selectedValueFormat.formatString : '';
406
+ }
407
+
408
+ try {
409
+ if (AppGeneral.updateCellValueAndFormat) {
410
+ AppGeneral.updateCellValueAndFormat(cellData.coord, inputValue, formatting);
411
+ } else {
412
+ if (cellData.okfn) {
413
+ cellData.okfn(inputValue);
414
+ }
415
+ }
416
+ } catch (error) {
417
+ console.error('Error applying cell changes:', error);
418
+ if (cellData.okfn) cellData.okfn(inputValue);
419
+ }
420
+
421
+ if (cellData.cleanup) cellData.cleanup();
422
+ onClose();
423
+ };
424
+
425
+ const handleCancel = () => {
426
+ if (cellData?.cleanup) cellData.cleanup();
427
+ onClose();
428
+ };
429
+
430
+ return (
431
+ <div
432
+ className="sc-cell-modal-backdrop"
433
+ onClick={(e) => {
434
+ if (e.target === e.currentTarget) handleCancel();
435
+ }}
436
+ style={keyboardHeight > 0 ? { paddingBottom: `${keyboardHeight + 16}px` } : undefined}
437
+ >
438
+ <div className="sc-cell-modal-dialog" role="dialog" aria-modal="true">
439
+ <div className="sc-cell-modal-content">
440
+ {/* Header */}
441
+ <div className="sc-cell-header">
442
+ <div className="sc-cell-title-group">
443
+ <h2 className="sc-cell-title">{title}</h2>
444
+ <span className="sc-cell-coord-badge">{cellData.coord}</span>
445
+ </div>
446
+ <button
447
+ type="button"
448
+ className="sc-cell-close-btn"
449
+ onClick={handleCancel}
450
+ aria-label="Close"
451
+ >
452
+ &times;
453
+ </button>
454
+ </div>
455
+
456
+ {/* Input Section */}
457
+ <div className="sc-cell-input-section">
458
+ <input
459
+ ref={inputRef}
460
+ value={inputValue}
461
+ onChange={(e) => setInputValue(e.target.value)}
462
+ onKeyDown={(e) => {
463
+ e.stopPropagation();
464
+ if (e.key === 'Enter') handleApply();
465
+ if (e.key === 'Escape') handleCancel();
466
+ }}
467
+ placeholder="Enter value"
468
+ className="sc-cell-input"
469
+ />
470
+ {inputValue.length > 0 && (
471
+ <button
472
+ type="button"
473
+ className="sc-cell-clear-btn"
474
+ onClick={handleClear}
475
+ aria-label="Clear Input"
476
+ >
477
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
478
+ <line x1="18" y1="6" x2="6" y2="18"></line>
479
+ <line x1="6" y1="6" x2="18" y2="18"></line>
480
+ </svg>
481
+ </button>
482
+ )}
483
+ </div>
484
+
485
+ {/* Image / Logo Preview if present */}
486
+ {imagePreview && (
487
+ <div className="sc-image-preview-card">
488
+ <img src={imagePreview} alt="Cell Logo" className="sc-image-preview-thumb" />
489
+ <div className="sc-image-preview-info">
490
+ <span className="sc-image-preview-label">Image attached</span>
491
+ <button type="button" className="sc-image-remove-btn" onClick={handleRemoveImage}>
492
+ Remove
493
+ </button>
494
+ </div>
495
+ </div>
496
+ )}
497
+
498
+ {/* Options Accordion */}
499
+ <div className="sc-cell-accordion">
500
+ <button
501
+ type="button"
502
+ className="sc-accordion-toggle"
503
+ onClick={() => setShowOptions(!showOptions)}
504
+ >
505
+ <span>Options</span>
506
+ <span>{showOptions ? '▲' : '▼'}</span>
507
+ </button>
508
+
509
+ {showOptions && (
510
+ <div className="sc-accordion-body">
511
+ {/* 1. Font Color */}
512
+ <div className="sc-property-row">
513
+ <button
514
+ type="button"
515
+ className="sc-property-btn"
516
+ onClick={() => {
517
+ setShowFontColors(!showFontColors);
518
+ setShowBgColors(false);
519
+ setShowBorderOptions(false);
520
+ setShowFormatOptions(false);
521
+ }}
522
+ >
523
+ <div className="sc-property-btn-left">
524
+ <span>Text Color</span>
525
+ </div>
526
+ <div className="sc-property-btn-right">
527
+ {selectedFontColor?.color && (
528
+ <div className="sc-color-dot" style={{ background: selectedFontColor.color }} />
529
+ )}
530
+ <span>{selectedFontColor ? selectedFontColor.name : 'Default'}</span>
531
+ <span>{showFontColors ? '▲' : '▼'}</span>
532
+ </div>
533
+ </button>
534
+ {showFontColors && (
535
+ <div className="sc-color-grid">
536
+ {FONT_COLORS.map((c) => (
537
+ <div
538
+ key={c.name}
539
+ className={`sc-color-swatch ${selectedFontColor?.name === c.name ? 'selected' : ''} ${!c.color ? 'default-swatch' : ''}`}
540
+ style={c.color ? { background: c.color } : {}}
541
+ onClick={() => {
542
+ setSelectedFontColor(c);
543
+ setIsFontColorChanged(true);
544
+ setShowFontColors(false);
545
+ }}
546
+ title={c.name}
547
+ />
548
+ ))}
549
+ </div>
550
+ )}
551
+ </div>
552
+
553
+ {/* 2. Background Color */}
554
+ <div className="sc-property-row">
555
+ <button
556
+ type="button"
557
+ className="sc-property-btn"
558
+ onClick={() => {
559
+ setShowBgColors(!showBgColors);
560
+ setShowFontColors(false);
561
+ setShowBorderOptions(false);
562
+ setShowFormatOptions(false);
563
+ }}
564
+ >
565
+ <div className="sc-property-btn-left">
566
+ <span>Background Color</span>
567
+ </div>
568
+ <div className="sc-property-btn-right">
569
+ {selectedBgColor?.color && (
570
+ <div className="sc-color-dot" style={{ background: selectedBgColor.color }} />
571
+ )}
572
+ <span>{selectedBgColor ? selectedBgColor.name : 'Default'}</span>
573
+ <span>{showBgColors ? '▲' : '▼'}</span>
574
+ </div>
575
+ </button>
576
+ {showBgColors && (
577
+ <div className="sc-color-grid">
578
+ {BG_COLORS.map((c) => (
579
+ <div
580
+ key={c.name}
581
+ className={`sc-color-swatch ${selectedBgColor?.name === c.name ? 'selected' : ''} ${!c.color ? 'default-swatch' : ''}`}
582
+ style={c.color ? { background: c.color } : {}}
583
+ onClick={() => {
584
+ setSelectedBgColor(c);
585
+ setIsBgColorChanged(true);
586
+ setShowBgColors(false);
587
+ }}
588
+ title={c.name}
589
+ />
590
+ ))}
591
+ </div>
592
+ )}
593
+ </div>
594
+
595
+ {/* 3. Number & Date Format */}
596
+ <div className="sc-property-row">
597
+ <button
598
+ type="button"
599
+ className="sc-property-btn"
600
+ onClick={() => {
601
+ setShowFormatOptions(!showFormatOptions);
602
+ setShowFontColors(false);
603
+ setShowBgColors(false);
604
+ setShowBorderOptions(false);
605
+ }}
606
+ >
607
+ <div className="sc-property-btn-left">
608
+ <span>Format</span>
609
+ </div>
610
+ <div className="sc-property-btn-right">
611
+ <span className="sc-format-current-badge">
612
+ {selectedValueFormat ? selectedValueFormat.name : 'Default'}
613
+ </span>
614
+ <span>{showFormatOptions ? '▲' : '▼'}</span>
615
+ </div>
616
+ </button>
617
+
618
+ {showFormatOptions && (
619
+ <div className="sc-format-controls">
620
+ {/* Category Filter Pills */}
621
+ <div className="sc-format-category-pills">
622
+ {CELL_FORMAT_CATEGORIES.map((cat) => (
623
+ <button
624
+ key={cat.id}
625
+ type="button"
626
+ className={`sc-format-pill-btn ${selectedFormatCategory === cat.id ? 'active' : ''}`}
627
+ onClick={() => setSelectedFormatCategory(cat.id)}
628
+ >
629
+ {cat.label}
630
+ </button>
631
+ ))}
632
+ </div>
633
+
634
+ {/* Format Options Grid */}
635
+ <div className="sc-format-options-grid">
636
+ {POPULAR_CELL_FORMATS
637
+ .filter(
638
+ (f) =>
639
+ selectedFormatCategory === 'all' ||
640
+ f.category === selectedFormatCategory ||
641
+ f.id === 'default'
642
+ )
643
+ .map((format) => {
644
+ const isSelected =
645
+ selectedValueFormat?.formatString === format.formatString;
646
+ return (
647
+ <button
648
+ key={format.id}
649
+ type="button"
650
+ className={`sc-format-card-btn ${isSelected ? 'selected' : ''}`}
651
+ onClick={() => {
652
+ setSelectedValueFormat(format);
653
+ setIsValueFormatChanged(true);
654
+ }}
655
+ >
656
+ <span className="sc-format-card-name">{format.name}</span>
657
+ <span className="sc-format-card-preview">{format.preview}</span>
658
+ </button>
659
+ );
660
+ })}
661
+ </div>
662
+ </div>
663
+ )}
664
+ </div>
665
+
666
+ {/* 4. Borders Section */}
667
+ <div className="sc-property-row">
668
+ <button
669
+ type="button"
670
+ className="sc-property-btn"
671
+ onClick={() => {
672
+ setShowBorderOptions(!showBorderOptions);
673
+ setShowFontColors(false);
674
+ setShowBgColors(false);
675
+ setShowFormatOptions(false);
676
+ }}
677
+ >
678
+ <div className="sc-property-btn-left">
679
+ <span>Cell Borders</span>
680
+ </div>
681
+ <div className="sc-property-btn-right">
682
+ <span style={{ textTransform: 'capitalize' }}>{borderPlacement}</span>
683
+ <span>{showBorderOptions ? '▲' : '▼'}</span>
684
+ </div>
685
+ </button>
686
+
687
+ {showBorderOptions && (
688
+ <div className="sc-border-controls">
689
+ {/* Position Tabs */}
690
+ <div className="sc-border-position-tabs">
691
+ {BORDER_POSITIONS.map((pos) => (
692
+ <button
693
+ key={pos.id}
694
+ type="button"
695
+ className={`sc-border-pos-btn ${borderPlacement === pos.id ? 'active' : ''}`}
696
+ onClick={() => {
697
+ setBorderPlacement(pos.id);
698
+ setIsBorderChanged(true);
699
+ }}
700
+ >
701
+ {pos.label}
702
+ </button>
703
+ ))}
704
+ </div>
705
+
706
+ {borderPlacement !== 'none' && (
707
+ <>
708
+ {/* Style & Width */}
709
+ <div className="sc-border-inline-row">
710
+ <div className="sc-border-select-group">
711
+ <label>Style</label>
712
+ <select
713
+ value={borderStyle}
714
+ onChange={(e) => {
715
+ setBorderStyle(e.target.value);
716
+ setIsBorderChanged(true);
717
+ }}
718
+ className="sc-border-select"
719
+ >
720
+ {BORDER_STYLES.map((st) => (
721
+ <option key={st} value={st}>{st}</option>
722
+ ))}
723
+ </select>
724
+ </div>
725
+
726
+ <div className="sc-border-select-group">
727
+ <label>Width</label>
728
+ <select
729
+ value={borderWidth}
730
+ onChange={(e) => {
731
+ setBorderWidth(e.target.value);
732
+ setIsBorderChanged(true);
733
+ }}
734
+ className="sc-border-select"
735
+ >
736
+ {BORDER_WIDTHS.map((w) => (
737
+ <option key={w} value={w}>{w}</option>
738
+ ))}
739
+ </select>
740
+ </div>
741
+ </div>
742
+
743
+ {/* Border Color */}
744
+ <div className="sc-border-color-row">
745
+ <label>Color</label>
746
+ <div className="sc-color-grid mini">
747
+ {BORDER_COLORS.map((c) => (
748
+ <div
749
+ key={c.name}
750
+ className={`sc-color-swatch ${borderColor === c.color ? 'selected' : ''}`}
751
+ style={{ background: c.color }}
752
+ onClick={() => {
753
+ setBorderColor(c.color);
754
+ setIsBorderChanged(true);
755
+ }}
756
+ title={c.name}
757
+ />
758
+ ))}
759
+ </div>
760
+ </div>
761
+ </>
762
+ )}
763
+ </div>
764
+ )}
765
+ </div>
766
+
767
+ {/* 4. Logo / Image Upload Section */}
768
+ <div className="sc-property-row sc-logo-upload-row">
769
+ <input
770
+ type="file"
771
+ ref={fileInputRef}
772
+ accept="image/*"
773
+ onChange={handleImageSelect}
774
+ style={{ display: 'none' }}
775
+ />
776
+ <button
777
+ type="button"
778
+ className="sc-logo-upload-btn"
779
+ onClick={() => fileInputRef.current?.click()}
780
+ disabled={isProcessingImage}
781
+ >
782
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
783
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
784
+ <circle cx="8.5" cy="8.5" r="1.5"></circle>
785
+ <polyline points="21 15 16 10 5 21"></polyline>
786
+ </svg>
787
+ <span>{isProcessingImage ? 'Compressing...' : 'Insert Logo / Image'}</span>
788
+ </button>
789
+ </div>
790
+ </div>
791
+ )}
792
+ </div>
793
+
794
+ {/* Action Buttons */}
795
+ <div className="sc-cell-actions">
796
+ <button
797
+ type="button"
798
+ className="sc-cell-btn-cancel"
799
+ onClick={handleCancel}
800
+ >
801
+ Cancel
802
+ </button>
803
+ <button
804
+ type="button"
805
+ className="sc-cell-btn-apply"
806
+ onClick={handleApply}
807
+ >
808
+ Apply
809
+ </button>
810
+ </div>
811
+ </div>
812
+ </div>
813
+ </div>
814
+ );
815
+ };
816
+
817
+ export default CellEditModal;