@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.
@@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
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 XLSX = await import("xlsx");
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
- XLSX.utils.book_append_sheet(
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
- XLSX.utils.book_append_sheet(
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
- XLSX.utils.book_append_sheet(
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
- XLSX.utils.book_append_sheet(
1090
- wb,
1091
- XLSX.utils.json_to_sheet(summary),
1092
- "Summary"
1093
- );
1146
+ sheets.push(recordsToSheet("Summary", summary));
1147
+ }
1148
+ if (sheets.length === 0) {
1149
+ sheets.push({ name: options.sheetName || "Sheet1", rows: [] });
1094
1150
  }
1095
- XLSX.writeFile(wb, options.filename || "report.xlsx");
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
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ReportService,
3
3
  ScreenshotStudioService
4
- } from "../chunk-DNKRSL6D.mjs";
4
+ } from "../chunk-P6RJ75US.mjs";
5
5
  import "../chunk-UOGTEMIJ.mjs";
6
6
  import "../chunk-Z6IB4BQV.mjs";
7
7
  import "../chunk-MC6PNV2F.mjs";
@@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
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 XLSX = await import("xlsx");
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
- XLSX.utils.book_append_sheet(
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
- XLSX.utils.book_append_sheet(
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
- XLSX.utils.book_append_sheet(
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
- XLSX.utils.book_append_sheet(
302
- wb,
303
- XLSX.utils.json_to_sheet(summary),
304
- "Summary"
305
- );
358
+ sheets.push(recordsToSheet("Summary", summary));
359
+ }
360
+ if (sheets.length === 0) {
361
+ sheets.push({ name: options.sheetName || "Sheet1", rows: [] });
306
362
  }
307
- XLSX.writeFile(wb, options.filename || "report.xlsx");
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-DNKRSL6D.mjs";
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
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 XLSX = await import("xlsx");
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
- XLSX.utils.book_append_sheet(
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
- XLSX.utils.book_append_sheet(
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
- XLSX.utils.book_append_sheet(
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
- XLSX.utils.book_append_sheet(
1102
- wb,
1103
- XLSX.utils.json_to_sheet(summary),
1104
- "Summary"
1105
- );
1158
+ sheets.push(recordsToSheet("Summary", summary));
1106
1159
  }
1107
- XLSX.writeFile(wb, options.filename || "report.xlsx");
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-HJ24NSQA.mjs";
6
+ } from "./chunk-YRC4YPMO.mjs";
7
7
  import {
8
8
  ReportService,
9
9
  ScreenshotStudioService
10
- } from "./chunk-DNKRSL6D.mjs";
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
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 XLSX = await import("xlsx");
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
- XLSX.utils.book_append_sheet(
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
- XLSX.utils.book_append_sheet(
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
- XLSX.utils.book_append_sheet(
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
- XLSX.utils.book_append_sheet(
1574
- wb,
1575
- XLSX.utils.json_to_sheet(summary),
1576
- "Summary"
1577
- );
1630
+ sheets.push(recordsToSheet("Summary", summary));
1578
1631
  }
1579
- XLSX.writeFile(wb, options.filename || "report.xlsx");
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-HJ24NSQA.mjs";
7
- import "../chunk-DNKRSL6D.mjs";
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.1.0",
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 XLSX = await import('xlsx');
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
- XLSX.utils.book_append_sheet(
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
- XLSX.utils.book_append_sheet(
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
- XLSX.utils.book_append_sheet(
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
- XLSX.utils.book_append_sheet(
128
- wb,
129
- XLSX.utils.json_to_sheet(summary),
130
- 'Summary'
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
- XLSX.writeFile(wb, options.filename || 'report.xlsx');
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, '&amp;')
21
+ .replace(/</g, '&lt;')
22
+ .replace(/>/g, '&gt;')
23
+ .replace(/"/g, '&quot;')
24
+ .replace(/'/g, '&apos;');
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
  }