@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.
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));
1159
+ }
1160
+ if (sheets.length === 0) {
1161
+ sheets.push({ name: options.sheetName || "Sheet1", rows: [] });
1106
1162
  }
1107
- XLSX.writeFile(wb, options.filename || "report.xlsx");
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
@@ -1744,19 +1806,28 @@ function useReportExport() {
1744
1806
  var import_react4 = require("react");
1745
1807
  var DEFAULT_BRUSH_SIZE = 3;
1746
1808
  var DEFAULT_COLOR = "#1a1a1a";
1809
+ var DEFAULT_KEYBOARD_STEP = 4;
1810
+ var KEYBOARD_FAST_FACTOR = 4;
1747
1811
  function widthFor(stroke, point) {
1748
1812
  if (!stroke.dynamic) return stroke.size;
1749
1813
  return stroke.size * (0.5 + Math.min(1, Math.max(0, point.pressure)));
1750
1814
  }
1751
1815
  function useFreehandCanvas(options = {}) {
1752
- const { brushSize = DEFAULT_BRUSH_SIZE, color = DEFAULT_COLOR, pressureEnabled = true } = options;
1753
- const optionsRef = (0, import_react4.useRef)({ brushSize, color, pressureEnabled, onStrokeEnd: options.onStrokeEnd });
1754
- optionsRef.current = { brushSize, color, pressureEnabled, onStrokeEnd: options.onStrokeEnd };
1816
+ const {
1817
+ brushSize = DEFAULT_BRUSH_SIZE,
1818
+ color = DEFAULT_COLOR,
1819
+ pressureEnabled = true,
1820
+ keyboard = false,
1821
+ keyboardStep = DEFAULT_KEYBOARD_STEP
1822
+ } = options;
1823
+ const optionsRef = (0, import_react4.useRef)({ brushSize, color, pressureEnabled, keyboardStep, onStrokeEnd: options.onStrokeEnd });
1824
+ optionsRef.current = { brushSize, color, pressureEnabled, keyboardStep, onStrokeEnd: options.onStrokeEnd };
1755
1825
  const canvasRef = (0, import_react4.useRef)(null);
1756
1826
  const strokesRef = (0, import_react4.useRef)([]);
1757
1827
  const activeStrokeRef = (0, import_react4.useRef)(null);
1758
1828
  const activePointerIdRef = (0, import_react4.useRef)(null);
1759
1829
  const dprRef = (0, import_react4.useRef)(1);
1830
+ const penRef = (0, import_react4.useRef)(null);
1760
1831
  const [strokeCount, setStrokeCount] = (0, import_react4.useState)(0);
1761
1832
  const redraw = (0, import_react4.useCallback)(() => {
1762
1833
  const canvas = canvasRef.current;
@@ -1789,6 +1860,18 @@ function useFreehandCanvas(options = {}) {
1789
1860
  ctx.stroke();
1790
1861
  }
1791
1862
  }
1863
+ const pen = penRef.current;
1864
+ if (pen?.engaged && !pen.down) {
1865
+ const reach = 5;
1866
+ ctx.strokeStyle = "rgba(35, 37, 43, 0.55)";
1867
+ ctx.lineWidth = 1;
1868
+ ctx.beginPath();
1869
+ ctx.moveTo(pen.x - reach, pen.y);
1870
+ ctx.lineTo(pen.x + reach, pen.y);
1871
+ ctx.moveTo(pen.x, pen.y - reach);
1872
+ ctx.lineTo(pen.x, pen.y + reach);
1873
+ ctx.stroke();
1874
+ }
1792
1875
  }, []);
1793
1876
  const syncDimensions = (0, import_react4.useCallback)(() => {
1794
1877
  const canvas = canvasRef.current;
@@ -1807,6 +1890,8 @@ function useFreehandCanvas(options = {}) {
1807
1890
  if (!canvas) return;
1808
1891
  const el = canvas;
1809
1892
  el.style.touchAction = "none";
1893
+ if (keyboard && !el.hasAttribute("tabindex")) el.tabIndex = 0;
1894
+ if (!penRef.current) penRef.current = { x: el.clientWidth / 2, y: el.clientHeight / 2, down: false, engaged: false };
1810
1895
  syncDimensions();
1811
1896
  redraw();
1812
1897
  const observer = new ResizeObserver(() => {
@@ -1855,6 +1940,7 @@ function useFreehandCanvas(options = {}) {
1855
1940
  points: [pointFrom(event)]
1856
1941
  };
1857
1942
  activeStrokeRef.current = stroke;
1943
+ penRef.current = { x: stroke.points[0].x, y: stroke.points[0].y, down: false, engaged: false };
1858
1944
  drawDot(stroke, stroke.points[0]);
1859
1945
  }
1860
1946
  function onPointerMove(event) {
@@ -1878,18 +1964,81 @@ function useFreehandCanvas(options = {}) {
1878
1964
  setStrokeCount(strokesRef.current.length);
1879
1965
  optionsRef.current.onStrokeEnd?.(stroke);
1880
1966
  }
1967
+ function clampPen(pen) {
1968
+ pen.x = Math.min(Math.max(pen.x, 0), Math.max(0, el.clientWidth - 1));
1969
+ pen.y = Math.min(Math.max(pen.y, 0), Math.max(0, el.clientHeight - 1));
1970
+ }
1971
+ function startKeyboardStroke(pen) {
1972
+ const { brushSize: size, color: ink } = optionsRef.current;
1973
+ const stroke = {
1974
+ size,
1975
+ color: ink,
1976
+ dynamic: false,
1977
+ points: [{ x: pen.x, y: pen.y, pressure: 0.5 }]
1978
+ };
1979
+ activeStrokeRef.current = stroke;
1980
+ pen.down = true;
1981
+ drawDot(stroke, stroke.points[0]);
1982
+ }
1983
+ function commitKeyboardStroke(pen) {
1984
+ const stroke = activeStrokeRef.current;
1985
+ if (!stroke) return;
1986
+ activeStrokeRef.current = null;
1987
+ pen.down = false;
1988
+ strokesRef.current = [...strokesRef.current, stroke];
1989
+ setStrokeCount(strokesRef.current.length);
1990
+ optionsRef.current.onStrokeEnd?.(stroke);
1991
+ redraw();
1992
+ }
1993
+ function onKeyDown(event) {
1994
+ const pen = penRef.current;
1995
+ if (!pen) return;
1996
+ const fast = event.shiftKey ? KEYBOARD_FAST_FACTOR : 1;
1997
+ const step = optionsRef.current.keyboardStep * fast;
1998
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight" || event.key === "ArrowUp" || event.key === "ArrowDown") {
1999
+ event.preventDefault();
2000
+ pen.engaged = true;
2001
+ pen.x += event.key === "ArrowLeft" ? -step : event.key === "ArrowRight" ? step : 0;
2002
+ pen.y += event.key === "ArrowUp" ? -step : event.key === "ArrowDown" ? step : 0;
2003
+ clampPen(pen);
2004
+ const stroke = activeStrokeRef.current;
2005
+ if (pen.down && stroke) {
2006
+ const point = { x: pen.x, y: pen.y, pressure: 0.5 };
2007
+ const previous = stroke.points[stroke.points.length - 1];
2008
+ stroke.points.push(point);
2009
+ drawSegment(stroke, previous, point);
2010
+ } else {
2011
+ redraw();
2012
+ }
2013
+ return;
2014
+ }
2015
+ if (event.key === " " || event.key === "Enter") {
2016
+ event.preventDefault();
2017
+ pen.engaged = true;
2018
+ if (pen.down) commitKeyboardStroke(pen);
2019
+ else startKeyboardStroke(pen);
2020
+ return;
2021
+ }
2022
+ if (event.key === "Escape" && pen.down) {
2023
+ activeStrokeRef.current = null;
2024
+ pen.down = false;
2025
+ redraw();
2026
+ }
2027
+ }
1881
2028
  el.addEventListener("pointerdown", onPointerDown);
1882
2029
  el.addEventListener("pointermove", onPointerMove);
1883
2030
  el.addEventListener("pointerup", commit);
1884
2031
  el.addEventListener("pointercancel", commit);
2032
+ if (keyboard) el.addEventListener("keydown", onKeyDown);
1885
2033
  return () => {
1886
2034
  observer.disconnect();
1887
2035
  el.removeEventListener("pointerdown", onPointerDown);
1888
2036
  el.removeEventListener("pointermove", onPointerMove);
1889
2037
  el.removeEventListener("pointerup", commit);
1890
2038
  el.removeEventListener("pointercancel", commit);
2039
+ el.removeEventListener("keydown", onKeyDown);
1891
2040
  };
1892
- }, [redraw, syncDimensions]);
2041
+ }, [redraw, syncDimensions, keyboard]);
1893
2042
  const undo = (0, import_react4.useCallback)(() => {
1894
2043
  if (strokesRef.current.length === 0) return;
1895
2044
  strokesRef.current = strokesRef.current.slice(0, -1);
package/dist/index.mjs CHANGED
@@ -3,11 +3,11 @@ import {
3
3
  useFreehandCanvas,
4
4
  useReportExport,
5
5
  useScreenshotStudio
6
- } from "./chunk-YKXOSUMF.mjs";
6
+ } from "./chunk-7YLVULSN.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,
@@ -106,40 +106,14 @@ interface UseReportExportReturn {
106
106
  */
107
107
  declare function useReportExport(): UseReportExportReturn;
108
108
 
109
- /**
110
- * useFreehandCanvas Hook
111
- * React hook for pointer-based freehand drawing on a canvas
112
- */
113
-
114
- /**
115
- * Return value of the useFreehandCanvas hook
116
- */
117
109
  interface UseFreehandCanvasReturn {
118
- /** Ref to attach to the <canvas> element that captures drawing. */
119
110
  canvasRef: React.RefObject<HTMLCanvasElement | null>;
120
- /** Remove the most recent stroke. */
121
111
  undo: () => void;
122
- /** Remove all strokes. */
123
112
  clear: () => void;
124
- /** Number of committed strokes. */
125
113
  strokeCount: number;
126
- /** True when no strokes are committed. */
127
114
  isEmpty: boolean;
128
- /** Snapshot of committed strokes (safe to serialize). */
129
115
  getStrokes: () => FreehandStroke[];
130
116
  }
131
- /**
132
- * useFreehandCanvas hook
133
- *
134
- * Attaches pointer-event drawing to a canvas element. The canvas is sized to
135
- * its CSS box at device-pixel resolution (and re-rendered on resize), strokes
136
- * are replayed from captured points so undo/clear are lossless, and stylus
137
- * pressure modulates line width when the pointer reports it.
138
- *
139
- * The canvas gets `touch-action: none` applied so drawing works on touch
140
- * screens without scrolling the page. Consumers typically layer the canvas
141
- * over their own guide content (e.g. a faint glyph to trace).
142
- */
143
117
  declare function useFreehandCanvas(options?: UseFreehandCanvasOptions): UseFreehandCanvasReturn;
144
118
 
145
119
  export { type UseCanvasExportOptions, type UseCanvasExportReturn, type UseFreehandCanvasReturn, type UseReportExportReturn, type UseScreenshotStudioOptions, type UseScreenshotStudioReturn, useCanvasExport, useFreehandCanvas, useReportExport, useScreenshotStudio };
@@ -106,40 +106,14 @@ interface UseReportExportReturn {
106
106
  */
107
107
  declare function useReportExport(): UseReportExportReturn;
108
108
 
109
- /**
110
- * useFreehandCanvas Hook
111
- * React hook for pointer-based freehand drawing on a canvas
112
- */
113
-
114
- /**
115
- * Return value of the useFreehandCanvas hook
116
- */
117
109
  interface UseFreehandCanvasReturn {
118
- /** Ref to attach to the <canvas> element that captures drawing. */
119
110
  canvasRef: React.RefObject<HTMLCanvasElement | null>;
120
- /** Remove the most recent stroke. */
121
111
  undo: () => void;
122
- /** Remove all strokes. */
123
112
  clear: () => void;
124
- /** Number of committed strokes. */
125
113
  strokeCount: number;
126
- /** True when no strokes are committed. */
127
114
  isEmpty: boolean;
128
- /** Snapshot of committed strokes (safe to serialize). */
129
115
  getStrokes: () => FreehandStroke[];
130
116
  }
131
- /**
132
- * useFreehandCanvas hook
133
- *
134
- * Attaches pointer-event drawing to a canvas element. The canvas is sized to
135
- * its CSS box at device-pixel resolution (and re-rendered on resize), strokes
136
- * are replayed from captured points so undo/clear are lossless, and stylus
137
- * pressure modulates line width when the pointer reports it.
138
- *
139
- * The canvas gets `touch-action: none` applied so drawing works on touch
140
- * screens without scrolling the page. Consumers typically layer the canvas
141
- * over their own guide content (e.g. a faint glyph to trace).
142
- */
143
117
  declare function useFreehandCanvas(options?: UseFreehandCanvasOptions): UseFreehandCanvasReturn;
144
118
 
145
119
  export { type UseCanvasExportOptions, type UseCanvasExportReturn, type UseFreehandCanvasReturn, type UseReportExportReturn, type UseScreenshotStudioOptions, type UseScreenshotStudioReturn, useCanvasExport, useFreehandCanvas, useReportExport, useScreenshotStudio };
@@ -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));
1631
+ }
1632
+ if (sheets.length === 0) {
1633
+ sheets.push({ name: options.sheetName || "Sheet1", rows: [] });
1578
1634
  }
1579
- XLSX.writeFile(wb, options.filename || "report.xlsx");
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
@@ -1738,19 +1800,28 @@ function useReportExport() {
1738
1800
  var import_react4 = require("react");
1739
1801
  var DEFAULT_BRUSH_SIZE = 3;
1740
1802
  var DEFAULT_COLOR = "#1a1a1a";
1803
+ var DEFAULT_KEYBOARD_STEP = 4;
1804
+ var KEYBOARD_FAST_FACTOR = 4;
1741
1805
  function widthFor(stroke, point) {
1742
1806
  if (!stroke.dynamic) return stroke.size;
1743
1807
  return stroke.size * (0.5 + Math.min(1, Math.max(0, point.pressure)));
1744
1808
  }
1745
1809
  function useFreehandCanvas(options = {}) {
1746
- const { brushSize = DEFAULT_BRUSH_SIZE, color = DEFAULT_COLOR, pressureEnabled = true } = options;
1747
- const optionsRef = (0, import_react4.useRef)({ brushSize, color, pressureEnabled, onStrokeEnd: options.onStrokeEnd });
1748
- optionsRef.current = { brushSize, color, pressureEnabled, onStrokeEnd: options.onStrokeEnd };
1810
+ const {
1811
+ brushSize = DEFAULT_BRUSH_SIZE,
1812
+ color = DEFAULT_COLOR,
1813
+ pressureEnabled = true,
1814
+ keyboard = false,
1815
+ keyboardStep = DEFAULT_KEYBOARD_STEP
1816
+ } = options;
1817
+ const optionsRef = (0, import_react4.useRef)({ brushSize, color, pressureEnabled, keyboardStep, onStrokeEnd: options.onStrokeEnd });
1818
+ optionsRef.current = { brushSize, color, pressureEnabled, keyboardStep, onStrokeEnd: options.onStrokeEnd };
1749
1819
  const canvasRef = (0, import_react4.useRef)(null);
1750
1820
  const strokesRef = (0, import_react4.useRef)([]);
1751
1821
  const activeStrokeRef = (0, import_react4.useRef)(null);
1752
1822
  const activePointerIdRef = (0, import_react4.useRef)(null);
1753
1823
  const dprRef = (0, import_react4.useRef)(1);
1824
+ const penRef = (0, import_react4.useRef)(null);
1754
1825
  const [strokeCount, setStrokeCount] = (0, import_react4.useState)(0);
1755
1826
  const redraw = (0, import_react4.useCallback)(() => {
1756
1827
  const canvas = canvasRef.current;
@@ -1783,6 +1854,18 @@ function useFreehandCanvas(options = {}) {
1783
1854
  ctx.stroke();
1784
1855
  }
1785
1856
  }
1857
+ const pen = penRef.current;
1858
+ if (pen?.engaged && !pen.down) {
1859
+ const reach = 5;
1860
+ ctx.strokeStyle = "rgba(35, 37, 43, 0.55)";
1861
+ ctx.lineWidth = 1;
1862
+ ctx.beginPath();
1863
+ ctx.moveTo(pen.x - reach, pen.y);
1864
+ ctx.lineTo(pen.x + reach, pen.y);
1865
+ ctx.moveTo(pen.x, pen.y - reach);
1866
+ ctx.lineTo(pen.x, pen.y + reach);
1867
+ ctx.stroke();
1868
+ }
1786
1869
  }, []);
1787
1870
  const syncDimensions = (0, import_react4.useCallback)(() => {
1788
1871
  const canvas = canvasRef.current;
@@ -1801,6 +1884,8 @@ function useFreehandCanvas(options = {}) {
1801
1884
  if (!canvas) return;
1802
1885
  const el = canvas;
1803
1886
  el.style.touchAction = "none";
1887
+ if (keyboard && !el.hasAttribute("tabindex")) el.tabIndex = 0;
1888
+ if (!penRef.current) penRef.current = { x: el.clientWidth / 2, y: el.clientHeight / 2, down: false, engaged: false };
1804
1889
  syncDimensions();
1805
1890
  redraw();
1806
1891
  const observer = new ResizeObserver(() => {
@@ -1849,6 +1934,7 @@ function useFreehandCanvas(options = {}) {
1849
1934
  points: [pointFrom(event)]
1850
1935
  };
1851
1936
  activeStrokeRef.current = stroke;
1937
+ penRef.current = { x: stroke.points[0].x, y: stroke.points[0].y, down: false, engaged: false };
1852
1938
  drawDot(stroke, stroke.points[0]);
1853
1939
  }
1854
1940
  function onPointerMove(event) {
@@ -1872,18 +1958,81 @@ function useFreehandCanvas(options = {}) {
1872
1958
  setStrokeCount(strokesRef.current.length);
1873
1959
  optionsRef.current.onStrokeEnd?.(stroke);
1874
1960
  }
1961
+ function clampPen(pen) {
1962
+ pen.x = Math.min(Math.max(pen.x, 0), Math.max(0, el.clientWidth - 1));
1963
+ pen.y = Math.min(Math.max(pen.y, 0), Math.max(0, el.clientHeight - 1));
1964
+ }
1965
+ function startKeyboardStroke(pen) {
1966
+ const { brushSize: size, color: ink } = optionsRef.current;
1967
+ const stroke = {
1968
+ size,
1969
+ color: ink,
1970
+ dynamic: false,
1971
+ points: [{ x: pen.x, y: pen.y, pressure: 0.5 }]
1972
+ };
1973
+ activeStrokeRef.current = stroke;
1974
+ pen.down = true;
1975
+ drawDot(stroke, stroke.points[0]);
1976
+ }
1977
+ function commitKeyboardStroke(pen) {
1978
+ const stroke = activeStrokeRef.current;
1979
+ if (!stroke) return;
1980
+ activeStrokeRef.current = null;
1981
+ pen.down = false;
1982
+ strokesRef.current = [...strokesRef.current, stroke];
1983
+ setStrokeCount(strokesRef.current.length);
1984
+ optionsRef.current.onStrokeEnd?.(stroke);
1985
+ redraw();
1986
+ }
1987
+ function onKeyDown(event) {
1988
+ const pen = penRef.current;
1989
+ if (!pen) return;
1990
+ const fast = event.shiftKey ? KEYBOARD_FAST_FACTOR : 1;
1991
+ const step = optionsRef.current.keyboardStep * fast;
1992
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight" || event.key === "ArrowUp" || event.key === "ArrowDown") {
1993
+ event.preventDefault();
1994
+ pen.engaged = true;
1995
+ pen.x += event.key === "ArrowLeft" ? -step : event.key === "ArrowRight" ? step : 0;
1996
+ pen.y += event.key === "ArrowUp" ? -step : event.key === "ArrowDown" ? step : 0;
1997
+ clampPen(pen);
1998
+ const stroke = activeStrokeRef.current;
1999
+ if (pen.down && stroke) {
2000
+ const point = { x: pen.x, y: pen.y, pressure: 0.5 };
2001
+ const previous = stroke.points[stroke.points.length - 1];
2002
+ stroke.points.push(point);
2003
+ drawSegment(stroke, previous, point);
2004
+ } else {
2005
+ redraw();
2006
+ }
2007
+ return;
2008
+ }
2009
+ if (event.key === " " || event.key === "Enter") {
2010
+ event.preventDefault();
2011
+ pen.engaged = true;
2012
+ if (pen.down) commitKeyboardStroke(pen);
2013
+ else startKeyboardStroke(pen);
2014
+ return;
2015
+ }
2016
+ if (event.key === "Escape" && pen.down) {
2017
+ activeStrokeRef.current = null;
2018
+ pen.down = false;
2019
+ redraw();
2020
+ }
2021
+ }
1875
2022
  el.addEventListener("pointerdown", onPointerDown);
1876
2023
  el.addEventListener("pointermove", onPointerMove);
1877
2024
  el.addEventListener("pointerup", commit);
1878
2025
  el.addEventListener("pointercancel", commit);
2026
+ if (keyboard) el.addEventListener("keydown", onKeyDown);
1879
2027
  return () => {
1880
2028
  observer.disconnect();
1881
2029
  el.removeEventListener("pointerdown", onPointerDown);
1882
2030
  el.removeEventListener("pointermove", onPointerMove);
1883
2031
  el.removeEventListener("pointerup", commit);
1884
2032
  el.removeEventListener("pointercancel", commit);
2033
+ el.removeEventListener("keydown", onKeyDown);
1885
2034
  };
1886
- }, [redraw, syncDimensions]);
2035
+ }, [redraw, syncDimensions, keyboard]);
1887
2036
  const undo = (0, import_react4.useCallback)(() => {
1888
2037
  if (strokesRef.current.length === 0) return;
1889
2038
  strokesRef.current = strokesRef.current.slice(0, -1);