@cosxai/ui 0.11.1 → 0.12.0
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/package.json
CHANGED
|
@@ -415,18 +415,25 @@ export function ActionBar({
|
|
|
415
415
|
alignItems: "center",
|
|
416
416
|
gap: 4,
|
|
417
417
|
zIndex: 80,
|
|
418
|
-
// Bar
|
|
419
|
-
// whole-bar tint
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
//
|
|
418
|
+
// Bar background stays constant across modes — the earlier
|
|
419
|
+
// whole-bar tint composited under the item hover overlay
|
|
420
|
+
// (cool grey on tinted warm base) produced an awkward green
|
|
421
|
+
// cast on hover, and competed with the shield toggle for
|
|
422
|
+
// signal. Border colour DOES swap to accent when admin mode
|
|
423
|
+
// is on so the elevated state reads at a glance without a
|
|
424
|
+
// heavy surface change; the shield glyph adds a second cue
|
|
425
|
+
// (outline off → filled on) at the toggle itself.
|
|
424
426
|
background: "var(--ck-bg-surface)",
|
|
425
|
-
border:
|
|
427
|
+
border: `1px solid ${
|
|
428
|
+
adminMode
|
|
429
|
+
? "var(--ck-accent, #4f46e5)"
|
|
430
|
+
: "var(--ck-border-subtle)"
|
|
431
|
+
}`,
|
|
426
432
|
borderRadius: 999,
|
|
427
433
|
boxShadow: "var(--ck-shadow-3)",
|
|
428
434
|
fontFamily: "var(--ck-font-sans)",
|
|
429
435
|
color: "var(--ck-text-primary)",
|
|
436
|
+
transition: "border-color 200ms var(--ck-ease, ease)",
|
|
430
437
|
...style,
|
|
431
438
|
}}
|
|
432
439
|
>
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useImperativeHandle,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
type CSSProperties,
|
|
10
|
+
type PointerEvent as ReactPointerEvent,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
} from "react";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* SignaturePad — legally-binding e-signature capture.
|
|
16
|
+
*
|
|
17
|
+
* Two modes:
|
|
18
|
+
* - `typed` — signer types their name; canvas paints it in the
|
|
19
|
+
* Caveat cursive script + emits PNG dataURL.
|
|
20
|
+
* - `drawn` — signer draws with mouse / trackpad / touch; captured
|
|
21
|
+
* as a smoothed stroke path + rasterised to PNG.
|
|
22
|
+
*
|
|
23
|
+
* Both modes produce the same output shape: `image/png` dataURL via
|
|
24
|
+
* `canvas.toDataURL('image/png')`. Downstream consumer (metaroom
|
|
25
|
+
* SignDocumentPage) uploads that dataURL to mesh's signing endpoint;
|
|
26
|
+
* mesh stores as `signature_png` on the signing session + bakes into
|
|
27
|
+
* the certified PDF client-side.
|
|
28
|
+
*
|
|
29
|
+
* Compose with:
|
|
30
|
+
* - SigningCommitBar (footer state machine that owns the submit
|
|
31
|
+
* transition — SignaturePad is presentation only)
|
|
32
|
+
* - FieldWalker (DOM sequencer that focuses this pad when the
|
|
33
|
+
* current field is a signature block)
|
|
34
|
+
*
|
|
35
|
+
* Visual: see docs/components/signature-pad.
|
|
36
|
+
*
|
|
37
|
+
* Design decisions:
|
|
38
|
+
* - Controlled component. Parent owns `value` + `onChange`. This
|
|
39
|
+
* lets SigningCommitBar preserve captured state across phase
|
|
40
|
+
* transitions (fill → consent → review → back to fill).
|
|
41
|
+
* - `mode` toggle is controlled by parent too — no built-in tabs.
|
|
42
|
+
* Callers usually render a small pill switcher above the pad.
|
|
43
|
+
* - Font loading uses `document.fonts.ready` before rendering typed
|
|
44
|
+
* mode. Without the await, the first typed capture paints in the
|
|
45
|
+
* browser fallback (usually Brush Script or serif italic).
|
|
46
|
+
* - Drawn mode uses `touch-action: none` + `pointerdown`
|
|
47
|
+
* preventDefault to stop iOS Safari from scrolling instead of
|
|
48
|
+
* drawing.
|
|
49
|
+
* - `imperativeHandle` exposes `clear()` for the parent's Undo
|
|
50
|
+
* button.
|
|
51
|
+
*/
|
|
52
|
+
export interface SignaturePadProps {
|
|
53
|
+
/** Capture mode. Parent owns the toggle. */
|
|
54
|
+
mode: "typed" | "drawn";
|
|
55
|
+
/**
|
|
56
|
+
* Current captured PNG dataURL, or null when nothing captured.
|
|
57
|
+
* Controlled by the parent so state survives the parent's phase
|
|
58
|
+
* transitions.
|
|
59
|
+
*/
|
|
60
|
+
value: string | null;
|
|
61
|
+
/**
|
|
62
|
+
* Fires whenever the pad captures a new dataURL (typed input
|
|
63
|
+
* changed, drawn stroke completed, cleared). null on clear.
|
|
64
|
+
*/
|
|
65
|
+
onChange: (dataURL: string | null) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Typed-mode input. Parent may bind this to the signer's display
|
|
68
|
+
* name for a stable signature. Ignored in drawn mode.
|
|
69
|
+
*/
|
|
70
|
+
typedName?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Typed-mode text change — fires ONLY in typed mode, on every
|
|
73
|
+
* keystroke. Parent updates its state; the pad reruns the typed
|
|
74
|
+
* bake on the next paint. Ignored in drawn mode.
|
|
75
|
+
*/
|
|
76
|
+
onTypedNameChange?: (next: string) => void;
|
|
77
|
+
/** Canvas pixel width. Defaults to 480. */
|
|
78
|
+
width?: number;
|
|
79
|
+
/** Canvas pixel height. Defaults to 120. */
|
|
80
|
+
height?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Font family used in typed mode. Defaults to `"Caveat", "Segoe
|
|
83
|
+
* Script", cursive` — Caveat is the most common signature font
|
|
84
|
+
* available across the Google Fonts CDN + Adobe Fonts.
|
|
85
|
+
*/
|
|
86
|
+
typedFontFamily?: string;
|
|
87
|
+
/** Stroke color in drawn mode. Defaults to `#0b1220` (near-black). */
|
|
88
|
+
strokeColor?: string;
|
|
89
|
+
/** Stroke width in drawn mode. Defaults to 2. */
|
|
90
|
+
strokeWidth?: number;
|
|
91
|
+
/**
|
|
92
|
+
* Consumer-set className on the outer wrapper. Component ships its
|
|
93
|
+
* own inline styles; className is for layout only (grid parent
|
|
94
|
+
* spacing etc).
|
|
95
|
+
*/
|
|
96
|
+
className?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Imperative handle for parent-side controls (Undo, Clear buttons). */
|
|
100
|
+
export interface SignaturePadHandle {
|
|
101
|
+
/** Wipe the canvas + emit null via onChange. */
|
|
102
|
+
clear: () => void;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const DEFAULT_WIDTH = 480;
|
|
106
|
+
const DEFAULT_HEIGHT = 120;
|
|
107
|
+
const DEFAULT_TYPED_FONT = '"Caveat", "Segoe Script", cursive';
|
|
108
|
+
|
|
109
|
+
export const SignaturePad = forwardRef<SignaturePadHandle, SignaturePadProps>(
|
|
110
|
+
function SignaturePad(
|
|
111
|
+
{
|
|
112
|
+
mode,
|
|
113
|
+
value,
|
|
114
|
+
onChange,
|
|
115
|
+
typedName = "",
|
|
116
|
+
onTypedNameChange,
|
|
117
|
+
width = DEFAULT_WIDTH,
|
|
118
|
+
height = DEFAULT_HEIGHT,
|
|
119
|
+
typedFontFamily = DEFAULT_TYPED_FONT,
|
|
120
|
+
strokeColor = "#0b1220",
|
|
121
|
+
strokeWidth = 2,
|
|
122
|
+
className,
|
|
123
|
+
}: SignaturePadProps,
|
|
124
|
+
ref,
|
|
125
|
+
): ReactNode {
|
|
126
|
+
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
127
|
+
// Track whether the currently-painted canvas is "dirty" (has
|
|
128
|
+
// strokes / typed pixels) so clear() knows whether to emit null.
|
|
129
|
+
const dirtyRef = useRef<boolean>(value !== null);
|
|
130
|
+
|
|
131
|
+
// Drawn mode point buffer — used for smoothed line via quadratic
|
|
132
|
+
// interpolation. Reset on stroke end.
|
|
133
|
+
const drawStateRef = useRef<{
|
|
134
|
+
drawing: boolean;
|
|
135
|
+
lastX: number;
|
|
136
|
+
lastY: number;
|
|
137
|
+
}>({ drawing: false, lastX: 0, lastY: 0 });
|
|
138
|
+
|
|
139
|
+
// Font-load await for typed mode. Setting to `true` triggers a
|
|
140
|
+
// re-render + re-paint of the current typedName. First mount in
|
|
141
|
+
// typed mode WITHOUT this await paints in the browser fallback
|
|
142
|
+
// font — Ben's spec called this out as a bug we must not ship.
|
|
143
|
+
const [fontReady, setFontReady] = useState<boolean>(() => {
|
|
144
|
+
if (typeof document === "undefined") return true;
|
|
145
|
+
// Best-effort: document.fonts may be absent in older browsers.
|
|
146
|
+
// Fall through as ready so we don't block the pad forever.
|
|
147
|
+
const fonts = (document as Document & { fonts?: FontFaceSet }).fonts;
|
|
148
|
+
return !fonts;
|
|
149
|
+
});
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (fontReady) return;
|
|
152
|
+
if (typeof document === "undefined") return;
|
|
153
|
+
const fonts = (document as Document & { fonts?: FontFaceSet }).fonts;
|
|
154
|
+
if (!fonts) {
|
|
155
|
+
setFontReady(true);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
let cancelled = false;
|
|
159
|
+
void fonts.ready.then(() => {
|
|
160
|
+
if (cancelled) return;
|
|
161
|
+
setFontReady(true);
|
|
162
|
+
});
|
|
163
|
+
return () => {
|
|
164
|
+
cancelled = true;
|
|
165
|
+
};
|
|
166
|
+
}, [fontReady]);
|
|
167
|
+
|
|
168
|
+
// Repaint on typedName / mode / fontReady change. Drawn mode
|
|
169
|
+
// doesn't repaint imperatively — strokes are appended as the user
|
|
170
|
+
// draws.
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (mode !== "typed") return;
|
|
173
|
+
const canvas = canvasRef.current;
|
|
174
|
+
if (!canvas) return;
|
|
175
|
+
const ctx = canvas.getContext("2d");
|
|
176
|
+
if (!ctx) return;
|
|
177
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
178
|
+
if (!typedName.trim()) {
|
|
179
|
+
dirtyRef.current = false;
|
|
180
|
+
onChange(null);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (!fontReady) return;
|
|
184
|
+
// Paint the name centred vertically, left-aligned horizontally.
|
|
185
|
+
// Auto-fit font size to width: start at height * 0.6 and shrink
|
|
186
|
+
// until measureText fits within `width - 24` (12px padding each
|
|
187
|
+
// side).
|
|
188
|
+
let fontSize = Math.round(height * 0.7);
|
|
189
|
+
ctx.textBaseline = "middle";
|
|
190
|
+
ctx.fillStyle = strokeColor;
|
|
191
|
+
const availableWidth = width - 24;
|
|
192
|
+
while (fontSize > 10) {
|
|
193
|
+
ctx.font = `${fontSize}px ${typedFontFamily}`;
|
|
194
|
+
if (ctx.measureText(typedName).width <= availableWidth) break;
|
|
195
|
+
fontSize -= 2;
|
|
196
|
+
}
|
|
197
|
+
ctx.fillText(typedName, 12, height / 2);
|
|
198
|
+
const dataURL = canvas.toDataURL("image/png");
|
|
199
|
+
dirtyRef.current = true;
|
|
200
|
+
onChange(dataURL);
|
|
201
|
+
// onChange is stable-by-contract from parent (React best
|
|
202
|
+
// practice); avoid feeding it into deps to prevent infinite
|
|
203
|
+
// loop.
|
|
204
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
205
|
+
}, [
|
|
206
|
+
mode,
|
|
207
|
+
typedName,
|
|
208
|
+
fontReady,
|
|
209
|
+
width,
|
|
210
|
+
height,
|
|
211
|
+
typedFontFamily,
|
|
212
|
+
strokeColor,
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
// Restore a previously-captured value into the canvas on remount.
|
|
216
|
+
// Typed mode uses the effect above; drawn mode needs to explicitly
|
|
217
|
+
// paint the dataURL back onto the canvas so subsequent strokes
|
|
218
|
+
// continue on top of it (rather than starting from blank).
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (mode !== "drawn") return;
|
|
221
|
+
if (!value) return;
|
|
222
|
+
const canvas = canvasRef.current;
|
|
223
|
+
if (!canvas) return;
|
|
224
|
+
const ctx = canvas.getContext("2d");
|
|
225
|
+
if (!ctx) return;
|
|
226
|
+
const img = new Image();
|
|
227
|
+
img.onload = () => {
|
|
228
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
229
|
+
ctx.drawImage(img, 0, 0);
|
|
230
|
+
dirtyRef.current = true;
|
|
231
|
+
};
|
|
232
|
+
img.src = value;
|
|
233
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
234
|
+
}, [mode]);
|
|
235
|
+
|
|
236
|
+
// Mode switch — clear the canvas so a stale typed baseline
|
|
237
|
+
// doesn't hang around behind a drawn signature (or vice versa).
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
const canvas = canvasRef.current;
|
|
240
|
+
if (!canvas) return;
|
|
241
|
+
const ctx = canvas.getContext("2d");
|
|
242
|
+
if (!ctx) return;
|
|
243
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
244
|
+
dirtyRef.current = false;
|
|
245
|
+
onChange(null);
|
|
246
|
+
// Only fire when mode itself changes.
|
|
247
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
248
|
+
}, [mode]);
|
|
249
|
+
|
|
250
|
+
const clear = useCallback(() => {
|
|
251
|
+
const canvas = canvasRef.current;
|
|
252
|
+
if (canvas) {
|
|
253
|
+
const ctx = canvas.getContext("2d");
|
|
254
|
+
ctx?.clearRect(0, 0, canvas.width, canvas.height);
|
|
255
|
+
}
|
|
256
|
+
dirtyRef.current = false;
|
|
257
|
+
onChange(null);
|
|
258
|
+
if (mode === "typed") {
|
|
259
|
+
onTypedNameChange?.("");
|
|
260
|
+
}
|
|
261
|
+
}, [mode, onChange, onTypedNameChange]);
|
|
262
|
+
|
|
263
|
+
useImperativeHandle(ref, () => ({ clear }), [clear]);
|
|
264
|
+
|
|
265
|
+
// Drawn mode — pointer event handlers.
|
|
266
|
+
const onPointerDown = useCallback(
|
|
267
|
+
(e: ReactPointerEvent<HTMLCanvasElement>) => {
|
|
268
|
+
if (mode !== "drawn") return;
|
|
269
|
+
// touch-action: none on the canvas isn't enough on iOS
|
|
270
|
+
// Safari — need preventDefault too, otherwise the browser
|
|
271
|
+
// scrolls the page instead of drawing.
|
|
272
|
+
e.preventDefault();
|
|
273
|
+
const canvas = canvasRef.current;
|
|
274
|
+
if (!canvas) return;
|
|
275
|
+
const rect = canvas.getBoundingClientRect();
|
|
276
|
+
// Coordinate space: pointer client coords → canvas backing
|
|
277
|
+
// store coords. `rect` is CSS pixels; canvas.width is device
|
|
278
|
+
// pixels (or logical pixels when no DPR handling). Scale so
|
|
279
|
+
// strokes land at the visual point.
|
|
280
|
+
const scaleX = canvas.width / rect.width;
|
|
281
|
+
const scaleY = canvas.height / rect.height;
|
|
282
|
+
const x = (e.clientX - rect.left) * scaleX;
|
|
283
|
+
const y = (e.clientY - rect.top) * scaleY;
|
|
284
|
+
drawStateRef.current.drawing = true;
|
|
285
|
+
drawStateRef.current.lastX = x;
|
|
286
|
+
drawStateRef.current.lastY = y;
|
|
287
|
+
canvas.setPointerCapture(e.pointerId);
|
|
288
|
+
},
|
|
289
|
+
[mode],
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const onPointerMove = useCallback(
|
|
293
|
+
(e: ReactPointerEvent<HTMLCanvasElement>) => {
|
|
294
|
+
if (mode !== "drawn") return;
|
|
295
|
+
if (!drawStateRef.current.drawing) return;
|
|
296
|
+
const canvas = canvasRef.current;
|
|
297
|
+
if (!canvas) return;
|
|
298
|
+
const ctx = canvas.getContext("2d");
|
|
299
|
+
if (!ctx) return;
|
|
300
|
+
const rect = canvas.getBoundingClientRect();
|
|
301
|
+
const scaleX = canvas.width / rect.width;
|
|
302
|
+
const scaleY = canvas.height / rect.height;
|
|
303
|
+
const x = (e.clientX - rect.left) * scaleX;
|
|
304
|
+
const y = (e.clientY - rect.top) * scaleY;
|
|
305
|
+
ctx.strokeStyle = strokeColor;
|
|
306
|
+
ctx.lineWidth = strokeWidth;
|
|
307
|
+
ctx.lineCap = "round";
|
|
308
|
+
ctx.lineJoin = "round";
|
|
309
|
+
ctx.beginPath();
|
|
310
|
+
ctx.moveTo(drawStateRef.current.lastX, drawStateRef.current.lastY);
|
|
311
|
+
// Simple line-to; a smoother quadratic curve is possible via
|
|
312
|
+
// buffering the last N points, but the default line-to
|
|
313
|
+
// already reads well at 2px stroke on human-scale pads.
|
|
314
|
+
ctx.lineTo(x, y);
|
|
315
|
+
ctx.stroke();
|
|
316
|
+
drawStateRef.current.lastX = x;
|
|
317
|
+
drawStateRef.current.lastY = y;
|
|
318
|
+
dirtyRef.current = true;
|
|
319
|
+
},
|
|
320
|
+
[mode, strokeColor, strokeWidth],
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const onPointerUp = useCallback(
|
|
324
|
+
(e: ReactPointerEvent<HTMLCanvasElement>) => {
|
|
325
|
+
if (mode !== "drawn") return;
|
|
326
|
+
drawStateRef.current.drawing = false;
|
|
327
|
+
const canvas = canvasRef.current;
|
|
328
|
+
if (!canvas) return;
|
|
329
|
+
try {
|
|
330
|
+
canvas.releasePointerCapture(e.pointerId);
|
|
331
|
+
} catch {
|
|
332
|
+
// Some browsers throw when the pointer isn't captured
|
|
333
|
+
// (spurious pointerup); safe to ignore.
|
|
334
|
+
}
|
|
335
|
+
if (!dirtyRef.current) return;
|
|
336
|
+
const dataURL = canvas.toDataURL("image/png");
|
|
337
|
+
onChange(dataURL);
|
|
338
|
+
},
|
|
339
|
+
[mode, onChange],
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
// Wrapper + canvas styles. Inline (following the ck-* CSS var
|
|
343
|
+
// conventions) so consumers get a functional pad without needing
|
|
344
|
+
// to import a CSS file.
|
|
345
|
+
const wrapStyle: CSSProperties = useMemo(
|
|
346
|
+
() => ({
|
|
347
|
+
display: "flex",
|
|
348
|
+
flexDirection: "column",
|
|
349
|
+
gap: 8,
|
|
350
|
+
width: "fit-content",
|
|
351
|
+
}),
|
|
352
|
+
[],
|
|
353
|
+
);
|
|
354
|
+
const canvasStyle: CSSProperties = useMemo(
|
|
355
|
+
() => ({
|
|
356
|
+
width,
|
|
357
|
+
height,
|
|
358
|
+
border: "1px solid var(--ck-border-subtle, #d4d4d8)",
|
|
359
|
+
borderRadius: "var(--ck-radius-sm, 6px)",
|
|
360
|
+
background: "var(--ck-bg-surface, #ffffff)",
|
|
361
|
+
// touch-action: none is the load-bearing bit for iOS Safari
|
|
362
|
+
// drawn mode — otherwise the page scrolls instead of drawing.
|
|
363
|
+
touchAction: "none",
|
|
364
|
+
cursor: mode === "drawn" ? "crosshair" : "default",
|
|
365
|
+
display: "block",
|
|
366
|
+
}),
|
|
367
|
+
[mode, width, height],
|
|
368
|
+
);
|
|
369
|
+
const typedInputStyle: CSSProperties = useMemo(
|
|
370
|
+
() => ({
|
|
371
|
+
padding: "6px 8px",
|
|
372
|
+
border: "1px solid var(--ck-border-subtle, #d4d4d8)",
|
|
373
|
+
borderRadius: "var(--ck-radius-sm, 6px)",
|
|
374
|
+
background: "var(--ck-bg-surface, #ffffff)",
|
|
375
|
+
color: "var(--ck-text-primary, #0b1220)",
|
|
376
|
+
fontSize: 14,
|
|
377
|
+
fontFamily: "inherit",
|
|
378
|
+
width,
|
|
379
|
+
}),
|
|
380
|
+
[width],
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
return (
|
|
384
|
+
<div className={className} style={wrapStyle}>
|
|
385
|
+
{mode === "typed" ? (
|
|
386
|
+
<input
|
|
387
|
+
type="text"
|
|
388
|
+
value={typedName}
|
|
389
|
+
placeholder="Type your name"
|
|
390
|
+
onChange={(e) => onTypedNameChange?.(e.target.value)}
|
|
391
|
+
style={typedInputStyle}
|
|
392
|
+
aria-label="Type your signature"
|
|
393
|
+
/>
|
|
394
|
+
) : null}
|
|
395
|
+
<canvas
|
|
396
|
+
ref={canvasRef}
|
|
397
|
+
width={width}
|
|
398
|
+
height={height}
|
|
399
|
+
style={canvasStyle}
|
|
400
|
+
role="img"
|
|
401
|
+
aria-label={
|
|
402
|
+
mode === "typed"
|
|
403
|
+
? "Typed signature preview"
|
|
404
|
+
: "Draw your signature"
|
|
405
|
+
}
|
|
406
|
+
onPointerDown={onPointerDown}
|
|
407
|
+
onPointerMove={onPointerMove}
|
|
408
|
+
onPointerUp={onPointerUp}
|
|
409
|
+
onPointerCancel={onPointerUp}
|
|
410
|
+
onPointerLeave={onPointerUp}
|
|
411
|
+
/>
|
|
412
|
+
</div>
|
|
413
|
+
);
|
|
414
|
+
},
|
|
415
|
+
);
|
package/src/primitives/index.ts
CHANGED
|
@@ -28,3 +28,5 @@ export { PageHeader } from "./PageHeader";
|
|
|
28
28
|
export type { PageHeaderProps } from "./PageHeader";
|
|
29
29
|
export { MentionCombobox, findActiveMentionStart, extractQuery } from "./MentionCombobox";
|
|
30
30
|
export type { MentionComboboxProps, MentionComboboxHandle } from "./MentionCombobox";
|
|
31
|
+
export { SignaturePad } from "./SignaturePad";
|
|
32
|
+
export type { SignaturePadProps, SignaturePadHandle } from "./SignaturePad";
|