@nucleux/md3-handwriting 0.0.0-stage → 0.6.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/LICENSE +21 -0
- package/README.md +34 -2
- package/dist/index.cjs +598 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +576 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { cn } from '@nucleux/utils';
|
|
4
|
+
import { Md3Button } from '@nucleux/md3-button';
|
|
5
|
+
import { Md3Chip } from '@nucleux/md3-chip';
|
|
6
|
+
import { Md3LoadingIndicator } from '@nucleux/md3-loading-indicator';
|
|
7
|
+
import { Md3Menu, Md3MenuItem } from '@nucleux/md3-menu';
|
|
8
|
+
import { Md3TextField } from '@nucleux/md3-text-field';
|
|
9
|
+
import { AlertCircle, RotateCcw, Undo2, Eraser, Keyboard, PenLine, ScanText } from 'lucide-react';
|
|
10
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
11
|
+
|
|
12
|
+
// src/index.tsx
|
|
13
|
+
var useIsomorphicLayoutEffect = typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect;
|
|
14
|
+
var wordsText = (words) => words.map((w) => w.text).join(" ");
|
|
15
|
+
function UncertainWord({
|
|
16
|
+
word,
|
|
17
|
+
onChoose
|
|
18
|
+
}) {
|
|
19
|
+
const [open, setOpen] = React.useState(false);
|
|
20
|
+
const wrapRef = React.useRef(null);
|
|
21
|
+
const triggerRef = React.useRef(null);
|
|
22
|
+
const menuRef = React.useRef(null);
|
|
23
|
+
const menuId = React.useId();
|
|
24
|
+
const choices = [word.text, ...word.alternatives ?? []].filter((t, j, all) => all.indexOf(t) === j);
|
|
25
|
+
React.useEffect(() => {
|
|
26
|
+
if (!open) return;
|
|
27
|
+
requestAnimationFrame(() => menuRef.current?.querySelector('[role="menuitem"]')?.focus());
|
|
28
|
+
const onDown = (e) => {
|
|
29
|
+
if (!wrapRef.current?.contains(e.target)) setOpen(false);
|
|
30
|
+
};
|
|
31
|
+
document.addEventListener("pointerdown", onDown);
|
|
32
|
+
return () => document.removeEventListener("pointerdown", onDown);
|
|
33
|
+
}, [open]);
|
|
34
|
+
const close = () => {
|
|
35
|
+
setOpen(false);
|
|
36
|
+
triggerRef.current?.focus();
|
|
37
|
+
};
|
|
38
|
+
const onMenuKeyDown = (e) => {
|
|
39
|
+
const items = Array.from(menuRef.current?.querySelectorAll('[role="menuitem"]') ?? []);
|
|
40
|
+
const i = items.indexOf(document.activeElement);
|
|
41
|
+
if (e.key === "Escape" || e.key === "Tab") {
|
|
42
|
+
if (e.key === "Escape") e.preventDefault();
|
|
43
|
+
close();
|
|
44
|
+
} else if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
const step = e.key === "ArrowDown" ? 1 : -1;
|
|
47
|
+
items[(i + step + items.length) % items.length]?.focus();
|
|
48
|
+
} else if (e.key === "Home" || e.key === "End") {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
items[e.key === "Home" ? 0 : items.length - 1]?.focus();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
return /* @__PURE__ */ jsxs("span", { ref: wrapRef, className: "relative inline-flex", children: [
|
|
54
|
+
/* @__PURE__ */ jsxs(
|
|
55
|
+
"button",
|
|
56
|
+
{
|
|
57
|
+
ref: triggerRef,
|
|
58
|
+
type: "button",
|
|
59
|
+
"aria-label": `${word.text} \u2014 uncertain, choose the right word`,
|
|
60
|
+
"aria-haspopup": "menu",
|
|
61
|
+
"aria-expanded": open,
|
|
62
|
+
"aria-controls": open ? menuId : void 0,
|
|
63
|
+
onClick: () => setOpen((o) => !o),
|
|
64
|
+
onKeyDown: (e) => {
|
|
65
|
+
if (e.key === "ArrowDown") {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
setOpen(true);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
className: cn(
|
|
71
|
+
"relative inline-flex h-8 items-center gap-0.5 overflow-hidden rounded-md-sm px-1.5 text-sm text-md-on-surface",
|
|
72
|
+
"underline decoration-md-error decoration-dotted decoration-2 underline-offset-4",
|
|
73
|
+
"before:absolute before:inset-0 before:bg-md-primary before:opacity-0 before:transition-opacity before:content-['']",
|
|
74
|
+
"hover:before:opacity-[0.08] focus-visible:before:opacity-[0.12] active:before:opacity-[0.12]",
|
|
75
|
+
"outline-none focus-visible:ring-2 focus-visible:ring-md-primary",
|
|
76
|
+
"transition-transform duration-200 active:scale-95 motion-reduce:transition-none"
|
|
77
|
+
),
|
|
78
|
+
children: [
|
|
79
|
+
/* @__PURE__ */ jsx("span", { className: "relative", children: word.text }),
|
|
80
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "relative font-semibold text-md-error no-underline", children: "?" })
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
),
|
|
84
|
+
open && /* @__PURE__ */ jsxs(
|
|
85
|
+
Md3Menu,
|
|
86
|
+
{
|
|
87
|
+
ref: menuRef,
|
|
88
|
+
id: menuId,
|
|
89
|
+
"aria-label": "Did you write\u2026",
|
|
90
|
+
onKeyDown: onMenuKeyDown,
|
|
91
|
+
className: "absolute left-0 top-full z-50 mt-1 animate-nx-fade-in motion-reduce:animate-none",
|
|
92
|
+
children: [
|
|
93
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "block px-3 pb-1 pt-1 text-xs font-medium text-md-on-surface-variant", children: "Did you write\u2026" }),
|
|
94
|
+
choices.map((alt) => /* @__PURE__ */ jsx(
|
|
95
|
+
Md3MenuItem,
|
|
96
|
+
{
|
|
97
|
+
tabIndex: -1,
|
|
98
|
+
onClick: () => {
|
|
99
|
+
onChoose(alt);
|
|
100
|
+
setOpen(false);
|
|
101
|
+
},
|
|
102
|
+
children: alt
|
|
103
|
+
},
|
|
104
|
+
alt
|
|
105
|
+
))
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
] });
|
|
110
|
+
}
|
|
111
|
+
var Md3HandwritingInput = React.forwardRef(
|
|
112
|
+
({
|
|
113
|
+
variant = "draw-pad",
|
|
114
|
+
onRecognize,
|
|
115
|
+
convert,
|
|
116
|
+
autoDelay = 900,
|
|
117
|
+
onAccept,
|
|
118
|
+
strokes: strokesProp,
|
|
119
|
+
defaultStrokes,
|
|
120
|
+
onStrokesChange,
|
|
121
|
+
defaultRecognition,
|
|
122
|
+
defaultMode = "draw",
|
|
123
|
+
strokeWidth = variant === "annotation-stroke" ? 3 : 2.5,
|
|
124
|
+
label = "Handwriting pad",
|
|
125
|
+
placeholder,
|
|
126
|
+
statusText = "Recognising your writing\u2026",
|
|
127
|
+
failureText = "Couldn\u2019t recognise this writing.",
|
|
128
|
+
acceptLabel = "Add to message",
|
|
129
|
+
typingLabel = "Type your request",
|
|
130
|
+
disabled = false,
|
|
131
|
+
className,
|
|
132
|
+
children,
|
|
133
|
+
...props
|
|
134
|
+
}, ref) => {
|
|
135
|
+
const mode = convert ?? (variant === "live-ocr" ? "auto" : "review");
|
|
136
|
+
const isAnnotation = variant === "annotation-stroke";
|
|
137
|
+
const [internalStrokes, setInternalStrokes] = React.useState(defaultStrokes ?? []);
|
|
138
|
+
const strokes = strokesProp ?? internalStrokes;
|
|
139
|
+
const [inputMode, setInputMode] = React.useState(defaultMode);
|
|
140
|
+
const [status, setStatus] = React.useState(defaultRecognition ? "recognized" : "idle");
|
|
141
|
+
const [recognition, setRecognition] = React.useState(defaultRecognition ?? null);
|
|
142
|
+
const [words, setWords] = React.useState(defaultRecognition?.words ?? null);
|
|
143
|
+
const [editedText, setEditedText] = React.useState(null);
|
|
144
|
+
const [typed, setTyped] = React.useState("");
|
|
145
|
+
const [announcement, setAnnouncement] = React.useState("");
|
|
146
|
+
const canvasRef = React.useRef(null);
|
|
147
|
+
const padRef = React.useRef(null);
|
|
148
|
+
const drawing = React.useRef(null);
|
|
149
|
+
const requestId = React.useRef(0);
|
|
150
|
+
const strokesRef = React.useRef(strokes);
|
|
151
|
+
strokesRef.current = strokes;
|
|
152
|
+
const resultId = React.useId();
|
|
153
|
+
const resultInputRef = React.useRef(null);
|
|
154
|
+
const setStrokes = (next) => {
|
|
155
|
+
if (strokesProp === void 0) setInternalStrokes(next);
|
|
156
|
+
onStrokesChange?.(next);
|
|
157
|
+
};
|
|
158
|
+
const widthFor = (stroke, p) => stroke.pointerType === "pen" && p.pressure > 0 ? strokeWidth * (0.4 + p.pressure * 1.2) : strokeWidth;
|
|
159
|
+
const noCanvas = React.useRef(false);
|
|
160
|
+
const getContext = () => {
|
|
161
|
+
const canvas = canvasRef.current;
|
|
162
|
+
if (!canvas || noCanvas.current) return null;
|
|
163
|
+
let ctx = null;
|
|
164
|
+
try {
|
|
165
|
+
ctx = canvas.getContext("2d");
|
|
166
|
+
} catch {
|
|
167
|
+
ctx = null;
|
|
168
|
+
}
|
|
169
|
+
if (!ctx) {
|
|
170
|
+
noCanvas.current = true;
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
|
|
174
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
175
|
+
ctx.lineCap = "round";
|
|
176
|
+
ctx.lineJoin = "round";
|
|
177
|
+
const color = typeof getComputedStyle === "function" ? getComputedStyle(canvas).color : "";
|
|
178
|
+
ctx.strokeStyle = color || "currentColor";
|
|
179
|
+
ctx.fillStyle = ctx.strokeStyle;
|
|
180
|
+
return ctx;
|
|
181
|
+
};
|
|
182
|
+
const drawSegment = (ctx, stroke, a, b) => {
|
|
183
|
+
ctx.lineWidth = widthFor(stroke, b);
|
|
184
|
+
ctx.beginPath();
|
|
185
|
+
ctx.moveTo(a.x, a.y);
|
|
186
|
+
ctx.lineTo(b.x, b.y);
|
|
187
|
+
ctx.stroke();
|
|
188
|
+
};
|
|
189
|
+
const drawDot = (ctx, stroke, p) => {
|
|
190
|
+
ctx.beginPath();
|
|
191
|
+
ctx.arc(p.x, p.y, widthFor(stroke, p) / 2, 0, Math.PI * 2);
|
|
192
|
+
ctx.fill();
|
|
193
|
+
};
|
|
194
|
+
const redraw = React.useCallback(() => {
|
|
195
|
+
const canvas = canvasRef.current;
|
|
196
|
+
if (!canvas) return;
|
|
197
|
+
const ctx = getContext();
|
|
198
|
+
if (!ctx) return;
|
|
199
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
200
|
+
for (const stroke of strokesRef.current) {
|
|
201
|
+
const pts = stroke.points;
|
|
202
|
+
if (pts.length === 1) drawDot(ctx, stroke, pts[0]);
|
|
203
|
+
for (let i = 1; i < pts.length; i++) drawSegment(ctx, stroke, pts[i - 1], pts[i]);
|
|
204
|
+
}
|
|
205
|
+
}, [strokeWidth]);
|
|
206
|
+
React.useEffect(() => {
|
|
207
|
+
const pad = padRef.current;
|
|
208
|
+
const canvas = canvasRef.current;
|
|
209
|
+
if (!pad || !canvas || typeof ResizeObserver === "undefined") return;
|
|
210
|
+
const ro = new ResizeObserver(() => {
|
|
211
|
+
const rect = pad.getBoundingClientRect();
|
|
212
|
+
if (!rect.width || !rect.height) return;
|
|
213
|
+
const dpr = window.devicePixelRatio || 1;
|
|
214
|
+
canvas.width = Math.round(rect.width * dpr);
|
|
215
|
+
canvas.height = Math.round(rect.height * dpr);
|
|
216
|
+
redraw();
|
|
217
|
+
});
|
|
218
|
+
ro.observe(pad);
|
|
219
|
+
return () => ro.disconnect();
|
|
220
|
+
}, [redraw, inputMode]);
|
|
221
|
+
const hasDrawn = React.useRef(false);
|
|
222
|
+
useIsomorphicLayoutEffect(() => {
|
|
223
|
+
if (strokes.length === 0 && !hasDrawn.current) return;
|
|
224
|
+
hasDrawn.current = true;
|
|
225
|
+
redraw();
|
|
226
|
+
}, [strokes, redraw]);
|
|
227
|
+
const pointFrom = (e) => {
|
|
228
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
229
|
+
return {
|
|
230
|
+
x: (e.clientX ?? 0) - rect.left,
|
|
231
|
+
y: (e.clientY ?? 0) - rect.top,
|
|
232
|
+
pressure: typeof e.pressure === "number" && e.pressure > 0 ? e.pressure : 0.5,
|
|
233
|
+
t: e.timeStamp ?? Date.now()
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
const onPointerDown = (e) => {
|
|
237
|
+
if (disabled || status === "recognizing") return;
|
|
238
|
+
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
239
|
+
e.preventDefault();
|
|
240
|
+
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
241
|
+
const stroke = { pointerType: e.pointerType || "mouse", points: [pointFrom(e)] };
|
|
242
|
+
drawing.current = stroke;
|
|
243
|
+
const ctx = getContext();
|
|
244
|
+
if (ctx) drawDot(ctx, stroke, stroke.points[0]);
|
|
245
|
+
};
|
|
246
|
+
const onPointerMove = (e) => {
|
|
247
|
+
const stroke = drawing.current;
|
|
248
|
+
if (!stroke) return;
|
|
249
|
+
const native = e.nativeEvent;
|
|
250
|
+
const events = native.getCoalescedEvents?.() ?? [];
|
|
251
|
+
const ctx = getContext();
|
|
252
|
+
for (const ev of events.length ? events : [native]) {
|
|
253
|
+
const p = pointFrom(ev);
|
|
254
|
+
const prev = stroke.points[stroke.points.length - 1];
|
|
255
|
+
stroke.points.push(p);
|
|
256
|
+
if (ctx) drawSegment(ctx, stroke, prev, p);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
const endStroke = () => {
|
|
260
|
+
const stroke = drawing.current;
|
|
261
|
+
if (!stroke) return;
|
|
262
|
+
drawing.current = null;
|
|
263
|
+
setStrokes([...strokesRef.current, stroke]);
|
|
264
|
+
};
|
|
265
|
+
const toImage = (type = "image/png") => new Promise((resolve) => {
|
|
266
|
+
const canvas = canvasRef.current;
|
|
267
|
+
if (!canvas || typeof canvas.toBlob !== "function") return resolve(null);
|
|
268
|
+
try {
|
|
269
|
+
canvas.toBlob((blob) => resolve(blob), type);
|
|
270
|
+
} catch {
|
|
271
|
+
resolve(null);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
const applyRecognition = (result) => {
|
|
275
|
+
setRecognition(result);
|
|
276
|
+
setWords(result.words ?? null);
|
|
277
|
+
setEditedText(null);
|
|
278
|
+
setStatus("recognized");
|
|
279
|
+
const unsure = result.words?.filter((w) => w.uncertain).length ?? 0;
|
|
280
|
+
setAnnouncement(
|
|
281
|
+
`Recognised: ${result.words ? wordsText(result.words) : result.text}.` + (unsure ? ` ${unsure} ${unsure === 1 ? "word needs" : "words need"} checking.` : "")
|
|
282
|
+
);
|
|
283
|
+
};
|
|
284
|
+
const recognize = async () => {
|
|
285
|
+
if (!onRecognize || strokesRef.current.length === 0) return;
|
|
286
|
+
const id = ++requestId.current;
|
|
287
|
+
setStatus("recognizing");
|
|
288
|
+
setAnnouncement(typeof statusText === "string" ? statusText : "Recognising your writing\u2026");
|
|
289
|
+
const rect = padRef.current?.getBoundingClientRect();
|
|
290
|
+
try {
|
|
291
|
+
const result = await onRecognize({
|
|
292
|
+
strokes: strokesRef.current,
|
|
293
|
+
width: rect?.width ?? 0,
|
|
294
|
+
height: rect?.height ?? 0,
|
|
295
|
+
toImage
|
|
296
|
+
});
|
|
297
|
+
if (id !== requestId.current) return;
|
|
298
|
+
applyRecognition(result);
|
|
299
|
+
} catch {
|
|
300
|
+
if (id !== requestId.current) return;
|
|
301
|
+
setStatus("error");
|
|
302
|
+
setAnnouncement(typeof failureText === "string" ? failureText : "Couldn\u2019t recognise this writing.");
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
const prevStrokes = React.useRef(strokes);
|
|
306
|
+
React.useEffect(() => {
|
|
307
|
+
if (prevStrokes.current === strokes) return;
|
|
308
|
+
prevStrokes.current = strokes;
|
|
309
|
+
requestId.current += 1;
|
|
310
|
+
if (strokes.length === 0) {
|
|
311
|
+
setStatus("idle");
|
|
312
|
+
setRecognition(null);
|
|
313
|
+
setWords(null);
|
|
314
|
+
setEditedText(null);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (mode === "auto" && onRecognize) {
|
|
318
|
+
const timer = setTimeout(() => void recognize(), autoDelay);
|
|
319
|
+
return () => clearTimeout(timer);
|
|
320
|
+
}
|
|
321
|
+
setStatus("idle");
|
|
322
|
+
setRecognition(null);
|
|
323
|
+
setWords(null);
|
|
324
|
+
setEditedText(null);
|
|
325
|
+
}, [strokes]);
|
|
326
|
+
const unsettled = editedText === null && !!words?.some((w) => w.uncertain);
|
|
327
|
+
const resultText = editedText ?? (words ? wordsText(words) : recognition?.text ?? "");
|
|
328
|
+
const settleWord = (index, text) => {
|
|
329
|
+
if (!words) return;
|
|
330
|
+
const next = words.map((w, i) => i === index ? { text, uncertain: false } : w);
|
|
331
|
+
setWords(next);
|
|
332
|
+
const left = next.filter((w) => w.uncertain).length;
|
|
333
|
+
if (!left) requestAnimationFrame(() => resultInputRef.current?.focus());
|
|
334
|
+
setAnnouncement(`\u201C${text}\u201D chosen.${left ? ` ${left} still to check.` : " Ready to add."}`);
|
|
335
|
+
};
|
|
336
|
+
const reset = () => {
|
|
337
|
+
requestId.current += 1;
|
|
338
|
+
setStrokes([]);
|
|
339
|
+
setStatus("idle");
|
|
340
|
+
setRecognition(null);
|
|
341
|
+
setWords(null);
|
|
342
|
+
setEditedText(null);
|
|
343
|
+
};
|
|
344
|
+
const accept = () => {
|
|
345
|
+
if (inputMode === "type") {
|
|
346
|
+
const text = typed.trim();
|
|
347
|
+
if (!text) return;
|
|
348
|
+
onAccept?.(text, { source: "typed", strokes: [] });
|
|
349
|
+
setTyped("");
|
|
350
|
+
} else {
|
|
351
|
+
const text = resultText.trim();
|
|
352
|
+
if (!text || unsettled) return;
|
|
353
|
+
onAccept?.(text, { source: "handwriting", strokes, recognition: recognition ?? void 0 });
|
|
354
|
+
reset();
|
|
355
|
+
}
|
|
356
|
+
setAnnouncement("Added to message.");
|
|
357
|
+
};
|
|
358
|
+
const undoStroke = () => setStrokes(strokes.slice(0, -1));
|
|
359
|
+
const setRef = (node) => {
|
|
360
|
+
if (typeof ref === "function") ref(node);
|
|
361
|
+
else if (ref) ref.current = node;
|
|
362
|
+
};
|
|
363
|
+
const emptyHint = placeholder ?? (isAnnotation ? "Draw on the content to mark what you mean" : "Draw or write\u2026");
|
|
364
|
+
return /* @__PURE__ */ jsxs(
|
|
365
|
+
"div",
|
|
366
|
+
{
|
|
367
|
+
ref: setRef,
|
|
368
|
+
role: "group",
|
|
369
|
+
"aria-label": isAnnotation ? "Annotation input" : "Handwriting input",
|
|
370
|
+
"data-variant": variant,
|
|
371
|
+
"data-status": status,
|
|
372
|
+
className: cn("flex flex-col gap-3 text-md-on-surface", className),
|
|
373
|
+
...props,
|
|
374
|
+
children: [
|
|
375
|
+
inputMode === "type" ? /* @__PURE__ */ jsx(
|
|
376
|
+
"textarea",
|
|
377
|
+
{
|
|
378
|
+
"aria-label": typingLabel,
|
|
379
|
+
placeholder: typingLabel,
|
|
380
|
+
value: typed,
|
|
381
|
+
onChange: (e) => setTyped(e.target.value),
|
|
382
|
+
disabled,
|
|
383
|
+
rows: 3,
|
|
384
|
+
className: cn(
|
|
385
|
+
"w-full resize-none rounded-md-xs border border-md-outline bg-transparent px-4 py-3 text-base text-md-on-surface outline-none transition-colors",
|
|
386
|
+
"placeholder:text-md-on-surface-variant focus:border-2 focus:border-md-primary focus:px-[15px] focus:py-[11px]",
|
|
387
|
+
"disabled:cursor-not-allowed disabled:opacity-[0.38]"
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
) : /* @__PURE__ */ jsxs(
|
|
391
|
+
"div",
|
|
392
|
+
{
|
|
393
|
+
ref: padRef,
|
|
394
|
+
className: cn(
|
|
395
|
+
"relative overflow-hidden rounded-md-lg",
|
|
396
|
+
isAnnotation ? "min-h-40 border border-md-outline-variant" : "h-44 border-[1.5px] border-dashed border-md-outline bg-md-surface-container-low",
|
|
397
|
+
variant === "live-ocr" && "h-36 border border-solid border-md-outline-variant",
|
|
398
|
+
disabled && "opacity-[0.38]"
|
|
399
|
+
),
|
|
400
|
+
"aria-busy": status === "recognizing" || void 0,
|
|
401
|
+
children: [
|
|
402
|
+
isAnnotation && children,
|
|
403
|
+
/* @__PURE__ */ jsx(
|
|
404
|
+
"canvas",
|
|
405
|
+
{
|
|
406
|
+
ref: canvasRef,
|
|
407
|
+
role: "img",
|
|
408
|
+
"aria-label": `${label}${strokes.length ? ` \u2014 ${strokes.length} ${strokes.length === 1 ? "stroke" : "strokes"}` : " \u2014 empty"}`,
|
|
409
|
+
onPointerDown,
|
|
410
|
+
onPointerMove,
|
|
411
|
+
onPointerUp: endStroke,
|
|
412
|
+
onPointerCancel: endStroke,
|
|
413
|
+
className: cn(
|
|
414
|
+
"absolute inset-0 size-full touch-none",
|
|
415
|
+
disabled ? "cursor-not-allowed" : "cursor-crosshair",
|
|
416
|
+
isAnnotation ? "text-md-error" : "text-md-on-surface"
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
),
|
|
420
|
+
strokes.length === 0 && /* @__PURE__ */ jsx(
|
|
421
|
+
"p",
|
|
422
|
+
{
|
|
423
|
+
"aria-hidden": "true",
|
|
424
|
+
className: cn(
|
|
425
|
+
"pointer-events-none absolute text-xs text-md-on-surface-variant",
|
|
426
|
+
isAnnotation ? "bottom-2 right-2 rounded-full bg-md-surface-container-high px-2.5 py-1 font-medium" : "inset-x-0 bottom-3 text-center"
|
|
427
|
+
),
|
|
428
|
+
children: emptyHint
|
|
429
|
+
}
|
|
430
|
+
),
|
|
431
|
+
isAnnotation && status === "recognized" && resultText && /* @__PURE__ */ jsxs(
|
|
432
|
+
"span",
|
|
433
|
+
{
|
|
434
|
+
"aria-hidden": "true",
|
|
435
|
+
className: "pointer-events-none absolute bottom-2 right-2 rounded-full bg-md-surface-container-high px-2.5 py-1 text-xs font-medium text-md-on-surface shadow-md-1",
|
|
436
|
+
children: [
|
|
437
|
+
"\u201C",
|
|
438
|
+
resultText,
|
|
439
|
+
"\u201D"
|
|
440
|
+
]
|
|
441
|
+
}
|
|
442
|
+
),
|
|
443
|
+
status === "recognizing" && // A slow, low-contrast sheen while the ink is read; the ink stays visible beneath it.
|
|
444
|
+
/* @__PURE__ */ jsx(
|
|
445
|
+
"span",
|
|
446
|
+
{
|
|
447
|
+
"aria-hidden": "true",
|
|
448
|
+
className: "pointer-events-none absolute inset-0 animate-pulse bg-md-primary/[0.08] motion-reduce:animate-none"
|
|
449
|
+
}
|
|
450
|
+
)
|
|
451
|
+
]
|
|
452
|
+
}
|
|
453
|
+
),
|
|
454
|
+
inputMode === "draw" && status === "recognizing" && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm text-md-on-surface-variant", children: [
|
|
455
|
+
/* @__PURE__ */ jsx(Md3LoadingIndicator, { size: 24, "aria-hidden": "true" }),
|
|
456
|
+
statusText
|
|
457
|
+
] }),
|
|
458
|
+
inputMode === "draw" && status === "error" && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 rounded-md-md bg-md-error-container px-3 py-2.5 text-md-on-error-container", children: [
|
|
459
|
+
/* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 text-sm", children: [
|
|
460
|
+
/* @__PURE__ */ jsx(AlertCircle, { className: "size-[18px] shrink-0", "aria-hidden": "true" }),
|
|
461
|
+
failureText,
|
|
462
|
+
" Your writing is kept."
|
|
463
|
+
] }),
|
|
464
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
465
|
+
/* @__PURE__ */ jsx(Md3Button, { variant: "filled", icon: /* @__PURE__ */ jsx(RotateCcw, { "aria-hidden": "true" }), onClick: () => void recognize(), children: "Retry" }),
|
|
466
|
+
/* @__PURE__ */ jsx(Md3Button, { variant: "text", onClick: reset, className: "text-md-on-error-container", children: "Rewrite" })
|
|
467
|
+
] })
|
|
468
|
+
] }),
|
|
469
|
+
inputMode === "draw" && status === "recognized" && recognition && /* @__PURE__ */ jsxs(
|
|
470
|
+
"div",
|
|
471
|
+
{
|
|
472
|
+
id: resultId,
|
|
473
|
+
className: "flex flex-col gap-2 border-t border-md-outline-variant pt-3",
|
|
474
|
+
"data-slot": "handwriting-result",
|
|
475
|
+
children: [
|
|
476
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium tracking-[0.5px] text-md-on-surface-variant", children: isAnnotation ? "Read as an instruction" : "Recognised" }),
|
|
477
|
+
unsettled && words ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
478
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-1 text-base text-md-on-surface", children: words.map(
|
|
479
|
+
(w, i) => w.uncertain ? /* @__PURE__ */ jsx(UncertainWord, { word: w, onChoose: (text) => settleWord(i, text) }, i) : /* @__PURE__ */ jsx("span", { className: "animate-nx-fade-in motion-reduce:animate-none", children: w.text }, i)
|
|
480
|
+
) }),
|
|
481
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs text-md-on-surface-variant", children: [
|
|
482
|
+
"Check the words marked \u201C?\u201D to add this to your message.",
|
|
483
|
+
" ",
|
|
484
|
+
/* @__PURE__ */ jsx(
|
|
485
|
+
"button",
|
|
486
|
+
{
|
|
487
|
+
type: "button",
|
|
488
|
+
onClick: () => setEditedText(resultText),
|
|
489
|
+
className: "rounded-md-xs font-medium text-md-primary underline-offset-2 outline-none hover:underline focus-visible:ring-2 focus-visible:ring-md-primary",
|
|
490
|
+
children: "Edit text instead"
|
|
491
|
+
}
|
|
492
|
+
)
|
|
493
|
+
] })
|
|
494
|
+
] }) : /* @__PURE__ */ jsx(
|
|
495
|
+
Md3TextField,
|
|
496
|
+
{
|
|
497
|
+
ref: resultInputRef,
|
|
498
|
+
label: "Recognised text",
|
|
499
|
+
value: resultText,
|
|
500
|
+
onChange: (e) => setEditedText(e.target.value),
|
|
501
|
+
disabled
|
|
502
|
+
}
|
|
503
|
+
),
|
|
504
|
+
!unsettled && recognition.alternatives && recognition.alternatives.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", role: "group", "aria-label": "Other readings", children: [
|
|
505
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-md-on-surface-variant", children: "Other readings:" }),
|
|
506
|
+
recognition.alternatives.map((alt) => /* @__PURE__ */ jsx(Md3Chip, { variant: "filter", selected: resultText === alt, onClick: () => setEditedText(alt), children: alt }, alt))
|
|
507
|
+
] })
|
|
508
|
+
]
|
|
509
|
+
}
|
|
510
|
+
),
|
|
511
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1", children: [
|
|
512
|
+
inputMode === "draw" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
513
|
+
/* @__PURE__ */ jsx(
|
|
514
|
+
Md3Button,
|
|
515
|
+
{
|
|
516
|
+
variant: "text",
|
|
517
|
+
icon: /* @__PURE__ */ jsx(Undo2, { "aria-hidden": "true" }),
|
|
518
|
+
onClick: undoStroke,
|
|
519
|
+
disabled: disabled || strokes.length === 0 || status === "recognizing",
|
|
520
|
+
children: "Undo stroke"
|
|
521
|
+
}
|
|
522
|
+
),
|
|
523
|
+
/* @__PURE__ */ jsx(
|
|
524
|
+
Md3Button,
|
|
525
|
+
{
|
|
526
|
+
variant: "text",
|
|
527
|
+
icon: /* @__PURE__ */ jsx(Eraser, { "aria-hidden": "true" }),
|
|
528
|
+
onClick: reset,
|
|
529
|
+
disabled: disabled || strokes.length === 0,
|
|
530
|
+
children: "Clear"
|
|
531
|
+
}
|
|
532
|
+
)
|
|
533
|
+
] }),
|
|
534
|
+
/* @__PURE__ */ jsx(
|
|
535
|
+
Md3Button,
|
|
536
|
+
{
|
|
537
|
+
variant: "text",
|
|
538
|
+
icon: inputMode === "draw" ? /* @__PURE__ */ jsx(Keyboard, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(PenLine, { "aria-hidden": "true" }),
|
|
539
|
+
onClick: () => setInputMode(inputMode === "draw" ? "type" : "draw"),
|
|
540
|
+
disabled,
|
|
541
|
+
children: inputMode === "draw" ? "Type instead" : "Write instead"
|
|
542
|
+
}
|
|
543
|
+
),
|
|
544
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1" }),
|
|
545
|
+
inputMode === "draw" && onRecognize && (status !== "recognized" || mode === "review") && /* @__PURE__ */ jsx(
|
|
546
|
+
Md3Button,
|
|
547
|
+
{
|
|
548
|
+
variant: "tonal",
|
|
549
|
+
icon: /* @__PURE__ */ jsx(ScanText, { "aria-hidden": "true" }),
|
|
550
|
+
onClick: () => void recognize(),
|
|
551
|
+
disabled: disabled || strokes.length === 0 || status === "recognizing",
|
|
552
|
+
children: "Recognise"
|
|
553
|
+
}
|
|
554
|
+
),
|
|
555
|
+
/* @__PURE__ */ jsx(
|
|
556
|
+
Md3Button,
|
|
557
|
+
{
|
|
558
|
+
variant: "filled",
|
|
559
|
+
onClick: accept,
|
|
560
|
+
disabled: disabled || (inputMode === "type" ? !typed.trim() : status !== "recognized" || unsettled || !resultText.trim()),
|
|
561
|
+
"aria-describedby": inputMode === "draw" && status === "recognized" ? resultId : void 0,
|
|
562
|
+
children: acceptLabel
|
|
563
|
+
}
|
|
564
|
+
)
|
|
565
|
+
] }),
|
|
566
|
+
/* @__PURE__ */ jsx("p", { role: "status", "aria-live": "polite", className: "sr-only", children: announcement })
|
|
567
|
+
]
|
|
568
|
+
}
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
);
|
|
572
|
+
Md3HandwritingInput.displayName = "Md3HandwritingInput";
|
|
573
|
+
|
|
574
|
+
export { Md3HandwritingInput };
|
|
575
|
+
//# sourceMappingURL=index.js.map
|
|
576
|
+
//# sourceMappingURL=index.js.map
|