@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/application/index.js +85 -23
- package/dist/application/index.mjs +1 -1
- package/dist/{chunk-YKXOSUMF.mjs → chunk-7YLVULSN.mjs} +92 -5
- package/dist/{chunk-DNKRSL6D.mjs → chunk-P6RJ75US.mjs} +85 -23
- package/dist/domain/index.d.mts +2 -22
- package/dist/domain/index.d.ts +2 -22
- package/dist/index.js +176 -27
- package/dist/index.mjs +2 -2
- package/dist/presentation/index.d.mts +0 -26
- package/dist/presentation/index.d.ts +0 -26
- package/dist/presentation/index.js +176 -27
- package/dist/presentation/index.mjs +2 -2
- package/package.json +4 -5
- package/src/application/services/ReportService.ts +31 -23
- package/src/domain/types/freehand.types.ts +2 -23
- package/src/infrastructure/services/minimalXlsx.ts +122 -0
- package/src/presentation/hooks/useFreehandCanvas.ts +102 -40
package/dist/index.js
CHANGED
|
@@ -1010,7 +1010,76 @@ ${project.appMetadata.whatsNew}`;
|
|
|
1010
1010
|
}
|
|
1011
1011
|
};
|
|
1012
1012
|
|
|
1013
|
+
// src/infrastructure/services/minimalXlsx.ts
|
|
1014
|
+
var XML_HEADER = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
|
|
1015
|
+
function escapeXml(value) {
|
|
1016
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1017
|
+
}
|
|
1018
|
+
function safeSheetName(name, index) {
|
|
1019
|
+
const cleaned = name.replace(/[:\\/?*[\]]/g, " ").slice(0, 31).trim();
|
|
1020
|
+
return cleaned.length > 0 ? cleaned : `Sheet${index + 1}`;
|
|
1021
|
+
}
|
|
1022
|
+
function columnName(index) {
|
|
1023
|
+
let name = "";
|
|
1024
|
+
let n = index;
|
|
1025
|
+
while (n >= 0) {
|
|
1026
|
+
name = String.fromCharCode(65 + n % 26) + name;
|
|
1027
|
+
n = Math.floor(n / 26) - 1;
|
|
1028
|
+
}
|
|
1029
|
+
return name;
|
|
1030
|
+
}
|
|
1031
|
+
function cellXml(value, row, column) {
|
|
1032
|
+
const ref = `${columnName(column)}${row}`;
|
|
1033
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1034
|
+
return `<c r="${ref}"><v>${value}</v></c>`;
|
|
1035
|
+
}
|
|
1036
|
+
const text = value === null || value === void 0 ? "" : typeof value === "boolean" ? value ? "TRUE" : "FALSE" : String(value);
|
|
1037
|
+
return `<c r="${ref}" t="inlineStr"><is><t xml:space="preserve">${escapeXml(text)}</t></is></c>`;
|
|
1038
|
+
}
|
|
1039
|
+
function sheetXml(rows) {
|
|
1040
|
+
const body = rows.map((row, rowIndex) => {
|
|
1041
|
+
const cells = row.map((value, columnIndex) => cellXml(value, rowIndex + 1, columnIndex)).join("");
|
|
1042
|
+
return `<row r="${rowIndex + 1}">${cells}</row>`;
|
|
1043
|
+
}).join("");
|
|
1044
|
+
return `${XML_HEADER}<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${body}</sheetData></worksheet>`;
|
|
1045
|
+
}
|
|
1046
|
+
async function writeXlsxBlob(sheets, JSZip) {
|
|
1047
|
+
const zip = new JSZip();
|
|
1048
|
+
const safeNames = sheets.map((sheet, index) => safeSheetName(sheet.name, index));
|
|
1049
|
+
zip.file(
|
|
1050
|
+
"[Content_Types].xml",
|
|
1051
|
+
`${XML_HEADER}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>` + safeNames.map(
|
|
1052
|
+
(_, index) => `<Override PartName="/xl/worksheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`
|
|
1053
|
+
).join("") + "</Types>"
|
|
1054
|
+
);
|
|
1055
|
+
zip.file(
|
|
1056
|
+
"_rels/.rels",
|
|
1057
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>`
|
|
1058
|
+
);
|
|
1059
|
+
zip.file(
|
|
1060
|
+
"xl/workbook.xml",
|
|
1061
|
+
`${XML_HEADER}<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>` + safeNames.map((name, index) => `<sheet name="${escapeXml(name)}" sheetId="${index + 1}" r:id="rId${index + 1}"/>`).join("") + "</sheets></workbook>"
|
|
1062
|
+
);
|
|
1063
|
+
zip.file(
|
|
1064
|
+
"xl/_rels/workbook.xml.rels",
|
|
1065
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` + safeNames.map(
|
|
1066
|
+
(_, index) => `<Relationship Id="rId${index + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${index + 1}.xml"/>`
|
|
1067
|
+
).join("") + "</Relationships>"
|
|
1068
|
+
);
|
|
1069
|
+
sheets.forEach((sheet, index) => {
|
|
1070
|
+
zip.file(`xl/worksheets/sheet${index + 1}.xml`, sheetXml(sheet.rows));
|
|
1071
|
+
});
|
|
1072
|
+
return zip.generateAsync({ type: "blob" });
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1013
1075
|
// src/application/services/ReportService.ts
|
|
1076
|
+
function recordsToSheet(name, records) {
|
|
1077
|
+
const headers = [...new Set(records.flatMap((record) => Object.keys(record)))];
|
|
1078
|
+
return {
|
|
1079
|
+
name,
|
|
1080
|
+
rows: [headers, ...records.map((record) => headers.map((header) => record[header]))]
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1014
1083
|
var ReportService = class _ReportService {
|
|
1015
1084
|
static getInstance() {
|
|
1016
1085
|
if (!_ReportService.instance) {
|
|
@@ -1056,31 +1125,19 @@ var ReportService = class _ReportService {
|
|
|
1056
1125
|
* Export data to Excel
|
|
1057
1126
|
*/
|
|
1058
1127
|
async exportToExcel(data, options = {}) {
|
|
1059
|
-
const
|
|
1060
|
-
const wb = XLSX.utils.book_new();
|
|
1128
|
+
const JSZip = (await import("jszip")).default;
|
|
1061
1129
|
const apps = data.items.filter((i) => i.type === "app").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1062
1130
|
const campaigns = data.items.filter((i) => i.type === "campaign").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1063
1131
|
const posts = data.items.filter((i) => i.type === "post").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1132
|
+
const sheets = [];
|
|
1064
1133
|
if (apps.length > 0) {
|
|
1065
|
-
|
|
1066
|
-
wb,
|
|
1067
|
-
XLSX.utils.json_to_sheet(apps),
|
|
1068
|
-
options.sheetName || "Apps"
|
|
1069
|
-
);
|
|
1134
|
+
sheets.push(recordsToSheet(options.sheetName || "Apps", apps));
|
|
1070
1135
|
}
|
|
1071
1136
|
if (campaigns.length > 0) {
|
|
1072
|
-
|
|
1073
|
-
wb,
|
|
1074
|
-
XLSX.utils.json_to_sheet(campaigns),
|
|
1075
|
-
"Campaigns"
|
|
1076
|
-
);
|
|
1137
|
+
sheets.push(recordsToSheet("Campaigns", campaigns));
|
|
1077
1138
|
}
|
|
1078
1139
|
if (posts.length > 0) {
|
|
1079
|
-
|
|
1080
|
-
wb,
|
|
1081
|
-
XLSX.utils.json_to_sheet(posts),
|
|
1082
|
-
"Posts"
|
|
1083
|
-
);
|
|
1140
|
+
sheets.push(recordsToSheet("Posts", posts));
|
|
1084
1141
|
}
|
|
1085
1142
|
if (options.includeSummary !== false) {
|
|
1086
1143
|
const summary = [
|
|
@@ -1098,13 +1155,18 @@ var ReportService = class _ReportService {
|
|
|
1098
1155
|
Value: (/* @__PURE__ */ new Date()).toLocaleDateString()
|
|
1099
1156
|
}
|
|
1100
1157
|
];
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
);
|
|
1158
|
+
sheets.push(recordsToSheet("Summary", summary));
|
|
1159
|
+
}
|
|
1160
|
+
if (sheets.length === 0) {
|
|
1161
|
+
sheets.push({ name: options.sheetName || "Sheet1", rows: [] });
|
|
1106
1162
|
}
|
|
1107
|
-
|
|
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 {
|
|
1753
|
-
|
|
1754
|
-
|
|
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-
|
|
6
|
+
} from "./chunk-7YLVULSN.mjs";
|
|
7
7
|
import {
|
|
8
8
|
ReportService,
|
|
9
9
|
ScreenshotStudioService
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-P6RJ75US.mjs";
|
|
11
11
|
import {
|
|
12
12
|
CanvasRenderer,
|
|
13
13
|
DeviceConfigService,
|
|
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1489
|
+
}
|
|
1490
|
+
function safeSheetName(name, index) {
|
|
1491
|
+
const cleaned = name.replace(/[:\\/?*[\]]/g, " ").slice(0, 31).trim();
|
|
1492
|
+
return cleaned.length > 0 ? cleaned : `Sheet${index + 1}`;
|
|
1493
|
+
}
|
|
1494
|
+
function columnName(index) {
|
|
1495
|
+
let name = "";
|
|
1496
|
+
let n = index;
|
|
1497
|
+
while (n >= 0) {
|
|
1498
|
+
name = String.fromCharCode(65 + n % 26) + name;
|
|
1499
|
+
n = Math.floor(n / 26) - 1;
|
|
1500
|
+
}
|
|
1501
|
+
return name;
|
|
1502
|
+
}
|
|
1503
|
+
function cellXml(value, row, column) {
|
|
1504
|
+
const ref = `${columnName(column)}${row}`;
|
|
1505
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1506
|
+
return `<c r="${ref}"><v>${value}</v></c>`;
|
|
1507
|
+
}
|
|
1508
|
+
const text = value === null || value === void 0 ? "" : typeof value === "boolean" ? value ? "TRUE" : "FALSE" : String(value);
|
|
1509
|
+
return `<c r="${ref}" t="inlineStr"><is><t xml:space="preserve">${escapeXml(text)}</t></is></c>`;
|
|
1510
|
+
}
|
|
1511
|
+
function sheetXml(rows) {
|
|
1512
|
+
const body = rows.map((row, rowIndex) => {
|
|
1513
|
+
const cells = row.map((value, columnIndex) => cellXml(value, rowIndex + 1, columnIndex)).join("");
|
|
1514
|
+
return `<row r="${rowIndex + 1}">${cells}</row>`;
|
|
1515
|
+
}).join("");
|
|
1516
|
+
return `${XML_HEADER}<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${body}</sheetData></worksheet>`;
|
|
1517
|
+
}
|
|
1518
|
+
async function writeXlsxBlob(sheets, JSZip) {
|
|
1519
|
+
const zip = new JSZip();
|
|
1520
|
+
const safeNames = sheets.map((sheet, index) => safeSheetName(sheet.name, index));
|
|
1521
|
+
zip.file(
|
|
1522
|
+
"[Content_Types].xml",
|
|
1523
|
+
`${XML_HEADER}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>` + safeNames.map(
|
|
1524
|
+
(_, index) => `<Override PartName="/xl/worksheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`
|
|
1525
|
+
).join("") + "</Types>"
|
|
1526
|
+
);
|
|
1527
|
+
zip.file(
|
|
1528
|
+
"_rels/.rels",
|
|
1529
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>`
|
|
1530
|
+
);
|
|
1531
|
+
zip.file(
|
|
1532
|
+
"xl/workbook.xml",
|
|
1533
|
+
`${XML_HEADER}<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>` + safeNames.map((name, index) => `<sheet name="${escapeXml(name)}" sheetId="${index + 1}" r:id="rId${index + 1}"/>`).join("") + "</sheets></workbook>"
|
|
1534
|
+
);
|
|
1535
|
+
zip.file(
|
|
1536
|
+
"xl/_rels/workbook.xml.rels",
|
|
1537
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` + safeNames.map(
|
|
1538
|
+
(_, index) => `<Relationship Id="rId${index + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${index + 1}.xml"/>`
|
|
1539
|
+
).join("") + "</Relationships>"
|
|
1540
|
+
);
|
|
1541
|
+
sheets.forEach((sheet, index) => {
|
|
1542
|
+
zip.file(`xl/worksheets/sheet${index + 1}.xml`, sheetXml(sheet.rows));
|
|
1543
|
+
});
|
|
1544
|
+
return zip.generateAsync({ type: "blob" });
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1485
1547
|
// src/application/services/ReportService.ts
|
|
1548
|
+
function recordsToSheet(name, records) {
|
|
1549
|
+
const headers = [...new Set(records.flatMap((record) => Object.keys(record)))];
|
|
1550
|
+
return {
|
|
1551
|
+
name,
|
|
1552
|
+
rows: [headers, ...records.map((record) => headers.map((header) => record[header]))]
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1486
1555
|
var ReportService = class _ReportService {
|
|
1487
1556
|
static getInstance() {
|
|
1488
1557
|
if (!_ReportService.instance) {
|
|
@@ -1528,31 +1597,19 @@ var ReportService = class _ReportService {
|
|
|
1528
1597
|
* Export data to Excel
|
|
1529
1598
|
*/
|
|
1530
1599
|
async exportToExcel(data, options = {}) {
|
|
1531
|
-
const
|
|
1532
|
-
const wb = XLSX.utils.book_new();
|
|
1600
|
+
const JSZip = (await import("jszip")).default;
|
|
1533
1601
|
const apps = data.items.filter((i) => i.type === "app").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1534
1602
|
const campaigns = data.items.filter((i) => i.type === "campaign").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1535
1603
|
const posts = data.items.filter((i) => i.type === "post").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1604
|
+
const sheets = [];
|
|
1536
1605
|
if (apps.length > 0) {
|
|
1537
|
-
|
|
1538
|
-
wb,
|
|
1539
|
-
XLSX.utils.json_to_sheet(apps),
|
|
1540
|
-
options.sheetName || "Apps"
|
|
1541
|
-
);
|
|
1606
|
+
sheets.push(recordsToSheet(options.sheetName || "Apps", apps));
|
|
1542
1607
|
}
|
|
1543
1608
|
if (campaigns.length > 0) {
|
|
1544
|
-
|
|
1545
|
-
wb,
|
|
1546
|
-
XLSX.utils.json_to_sheet(campaigns),
|
|
1547
|
-
"Campaigns"
|
|
1548
|
-
);
|
|
1609
|
+
sheets.push(recordsToSheet("Campaigns", campaigns));
|
|
1549
1610
|
}
|
|
1550
1611
|
if (posts.length > 0) {
|
|
1551
|
-
|
|
1552
|
-
wb,
|
|
1553
|
-
XLSX.utils.json_to_sheet(posts),
|
|
1554
|
-
"Posts"
|
|
1555
|
-
);
|
|
1612
|
+
sheets.push(recordsToSheet("Posts", posts));
|
|
1556
1613
|
}
|
|
1557
1614
|
if (options.includeSummary !== false) {
|
|
1558
1615
|
const summary = [
|
|
@@ -1570,13 +1627,18 @@ var ReportService = class _ReportService {
|
|
|
1570
1627
|
Value: (/* @__PURE__ */ new Date()).toLocaleDateString()
|
|
1571
1628
|
}
|
|
1572
1629
|
];
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
);
|
|
1630
|
+
sheets.push(recordsToSheet("Summary", summary));
|
|
1631
|
+
}
|
|
1632
|
+
if (sheets.length === 0) {
|
|
1633
|
+
sheets.push({ name: options.sheetName || "Sheet1", rows: [] });
|
|
1578
1634
|
}
|
|
1579
|
-
|
|
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 {
|
|
1747
|
-
|
|
1748
|
-
|
|
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);
|