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,555 @@
1
+ import React, { useState, useMemo, useEffect } from "react";
2
+ import {
3
+ IonModal,
4
+ IonHeader,
5
+ IonToolbar,
6
+ IonTitle,
7
+ IonButtons,
8
+ IonButton,
9
+ IonIcon,
10
+ IonContent,
11
+ IonList,
12
+ IonItem,
13
+ IonLabel,
14
+ IonToggle,
15
+ IonInput,
16
+ IonSelect,
17
+ IonSelectOption,
18
+ IonBadge,
19
+ IonCard,
20
+ IonCardContent,
21
+ IonSearchbar,
22
+ IonSegment,
23
+ IonSegmentButton,
24
+ } from "@ionic/react";
25
+ import {
26
+ closeOutline,
27
+ addOutline,
28
+ trashOutline,
29
+ checkboxOutline,
30
+ searchOutline,
31
+ layersOutline,
32
+ imageOutline,
33
+ documentTextOutline,
34
+ checkmarkCircleOutline,
35
+ } from "ionicons/icons";
36
+ import "./EditableCellsModal.css";
37
+
38
+ export interface EditableCellItem {
39
+ id: string;
40
+ sheet: string;
41
+ path: (string | number)[];
42
+ title: string;
43
+ cell: string;
44
+ type: "text" | "image" | "table" | "form" | string;
45
+ editable: boolean;
46
+ isNested?: boolean;
47
+ }
48
+
49
+ /**
50
+ * Pure helper: parse appMapping into flat list of cell mapping items for UI display
51
+ */
52
+ export function parseAppMappingToItems(appMapping: any, targetSheet?: string): EditableCellItem[] {
53
+ const items: EditableCellItem[] = [];
54
+ if (!appMapping || typeof appMapping !== "object") return items;
55
+
56
+ const sheets = targetSheet && targetSheet !== "all" && appMapping[targetSheet]
57
+ ? [targetSheet]
58
+ : Object.keys(appMapping);
59
+
60
+ for (const sheet of sheets) {
61
+ const sheetData = appMapping[sheet];
62
+ if (!sheetData || typeof sheetData !== "object") continue;
63
+
64
+ for (const [topKey, topVal] of Object.entries(sheetData)) {
65
+ const val: any = topVal;
66
+ if (!val || typeof val !== "object") continue;
67
+
68
+ // Case 1: Form with nested formContent
69
+ if (val.type === "form" && val.formContent && typeof val.formContent === "object") {
70
+ for (const [fieldKey, fieldVal] of Object.entries(val.formContent)) {
71
+ const fVal: any = fieldVal;
72
+ if (fVal && fVal.cell) {
73
+ items.push({
74
+ id: `${sheet}_${topKey}_${fieldKey}`,
75
+ sheet,
76
+ path: [sheet, topKey, "formContent", fieldKey],
77
+ title: `${topKey} → ${fieldKey}`,
78
+ cell: String(fVal.cell).toUpperCase().trim(),
79
+ type: fVal.type || "text",
80
+ editable: Boolean(fVal.editable),
81
+ isNested: true,
82
+ });
83
+ }
84
+ }
85
+ }
86
+ // Case 2: Table with columns
87
+ else if (val.type === "table" && val.col && typeof val.col === "object") {
88
+ for (const [colKey, colVal] of Object.entries(val.col)) {
89
+ const cVal: any = colVal;
90
+ if (cVal && cVal.cell) {
91
+ const rowRange = val.rows ? `${val.rows.start}-${val.rows.end}` : "";
92
+ items.push({
93
+ id: `${sheet}_${topKey}_${colKey}`,
94
+ sheet,
95
+ path: [sheet, topKey, "col", colKey],
96
+ title: `${topKey} → ${colKey}${rowRange ? ` (${rowRange})` : ""}`,
97
+ cell: String(cVal.cell).toUpperCase().trim(),
98
+ type: cVal.type || "text",
99
+ editable: Boolean(cVal.editable),
100
+ isNested: true,
101
+ });
102
+ }
103
+ }
104
+ }
105
+ // Case 3: Direct cell item (text, image, etc.)
106
+ else if (val.cell) {
107
+ items.push({
108
+ id: `${sheet}_${topKey}`,
109
+ sheet,
110
+ path: [sheet, topKey],
111
+ title: topKey,
112
+ cell: String(val.cell).toUpperCase().trim(),
113
+ type: val.type || "text",
114
+ editable: Boolean(val.editable),
115
+ isNested: false,
116
+ });
117
+ }
118
+ }
119
+ }
120
+
121
+ // Sort by cell coordinate alphanumeric
122
+ items.sort((a, b) => a.cell.localeCompare(b.cell, undefined, { numeric: true }));
123
+ return items;
124
+ }
125
+
126
+ /**
127
+ * Pure helper: toggle editable boolean for a given item in appMapping
128
+ */
129
+ export function toggleCellEditable(appMapping: any, item: EditableCellItem, newEditable: boolean): any {
130
+ const cloned = JSON.parse(JSON.stringify(appMapping || {}));
131
+ let curr = cloned;
132
+ for (let i = 0; i < item.path.length - 1; i++) {
133
+ const key = item.path[i];
134
+ if (!curr[key]) return cloned;
135
+ curr = curr[key];
136
+ }
137
+ const lastKey = item.path[item.path.length - 1];
138
+ if (curr[lastKey]) {
139
+ curr[lastKey].editable = newEditable;
140
+ }
141
+ return cloned;
142
+ }
143
+
144
+ /**
145
+ * Pure helper: delete a cell mapping item from appMapping
146
+ */
147
+ export function deleteCellMapping(appMapping: any, item: EditableCellItem): any {
148
+ const cloned = JSON.parse(JSON.stringify(appMapping || {}));
149
+ let curr = cloned;
150
+ for (let i = 0; i < item.path.length - 1; i++) {
151
+ const key = item.path[i];
152
+ if (!curr[key]) return cloned;
153
+ curr = curr[key];
154
+ }
155
+ const lastKey = item.path[item.path.length - 1];
156
+ if (curr && curr[lastKey]) {
157
+ delete curr[lastKey];
158
+ }
159
+ return cloned;
160
+ }
161
+
162
+ /**
163
+ * Pure helper: add a new cell mapping to appMapping
164
+ */
165
+ export function addCellMapping(
166
+ appMapping: any,
167
+ sheet: string,
168
+ entry: { cell: string; title: string; type?: string; editable?: boolean }
169
+ ): any {
170
+ const cloned = JSON.parse(JSON.stringify(appMapping || {}));
171
+ const sheetKey = sheet || "sheet1";
172
+ if (!cloned[sheetKey]) {
173
+ cloned[sheetKey] = {};
174
+ }
175
+
176
+ const cleanCell = entry.cell.toUpperCase().trim();
177
+ // Create a clean key from title (remove special chars, fallback to Cell)
178
+ const cleanKey = entry.title.replace(/[^a-zA-Z0-9_]/g, "") || `Cell_${cleanCell}`;
179
+
180
+ cloned[sheetKey][cleanKey] = {
181
+ cell: cleanCell,
182
+ type: entry.type || "text",
183
+ editable: entry.editable !== undefined ? entry.editable : true,
184
+ name: entry.title.trim(),
185
+ };
186
+
187
+ return cloned;
188
+ }
189
+
190
+ export interface EditableCellsModalProps {
191
+ isOpen: boolean;
192
+ onClose: () => void;
193
+ appMapping?: any;
194
+ currentSheet?: string;
195
+ isStandalone?: boolean;
196
+ onUpdateAppMapping?: (newAppMapping: any) => void;
197
+ }
198
+
199
+ export const EditableCellsModal: React.FC<EditableCellsModalProps> = ({
200
+ isOpen,
201
+ onClose,
202
+ appMapping: externalMapping,
203
+ currentSheet = "sheet1",
204
+ isStandalone = false,
205
+ onUpdateAppMapping,
206
+ }) => {
207
+ const [internalMapping, setInternalMapping] = useState<any>({});
208
+ const appMapping = externalMapping !== undefined ? externalMapping : internalMapping;
209
+
210
+ const updateMapping = (newMap: any) => {
211
+ if (onUpdateAppMapping) {
212
+ onUpdateAppMapping(newMap);
213
+ }
214
+ setInternalMapping(newMap);
215
+ };
216
+
217
+ const availableSheets = useMemo(() => {
218
+ // If explicitly in standalone template mode, always show only the current sheet
219
+ if (isStandalone) {
220
+ return [currentSheet || "sheet1"];
221
+ }
222
+
223
+ // Check if live SocialCalc workbook only has 1 sheet
224
+ try {
225
+ const sc = (window as any).SocialCalc;
226
+ const ctrl = sc?.GetCurrentWorkBookControl?.();
227
+ if (ctrl?.workbook?.sheetArr) {
228
+ const liveSheets = Object.keys(ctrl.workbook.sheetArr);
229
+ if (liveSheets.length <= 1) {
230
+ return [currentSheet || liveSheets[0] || "sheet1"];
231
+ }
232
+ }
233
+ if (ctrl?.sheetButtonArr) {
234
+ const btnSheets = Object.keys(ctrl.sheetButtonArr);
235
+ if (btnSheets.length <= 1) {
236
+ return [currentSheet || btnSheets[0] || "sheet1"];
237
+ }
238
+ }
239
+ } catch (_) {}
240
+
241
+ const keys = Object.keys(appMapping || {});
242
+ if (keys.length <= 1) {
243
+ return keys.length === 1 ? keys : [currentSheet || "sheet1"];
244
+ }
245
+ return keys;
246
+ }, [appMapping, currentSheet, isStandalone, isOpen]);
247
+
248
+ const [selectedSheet, setSelectedSheet] = useState<string>(() => {
249
+ if (appMapping && appMapping[currentSheet]) return currentSheet;
250
+ return availableSheets[0] || "sheet1";
251
+ });
252
+
253
+ useEffect(() => {
254
+ if (isStandalone) {
255
+ setSelectedSheet(currentSheet || availableSheets[0] || "sheet1");
256
+ } else if (appMapping && appMapping[currentSheet]) {
257
+ setSelectedSheet(currentSheet);
258
+ } else if (availableSheets.length > 0) {
259
+ setSelectedSheet(availableSheets[0]);
260
+ }
261
+ }, [currentSheet, availableSheets, appMapping, isStandalone]);
262
+
263
+ const [searchQuery, setSearchQuery] = useState("");
264
+ const [showAddForm, setShowAddForm] = useState(false);
265
+
266
+ // New cell form state
267
+ const [newCell, setNewCell] = useState("");
268
+ const [newTitle, setNewTitle] = useState("");
269
+ const [newType, setNewType] = useState<"text" | "image">("text");
270
+ const [newEditable, setNewEditable] = useState(true);
271
+ const [formError, setFormError] = useState("");
272
+
273
+ // Items for the selected sheet
274
+ const items = useMemo(() => {
275
+ return parseAppMappingToItems(appMapping, selectedSheet);
276
+ }, [appMapping, selectedSheet]);
277
+
278
+ // Filtered by search
279
+ const filteredItems = useMemo(() => {
280
+ if (!searchQuery.trim()) return items;
281
+ const q = searchQuery.toLowerCase().trim();
282
+ return items.filter(
283
+ (it) =>
284
+ it.cell.toLowerCase().includes(q) ||
285
+ it.title.toLowerCase().includes(q) ||
286
+ it.type.toLowerCase().includes(q)
287
+ );
288
+ }, [items, searchQuery]);
289
+
290
+ const editableCount = useMemo(() => items.filter((it) => it.editable).length, [items]);
291
+
292
+ // Handlers
293
+ const handleToggleItem = (item: EditableCellItem, checked: boolean) => {
294
+ const updated = toggleCellEditable(appMapping, item, checked);
295
+ updateMapping(updated);
296
+ };
297
+
298
+ const handleDeleteItem = (item: EditableCellItem) => {
299
+ const updated = deleteCellMapping(appMapping, item);
300
+ updateMapping(updated);
301
+ };
302
+
303
+ const handleAddSubmit = (e: React.FormEvent) => {
304
+ e.preventDefault();
305
+ const cellTrimmed = newCell.trim().toUpperCase();
306
+ if (!cellTrimmed) {
307
+ setFormError("Please enter a valid cell coordinate (e.g. C5).");
308
+ return;
309
+ }
310
+ // Basic cell coordinate check (letters followed by digits)
311
+ if (!/^[A-Z]+[0-9]+$/.test(cellTrimmed)) {
312
+ setFormError("Invalid cell format. Use standard reference like B2, C14, D20.");
313
+ return;
314
+ }
315
+ if (!newTitle.trim()) {
316
+ setFormError("Please enter a title or label for this cell.");
317
+ return;
318
+ }
319
+
320
+ const updated = addCellMapping(appMapping, selectedSheet, {
321
+ cell: cellTrimmed,
322
+ title: newTitle.trim(),
323
+ type: newType,
324
+ editable: newEditable,
325
+ });
326
+
327
+ updateMapping(updated);
328
+
329
+ // Reset form
330
+ setNewCell("");
331
+ setNewTitle("");
332
+ setNewType("text");
333
+ setNewEditable(true);
334
+ setFormError("");
335
+ setShowAddForm(false);
336
+ };
337
+
338
+ return (
339
+ <IonModal isOpen={isOpen} onDidDismiss={onClose} className="editable-cells-modal">
340
+ <IonHeader>
341
+ <IonToolbar color="primary" className="editable-cells-modal-toolbar">
342
+ <div className="editable-cells-modal-header-wrapper">
343
+ <IonIcon icon={checkboxOutline} className="editable-cells-header-icon" />
344
+ <IonTitle className="editable-cells-modal-title">Editable Cells & Mappings</IonTitle>
345
+ </div>
346
+ <IonButtons slot="end">
347
+ <IonButton fill="clear" onClick={onClose} className="editable-cells-close-btn">
348
+ <IonIcon icon={closeOutline} slot="icon-only" />
349
+ </IonButton>
350
+ </IonButtons>
351
+ </IonToolbar>
352
+
353
+ {/* Sheet Selector Segment if multiple sheets exist */}
354
+ {availableSheets.length > 1 && (
355
+ <IonToolbar className="editable-cells-sheet-toolbar">
356
+ <IonSegment
357
+ value={selectedSheet}
358
+ onIonChange={(e) => setSelectedSheet(String(e.detail.value))}
359
+ >
360
+ {availableSheets.map((sh) => (
361
+ <IonSegmentButton key={sh} value={sh}>
362
+ <IonLabel>{sh}</IonLabel>
363
+ </IonSegmentButton>
364
+ ))}
365
+ </IonSegment>
366
+ </IonToolbar>
367
+ )}
368
+ </IonHeader>
369
+
370
+ <IonContent fullscreen className="editable-cells-modal-content ion-padding">
371
+ {/* Top Summary Bar */}
372
+ <div className="editable-cells-summary-row">
373
+ <div className="editable-cells-stats">
374
+ <IonBadge color="primary" className="editable-cells-badge">
375
+ Total Mapped: {items.length}
376
+ </IonBadge>
377
+ <IonBadge color="success" className="editable-cells-badge">
378
+ Editable: {editableCount}
379
+ </IonBadge>
380
+ <IonBadge color="medium" className="editable-cells-badge">
381
+ Locked: {items.length - editableCount}
382
+ </IonBadge>
383
+ </div>
384
+
385
+ <IonButton
386
+ size="small"
387
+ fill={showAddForm ? "outline" : "solid"}
388
+ color="primary"
389
+ onClick={() => {
390
+ setShowAddForm(!showAddForm);
391
+ setFormError("");
392
+ }}
393
+ className="editable-cells-add-btn"
394
+ >
395
+ <IonIcon icon={addOutline} slot="start" />
396
+ {showAddForm ? "Cancel" : "Add Cell"}
397
+ </IonButton>
398
+ </div>
399
+
400
+ {/* Add New Cell Form Card */}
401
+ {showAddForm && (
402
+ <IonCard className="editable-cells-form-card">
403
+ <IonCardContent>
404
+ <div className="editable-cells-form-header">
405
+ <span className="editable-cells-form-title">Add New Cell Mapping</span>
406
+ <span className="editable-cells-form-subtitle">Target Sheet: {selectedSheet}</span>
407
+ </div>
408
+
409
+ {formError && <div className="editable-cells-form-error">{formError}</div>}
410
+
411
+ <form onSubmit={handleAddSubmit} className="editable-cells-form">
412
+ <div className="editable-cells-inputs-grid">
413
+ <div className="editable-cells-input-group">
414
+ <IonLabel className="editable-cells-input-label">Cell Reference *</IonLabel>
415
+ <IonInput
416
+ value={newCell}
417
+ placeholder="e.g. C5, E18, B2"
418
+ onIonInput={(e) => setNewCell(String(e.detail.value || ""))}
419
+ className="editable-cells-custom-input cell-ref-input"
420
+ autocapitalize="characters"
421
+ />
422
+ </div>
423
+
424
+ <div className="editable-cells-input-group">
425
+ <IonLabel className="editable-cells-input-label">Field Title / Name *</IonLabel>
426
+ <IonInput
427
+ value={newTitle}
428
+ placeholder="e.g. Due Date, Notes"
429
+ onIonInput={(e) => setNewTitle(String(e.detail.value || ""))}
430
+ className="editable-cells-custom-input"
431
+ />
432
+ </div>
433
+
434
+ <div className="editable-cells-input-group">
435
+ <IonLabel className="editable-cells-input-label">Field Type</IonLabel>
436
+ <IonSelect
437
+ value={newType}
438
+ onIonChange={(e) => setNewType(e.detail.value)}
439
+ interface="popover"
440
+ className="editable-cells-custom-select"
441
+ >
442
+ <IonSelectOption value="text">Text / Number</IonSelectOption>
443
+ <IonSelectOption value="image">Image / Logo</IonSelectOption>
444
+ </IonSelect>
445
+ </div>
446
+
447
+ <div className="editable-cells-toggle-group">
448
+ <IonLabel className="editable-cells-input-label">Editable</IonLabel>
449
+ <div className="editable-toggle-wrapper">
450
+ <IonToggle
451
+ checked={newEditable}
452
+ onIonChange={(e) => setNewEditable(e.detail.checked)}
453
+ />
454
+ <span className="editable-toggle-status">
455
+ {newEditable ? "Allowed" : "Locked"}
456
+ </span>
457
+ </div>
458
+ </div>
459
+ </div>
460
+
461
+ <div className="editable-cells-form-actions">
462
+ <IonButton
463
+ type="submit"
464
+ fill="solid"
465
+ color="primary"
466
+ size="small"
467
+ className="editable-cells-submit-btn"
468
+ >
469
+ <IonIcon icon={checkmarkCircleOutline} slot="start" />
470
+ Add Mapping
471
+ </IonButton>
472
+ </div>
473
+ </form>
474
+ </IonCardContent>
475
+ </IonCard>
476
+ )}
477
+
478
+ {/* Searchbar */}
479
+ <div className="editable-cells-search-wrapper">
480
+ <IonSearchbar
481
+ value={searchQuery}
482
+ onIonInput={(e) => setSearchQuery(e.detail.value || "")}
483
+ placeholder="Search mapped cells or titles..."
484
+ debounce={100}
485
+ className="editable-cells-searchbar"
486
+ />
487
+ </div>
488
+
489
+ {/* Mapped Cells List */}
490
+ <IonList className="editable-cells-list" lines="full">
491
+ {filteredItems.length === 0 ? (
492
+ <div className="editable-cells-empty-state">
493
+ <IonIcon icon={layersOutline} className="editable-cells-empty-icon" />
494
+ <div className="editable-cells-empty-title">
495
+ {items.length === 0
496
+ ? "No cell mappings found in this sheet"
497
+ : "No matching cells found"}
498
+ </div>
499
+ <div className="editable-cells-empty-desc">
500
+ {items.length === 0
501
+ ? "Click 'Add Cell' above to map an editable cell."
502
+ : "Try a different search term or clear the filter."}
503
+ </div>
504
+ </div>
505
+ ) : (
506
+ filteredItems.map((item) => (
507
+ <IonItem key={item.id} className="editable-cell-item">
508
+ {/* Cell Ref Badge */}
509
+ <div slot="start" className="editable-cell-badge-wrap">
510
+ <span className="editable-cell-coord">{item.cell}</span>
511
+ </div>
512
+
513
+ {/* Title & Metadata */}
514
+ <IonLabel className="editable-cell-label">
515
+ <div className="editable-cell-title-row">
516
+ <span className="editable-cell-title">{item.title}</span>
517
+ <IonBadge
518
+ color={item.type === "image" ? "secondary" : "light"}
519
+ className="editable-cell-type-badge"
520
+ >
521
+ <IonIcon
522
+ icon={item.type === "image" ? imageOutline : documentTextOutline}
523
+ className="editable-cell-type-icon"
524
+ />
525
+ {item.type.toUpperCase()}
526
+ </IonBadge>
527
+ </div>
528
+ </IonLabel>
529
+
530
+ {/* Editable Toggle */}
531
+ <div slot="end" className="editable-cell-actions">
532
+ <IonToggle
533
+ checked={item.editable}
534
+ onIonChange={(e) => handleToggleItem(item, e.detail.checked)}
535
+ className="editable-cell-toggle"
536
+ />
537
+ <IonButton
538
+ fill="clear"
539
+ color="danger"
540
+ size="small"
541
+ onClick={() => handleDeleteItem(item)}
542
+ className="editable-cell-delete-btn"
543
+ title="Delete cell mapping"
544
+ >
545
+ <IonIcon icon={trashOutline} slot="icon-only" />
546
+ </IonButton>
547
+ </div>
548
+ </IonItem>
549
+ ))
550
+ )}
551
+ </IonList>
552
+ </IonContent>
553
+ </IonModal>
554
+ );
555
+ };
@@ -0,0 +1,127 @@
1
+ .sc-hscroll-container {
2
+ display: flex;
3
+ align-items: center;
4
+ justify-content: space-between;
5
+ gap: 12px;
6
+ padding: 10px 16px;
7
+ background: #ffffff;
8
+ border-top: 1px solid #e2e8f0;
9
+ box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.05);
10
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
11
+ user-select: none;
12
+ width: 100%;
13
+ box-sizing: border-box;
14
+ z-index: 100;
15
+ }
16
+
17
+ .sc-hscroll-btn {
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ width: 38px;
22
+ height: 38px;
23
+ border-radius: 10px;
24
+ border: 1px solid #e2e8f0;
25
+ background: #f8fafc;
26
+ color: #334155;
27
+ cursor: pointer;
28
+ transition: all 0.15s ease;
29
+ flex-shrink: 0;
30
+ font-size: 16px;
31
+ padding: 0;
32
+ }
33
+
34
+ .sc-hscroll-btn:hover:not(:disabled) {
35
+ background: #f1f5f9;
36
+ border-color: #cbd5e1;
37
+ color: #0f172a;
38
+ transform: translateY(-1px);
39
+ }
40
+
41
+ .sc-hscroll-btn:active:not(:disabled) {
42
+ transform: translateY(0);
43
+ background: #e2e8f0;
44
+ }
45
+
46
+ .sc-hscroll-btn:disabled {
47
+ opacity: 0.4;
48
+ cursor: not-allowed;
49
+ background: #f8fafc;
50
+ }
51
+
52
+ .sc-hscroll-track-wrapper {
53
+ flex: 1;
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 14px;
57
+ min-width: 0;
58
+ }
59
+
60
+ .sc-hscroll-slider {
61
+ flex: 1;
62
+ -webkit-appearance: none;
63
+ appearance: none;
64
+ height: 8px;
65
+ border-radius: 4px;
66
+ background: #e2e8f0;
67
+ outline: none;
68
+ cursor: pointer;
69
+ transition: background 0.15s ease;
70
+ }
71
+
72
+ .sc-hscroll-slider:hover {
73
+ background: #cbd5e1;
74
+ }
75
+
76
+ .sc-hscroll-slider::-webkit-slider-thumb {
77
+ -webkit-appearance: none;
78
+ appearance: none;
79
+ width: 22px;
80
+ height: 22px;
81
+ border-radius: 50%;
82
+ background: #3b82f6;
83
+ border: 3px solid #ffffff;
84
+ box-shadow: 0 2px 6px rgba(59, 130, 246, 0.4);
85
+ cursor: grab;
86
+ transition: transform 0.1s ease, box-shadow 0.1s ease;
87
+ }
88
+
89
+ .sc-hscroll-slider::-webkit-slider-thumb:hover {
90
+ transform: scale(1.15);
91
+ box-shadow: 0 3px 8px rgba(59, 130, 246, 0.6);
92
+ }
93
+
94
+ .sc-hscroll-slider::-webkit-slider-thumb:active {
95
+ cursor: grabbing;
96
+ transform: scale(1.05);
97
+ }
98
+
99
+ .sc-hscroll-slider::-moz-range-thumb {
100
+ width: 22px;
101
+ height: 22px;
102
+ border-radius: 50%;
103
+ background: #3b82f6;
104
+ border: 3px solid #ffffff;
105
+ box-shadow: 0 2px 6px rgba(59, 130, 246, 0.4);
106
+ cursor: grab;
107
+ }
108
+
109
+ .sc-hscroll-indicator {
110
+ display: inline-flex;
111
+ align-items: center;
112
+ gap: 4px;
113
+ background: #eff6ff;
114
+ color: #2563eb;
115
+ border: 1px solid #bfdbfe;
116
+ padding: 4px 10px;
117
+ border-radius: 20px;
118
+ font-size: 12px;
119
+ font-weight: 600;
120
+ white-space: nowrap;
121
+ flex-shrink: 0;
122
+ box-shadow: 0 1px 2px rgba(37, 99, 235, 0.05);
123
+ }
124
+
125
+ .sc-hscroll-indicator-icon {
126
+ font-size: 14px;
127
+ }