@umituz/web-canvas 1.2.0 → 1.2.2

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.
Files changed (35) hide show
  1. package/dist/{chunk-YRC4YPMO.mjs → chunk-7YLVULSN.mjs} +91 -4
  2. package/dist/domain/index.d.mts +2 -22
  3. package/dist/domain/index.d.ts +2 -22
  4. package/dist/index.js +91 -4
  5. package/dist/index.mjs +1 -1
  6. package/dist/presentation/index.d.mts +0 -26
  7. package/dist/presentation/index.d.ts +0 -26
  8. package/dist/presentation/index.js +91 -4
  9. package/dist/presentation/index.mjs +1 -1
  10. package/package.json +1 -1
  11. package/src/application/index.ts +0 -4
  12. package/src/application/services/ReportService.ts +0 -22
  13. package/src/application/services/ScreenshotStudioService.ts +1 -35
  14. package/src/domain/config/CanvasConfig.ts +0 -16
  15. package/src/domain/config/DeviceConfig.ts +0 -18
  16. package/src/domain/config/ScreenshotStudioConfig.ts +0 -29
  17. package/src/domain/config/index.ts +0 -4
  18. package/src/domain/index.ts +0 -4
  19. package/src/domain/types/canvas.types.ts +1 -35
  20. package/src/domain/types/freehand.types.ts +2 -23
  21. package/src/domain/types/index.ts +0 -4
  22. package/src/domain/types/report.types.ts +0 -31
  23. package/src/domain/types/screenshot.types.ts +1 -65
  24. package/src/index.ts +0 -12
  25. package/src/infrastructure/index.ts +0 -4
  26. package/src/infrastructure/renderers/CanvasRenderer.ts +0 -55
  27. package/src/infrastructure/renderers/PatternRenderer.ts +0 -30
  28. package/src/infrastructure/renderers/ScreenshotRenderer.ts +0 -48
  29. package/src/infrastructure/services/DeviceConfigService.ts +0 -10
  30. package/src/infrastructure/services/minimalXlsx.ts +0 -11
  31. package/src/presentation/hooks/useCanvasExport.ts +2 -22
  32. package/src/presentation/hooks/useFreehandCanvas.ts +102 -40
  33. package/src/presentation/hooks/useReportExport.ts +0 -12
  34. package/src/presentation/hooks/useScreenshotStudio.ts +1 -45
  35. package/src/presentation/index.ts +0 -4
@@ -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);
@@ -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
 
package/dist/index.js CHANGED
@@ -1806,19 +1806,28 @@ function useReportExport() {
1806
1806
  var import_react4 = require("react");
1807
1807
  var DEFAULT_BRUSH_SIZE = 3;
1808
1808
  var DEFAULT_COLOR = "#1a1a1a";
1809
+ var DEFAULT_KEYBOARD_STEP = 4;
1810
+ var KEYBOARD_FAST_FACTOR = 4;
1809
1811
  function widthFor(stroke, point) {
1810
1812
  if (!stroke.dynamic) return stroke.size;
1811
1813
  return stroke.size * (0.5 + Math.min(1, Math.max(0, point.pressure)));
1812
1814
  }
1813
1815
  function useFreehandCanvas(options = {}) {
1814
- const { brushSize = DEFAULT_BRUSH_SIZE, color = DEFAULT_COLOR, pressureEnabled = true } = options;
1815
- const optionsRef = (0, import_react4.useRef)({ brushSize, color, pressureEnabled, onStrokeEnd: options.onStrokeEnd });
1816
- 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 };
1817
1825
  const canvasRef = (0, import_react4.useRef)(null);
1818
1826
  const strokesRef = (0, import_react4.useRef)([]);
1819
1827
  const activeStrokeRef = (0, import_react4.useRef)(null);
1820
1828
  const activePointerIdRef = (0, import_react4.useRef)(null);
1821
1829
  const dprRef = (0, import_react4.useRef)(1);
1830
+ const penRef = (0, import_react4.useRef)(null);
1822
1831
  const [strokeCount, setStrokeCount] = (0, import_react4.useState)(0);
1823
1832
  const redraw = (0, import_react4.useCallback)(() => {
1824
1833
  const canvas = canvasRef.current;
@@ -1851,6 +1860,18 @@ function useFreehandCanvas(options = {}) {
1851
1860
  ctx.stroke();
1852
1861
  }
1853
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
+ }
1854
1875
  }, []);
1855
1876
  const syncDimensions = (0, import_react4.useCallback)(() => {
1856
1877
  const canvas = canvasRef.current;
@@ -1869,6 +1890,8 @@ function useFreehandCanvas(options = {}) {
1869
1890
  if (!canvas) return;
1870
1891
  const el = canvas;
1871
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 };
1872
1895
  syncDimensions();
1873
1896
  redraw();
1874
1897
  const observer = new ResizeObserver(() => {
@@ -1917,6 +1940,7 @@ function useFreehandCanvas(options = {}) {
1917
1940
  points: [pointFrom(event)]
1918
1941
  };
1919
1942
  activeStrokeRef.current = stroke;
1943
+ penRef.current = { x: stroke.points[0].x, y: stroke.points[0].y, down: false, engaged: false };
1920
1944
  drawDot(stroke, stroke.points[0]);
1921
1945
  }
1922
1946
  function onPointerMove(event) {
@@ -1940,18 +1964,81 @@ function useFreehandCanvas(options = {}) {
1940
1964
  setStrokeCount(strokesRef.current.length);
1941
1965
  optionsRef.current.onStrokeEnd?.(stroke);
1942
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
+ }
1943
2028
  el.addEventListener("pointerdown", onPointerDown);
1944
2029
  el.addEventListener("pointermove", onPointerMove);
1945
2030
  el.addEventListener("pointerup", commit);
1946
2031
  el.addEventListener("pointercancel", commit);
2032
+ if (keyboard) el.addEventListener("keydown", onKeyDown);
1947
2033
  return () => {
1948
2034
  observer.disconnect();
1949
2035
  el.removeEventListener("pointerdown", onPointerDown);
1950
2036
  el.removeEventListener("pointermove", onPointerMove);
1951
2037
  el.removeEventListener("pointerup", commit);
1952
2038
  el.removeEventListener("pointercancel", commit);
2039
+ el.removeEventListener("keydown", onKeyDown);
1953
2040
  };
1954
- }, [redraw, syncDimensions]);
2041
+ }, [redraw, syncDimensions, keyboard]);
1955
2042
  const undo = (0, import_react4.useCallback)(() => {
1956
2043
  if (strokesRef.current.length === 0) return;
1957
2044
  strokesRef.current = strokesRef.current.slice(0, -1);
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  useFreehandCanvas,
4
4
  useReportExport,
5
5
  useScreenshotStudio
6
- } from "./chunk-YRC4YPMO.mjs";
6
+ } from "./chunk-7YLVULSN.mjs";
7
7
  import {
8
8
  ReportService,
9
9
  ScreenshotStudioService
@@ -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 };
@@ -1800,19 +1800,28 @@ function useReportExport() {
1800
1800
  var import_react4 = require("react");
1801
1801
  var DEFAULT_BRUSH_SIZE = 3;
1802
1802
  var DEFAULT_COLOR = "#1a1a1a";
1803
+ var DEFAULT_KEYBOARD_STEP = 4;
1804
+ var KEYBOARD_FAST_FACTOR = 4;
1803
1805
  function widthFor(stroke, point) {
1804
1806
  if (!stroke.dynamic) return stroke.size;
1805
1807
  return stroke.size * (0.5 + Math.min(1, Math.max(0, point.pressure)));
1806
1808
  }
1807
1809
  function useFreehandCanvas(options = {}) {
1808
- const { brushSize = DEFAULT_BRUSH_SIZE, color = DEFAULT_COLOR, pressureEnabled = true } = options;
1809
- const optionsRef = (0, import_react4.useRef)({ brushSize, color, pressureEnabled, onStrokeEnd: options.onStrokeEnd });
1810
- 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 };
1811
1819
  const canvasRef = (0, import_react4.useRef)(null);
1812
1820
  const strokesRef = (0, import_react4.useRef)([]);
1813
1821
  const activeStrokeRef = (0, import_react4.useRef)(null);
1814
1822
  const activePointerIdRef = (0, import_react4.useRef)(null);
1815
1823
  const dprRef = (0, import_react4.useRef)(1);
1824
+ const penRef = (0, import_react4.useRef)(null);
1816
1825
  const [strokeCount, setStrokeCount] = (0, import_react4.useState)(0);
1817
1826
  const redraw = (0, import_react4.useCallback)(() => {
1818
1827
  const canvas = canvasRef.current;
@@ -1845,6 +1854,18 @@ function useFreehandCanvas(options = {}) {
1845
1854
  ctx.stroke();
1846
1855
  }
1847
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
+ }
1848
1869
  }, []);
1849
1870
  const syncDimensions = (0, import_react4.useCallback)(() => {
1850
1871
  const canvas = canvasRef.current;
@@ -1863,6 +1884,8 @@ function useFreehandCanvas(options = {}) {
1863
1884
  if (!canvas) return;
1864
1885
  const el = canvas;
1865
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 };
1866
1889
  syncDimensions();
1867
1890
  redraw();
1868
1891
  const observer = new ResizeObserver(() => {
@@ -1911,6 +1934,7 @@ function useFreehandCanvas(options = {}) {
1911
1934
  points: [pointFrom(event)]
1912
1935
  };
1913
1936
  activeStrokeRef.current = stroke;
1937
+ penRef.current = { x: stroke.points[0].x, y: stroke.points[0].y, down: false, engaged: false };
1914
1938
  drawDot(stroke, stroke.points[0]);
1915
1939
  }
1916
1940
  function onPointerMove(event) {
@@ -1934,18 +1958,81 @@ function useFreehandCanvas(options = {}) {
1934
1958
  setStrokeCount(strokesRef.current.length);
1935
1959
  optionsRef.current.onStrokeEnd?.(stroke);
1936
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
+ }
1937
2022
  el.addEventListener("pointerdown", onPointerDown);
1938
2023
  el.addEventListener("pointermove", onPointerMove);
1939
2024
  el.addEventListener("pointerup", commit);
1940
2025
  el.addEventListener("pointercancel", commit);
2026
+ if (keyboard) el.addEventListener("keydown", onKeyDown);
1941
2027
  return () => {
1942
2028
  observer.disconnect();
1943
2029
  el.removeEventListener("pointerdown", onPointerDown);
1944
2030
  el.removeEventListener("pointermove", onPointerMove);
1945
2031
  el.removeEventListener("pointerup", commit);
1946
2032
  el.removeEventListener("pointercancel", commit);
2033
+ el.removeEventListener("keydown", onKeyDown);
1947
2034
  };
1948
- }, [redraw, syncDimensions]);
2035
+ }, [redraw, syncDimensions, keyboard]);
1949
2036
  const undo = (0, import_react4.useCallback)(() => {
1950
2037
  if (strokesRef.current.length === 0) return;
1951
2038
  strokesRef.current = strokesRef.current.slice(0, -1);
@@ -3,7 +3,7 @@ import {
3
3
  useFreehandCanvas,
4
4
  useReportExport,
5
5
  useScreenshotStudio
6
- } from "../chunk-YRC4YPMO.mjs";
6
+ } from "../chunk-7YLVULSN.mjs";
7
7
  import "../chunk-P6RJ75US.mjs";
8
8
  import "../chunk-UOGTEMIJ.mjs";
9
9
  import "../chunk-Z6IB4BQV.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/web-canvas",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
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",
@@ -1,7 +1,3 @@
1
- /**
2
- * Application Layer Index
3
- * Export all application services
4
- */
5
1
 
6
2
  export * from './services/ScreenshotStudioService';
7
3
  export * from './services/ReportService';