{"version":3,"file":"SignaturePad.cjs","names":[],"sources":["../../../src/components/SignaturePad/SignaturePad.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the canvas (width,\n * height, penColor, penWidth), the lifecycle an app needs to enable its save button\n * (onBegin, onEnd, onEmptyChange) and the built-in actions with their copy\n * (showActions, clearLabel, undoLabel, label). The body is the stroke buffer plus\n * undo history, which are the same array.\n */\nimport { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from \"react\";\nimport type { PointerEvent as ReactPointerEvent } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { Button } from \"../Button\";\nimport styles from \"./SignaturePad.module.css\";\n\n/** A single sampled point of a stroke, in CSS pixels relative to the canvas. */\ninterface Point {\n    x: number;\n    y: number;\n}\n\n/** Imperative handle exposed through `ref`. */\nexport interface SignaturePadHandle {\n    /** Drop every stroke. */\n    clear: () => void;\n    /** Drop the last stroke. */\n    undo: () => void;\n    /** `true` while nothing has been drawn. */\n    isEmpty: () => boolean;\n    /** Export as a data URL. Returns `\"\"` when the canvas is unavailable. */\n    toDataURL: (type?: string, quality?: number) => string;\n    /** Export as a `Blob` — what you actually upload. `null` when unavailable. */\n    toBlob: (type?: string, quality?: number) => Promise<Blob | null>;\n}\n\nexport interface SignaturePadProps {\n    /** Drawing surface width in CSS pixels. Default `400`. */\n    width?: number;\n    /** Drawing surface height in CSS pixels. Default `160`. */\n    height?: number;\n    /**\n     * Stroke color. Defaults to the canvas' computed `color`, which the\n     * stylesheet binds to `--tempest-text` — so the signature follows the theme\n     * (including dark mode) instead of being hardcoded black.\n     */\n    penColor?: string;\n    /** Stroke width in CSS pixels. Default `2`. */\n    penWidth?: number;\n    /** Blocks drawing and dims the surface. */\n    disabled?: boolean;\n    /** Accessible name of the surface. Default `\"Signature\"`. */\n    label?: string;\n    /** Called on the first move of each stroke. */\n    onBegin?: () => void;\n    /** Called when a stroke ends, with the current image as a data URL. */\n    onEnd?: (dataUrl: string) => void;\n    /** Called whenever the emptiness changes — wire it to a submit button. */\n    onEmptyChange?: (isEmpty: boolean) => void;\n    /** Renders the Clear/Undo buttons. Default `true`. */\n    showActions?: boolean;\n    clearLabel?: string;\n    undoLabel?: string;\n    className?: string;\n}\n\n/**\n * Signature capture on a canvas — the \"sign here\" field of a delivery receipt,\n * a service order, a term of acceptance.\n *\n * Strokes are kept as point lists and the canvas is **redrawn** from them, which\n * is what makes `undo` possible at all: a canvas holds pixels, not history, so\n * removing the last stroke means replaying the rest.\n *\n * The backing store is scaled by `devicePixelRatio`, so the line is crisp on a\n * phone instead of the blurry 1x bitmap a naive canvas produces.\n *\n * @example\n * ```tsx\n * const pad = useRef<SignaturePadHandle>(null);\n * const [empty, setEmpty] = useState(true);\n *\n * async function submit() {\n *   const blob = await pad.current?.toBlob(\"image/png\");\n *   if (blob) await api.upload(\"/receipts/42/signature\", blob);\n * }\n *\n * <SignaturePad ref={pad} label=\"Assinatura do cliente\" onEmptyChange={setEmpty} />\n * <Button disabled={empty} onClick={submit}>Enviar</Button>\n * ```\n */\nexport const SignaturePad = forwardRef<SignaturePadHandle, SignaturePadProps>(function SignaturePad(\n    {\n        width = 400,\n        height = 160,\n        penColor,\n        penWidth = 2,\n        disabled = false,\n        label = \"Signature\",\n        onBegin,\n        onEnd,\n        onEmptyChange,\n        showActions = true,\n        clearLabel = \"Clear\",\n        undoLabel = \"Undo\",\n        className,\n    },\n    ref,\n) {\n    const canvasRef = useRef<HTMLCanvasElement>(null);\n    const strokesRef = useRef<Point[][]>([]);\n    const drawingRef = useRef(false);\n    const [isEmpty, setIsEmpty] = useState(true);\n\n    const resolvePenColor = useCallback((): string => {\n        if (penColor) return penColor;\n        const canvas = canvasRef.current;\n        if (!canvas || typeof window === \"undefined\") return \"#101828\";\n        return window.getComputedStyle(canvas).color || \"#101828\";\n    }, [penColor]);\n\n    /**\n     * Repaint the whole surface from the stroke list, honouring the pixel ratio.\n     *\n     * A one-point stroke (a tap) gets a line back to its own origin: `stroke()`\n     * over a path with a single point paints nothing, so without it a tap would\n     * silently produce no dot while still counting as \"not empty\".\n     */\n    const redraw = useCallback((): void => {\n        const canvas = canvasRef.current;\n        const context = canvas?.getContext(\"2d\");\n        if (!canvas || !context) return;\n\n        const ratio = typeof window === \"undefined\" ? 1 : window.devicePixelRatio || 1;\n        canvas.width = width * ratio;\n        canvas.height = height * ratio;\n        context.setTransform(ratio, 0, 0, ratio, 0, 0);\n        context.clearRect(0, 0, width, height);\n\n        context.lineWidth = penWidth;\n        context.lineCap = \"round\";\n        context.lineJoin = \"round\";\n        context.strokeStyle = resolvePenColor();\n\n        for (const stroke of strokesRef.current) {\n            if (stroke.length === 0) continue;\n            context.beginPath();\n            context.moveTo(stroke[0].x, stroke[0].y);\n            for (const point of stroke.slice(1)) {\n                context.lineTo(point.x, point.y);\n            }\n            if (stroke.length === 1) context.lineTo(stroke[0].x, stroke[0].y);\n            context.stroke();\n        }\n    }, [height, penWidth, resolvePenColor, width]);\n\n    useEffect(() => {\n        redraw();\n    }, [redraw]);\n\n    const setEmptiness = useCallback(\n        (next: boolean): void => {\n            setIsEmpty((current) => {\n                if (current !== next) onEmptyChange?.(next);\n                return next;\n            });\n        },\n        [onEmptyChange],\n    );\n\n    const pointFrom = useCallback((event: ReactPointerEvent<HTMLCanvasElement>): Point => {\n        const rect = canvasRef.current?.getBoundingClientRect();\n        return {\n            x: event.clientX - (rect?.left ?? 0),\n            y: event.clientY - (rect?.top ?? 0),\n        };\n    }, []);\n\n    const handlePointerDown = useCallback(\n        (event: ReactPointerEvent<HTMLCanvasElement>): void => {\n            if (disabled) return;\n            drawingRef.current = true;\n            strokesRef.current = [...strokesRef.current, [pointFrom(event)]];\n            canvasRef.current?.setPointerCapture?.(event.pointerId);\n            onBegin?.();\n            setEmptiness(false);\n            redraw();\n        },\n        [disabled, onBegin, pointFrom, redraw, setEmptiness],\n    );\n\n    const handlePointerMove = useCallback(\n        (event: ReactPointerEvent<HTMLCanvasElement>): void => {\n            if (!drawingRef.current || disabled) return;\n            const strokes = strokesRef.current;\n            strokes[strokes.length - 1].push(pointFrom(event));\n            redraw();\n        },\n        [disabled, pointFrom, redraw],\n    );\n\n    const handlePointerUp = useCallback(\n        (event: ReactPointerEvent<HTMLCanvasElement>): void => {\n            if (!drawingRef.current) return;\n            drawingRef.current = false;\n            canvasRef.current?.releasePointerCapture?.(event.pointerId);\n            onEnd?.(canvasRef.current?.toDataURL() ?? \"\");\n        },\n        [onEnd],\n    );\n\n    const clear = useCallback((): void => {\n        strokesRef.current = [];\n        setEmptiness(true);\n        redraw();\n    }, [redraw, setEmptiness]);\n\n    const undo = useCallback((): void => {\n        strokesRef.current = strokesRef.current.slice(0, -1);\n        setEmptiness(strokesRef.current.length === 0);\n        redraw();\n    }, [redraw, setEmptiness]);\n\n    useImperativeHandle(\n        ref,\n        () => ({\n            clear,\n            undo,\n            isEmpty: () => strokesRef.current.length === 0,\n            toDataURL: (type, quality) => canvasRef.current?.toDataURL(type, quality) ?? \"\",\n            toBlob: (type, quality) =>\n                new Promise((resolve) => {\n                    const canvas = canvasRef.current;\n                    if (!canvas?.toBlob) {\n                        resolve(null);\n                        return;\n                    }\n                    canvas.toBlob((blob) => resolve(blob), type, quality);\n                }),\n        }),\n        [clear, undo],\n    );\n\n    return (\n        <div className={cn(styles.pad, disabled && styles.disabled, className)}>\n            <canvas\n                ref={canvasRef}\n                role=\"img\"\n                aria-label={label}\n                aria-disabled={disabled || undefined}\n                className={styles.canvas}\n                style={{ width, height, touchAction: \"none\" }}\n                onPointerDown={handlePointerDown}\n                onPointerMove={handlePointerMove}\n                onPointerUp={handlePointerUp}\n                onPointerLeave={handlePointerUp}\n            />\n            {showActions ? (\n                <div className={styles.actions}>\n                    <Button\n                        type=\"button\"\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        onClick={undo}\n                        disabled={disabled || isEmpty}\n                    >\n                        {undoLabel}\n                    </Button>\n                    <Button\n                        type=\"button\"\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        onClick={clear}\n                        disabled={disabled || isEmpty}\n                    >\n                        {clearLabel}\n                    </Button>\n                </div>\n            ) : null}\n        </div>\n    );\n});\n"],"mappings":"qKAwFA,IAAa,GAAA,EAAe,EAAA,WAAA,CAAkD,SAC1E,CACI,QAAQ,IACR,SAAS,IACT,WACA,WAAW,EACX,WAAW,GACX,QAAQ,YACR,UACA,QACA,gBACA,cAAc,GACd,aAAa,QACb,YAAY,OACZ,aAEJ,EACF,CACE,IAAM,GAAA,EAAY,EAAA,OAAA,CAA0B,IAAI,EAC1C,GAAA,EAAa,EAAA,OAAA,CAAkB,CAAC,CAAC,EACjC,GAAA,EAAa,EAAA,OAAA,CAAO,EAAK,EACzB,CAAC,EAAS,IAAA,EAAc,EAAA,SAAA,CAAS,EAAI,EAErC,GAAA,EAAkB,EAAA,YAAA,KAA0B,CAC9C,GAAI,EAAU,OAAO,EACrB,IAAM,EAAS,EAAU,QAEzB,MADI,CAAC,GAAU,OAAO,OAAW,IAAoB,UAC9C,OAAO,iBAAiB,CAAM,CAAC,CAAC,OAAS,SACpD,EAAG,CAAC,CAAQ,CAAC,EASP,GAAA,EAAS,EAAA,YAAA,KAAwB,CACnC,IAAM,EAAS,EAAU,QACnB,EAAU,GAAQ,WAAW,IAAI,EACvC,GAAI,CAAC,GAAU,CAAC,EAAS,OAEzB,IAAM,EAAQ,OAAO,OAAW,IAAc,EAAI,OAAO,kBAAoB,EAC7E,EAAO,MAAQ,EAAQ,EACvB,EAAO,OAAS,EAAS,EACzB,EAAQ,aAAa,EAAO,EAAG,EAAG,EAAO,EAAG,CAAC,EAC7C,EAAQ,UAAU,EAAG,EAAG,EAAO,CAAM,EAErC,EAAQ,UAAY,EACpB,EAAQ,QAAU,QAClB,EAAQ,SAAW,QACnB,EAAQ,YAAc,EAAgB,EAEtC,IAAK,IAAM,KAAU,EAAW,QACxB,KAAO,SAAW,EAEtB,CADA,EAAQ,UAAU,EAClB,EAAQ,OAAO,EAAO,EAAE,CAAC,EAAG,EAAO,EAAE,CAAC,CAAC,EACvC,IAAK,IAAM,KAAS,EAAO,MAAM,CAAC,EAC9B,EAAQ,OAAO,EAAM,EAAG,EAAM,CAAC,EAE/B,EAAO,SAAW,GAAG,EAAQ,OAAO,EAAO,EAAE,CAAC,EAAG,EAAO,EAAE,CAAC,CAAC,EAChE,EAAQ,OAAO,CALwB,CAO/C,EAAG,CAAC,EAAQ,EAAU,EAAiB,CAAK,CAAC,GAE7C,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAO,CACX,EAAG,CAAC,CAAM,CAAC,EAEX,IAAM,GAAA,EAAe,EAAA,YAAA,CAChB,GAAwB,CACrB,EAAY,IACJ,IAAY,GAAM,IAAgB,CAAI,EACnC,EACV,CACL,EACA,CAAC,CAAa,CAClB,EAEM,GAAA,EAAY,EAAA,YAAA,CAAa,GAAuD,CAClF,IAAM,EAAO,EAAU,SAAS,sBAAsB,EACtD,MAAO,CACH,EAAG,EAAM,SAAW,GAAM,MAAQ,GAClC,EAAG,EAAM,SAAW,GAAM,KAAO,EACrC,CACJ,EAAG,CAAC,CAAC,EAEC,GAAA,EAAoB,EAAA,YAAA,CACrB,GAAsD,CAC/C,IACJ,EAAW,QAAU,GACrB,EAAW,QAAU,CAAC,GAAG,EAAW,QAAS,CAAC,EAAU,CAAK,CAAC,CAAC,EAC/D,EAAU,SAAS,oBAAoB,EAAM,SAAS,EACtD,IAAU,EACV,EAAa,EAAK,EAClB,EAAO,EACX,EACA,CAAC,EAAU,EAAS,EAAW,EAAQ,CAAY,CACvD,EAEM,GAAA,EAAoB,EAAA,YAAA,CACrB,GAAsD,CACnD,GAAI,CAAC,EAAW,SAAW,EAAU,OACrC,IAAM,EAAU,EAAW,QAC3B,EAAQ,EAAQ,OAAS,EAAE,CAAC,KAAK,EAAU,CAAK,CAAC,EACjD,EAAO,CACX,EACA,CAAC,EAAU,EAAW,CAAM,CAChC,EAEM,GAAA,EAAkB,EAAA,YAAA,CACnB,GAAsD,CAC9C,EAAW,UAChB,EAAW,QAAU,GACrB,EAAU,SAAS,wBAAwB,EAAM,SAAS,EAC1D,IAAQ,EAAU,SAAS,UAAU,GAAK,EAAE,EAChD,EACA,CAAC,CAAK,CACV,EAEM,GAAA,EAAQ,EAAA,YAAA,KAAwB,CAClC,EAAW,QAAU,CAAC,EACtB,EAAa,EAAI,EACjB,EAAO,CACX,EAAG,CAAC,EAAQ,CAAY,CAAC,EAEnB,GAAA,EAAO,EAAA,YAAA,KAAwB,CACjC,EAAW,QAAU,EAAW,QAAQ,MAAM,EAAG,EAAE,EACnD,EAAa,EAAW,QAAQ,SAAW,CAAC,EAC5C,EAAO,CACX,EAAG,CAAC,EAAQ,CAAY,CAAC,EAsBzB,OApBA,EAAA,EAAA,oBAAA,CACI,OACO,CACH,QACA,OACA,YAAe,EAAW,QAAQ,SAAW,EAC7C,WAAY,EAAM,IAAY,EAAU,SAAS,UAAU,EAAM,CAAO,GAAK,GAC7E,QAAS,EAAM,IACX,IAAI,QAAS,GAAY,CACrB,IAAM,EAAS,EAAU,QACzB,GAAI,CAAC,GAAQ,OAAQ,CACjB,EAAQ,IAAI,EACZ,MACJ,CACA,EAAO,OAAQ,GAAS,EAAQ,CAAI,EAAG,EAAM,CAAO,CACxD,CAAC,CACT,GACA,CAAC,EAAO,CAAI,CAChB,GAGI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,IAAK,GAAY,EAAA,QAAO,SAAU,CAAS,EAArE,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,IAAK,EACL,KAAK,MACL,aAAY,EACZ,gBAAe,GAAY,IAAA,GAC3B,UAAW,EAAA,QAAO,OAClB,MAAO,CAAE,QAAO,SAAQ,YAAa,MAAO,EAC5C,cAAe,EACf,cAAe,EACf,YAAa,EACb,eAAgB,CACnB,CAAA,EACA,GACG,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,QAAvB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,EAAA,OAAD,CACI,KAAK,SACL,QAAQ,QACR,KAAK,KACL,QAAS,EACT,SAAU,GAAY,EAErB,SAAA,CACG,CAAA,GACR,EAAA,EAAA,IAAA,CAAC,EAAA,OAAD,CACI,KAAK,SACL,QAAQ,QACR,KAAK,KACL,QAAS,EACT,SAAU,GAAY,EAErB,SAAA,CACG,CAAA,CACP,CACL,CAAA,EAAA,IACH,GAEb,CAAC"}