@umituz/web-canvas 1.1.1 → 1.2.1

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";
@@ -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";
@@ -604,19 +604,28 @@ function useReportExport() {
604
604
  import { useCallback as useCallback4, useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
605
605
  var DEFAULT_BRUSH_SIZE = 3;
606
606
  var DEFAULT_COLOR = "#1a1a1a";
607
+ var DEFAULT_KEYBOARD_STEP = 4;
608
+ var KEYBOARD_FAST_FACTOR = 4;
607
609
  function widthFor(stroke, point) {
608
610
  if (!stroke.dynamic) return stroke.size;
609
611
  return stroke.size * (0.5 + Math.min(1, Math.max(0, point.pressure)));
610
612
  }
611
613
  function useFreehandCanvas(options = {}) {
612
- const { brushSize = DEFAULT_BRUSH_SIZE, color = DEFAULT_COLOR, pressureEnabled = true } = options;
613
- const optionsRef = useRef4({ brushSize, color, pressureEnabled, onStrokeEnd: options.onStrokeEnd });
614
- optionsRef.current = { brushSize, color, pressureEnabled, onStrokeEnd: options.onStrokeEnd };
614
+ const {
615
+ brushSize = DEFAULT_BRUSH_SIZE,
616
+ color = DEFAULT_COLOR,
617
+ pressureEnabled = true,
618
+ keyboard = false,
619
+ keyboardStep = DEFAULT_KEYBOARD_STEP
620
+ } = options;
621
+ const optionsRef = useRef4({ brushSize, color, pressureEnabled, keyboardStep, onStrokeEnd: options.onStrokeEnd });
622
+ optionsRef.current = { brushSize, color, pressureEnabled, keyboardStep, onStrokeEnd: options.onStrokeEnd };
615
623
  const canvasRef = useRef4(null);
616
624
  const strokesRef = useRef4([]);
617
625
  const activeStrokeRef = useRef4(null);
618
626
  const activePointerIdRef = useRef4(null);
619
627
  const dprRef = useRef4(1);
628
+ const penRef = useRef4(null);
620
629
  const [strokeCount, setStrokeCount] = useState4(0);
621
630
  const redraw = useCallback4(() => {
622
631
  const canvas = canvasRef.current;
@@ -649,6 +658,18 @@ function useFreehandCanvas(options = {}) {
649
658
  ctx.stroke();
650
659
  }
651
660
  }
661
+ const pen = penRef.current;
662
+ if (pen?.engaged && !pen.down) {
663
+ const reach = 5;
664
+ ctx.strokeStyle = "rgba(35, 37, 43, 0.55)";
665
+ ctx.lineWidth = 1;
666
+ ctx.beginPath();
667
+ ctx.moveTo(pen.x - reach, pen.y);
668
+ ctx.lineTo(pen.x + reach, pen.y);
669
+ ctx.moveTo(pen.x, pen.y - reach);
670
+ ctx.lineTo(pen.x, pen.y + reach);
671
+ ctx.stroke();
672
+ }
652
673
  }, []);
653
674
  const syncDimensions = useCallback4(() => {
654
675
  const canvas = canvasRef.current;
@@ -667,6 +688,8 @@ function useFreehandCanvas(options = {}) {
667
688
  if (!canvas) return;
668
689
  const el = canvas;
669
690
  el.style.touchAction = "none";
691
+ if (keyboard && !el.hasAttribute("tabindex")) el.tabIndex = 0;
692
+ if (!penRef.current) penRef.current = { x: el.clientWidth / 2, y: el.clientHeight / 2, down: false, engaged: false };
670
693
  syncDimensions();
671
694
  redraw();
672
695
  const observer = new ResizeObserver(() => {
@@ -715,6 +738,7 @@ function useFreehandCanvas(options = {}) {
715
738
  points: [pointFrom(event)]
716
739
  };
717
740
  activeStrokeRef.current = stroke;
741
+ penRef.current = { x: stroke.points[0].x, y: stroke.points[0].y, down: false, engaged: false };
718
742
  drawDot(stroke, stroke.points[0]);
719
743
  }
720
744
  function onPointerMove(event) {
@@ -738,18 +762,81 @@ function useFreehandCanvas(options = {}) {
738
762
  setStrokeCount(strokesRef.current.length);
739
763
  optionsRef.current.onStrokeEnd?.(stroke);
740
764
  }
765
+ function clampPen(pen) {
766
+ pen.x = Math.min(Math.max(pen.x, 0), Math.max(0, el.clientWidth - 1));
767
+ pen.y = Math.min(Math.max(pen.y, 0), Math.max(0, el.clientHeight - 1));
768
+ }
769
+ function startKeyboardStroke(pen) {
770
+ const { brushSize: size, color: ink } = optionsRef.current;
771
+ const stroke = {
772
+ size,
773
+ color: ink,
774
+ dynamic: false,
775
+ points: [{ x: pen.x, y: pen.y, pressure: 0.5 }]
776
+ };
777
+ activeStrokeRef.current = stroke;
778
+ pen.down = true;
779
+ drawDot(stroke, stroke.points[0]);
780
+ }
781
+ function commitKeyboardStroke(pen) {
782
+ const stroke = activeStrokeRef.current;
783
+ if (!stroke) return;
784
+ activeStrokeRef.current = null;
785
+ pen.down = false;
786
+ strokesRef.current = [...strokesRef.current, stroke];
787
+ setStrokeCount(strokesRef.current.length);
788
+ optionsRef.current.onStrokeEnd?.(stroke);
789
+ redraw();
790
+ }
791
+ function onKeyDown(event) {
792
+ const pen = penRef.current;
793
+ if (!pen) return;
794
+ const fast = event.shiftKey ? KEYBOARD_FAST_FACTOR : 1;
795
+ const step = optionsRef.current.keyboardStep * fast;
796
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight" || event.key === "ArrowUp" || event.key === "ArrowDown") {
797
+ event.preventDefault();
798
+ pen.engaged = true;
799
+ pen.x += event.key === "ArrowLeft" ? -step : event.key === "ArrowRight" ? step : 0;
800
+ pen.y += event.key === "ArrowUp" ? -step : event.key === "ArrowDown" ? step : 0;
801
+ clampPen(pen);
802
+ const stroke = activeStrokeRef.current;
803
+ if (pen.down && stroke) {
804
+ const point = { x: pen.x, y: pen.y, pressure: 0.5 };
805
+ const previous = stroke.points[stroke.points.length - 1];
806
+ stroke.points.push(point);
807
+ drawSegment(stroke, previous, point);
808
+ } else {
809
+ redraw();
810
+ }
811
+ return;
812
+ }
813
+ if (event.key === " " || event.key === "Enter") {
814
+ event.preventDefault();
815
+ pen.engaged = true;
816
+ if (pen.down) commitKeyboardStroke(pen);
817
+ else startKeyboardStroke(pen);
818
+ return;
819
+ }
820
+ if (event.key === "Escape" && pen.down) {
821
+ activeStrokeRef.current = null;
822
+ pen.down = false;
823
+ redraw();
824
+ }
825
+ }
741
826
  el.addEventListener("pointerdown", onPointerDown);
742
827
  el.addEventListener("pointermove", onPointerMove);
743
828
  el.addEventListener("pointerup", commit);
744
829
  el.addEventListener("pointercancel", commit);
830
+ if (keyboard) el.addEventListener("keydown", onKeyDown);
745
831
  return () => {
746
832
  observer.disconnect();
747
833
  el.removeEventListener("pointerdown", onPointerDown);
748
834
  el.removeEventListener("pointermove", onPointerMove);
749
835
  el.removeEventListener("pointerup", commit);
750
836
  el.removeEventListener("pointercancel", commit);
837
+ el.removeEventListener("keydown", onKeyDown);
751
838
  };
752
- }, [redraw, syncDimensions]);
839
+ }, [redraw, syncDimensions, keyboard]);
753
840
  const undo = useCallback4(() => {
754
841
  if (strokesRef.current.length === 0) return;
755
842
  strokesRef.current = strokesRef.current.slice(0, -1);
@@ -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
@@ -2,43 +2,23 @@ export { A as AppStoreMetadata, B as BackgroundPattern, u as BackgroundPatternEn
2
2
  export { C as CSVExportOptions, E as ExcelExportOptions, a as ExportFormat, P as PDFExportOptions, R as ReportData, b as ReportExportResult, c as ReportItem, d as ReportItemType, e as ReportSummary } from '../report.types-DWDnprBx.mjs';
3
3
  export { AIConfig, DEFAULT_DEVICE_CONFIG, DEFAULT_SCREENSHOT_STUDIO_CONFIG, DEVICE_DIMENSIONS, DeviceConfigOption, ExportConfig, ScreenshotStudioConfig } from './config/index.mjs';
4
4
 
5
- /**
6
- * Freehand Types
7
- * Types for pointer-based freehand drawing on a canvas
8
- */
9
- /**
10
- * A single sampled point along a stroke, in CSS pixels relative to the canvas
11
- */
12
5
  interface FreehandPoint {
13
6
  x: number;
14
7
  y: number;
15
- /** Normalized stylus pressure (0–1). ~0.5 for mouse/touch when unsupported. */
16
8
  pressure: number;
17
9
  }
18
- /**
19
- * A completed (or in-progress) freehand stroke
20
- */
21
10
  interface FreehandStroke {
22
- /** Base brush size in CSS pixels. */
23
11
  size: number;
24
- /** CSS color of the stroke. */
25
12
  color: string;
26
- /** True when the stroke should vary width with per-point pressure. */
27
13
  dynamic: boolean;
28
- /** Ordered points from pointer-down to pointer-up. */
29
14
  points: FreehandPoint[];
30
15
  }
31
- /**
32
- * Options for the useFreehandCanvas hook
33
- */
34
16
  interface UseFreehandCanvasOptions {
35
- /** Base brush size in CSS pixels. Default: 3 */
36
17
  brushSize?: number;
37
- /** Stroke color. Default: '#1a1a1a' */
38
18
  color?: string;
39
- /** Vary stroke width with stylus pressure (mouse/touch stay constant). Default: true */
40
19
  pressureEnabled?: boolean;
41
- /** Called after each completed stroke (pointer-up). */
20
+ keyboard?: boolean;
21
+ keyboardStep?: number;
42
22
  onStrokeEnd?: (stroke: FreehandStroke) => void;
43
23
  }
44
24
 
@@ -2,43 +2,23 @@ export { A as AppStoreMetadata, B as BackgroundPattern, u as BackgroundPatternEn
2
2
  export { C as CSVExportOptions, E as ExcelExportOptions, a as ExportFormat, P as PDFExportOptions, R as ReportData, b as ReportExportResult, c as ReportItem, d as ReportItemType, e as ReportSummary } from '../report.types-DWDnprBx.js';
3
3
  export { AIConfig, DEFAULT_DEVICE_CONFIG, DEFAULT_SCREENSHOT_STUDIO_CONFIG, DEVICE_DIMENSIONS, DeviceConfigOption, ExportConfig, ScreenshotStudioConfig } from './config/index.js';
4
4
 
5
- /**
6
- * Freehand Types
7
- * Types for pointer-based freehand drawing on a canvas
8
- */
9
- /**
10
- * A single sampled point along a stroke, in CSS pixels relative to the canvas
11
- */
12
5
  interface FreehandPoint {
13
6
  x: number;
14
7
  y: number;
15
- /** Normalized stylus pressure (0–1). ~0.5 for mouse/touch when unsupported. */
16
8
  pressure: number;
17
9
  }
18
- /**
19
- * A completed (or in-progress) freehand stroke
20
- */
21
10
  interface FreehandStroke {
22
- /** Base brush size in CSS pixels. */
23
11
  size: number;
24
- /** CSS color of the stroke. */
25
12
  color: string;
26
- /** True when the stroke should vary width with per-point pressure. */
27
13
  dynamic: boolean;
28
- /** Ordered points from pointer-down to pointer-up. */
29
14
  points: FreehandPoint[];
30
15
  }
31
- /**
32
- * Options for the useFreehandCanvas hook
33
- */
34
16
  interface UseFreehandCanvasOptions {
35
- /** Base brush size in CSS pixels. Default: 3 */
36
17
  brushSize?: number;
37
- /** Stroke color. Default: '#1a1a1a' */
38
18
  color?: string;
39
- /** Vary stroke width with stylus pressure (mouse/touch stay constant). Default: true */
40
19
  pressureEnabled?: boolean;
41
- /** Called after each completed stroke (pointer-up). */
20
+ keyboard?: boolean;
21
+ keyboardStep?: number;
42
22
  onStrokeEnd?: (stroke: FreehandStroke) => void;
43
23
  }
44
24