@umituz/web-canvas 1.1.0 → 1.2.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/dist/application/index.js +85 -23
- package/dist/application/index.mjs +1 -1
- package/dist/{chunk-DNKRSL6D.mjs → chunk-P6RJ75US.mjs} +85 -23
- package/dist/{chunk-HJ24NSQA.mjs → chunk-YRC4YPMO.mjs} +7 -3
- package/dist/index.js +91 -25
- package/dist/index.mjs +2 -2
- package/dist/presentation/index.js +91 -25
- package/dist/presentation/index.mjs +2 -2
- package/package.json +3 -4
- package/src/application/services/ReportService.ts +31 -23
- package/src/infrastructure/services/minimalXlsx.ts +122 -0
- package/src/presentation/hooks/useFreehandCanvas.ts +8 -2
|
@@ -998,7 +998,76 @@ ${project.appMetadata.whatsNew}`;
|
|
|
998
998
|
}
|
|
999
999
|
};
|
|
1000
1000
|
|
|
1001
|
+
// src/infrastructure/services/minimalXlsx.ts
|
|
1002
|
+
var XML_HEADER = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
|
|
1003
|
+
function escapeXml(value) {
|
|
1004
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1005
|
+
}
|
|
1006
|
+
function safeSheetName(name, index) {
|
|
1007
|
+
const cleaned = name.replace(/[:\\/?*[\]]/g, " ").slice(0, 31).trim();
|
|
1008
|
+
return cleaned.length > 0 ? cleaned : `Sheet${index + 1}`;
|
|
1009
|
+
}
|
|
1010
|
+
function columnName(index) {
|
|
1011
|
+
let name = "";
|
|
1012
|
+
let n = index;
|
|
1013
|
+
while (n >= 0) {
|
|
1014
|
+
name = String.fromCharCode(65 + n % 26) + name;
|
|
1015
|
+
n = Math.floor(n / 26) - 1;
|
|
1016
|
+
}
|
|
1017
|
+
return name;
|
|
1018
|
+
}
|
|
1019
|
+
function cellXml(value, row, column) {
|
|
1020
|
+
const ref = `${columnName(column)}${row}`;
|
|
1021
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1022
|
+
return `<c r="${ref}"><v>${value}</v></c>`;
|
|
1023
|
+
}
|
|
1024
|
+
const text = value === null || value === void 0 ? "" : typeof value === "boolean" ? value ? "TRUE" : "FALSE" : String(value);
|
|
1025
|
+
return `<c r="${ref}" t="inlineStr"><is><t xml:space="preserve">${escapeXml(text)}</t></is></c>`;
|
|
1026
|
+
}
|
|
1027
|
+
function sheetXml(rows) {
|
|
1028
|
+
const body = rows.map((row, rowIndex) => {
|
|
1029
|
+
const cells = row.map((value, columnIndex) => cellXml(value, rowIndex + 1, columnIndex)).join("");
|
|
1030
|
+
return `<row r="${rowIndex + 1}">${cells}</row>`;
|
|
1031
|
+
}).join("");
|
|
1032
|
+
return `${XML_HEADER}<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${body}</sheetData></worksheet>`;
|
|
1033
|
+
}
|
|
1034
|
+
async function writeXlsxBlob(sheets, JSZip) {
|
|
1035
|
+
const zip = new JSZip();
|
|
1036
|
+
const safeNames = sheets.map((sheet, index) => safeSheetName(sheet.name, index));
|
|
1037
|
+
zip.file(
|
|
1038
|
+
"[Content_Types].xml",
|
|
1039
|
+
`${XML_HEADER}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>` + safeNames.map(
|
|
1040
|
+
(_, index) => `<Override PartName="/xl/worksheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`
|
|
1041
|
+
).join("") + "</Types>"
|
|
1042
|
+
);
|
|
1043
|
+
zip.file(
|
|
1044
|
+
"_rels/.rels",
|
|
1045
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>`
|
|
1046
|
+
);
|
|
1047
|
+
zip.file(
|
|
1048
|
+
"xl/workbook.xml",
|
|
1049
|
+
`${XML_HEADER}<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>` + safeNames.map((name, index) => `<sheet name="${escapeXml(name)}" sheetId="${index + 1}" r:id="rId${index + 1}"/>`).join("") + "</sheets></workbook>"
|
|
1050
|
+
);
|
|
1051
|
+
zip.file(
|
|
1052
|
+
"xl/_rels/workbook.xml.rels",
|
|
1053
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` + safeNames.map(
|
|
1054
|
+
(_, index) => `<Relationship Id="rId${index + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${index + 1}.xml"/>`
|
|
1055
|
+
).join("") + "</Relationships>"
|
|
1056
|
+
);
|
|
1057
|
+
sheets.forEach((sheet, index) => {
|
|
1058
|
+
zip.file(`xl/worksheets/sheet${index + 1}.xml`, sheetXml(sheet.rows));
|
|
1059
|
+
});
|
|
1060
|
+
return zip.generateAsync({ type: "blob" });
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1001
1063
|
// src/application/services/ReportService.ts
|
|
1064
|
+
function recordsToSheet(name, records) {
|
|
1065
|
+
const headers = [...new Set(records.flatMap((record) => Object.keys(record)))];
|
|
1066
|
+
return {
|
|
1067
|
+
name,
|
|
1068
|
+
rows: [headers, ...records.map((record) => headers.map((header) => record[header]))]
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1002
1071
|
var ReportService = class _ReportService {
|
|
1003
1072
|
static getInstance() {
|
|
1004
1073
|
if (!_ReportService.instance) {
|
|
@@ -1044,31 +1113,19 @@ var ReportService = class _ReportService {
|
|
|
1044
1113
|
* Export data to Excel
|
|
1045
1114
|
*/
|
|
1046
1115
|
async exportToExcel(data, options = {}) {
|
|
1047
|
-
const
|
|
1048
|
-
const wb = XLSX.utils.book_new();
|
|
1116
|
+
const JSZip = (await import("jszip")).default;
|
|
1049
1117
|
const apps = data.items.filter((i) => i.type === "app").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1050
1118
|
const campaigns = data.items.filter((i) => i.type === "campaign").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1051
1119
|
const posts = data.items.filter((i) => i.type === "post").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1120
|
+
const sheets = [];
|
|
1052
1121
|
if (apps.length > 0) {
|
|
1053
|
-
|
|
1054
|
-
wb,
|
|
1055
|
-
XLSX.utils.json_to_sheet(apps),
|
|
1056
|
-
options.sheetName || "Apps"
|
|
1057
|
-
);
|
|
1122
|
+
sheets.push(recordsToSheet(options.sheetName || "Apps", apps));
|
|
1058
1123
|
}
|
|
1059
1124
|
if (campaigns.length > 0) {
|
|
1060
|
-
|
|
1061
|
-
wb,
|
|
1062
|
-
XLSX.utils.json_to_sheet(campaigns),
|
|
1063
|
-
"Campaigns"
|
|
1064
|
-
);
|
|
1125
|
+
sheets.push(recordsToSheet("Campaigns", campaigns));
|
|
1065
1126
|
}
|
|
1066
1127
|
if (posts.length > 0) {
|
|
1067
|
-
|
|
1068
|
-
wb,
|
|
1069
|
-
XLSX.utils.json_to_sheet(posts),
|
|
1070
|
-
"Posts"
|
|
1071
|
-
);
|
|
1128
|
+
sheets.push(recordsToSheet("Posts", posts));
|
|
1072
1129
|
}
|
|
1073
1130
|
if (options.includeSummary !== false) {
|
|
1074
1131
|
const summary = [
|
|
@@ -1086,13 +1143,18 @@ var ReportService = class _ReportService {
|
|
|
1086
1143
|
Value: (/* @__PURE__ */ new Date()).toLocaleDateString()
|
|
1087
1144
|
}
|
|
1088
1145
|
];
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
);
|
|
1146
|
+
sheets.push(recordsToSheet("Summary", summary));
|
|
1147
|
+
}
|
|
1148
|
+
if (sheets.length === 0) {
|
|
1149
|
+
sheets.push({ name: options.sheetName || "Sheet1", rows: [] });
|
|
1094
1150
|
}
|
|
1095
|
-
|
|
1151
|
+
const blob = await writeXlsxBlob(sheets, JSZip);
|
|
1152
|
+
const url = URL.createObjectURL(blob);
|
|
1153
|
+
const link = document.createElement("a");
|
|
1154
|
+
link.href = url;
|
|
1155
|
+
link.download = options.filename || "report.xlsx";
|
|
1156
|
+
link.click();
|
|
1157
|
+
URL.revokeObjectURL(url);
|
|
1096
1158
|
}
|
|
1097
1159
|
/**
|
|
1098
1160
|
* Export data to CSV
|
|
@@ -210,7 +210,76 @@ ${project.appMetadata.whatsNew}`;
|
|
|
210
210
|
}
|
|
211
211
|
};
|
|
212
212
|
|
|
213
|
+
// src/infrastructure/services/minimalXlsx.ts
|
|
214
|
+
var XML_HEADER = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
|
|
215
|
+
function escapeXml(value) {
|
|
216
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
217
|
+
}
|
|
218
|
+
function safeSheetName(name, index) {
|
|
219
|
+
const cleaned = name.replace(/[:\\/?*[\]]/g, " ").slice(0, 31).trim();
|
|
220
|
+
return cleaned.length > 0 ? cleaned : `Sheet${index + 1}`;
|
|
221
|
+
}
|
|
222
|
+
function columnName(index) {
|
|
223
|
+
let name = "";
|
|
224
|
+
let n = index;
|
|
225
|
+
while (n >= 0) {
|
|
226
|
+
name = String.fromCharCode(65 + n % 26) + name;
|
|
227
|
+
n = Math.floor(n / 26) - 1;
|
|
228
|
+
}
|
|
229
|
+
return name;
|
|
230
|
+
}
|
|
231
|
+
function cellXml(value, row, column) {
|
|
232
|
+
const ref = `${columnName(column)}${row}`;
|
|
233
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
234
|
+
return `<c r="${ref}"><v>${value}</v></c>`;
|
|
235
|
+
}
|
|
236
|
+
const text = value === null || value === void 0 ? "" : typeof value === "boolean" ? value ? "TRUE" : "FALSE" : String(value);
|
|
237
|
+
return `<c r="${ref}" t="inlineStr"><is><t xml:space="preserve">${escapeXml(text)}</t></is></c>`;
|
|
238
|
+
}
|
|
239
|
+
function sheetXml(rows) {
|
|
240
|
+
const body = rows.map((row, rowIndex) => {
|
|
241
|
+
const cells = row.map((value, columnIndex) => cellXml(value, rowIndex + 1, columnIndex)).join("");
|
|
242
|
+
return `<row r="${rowIndex + 1}">${cells}</row>`;
|
|
243
|
+
}).join("");
|
|
244
|
+
return `${XML_HEADER}<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${body}</sheetData></worksheet>`;
|
|
245
|
+
}
|
|
246
|
+
async function writeXlsxBlob(sheets, JSZip) {
|
|
247
|
+
const zip = new JSZip();
|
|
248
|
+
const safeNames = sheets.map((sheet, index) => safeSheetName(sheet.name, index));
|
|
249
|
+
zip.file(
|
|
250
|
+
"[Content_Types].xml",
|
|
251
|
+
`${XML_HEADER}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>` + safeNames.map(
|
|
252
|
+
(_, index) => `<Override PartName="/xl/worksheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`
|
|
253
|
+
).join("") + "</Types>"
|
|
254
|
+
);
|
|
255
|
+
zip.file(
|
|
256
|
+
"_rels/.rels",
|
|
257
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>`
|
|
258
|
+
);
|
|
259
|
+
zip.file(
|
|
260
|
+
"xl/workbook.xml",
|
|
261
|
+
`${XML_HEADER}<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>` + safeNames.map((name, index) => `<sheet name="${escapeXml(name)}" sheetId="${index + 1}" r:id="rId${index + 1}"/>`).join("") + "</sheets></workbook>"
|
|
262
|
+
);
|
|
263
|
+
zip.file(
|
|
264
|
+
"xl/_rels/workbook.xml.rels",
|
|
265
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` + safeNames.map(
|
|
266
|
+
(_, index) => `<Relationship Id="rId${index + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${index + 1}.xml"/>`
|
|
267
|
+
).join("") + "</Relationships>"
|
|
268
|
+
);
|
|
269
|
+
sheets.forEach((sheet, index) => {
|
|
270
|
+
zip.file(`xl/worksheets/sheet${index + 1}.xml`, sheetXml(sheet.rows));
|
|
271
|
+
});
|
|
272
|
+
return zip.generateAsync({ type: "blob" });
|
|
273
|
+
}
|
|
274
|
+
|
|
213
275
|
// src/application/services/ReportService.ts
|
|
276
|
+
function recordsToSheet(name, records) {
|
|
277
|
+
const headers = [...new Set(records.flatMap((record) => Object.keys(record)))];
|
|
278
|
+
return {
|
|
279
|
+
name,
|
|
280
|
+
rows: [headers, ...records.map((record) => headers.map((header) => record[header]))]
|
|
281
|
+
};
|
|
282
|
+
}
|
|
214
283
|
var ReportService = class _ReportService {
|
|
215
284
|
static getInstance() {
|
|
216
285
|
if (!_ReportService.instance) {
|
|
@@ -256,31 +325,19 @@ var ReportService = class _ReportService {
|
|
|
256
325
|
* Export data to Excel
|
|
257
326
|
*/
|
|
258
327
|
async exportToExcel(data, options = {}) {
|
|
259
|
-
const
|
|
260
|
-
const wb = XLSX.utils.book_new();
|
|
328
|
+
const JSZip = (await import("jszip")).default;
|
|
261
329
|
const apps = data.items.filter((i) => i.type === "app").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
262
330
|
const campaigns = data.items.filter((i) => i.type === "campaign").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
263
331
|
const posts = data.items.filter((i) => i.type === "post").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
332
|
+
const sheets = [];
|
|
264
333
|
if (apps.length > 0) {
|
|
265
|
-
|
|
266
|
-
wb,
|
|
267
|
-
XLSX.utils.json_to_sheet(apps),
|
|
268
|
-
options.sheetName || "Apps"
|
|
269
|
-
);
|
|
334
|
+
sheets.push(recordsToSheet(options.sheetName || "Apps", apps));
|
|
270
335
|
}
|
|
271
336
|
if (campaigns.length > 0) {
|
|
272
|
-
|
|
273
|
-
wb,
|
|
274
|
-
XLSX.utils.json_to_sheet(campaigns),
|
|
275
|
-
"Campaigns"
|
|
276
|
-
);
|
|
337
|
+
sheets.push(recordsToSheet("Campaigns", campaigns));
|
|
277
338
|
}
|
|
278
339
|
if (posts.length > 0) {
|
|
279
|
-
|
|
280
|
-
wb,
|
|
281
|
-
XLSX.utils.json_to_sheet(posts),
|
|
282
|
-
"Posts"
|
|
283
|
-
);
|
|
340
|
+
sheets.push(recordsToSheet("Posts", posts));
|
|
284
341
|
}
|
|
285
342
|
if (options.includeSummary !== false) {
|
|
286
343
|
const summary = [
|
|
@@ -298,13 +355,18 @@ var ReportService = class _ReportService {
|
|
|
298
355
|
Value: (/* @__PURE__ */ new Date()).toLocaleDateString()
|
|
299
356
|
}
|
|
300
357
|
];
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
);
|
|
358
|
+
sheets.push(recordsToSheet("Summary", summary));
|
|
359
|
+
}
|
|
360
|
+
if (sheets.length === 0) {
|
|
361
|
+
sheets.push({ name: options.sheetName || "Sheet1", rows: [] });
|
|
306
362
|
}
|
|
307
|
-
|
|
363
|
+
const blob = await writeXlsxBlob(sheets, JSZip);
|
|
364
|
+
const url = URL.createObjectURL(blob);
|
|
365
|
+
const link = document.createElement("a");
|
|
366
|
+
link.href = url;
|
|
367
|
+
link.download = options.filename || "report.xlsx";
|
|
368
|
+
link.click();
|
|
369
|
+
URL.revokeObjectURL(url);
|
|
308
370
|
}
|
|
309
371
|
/**
|
|
310
372
|
* Export data to CSV
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ReportService,
|
|
3
3
|
ScreenshotStudioService
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-P6RJ75US.mjs";
|
|
5
5
|
import {
|
|
6
6
|
CanvasRenderer
|
|
7
7
|
} from "./chunk-UOGTEMIJ.mjs";
|
|
@@ -615,6 +615,7 @@ function useFreehandCanvas(options = {}) {
|
|
|
615
615
|
const canvasRef = useRef4(null);
|
|
616
616
|
const strokesRef = useRef4([]);
|
|
617
617
|
const activeStrokeRef = useRef4(null);
|
|
618
|
+
const activePointerIdRef = useRef4(null);
|
|
618
619
|
const dprRef = useRef4(1);
|
|
619
620
|
const [strokeCount, setStrokeCount] = useState4(0);
|
|
620
621
|
const redraw = useCallback4(() => {
|
|
@@ -702,8 +703,10 @@ function useFreehandCanvas(options = {}) {
|
|
|
702
703
|
}
|
|
703
704
|
function onPointerDown(event) {
|
|
704
705
|
if (event.button !== 0 && event.pointerType === "mouse") return;
|
|
706
|
+
if (activeStrokeRef.current != null) return;
|
|
705
707
|
event.preventDefault();
|
|
706
708
|
el.setPointerCapture(event.pointerId);
|
|
709
|
+
activePointerIdRef.current = event.pointerId;
|
|
707
710
|
const { brushSize: size, color: ink, pressureEnabled: dynamic } = optionsRef.current;
|
|
708
711
|
const stroke = {
|
|
709
712
|
size,
|
|
@@ -716,7 +719,7 @@ function useFreehandCanvas(options = {}) {
|
|
|
716
719
|
}
|
|
717
720
|
function onPointerMove(event) {
|
|
718
721
|
const stroke = activeStrokeRef.current;
|
|
719
|
-
if (!stroke) return;
|
|
722
|
+
if (!stroke || event.pointerId !== activePointerIdRef.current) return;
|
|
720
723
|
event.preventDefault();
|
|
721
724
|
const point = pointFrom(event);
|
|
722
725
|
const previous = stroke.points[stroke.points.length - 1];
|
|
@@ -725,8 +728,9 @@ function useFreehandCanvas(options = {}) {
|
|
|
725
728
|
}
|
|
726
729
|
function commit(event) {
|
|
727
730
|
const stroke = activeStrokeRef.current;
|
|
728
|
-
if (!stroke) return;
|
|
731
|
+
if (!stroke || event.pointerId !== activePointerIdRef.current) return;
|
|
729
732
|
activeStrokeRef.current = null;
|
|
733
|
+
activePointerIdRef.current = null;
|
|
730
734
|
if (el.hasPointerCapture(event.pointerId)) {
|
|
731
735
|
el.releasePointerCapture(event.pointerId);
|
|
732
736
|
}
|
package/dist/index.js
CHANGED
|
@@ -1010,7 +1010,76 @@ ${project.appMetadata.whatsNew}`;
|
|
|
1010
1010
|
}
|
|
1011
1011
|
};
|
|
1012
1012
|
|
|
1013
|
+
// src/infrastructure/services/minimalXlsx.ts
|
|
1014
|
+
var XML_HEADER = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
|
|
1015
|
+
function escapeXml(value) {
|
|
1016
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1017
|
+
}
|
|
1018
|
+
function safeSheetName(name, index) {
|
|
1019
|
+
const cleaned = name.replace(/[:\\/?*[\]]/g, " ").slice(0, 31).trim();
|
|
1020
|
+
return cleaned.length > 0 ? cleaned : `Sheet${index + 1}`;
|
|
1021
|
+
}
|
|
1022
|
+
function columnName(index) {
|
|
1023
|
+
let name = "";
|
|
1024
|
+
let n = index;
|
|
1025
|
+
while (n >= 0) {
|
|
1026
|
+
name = String.fromCharCode(65 + n % 26) + name;
|
|
1027
|
+
n = Math.floor(n / 26) - 1;
|
|
1028
|
+
}
|
|
1029
|
+
return name;
|
|
1030
|
+
}
|
|
1031
|
+
function cellXml(value, row, column) {
|
|
1032
|
+
const ref = `${columnName(column)}${row}`;
|
|
1033
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1034
|
+
return `<c r="${ref}"><v>${value}</v></c>`;
|
|
1035
|
+
}
|
|
1036
|
+
const text = value === null || value === void 0 ? "" : typeof value === "boolean" ? value ? "TRUE" : "FALSE" : String(value);
|
|
1037
|
+
return `<c r="${ref}" t="inlineStr"><is><t xml:space="preserve">${escapeXml(text)}</t></is></c>`;
|
|
1038
|
+
}
|
|
1039
|
+
function sheetXml(rows) {
|
|
1040
|
+
const body = rows.map((row, rowIndex) => {
|
|
1041
|
+
const cells = row.map((value, columnIndex) => cellXml(value, rowIndex + 1, columnIndex)).join("");
|
|
1042
|
+
return `<row r="${rowIndex + 1}">${cells}</row>`;
|
|
1043
|
+
}).join("");
|
|
1044
|
+
return `${XML_HEADER}<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${body}</sheetData></worksheet>`;
|
|
1045
|
+
}
|
|
1046
|
+
async function writeXlsxBlob(sheets, JSZip) {
|
|
1047
|
+
const zip = new JSZip();
|
|
1048
|
+
const safeNames = sheets.map((sheet, index) => safeSheetName(sheet.name, index));
|
|
1049
|
+
zip.file(
|
|
1050
|
+
"[Content_Types].xml",
|
|
1051
|
+
`${XML_HEADER}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>` + safeNames.map(
|
|
1052
|
+
(_, index) => `<Override PartName="/xl/worksheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`
|
|
1053
|
+
).join("") + "</Types>"
|
|
1054
|
+
);
|
|
1055
|
+
zip.file(
|
|
1056
|
+
"_rels/.rels",
|
|
1057
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>`
|
|
1058
|
+
);
|
|
1059
|
+
zip.file(
|
|
1060
|
+
"xl/workbook.xml",
|
|
1061
|
+
`${XML_HEADER}<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>` + safeNames.map((name, index) => `<sheet name="${escapeXml(name)}" sheetId="${index + 1}" r:id="rId${index + 1}"/>`).join("") + "</sheets></workbook>"
|
|
1062
|
+
);
|
|
1063
|
+
zip.file(
|
|
1064
|
+
"xl/_rels/workbook.xml.rels",
|
|
1065
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` + safeNames.map(
|
|
1066
|
+
(_, index) => `<Relationship Id="rId${index + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${index + 1}.xml"/>`
|
|
1067
|
+
).join("") + "</Relationships>"
|
|
1068
|
+
);
|
|
1069
|
+
sheets.forEach((sheet, index) => {
|
|
1070
|
+
zip.file(`xl/worksheets/sheet${index + 1}.xml`, sheetXml(sheet.rows));
|
|
1071
|
+
});
|
|
1072
|
+
return zip.generateAsync({ type: "blob" });
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1013
1075
|
// src/application/services/ReportService.ts
|
|
1076
|
+
function recordsToSheet(name, records) {
|
|
1077
|
+
const headers = [...new Set(records.flatMap((record) => Object.keys(record)))];
|
|
1078
|
+
return {
|
|
1079
|
+
name,
|
|
1080
|
+
rows: [headers, ...records.map((record) => headers.map((header) => record[header]))]
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1014
1083
|
var ReportService = class _ReportService {
|
|
1015
1084
|
static getInstance() {
|
|
1016
1085
|
if (!_ReportService.instance) {
|
|
@@ -1056,31 +1125,19 @@ var ReportService = class _ReportService {
|
|
|
1056
1125
|
* Export data to Excel
|
|
1057
1126
|
*/
|
|
1058
1127
|
async exportToExcel(data, options = {}) {
|
|
1059
|
-
const
|
|
1060
|
-
const wb = XLSX.utils.book_new();
|
|
1128
|
+
const JSZip = (await import("jszip")).default;
|
|
1061
1129
|
const apps = data.items.filter((i) => i.type === "app").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1062
1130
|
const campaigns = data.items.filter((i) => i.type === "campaign").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1063
1131
|
const posts = data.items.filter((i) => i.type === "post").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1132
|
+
const sheets = [];
|
|
1064
1133
|
if (apps.length > 0) {
|
|
1065
|
-
|
|
1066
|
-
wb,
|
|
1067
|
-
XLSX.utils.json_to_sheet(apps),
|
|
1068
|
-
options.sheetName || "Apps"
|
|
1069
|
-
);
|
|
1134
|
+
sheets.push(recordsToSheet(options.sheetName || "Apps", apps));
|
|
1070
1135
|
}
|
|
1071
1136
|
if (campaigns.length > 0) {
|
|
1072
|
-
|
|
1073
|
-
wb,
|
|
1074
|
-
XLSX.utils.json_to_sheet(campaigns),
|
|
1075
|
-
"Campaigns"
|
|
1076
|
-
);
|
|
1137
|
+
sheets.push(recordsToSheet("Campaigns", campaigns));
|
|
1077
1138
|
}
|
|
1078
1139
|
if (posts.length > 0) {
|
|
1079
|
-
|
|
1080
|
-
wb,
|
|
1081
|
-
XLSX.utils.json_to_sheet(posts),
|
|
1082
|
-
"Posts"
|
|
1083
|
-
);
|
|
1140
|
+
sheets.push(recordsToSheet("Posts", posts));
|
|
1084
1141
|
}
|
|
1085
1142
|
if (options.includeSummary !== false) {
|
|
1086
1143
|
const summary = [
|
|
@@ -1098,13 +1155,18 @@ var ReportService = class _ReportService {
|
|
|
1098
1155
|
Value: (/* @__PURE__ */ new Date()).toLocaleDateString()
|
|
1099
1156
|
}
|
|
1100
1157
|
];
|
|
1101
|
-
|
|
1102
|
-
wb,
|
|
1103
|
-
XLSX.utils.json_to_sheet(summary),
|
|
1104
|
-
"Summary"
|
|
1105
|
-
);
|
|
1158
|
+
sheets.push(recordsToSheet("Summary", summary));
|
|
1106
1159
|
}
|
|
1107
|
-
|
|
1160
|
+
if (sheets.length === 0) {
|
|
1161
|
+
sheets.push({ name: options.sheetName || "Sheet1", rows: [] });
|
|
1162
|
+
}
|
|
1163
|
+
const blob = await writeXlsxBlob(sheets, JSZip);
|
|
1164
|
+
const url = URL.createObjectURL(blob);
|
|
1165
|
+
const link = document.createElement("a");
|
|
1166
|
+
link.href = url;
|
|
1167
|
+
link.download = options.filename || "report.xlsx";
|
|
1168
|
+
link.click();
|
|
1169
|
+
URL.revokeObjectURL(url);
|
|
1108
1170
|
}
|
|
1109
1171
|
/**
|
|
1110
1172
|
* Export data to CSV
|
|
@@ -1755,6 +1817,7 @@ function useFreehandCanvas(options = {}) {
|
|
|
1755
1817
|
const canvasRef = (0, import_react4.useRef)(null);
|
|
1756
1818
|
const strokesRef = (0, import_react4.useRef)([]);
|
|
1757
1819
|
const activeStrokeRef = (0, import_react4.useRef)(null);
|
|
1820
|
+
const activePointerIdRef = (0, import_react4.useRef)(null);
|
|
1758
1821
|
const dprRef = (0, import_react4.useRef)(1);
|
|
1759
1822
|
const [strokeCount, setStrokeCount] = (0, import_react4.useState)(0);
|
|
1760
1823
|
const redraw = (0, import_react4.useCallback)(() => {
|
|
@@ -1842,8 +1905,10 @@ function useFreehandCanvas(options = {}) {
|
|
|
1842
1905
|
}
|
|
1843
1906
|
function onPointerDown(event) {
|
|
1844
1907
|
if (event.button !== 0 && event.pointerType === "mouse") return;
|
|
1908
|
+
if (activeStrokeRef.current != null) return;
|
|
1845
1909
|
event.preventDefault();
|
|
1846
1910
|
el.setPointerCapture(event.pointerId);
|
|
1911
|
+
activePointerIdRef.current = event.pointerId;
|
|
1847
1912
|
const { brushSize: size, color: ink, pressureEnabled: dynamic } = optionsRef.current;
|
|
1848
1913
|
const stroke = {
|
|
1849
1914
|
size,
|
|
@@ -1856,7 +1921,7 @@ function useFreehandCanvas(options = {}) {
|
|
|
1856
1921
|
}
|
|
1857
1922
|
function onPointerMove(event) {
|
|
1858
1923
|
const stroke = activeStrokeRef.current;
|
|
1859
|
-
if (!stroke) return;
|
|
1924
|
+
if (!stroke || event.pointerId !== activePointerIdRef.current) return;
|
|
1860
1925
|
event.preventDefault();
|
|
1861
1926
|
const point = pointFrom(event);
|
|
1862
1927
|
const previous = stroke.points[stroke.points.length - 1];
|
|
@@ -1865,8 +1930,9 @@ function useFreehandCanvas(options = {}) {
|
|
|
1865
1930
|
}
|
|
1866
1931
|
function commit(event) {
|
|
1867
1932
|
const stroke = activeStrokeRef.current;
|
|
1868
|
-
if (!stroke) return;
|
|
1933
|
+
if (!stroke || event.pointerId !== activePointerIdRef.current) return;
|
|
1869
1934
|
activeStrokeRef.current = null;
|
|
1935
|
+
activePointerIdRef.current = null;
|
|
1870
1936
|
if (el.hasPointerCapture(event.pointerId)) {
|
|
1871
1937
|
el.releasePointerCapture(event.pointerId);
|
|
1872
1938
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
useFreehandCanvas,
|
|
4
4
|
useReportExport,
|
|
5
5
|
useScreenshotStudio
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-YRC4YPMO.mjs";
|
|
7
7
|
import {
|
|
8
8
|
ReportService,
|
|
9
9
|
ScreenshotStudioService
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-P6RJ75US.mjs";
|
|
11
11
|
import {
|
|
12
12
|
CanvasRenderer,
|
|
13
13
|
DeviceConfigService,
|
|
@@ -1482,7 +1482,76 @@ function useCanvasExport(options = {}) {
|
|
|
1482
1482
|
// src/presentation/hooks/useReportExport.ts
|
|
1483
1483
|
var import_react3 = require("react");
|
|
1484
1484
|
|
|
1485
|
+
// src/infrastructure/services/minimalXlsx.ts
|
|
1486
|
+
var XML_HEADER = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
|
|
1487
|
+
function escapeXml(value) {
|
|
1488
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1489
|
+
}
|
|
1490
|
+
function safeSheetName(name, index) {
|
|
1491
|
+
const cleaned = name.replace(/[:\\/?*[\]]/g, " ").slice(0, 31).trim();
|
|
1492
|
+
return cleaned.length > 0 ? cleaned : `Sheet${index + 1}`;
|
|
1493
|
+
}
|
|
1494
|
+
function columnName(index) {
|
|
1495
|
+
let name = "";
|
|
1496
|
+
let n = index;
|
|
1497
|
+
while (n >= 0) {
|
|
1498
|
+
name = String.fromCharCode(65 + n % 26) + name;
|
|
1499
|
+
n = Math.floor(n / 26) - 1;
|
|
1500
|
+
}
|
|
1501
|
+
return name;
|
|
1502
|
+
}
|
|
1503
|
+
function cellXml(value, row, column) {
|
|
1504
|
+
const ref = `${columnName(column)}${row}`;
|
|
1505
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1506
|
+
return `<c r="${ref}"><v>${value}</v></c>`;
|
|
1507
|
+
}
|
|
1508
|
+
const text = value === null || value === void 0 ? "" : typeof value === "boolean" ? value ? "TRUE" : "FALSE" : String(value);
|
|
1509
|
+
return `<c r="${ref}" t="inlineStr"><is><t xml:space="preserve">${escapeXml(text)}</t></is></c>`;
|
|
1510
|
+
}
|
|
1511
|
+
function sheetXml(rows) {
|
|
1512
|
+
const body = rows.map((row, rowIndex) => {
|
|
1513
|
+
const cells = row.map((value, columnIndex) => cellXml(value, rowIndex + 1, columnIndex)).join("");
|
|
1514
|
+
return `<row r="${rowIndex + 1}">${cells}</row>`;
|
|
1515
|
+
}).join("");
|
|
1516
|
+
return `${XML_HEADER}<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${body}</sheetData></worksheet>`;
|
|
1517
|
+
}
|
|
1518
|
+
async function writeXlsxBlob(sheets, JSZip) {
|
|
1519
|
+
const zip = new JSZip();
|
|
1520
|
+
const safeNames = sheets.map((sheet, index) => safeSheetName(sheet.name, index));
|
|
1521
|
+
zip.file(
|
|
1522
|
+
"[Content_Types].xml",
|
|
1523
|
+
`${XML_HEADER}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>` + safeNames.map(
|
|
1524
|
+
(_, index) => `<Override PartName="/xl/worksheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`
|
|
1525
|
+
).join("") + "</Types>"
|
|
1526
|
+
);
|
|
1527
|
+
zip.file(
|
|
1528
|
+
"_rels/.rels",
|
|
1529
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>`
|
|
1530
|
+
);
|
|
1531
|
+
zip.file(
|
|
1532
|
+
"xl/workbook.xml",
|
|
1533
|
+
`${XML_HEADER}<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>` + safeNames.map((name, index) => `<sheet name="${escapeXml(name)}" sheetId="${index + 1}" r:id="rId${index + 1}"/>`).join("") + "</sheets></workbook>"
|
|
1534
|
+
);
|
|
1535
|
+
zip.file(
|
|
1536
|
+
"xl/_rels/workbook.xml.rels",
|
|
1537
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` + safeNames.map(
|
|
1538
|
+
(_, index) => `<Relationship Id="rId${index + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${index + 1}.xml"/>`
|
|
1539
|
+
).join("") + "</Relationships>"
|
|
1540
|
+
);
|
|
1541
|
+
sheets.forEach((sheet, index) => {
|
|
1542
|
+
zip.file(`xl/worksheets/sheet${index + 1}.xml`, sheetXml(sheet.rows));
|
|
1543
|
+
});
|
|
1544
|
+
return zip.generateAsync({ type: "blob" });
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1485
1547
|
// src/application/services/ReportService.ts
|
|
1548
|
+
function recordsToSheet(name, records) {
|
|
1549
|
+
const headers = [...new Set(records.flatMap((record) => Object.keys(record)))];
|
|
1550
|
+
return {
|
|
1551
|
+
name,
|
|
1552
|
+
rows: [headers, ...records.map((record) => headers.map((header) => record[header]))]
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1486
1555
|
var ReportService = class _ReportService {
|
|
1487
1556
|
static getInstance() {
|
|
1488
1557
|
if (!_ReportService.instance) {
|
|
@@ -1528,31 +1597,19 @@ var ReportService = class _ReportService {
|
|
|
1528
1597
|
* Export data to Excel
|
|
1529
1598
|
*/
|
|
1530
1599
|
async exportToExcel(data, options = {}) {
|
|
1531
|
-
const
|
|
1532
|
-
const wb = XLSX.utils.book_new();
|
|
1600
|
+
const JSZip = (await import("jszip")).default;
|
|
1533
1601
|
const apps = data.items.filter((i) => i.type === "app").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1534
1602
|
const campaigns = data.items.filter((i) => i.type === "campaign").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1535
1603
|
const posts = data.items.filter((i) => i.type === "post").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1604
|
+
const sheets = [];
|
|
1536
1605
|
if (apps.length > 0) {
|
|
1537
|
-
|
|
1538
|
-
wb,
|
|
1539
|
-
XLSX.utils.json_to_sheet(apps),
|
|
1540
|
-
options.sheetName || "Apps"
|
|
1541
|
-
);
|
|
1606
|
+
sheets.push(recordsToSheet(options.sheetName || "Apps", apps));
|
|
1542
1607
|
}
|
|
1543
1608
|
if (campaigns.length > 0) {
|
|
1544
|
-
|
|
1545
|
-
wb,
|
|
1546
|
-
XLSX.utils.json_to_sheet(campaigns),
|
|
1547
|
-
"Campaigns"
|
|
1548
|
-
);
|
|
1609
|
+
sheets.push(recordsToSheet("Campaigns", campaigns));
|
|
1549
1610
|
}
|
|
1550
1611
|
if (posts.length > 0) {
|
|
1551
|
-
|
|
1552
|
-
wb,
|
|
1553
|
-
XLSX.utils.json_to_sheet(posts),
|
|
1554
|
-
"Posts"
|
|
1555
|
-
);
|
|
1612
|
+
sheets.push(recordsToSheet("Posts", posts));
|
|
1556
1613
|
}
|
|
1557
1614
|
if (options.includeSummary !== false) {
|
|
1558
1615
|
const summary = [
|
|
@@ -1570,13 +1627,18 @@ var ReportService = class _ReportService {
|
|
|
1570
1627
|
Value: (/* @__PURE__ */ new Date()).toLocaleDateString()
|
|
1571
1628
|
}
|
|
1572
1629
|
];
|
|
1573
|
-
|
|
1574
|
-
wb,
|
|
1575
|
-
XLSX.utils.json_to_sheet(summary),
|
|
1576
|
-
"Summary"
|
|
1577
|
-
);
|
|
1630
|
+
sheets.push(recordsToSheet("Summary", summary));
|
|
1578
1631
|
}
|
|
1579
|
-
|
|
1632
|
+
if (sheets.length === 0) {
|
|
1633
|
+
sheets.push({ name: options.sheetName || "Sheet1", rows: [] });
|
|
1634
|
+
}
|
|
1635
|
+
const blob = await writeXlsxBlob(sheets, JSZip);
|
|
1636
|
+
const url = URL.createObjectURL(blob);
|
|
1637
|
+
const link = document.createElement("a");
|
|
1638
|
+
link.href = url;
|
|
1639
|
+
link.download = options.filename || "report.xlsx";
|
|
1640
|
+
link.click();
|
|
1641
|
+
URL.revokeObjectURL(url);
|
|
1580
1642
|
}
|
|
1581
1643
|
/**
|
|
1582
1644
|
* Export data to CSV
|
|
@@ -1749,6 +1811,7 @@ function useFreehandCanvas(options = {}) {
|
|
|
1749
1811
|
const canvasRef = (0, import_react4.useRef)(null);
|
|
1750
1812
|
const strokesRef = (0, import_react4.useRef)([]);
|
|
1751
1813
|
const activeStrokeRef = (0, import_react4.useRef)(null);
|
|
1814
|
+
const activePointerIdRef = (0, import_react4.useRef)(null);
|
|
1752
1815
|
const dprRef = (0, import_react4.useRef)(1);
|
|
1753
1816
|
const [strokeCount, setStrokeCount] = (0, import_react4.useState)(0);
|
|
1754
1817
|
const redraw = (0, import_react4.useCallback)(() => {
|
|
@@ -1836,8 +1899,10 @@ function useFreehandCanvas(options = {}) {
|
|
|
1836
1899
|
}
|
|
1837
1900
|
function onPointerDown(event) {
|
|
1838
1901
|
if (event.button !== 0 && event.pointerType === "mouse") return;
|
|
1902
|
+
if (activeStrokeRef.current != null) return;
|
|
1839
1903
|
event.preventDefault();
|
|
1840
1904
|
el.setPointerCapture(event.pointerId);
|
|
1905
|
+
activePointerIdRef.current = event.pointerId;
|
|
1841
1906
|
const { brushSize: size, color: ink, pressureEnabled: dynamic } = optionsRef.current;
|
|
1842
1907
|
const stroke = {
|
|
1843
1908
|
size,
|
|
@@ -1850,7 +1915,7 @@ function useFreehandCanvas(options = {}) {
|
|
|
1850
1915
|
}
|
|
1851
1916
|
function onPointerMove(event) {
|
|
1852
1917
|
const stroke = activeStrokeRef.current;
|
|
1853
|
-
if (!stroke) return;
|
|
1918
|
+
if (!stroke || event.pointerId !== activePointerIdRef.current) return;
|
|
1854
1919
|
event.preventDefault();
|
|
1855
1920
|
const point = pointFrom(event);
|
|
1856
1921
|
const previous = stroke.points[stroke.points.length - 1];
|
|
@@ -1859,8 +1924,9 @@ function useFreehandCanvas(options = {}) {
|
|
|
1859
1924
|
}
|
|
1860
1925
|
function commit(event) {
|
|
1861
1926
|
const stroke = activeStrokeRef.current;
|
|
1862
|
-
if (!stroke) return;
|
|
1927
|
+
if (!stroke || event.pointerId !== activePointerIdRef.current) return;
|
|
1863
1928
|
activeStrokeRef.current = null;
|
|
1929
|
+
activePointerIdRef.current = null;
|
|
1864
1930
|
if (el.hasPointerCapture(event.pointerId)) {
|
|
1865
1931
|
el.releasePointerCapture(event.pointerId);
|
|
1866
1932
|
}
|
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
useFreehandCanvas,
|
|
4
4
|
useReportExport,
|
|
5
5
|
useScreenshotStudio
|
|
6
|
-
} from "../chunk-
|
|
7
|
-
import "../chunk-
|
|
6
|
+
} from "../chunk-YRC4YPMO.mjs";
|
|
7
|
+
import "../chunk-P6RJ75US.mjs";
|
|
8
8
|
import "../chunk-UOGTEMIJ.mjs";
|
|
9
9
|
import "../chunk-Z6IB4BQV.mjs";
|
|
10
10
|
import "../chunk-MC6PNV2F.mjs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/web-canvas",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Comprehensive canvas and screenshot generation library with device frames, patterns, and PDF export",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"license": "MIT",
|
|
78
78
|
"repository": {
|
|
79
79
|
"type": "git",
|
|
80
|
-
"url": "https://github.com/umituz/web-canvas"
|
|
80
|
+
"url": "git+https://github.com/umituz/web-canvas.git"
|
|
81
81
|
},
|
|
82
82
|
"bugs": {
|
|
83
83
|
"url": "https://github.com/umituz/web-canvas/issues"
|
|
@@ -97,7 +97,6 @@
|
|
|
97
97
|
"dependencies": {
|
|
98
98
|
"html2canvas": "^1.4.1",
|
|
99
99
|
"jspdf": "^4.2.1",
|
|
100
|
-
"jszip": "^3.10.1"
|
|
101
|
-
"xlsx": "^0.18.5"
|
|
100
|
+
"jszip": "^3.10.1"
|
|
102
101
|
}
|
|
103
102
|
}
|
|
@@ -5,6 +5,20 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ReportData } from '../../domain/types/report.types';
|
|
7
7
|
import type { PDFExportOptions, ExcelExportOptions } from '../../domain/types/report.types';
|
|
8
|
+
import { writeXlsxBlob } from '../../infrastructure/services/minimalXlsx';
|
|
9
|
+
import type { XlsxSheet } from '../../infrastructure/services/minimalXlsx';
|
|
10
|
+
|
|
11
|
+
/** Flat records → header + value rows, matching json_to_sheet's shape. */
|
|
12
|
+
function recordsToSheet(
|
|
13
|
+
name: string,
|
|
14
|
+
records: Record<string, string | number | boolean | null | undefined>[]
|
|
15
|
+
): XlsxSheet {
|
|
16
|
+
const headers = [...new Set(records.flatMap((record) => Object.keys(record)))];
|
|
17
|
+
return {
|
|
18
|
+
name,
|
|
19
|
+
rows: [headers, ...records.map((record) => headers.map((header) => record[header]))],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
8
22
|
|
|
9
23
|
export class ReportService {
|
|
10
24
|
private static instance: ReportService;
|
|
@@ -71,8 +85,7 @@ export class ReportService {
|
|
|
71
85
|
data: ReportData,
|
|
72
86
|
options: ExcelExportOptions = {}
|
|
73
87
|
): Promise<void> {
|
|
74
|
-
const
|
|
75
|
-
const wb = XLSX.utils.book_new();
|
|
88
|
+
const JSZip = (await import('jszip')).default;
|
|
76
89
|
|
|
77
90
|
// Group items by type
|
|
78
91
|
const apps = data.items
|
|
@@ -85,26 +98,15 @@ export class ReportService {
|
|
|
85
98
|
.filter((i) => i.type === 'post')
|
|
86
99
|
.map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
87
100
|
|
|
101
|
+
const sheets: XlsxSheet[] = [];
|
|
88
102
|
if (apps.length > 0) {
|
|
89
|
-
|
|
90
|
-
wb,
|
|
91
|
-
XLSX.utils.json_to_sheet(apps),
|
|
92
|
-
options.sheetName || 'Apps'
|
|
93
|
-
);
|
|
103
|
+
sheets.push(recordsToSheet(options.sheetName || 'Apps', apps));
|
|
94
104
|
}
|
|
95
105
|
if (campaigns.length > 0) {
|
|
96
|
-
|
|
97
|
-
wb,
|
|
98
|
-
XLSX.utils.json_to_sheet(campaigns),
|
|
99
|
-
'Campaigns'
|
|
100
|
-
);
|
|
106
|
+
sheets.push(recordsToSheet('Campaigns', campaigns));
|
|
101
107
|
}
|
|
102
108
|
if (posts.length > 0) {
|
|
103
|
-
|
|
104
|
-
wb,
|
|
105
|
-
XLSX.utils.json_to_sheet(posts),
|
|
106
|
-
'Posts'
|
|
107
|
-
);
|
|
109
|
+
sheets.push(recordsToSheet('Posts', posts));
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
// Summary sheet
|
|
@@ -124,14 +126,20 @@ export class ReportService {
|
|
|
124
126
|
Value: new Date().toLocaleDateString(),
|
|
125
127
|
},
|
|
126
128
|
];
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
);
|
|
129
|
+
sheets.push(recordsToSheet('Summary', summary));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (sheets.length === 0) {
|
|
133
|
+
sheets.push({ name: options.sheetName || 'Sheet1', rows: [] });
|
|
132
134
|
}
|
|
133
135
|
|
|
134
|
-
|
|
136
|
+
const blob = await writeXlsxBlob(sheets, JSZip);
|
|
137
|
+
const url = URL.createObjectURL(blob);
|
|
138
|
+
const link = document.createElement('a');
|
|
139
|
+
link.href = url;
|
|
140
|
+
link.download = options.filename || 'report.xlsx';
|
|
141
|
+
link.click();
|
|
142
|
+
URL.revokeObjectURL(url);
|
|
135
143
|
}
|
|
136
144
|
|
|
137
145
|
/**
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal XLSX writer — replaces the unmaintained npm `xlsx` (SheetJS)
|
|
3
|
+
* dependency, whose registry build carries open prototype-pollution and
|
|
4
|
+
* ReDoS advisories with no registry fix. The report export only ever
|
|
5
|
+
* writes flat tables (header row + primitive cells), so a hand-rolled
|
|
6
|
+
* SpreadsheetML package on top of jszip covers the need: inline-string
|
|
7
|
+
* and numeric cells, multiple sheets, Excel/LibreOffice/Sheets-compatible.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface XlsxSheet {
|
|
11
|
+
name: string;
|
|
12
|
+
/** Rows of primitive cells; the first row is the header. */
|
|
13
|
+
rows: (string | number | boolean | null | undefined)[][];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const XML_HEADER = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
|
|
17
|
+
|
|
18
|
+
function escapeXml(value: string): string {
|
|
19
|
+
return value
|
|
20
|
+
.replace(/&/g, '&')
|
|
21
|
+
.replace(/</g, '<')
|
|
22
|
+
.replace(/>/g, '>')
|
|
23
|
+
.replace(/"/g, '"')
|
|
24
|
+
.replace(/'/g, ''');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Excel sheet names: max 31 chars, no : \ / ? * [ ]. */
|
|
28
|
+
function safeSheetName(name: string, index: number): string {
|
|
29
|
+
const cleaned = name.replace(/[:\\/?*[\]]/g, ' ').slice(0, 31).trim();
|
|
30
|
+
return cleaned.length > 0 ? cleaned : `Sheet${index + 1}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function columnName(index: number): string {
|
|
34
|
+
let name = '';
|
|
35
|
+
let n = index;
|
|
36
|
+
while (n >= 0) {
|
|
37
|
+
name = String.fromCharCode(65 + (n % 26)) + name;
|
|
38
|
+
n = Math.floor(n / 26) - 1;
|
|
39
|
+
}
|
|
40
|
+
return name;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function cellXml(value: string | number | boolean | null | undefined, row: number, column: number): string {
|
|
44
|
+
const ref = `${columnName(column)}${row}`;
|
|
45
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
46
|
+
return `<c r="${ref}"><v>${value}</v></c>`;
|
|
47
|
+
}
|
|
48
|
+
const text =
|
|
49
|
+
value === null || value === undefined
|
|
50
|
+
? ''
|
|
51
|
+
: typeof value === 'boolean'
|
|
52
|
+
? value
|
|
53
|
+
? 'TRUE'
|
|
54
|
+
: 'FALSE'
|
|
55
|
+
: String(value);
|
|
56
|
+
return `<c r="${ref}" t="inlineStr"><is><t xml:space="preserve">${escapeXml(text)}</t></is></c>`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sheetXml(rows: XlsxSheet['rows']): string {
|
|
60
|
+
const body = rows
|
|
61
|
+
.map((row, rowIndex) => {
|
|
62
|
+
const cells = row.map((value, columnIndex) => cellXml(value, rowIndex + 1, columnIndex)).join('');
|
|
63
|
+
return `<row r="${rowIndex + 1}">${cells}</row>`;
|
|
64
|
+
})
|
|
65
|
+
.join('');
|
|
66
|
+
return `${XML_HEADER}<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${body}</sheetData></worksheet>`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Assembles the sheets into a .xlsx zip blob using jszip. */
|
|
70
|
+
export async function writeXlsxBlob(sheets: XlsxSheet[], JSZip: typeof import('jszip')): Promise<Blob> {
|
|
71
|
+
const zip = new JSZip();
|
|
72
|
+
const safeNames = sheets.map((sheet, index) => safeSheetName(sheet.name, index));
|
|
73
|
+
|
|
74
|
+
zip.file(
|
|
75
|
+
'[Content_Types].xml',
|
|
76
|
+
`${XML_HEADER}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">` +
|
|
77
|
+
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' +
|
|
78
|
+
'<Default Extension="xml" ContentType="application/xml"/>' +
|
|
79
|
+
'<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>' +
|
|
80
|
+
safeNames
|
|
81
|
+
.map(
|
|
82
|
+
(_, index) =>
|
|
83
|
+
`<Override PartName="/xl/worksheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`,
|
|
84
|
+
)
|
|
85
|
+
.join('') +
|
|
86
|
+
'</Types>',
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
zip.file(
|
|
90
|
+
'_rels/.rels',
|
|
91
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` +
|
|
92
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>' +
|
|
93
|
+
'</Relationships>',
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
zip.file(
|
|
97
|
+
'xl/workbook.xml',
|
|
98
|
+
`${XML_HEADER}<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>` +
|
|
99
|
+
safeNames
|
|
100
|
+
.map((name, index) => `<sheet name="${escapeXml(name)}" sheetId="${index + 1}" r:id="rId${index + 1}"/>`)
|
|
101
|
+
.join('') +
|
|
102
|
+
'</sheets></workbook>',
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
zip.file(
|
|
106
|
+
'xl/_rels/workbook.xml.rels',
|
|
107
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` +
|
|
108
|
+
safeNames
|
|
109
|
+
.map(
|
|
110
|
+
(_, index) =>
|
|
111
|
+
`<Relationship Id="rId${index + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${index + 1}.xml"/>`,
|
|
112
|
+
)
|
|
113
|
+
.join('') +
|
|
114
|
+
'</Relationships>',
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
sheets.forEach((sheet, index) => {
|
|
118
|
+
zip.file(`xl/worksheets/sheet${index + 1}.xml`, sheetXml(sheet.rows));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return zip.generateAsync({ type: 'blob' });
|
|
122
|
+
}
|
|
@@ -64,6 +64,7 @@ export function useFreehandCanvas(
|
|
|
64
64
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
65
65
|
const strokesRef = useRef<FreehandStroke[]>([]);
|
|
66
66
|
const activeStrokeRef = useRef<FreehandStroke | null>(null);
|
|
67
|
+
const activePointerIdRef = useRef<number | null>(null);
|
|
67
68
|
const dprRef = useRef(1);
|
|
68
69
|
|
|
69
70
|
const [strokeCount, setStrokeCount] = useState(0);
|
|
@@ -168,8 +169,12 @@ export function useFreehandCanvas(
|
|
|
168
169
|
|
|
169
170
|
function onPointerDown(event: PointerEvent): void {
|
|
170
171
|
if (event.button !== 0 && event.pointerType === 'mouse') return;
|
|
172
|
+
// Only one stroke at a time: a resting palm or second finger touching
|
|
173
|
+
// mid-stroke must not hijack or corrupt the active stroke.
|
|
174
|
+
if (activeStrokeRef.current != null) return;
|
|
171
175
|
event.preventDefault();
|
|
172
176
|
el.setPointerCapture(event.pointerId);
|
|
177
|
+
activePointerIdRef.current = event.pointerId;
|
|
173
178
|
const { brushSize: size, color: ink, pressureEnabled: dynamic } = optionsRef.current;
|
|
174
179
|
const stroke: FreehandStroke = {
|
|
175
180
|
size,
|
|
@@ -183,7 +188,7 @@ export function useFreehandCanvas(
|
|
|
183
188
|
|
|
184
189
|
function onPointerMove(event: PointerEvent): void {
|
|
185
190
|
const stroke = activeStrokeRef.current;
|
|
186
|
-
if (!stroke) return;
|
|
191
|
+
if (!stroke || event.pointerId !== activePointerIdRef.current) return;
|
|
187
192
|
event.preventDefault();
|
|
188
193
|
const point = pointFrom(event);
|
|
189
194
|
const previous = stroke.points[stroke.points.length - 1];
|
|
@@ -193,8 +198,9 @@ export function useFreehandCanvas(
|
|
|
193
198
|
|
|
194
199
|
function commit(event: PointerEvent): void {
|
|
195
200
|
const stroke = activeStrokeRef.current;
|
|
196
|
-
if (!stroke) return;
|
|
201
|
+
if (!stroke || event.pointerId !== activePointerIdRef.current) return;
|
|
197
202
|
activeStrokeRef.current = null;
|
|
203
|
+
activePointerIdRef.current = null;
|
|
198
204
|
if (el.hasPointerCapture(event.pointerId)) {
|
|
199
205
|
el.releasePointerCapture(event.pointerId);
|
|
200
206
|
}
|