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.
- package/README.md +268 -0
- package/components/CellEditModal/CellEditModal.css +584 -0
- package/components/CellEditModal/CellEditModal.tsx +817 -0
- package/components/DemoVideosModal/DemoVideosModal.css +307 -0
- package/components/DemoVideosModal/DemoVideosModal.tsx +289 -0
- package/components/EditableCellsModal/EditableCellsModal.css +346 -0
- package/components/EditableCellsModal/EditableCellsModal.tsx +555 -0
- package/components/HorizontalScrollBar.css +127 -0
- package/components/HorizontalScrollBar.tsx +140 -0
- package/components/RowActionPopover/RowActionPopover.css +95 -0
- package/components/RowActionPopover/RowActionPopover.tsx +82 -0
- package/core/constants.js +1039 -0
- package/core/core.js +7172 -0
- package/core/format-number.js +7034 -0
- package/core/formula.js +1675 -0
- package/core/index.js +28 -0
- package/core/spreadsheet-control.js +7646 -0
- package/core/table-editor.js +8156 -0
- package/index.d.ts +68 -0
- package/index.js +43 -0
- package/modules/device.js +22 -0
- package/modules/editable-cells.js +230 -0
- package/modules/exporters.js +22 -0
- package/modules/formatting.js +258 -0
- package/modules/grid-lines.js +127 -0
- package/modules/history.js +26 -0
- package/modules/horizontal-scroll.js +335 -0
- package/modules/init.js +94 -0
- package/modules/invoice.js +558 -0
- package/modules/listeners.js +531 -0
- package/modules/logos.js +191 -0
- package/modules/plugin-manager.js +208 -0
- package/modules/prompts.js +170 -0
- package/modules/row-col-headers.js +514 -0
- package/modules/sheets.js +151 -0
- package/modules/touch-scroll.js +474 -0
- package/modules/utils.js +317 -0
- package/modules/weight.js +287 -0
- package/package.json +67 -0
- package/utils/imageCompressor.ts +118 -0
- 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
|
+
}
|