@cosxai/ui 0.25.0 → 1.0.0-alpha.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/CHANGELOG.md +597 -0
- package/NOTICE +11 -0
- package/README.md +52 -0
- package/package.json +57 -7
- package/src/brand.css +13 -0
- package/src/components/Badge.tsx +68 -0
- package/src/components/Button.tsx +178 -0
- package/src/components/Card.tsx +42 -0
- package/src/components/Checkbox.tsx +67 -0
- package/src/components/Dialog.tsx +111 -0
- package/src/components/Field.tsx +66 -0
- package/src/components/Figure.tsx +30 -0
- package/src/components/Icon.tsx +32 -0
- package/src/components/IconButton.tsx +55 -0
- package/src/components/Input.tsx +74 -0
- package/src/components/Logo.tsx +62 -0
- package/src/components/Marker.tsx +26 -0
- package/src/components/MetaLabel.tsx +38 -0
- package/src/components/Radio.tsx +72 -0
- package/src/components/Select.tsx +351 -0
- package/src/components/Spinner.tsx +20 -0
- package/src/components/Switch.tsx +67 -0
- package/src/components/Table.tsx +196 -0
- package/src/components/Tabs.tsx +124 -0
- package/src/components/Tag.tsx +35 -0
- package/src/components/Textarea.tsx +46 -0
- package/src/components/ThemeProvider.tsx +80 -0
- package/src/components/Toast.tsx +159 -0
- package/src/components/Tooltip.tsx +57 -0
- package/src/components/core.test.tsx +242 -0
- package/src/components/forms-control.ts +36 -0
- package/src/components/forms.test.tsx +280 -0
- package/src/components/logo-art.ts +4 -0
- package/src/components/overlays.test.tsx +245 -0
- package/src/components/useButtonAction.ts +57 -0
- package/src/fonts.css +20 -0
- package/src/index.ts +34 -24
- package/src/lib/cn.ts +23 -6
- package/src/modes.css +10 -0
- package/src/styles.css +19 -0
- package/src/theme.css +143 -0
- package/src/tokens/colors.css +112 -0
- package/src/tokens/compat.css +36 -0
- package/src/tokens/elevation.css +14 -0
- package/src/tokens/motion.css +42 -0
- package/src/tokens/spacing.css +55 -0
- package/src/tokens/typography-cjk.css +42 -0
- package/src/tokens/typography.css +81 -0
- package/src/actionbar/ActionBar.tsx +0 -837
- package/src/actionbar/ActionBarButton.tsx +0 -111
- package/src/actionbar/ActionBarMenuGroup.tsx +0 -116
- package/src/actionbar/ActionBarModeHandle.tsx +0 -171
- package/src/actionbar/ActionBarProvider.tsx +0 -91
- package/src/actionbar/actionbar-context.ts +0 -45
- package/src/actionbar/index.ts +0 -28
- package/src/actionbar/types.ts +0 -147
- package/src/actionbar/useActionBarItems.ts +0 -60
- package/src/actionbar/useActionBarMode.ts +0 -144
- package/src/actionbar/useActionBarPanel.ts +0 -56
- package/src/actionbar/useActionBarStatusDot.ts +0 -49
- package/src/actionbar/useActionBarToast.ts +0 -33
- package/src/ambient/AmbientBackdrop.tsx +0 -74
- package/src/ambient/CommandInput.tsx +0 -107
- package/src/ambient/SuperbarStrip.tsx +0 -36
- package/src/ambient/index.ts +0 -6
- package/src/bento/BentoCell.tsx +0 -66
- package/src/bento/BentoGrid.tsx +0 -42
- package/src/bento/index.ts +0 -2
- package/src/command/CommandPalette.tsx +0 -329
- package/src/command/CommandProvider.tsx +0 -57
- package/src/command/command-context.ts +0 -12
- package/src/command/index.ts +0 -6
- package/src/command/rank.ts +0 -50
- package/src/command/types.ts +0 -37
- package/src/command/useCommandSource.ts +0 -37
- package/src/dialogs/DialogsProvider.tsx +0 -216
- package/src/dialogs/Modal.tsx +0 -260
- package/src/dialogs/Toast.tsx +0 -85
- package/src/dialogs/dialogs-context.ts +0 -6
- package/src/dialogs/index.ts +0 -10
- package/src/dialogs/types.ts +0 -37
- package/src/dialogs/useDialogs.ts +0 -8
- package/src/editorial/EditorialSpotlight.tsx +0 -63
- package/src/editorial/Folio.tsx +0 -52
- package/src/editorial/PlateMarker.tsx +0 -33
- package/src/editorial/RomanSection.tsx +0 -65
- package/src/editorial/RunningMarginalia.tsx +0 -65
- package/src/editorial/index.ts +0 -10
- package/src/hooks/index.ts +0 -5
- package/src/hooks/useKeyboardHotkey.ts +0 -93
- package/src/hooks/useReducedMotion.ts +0 -20
- package/src/hooks/useViewport.ts +0 -61
- package/src/layout/Breadcrumb.tsx +0 -74
- package/src/layout/LeftNavRail.tsx +0 -139
- package/src/layout/MobileTabBar.tsx +0 -101
- package/src/layout/NavItem.tsx +0 -128
- package/src/layout/NavSearchTrigger.tsx +0 -88
- package/src/layout/NavSection.tsx +0 -40
- package/src/layout/RightSidebarPanel.tsx +0 -111
- package/src/layout/Shell.tsx +0 -91
- package/src/layout/SidePanel.tsx +0 -316
- package/src/layout/StickyBanner.tsx +0 -83
- package/src/layout/Topbar.tsx +0 -68
- package/src/layout/index.ts +0 -24
- package/src/layout/useNavRailState.ts +0 -69
- package/src/lib/time-utils.ts +0 -44
- package/src/neobrutalism/Marquee.tsx +0 -81
- package/src/neobrutalism/Sticker.tsx +0 -71
- package/src/neobrutalism/index.ts +0 -4
- package/src/primitives/Avatar.tsx +0 -53
- package/src/primitives/Button.tsx +0 -67
- package/src/primitives/Card.tsx +0 -41
- package/src/primitives/Checkbox.tsx +0 -87
- package/src/primitives/Chip.tsx +0 -191
- package/src/primitives/Combobox.tsx +0 -507
- package/src/primitives/CopyField.tsx +0 -139
- package/src/primitives/CountBadge.tsx +0 -50
- package/src/primitives/Input.tsx +0 -204
- package/src/primitives/Kbd.tsx +0 -45
- package/src/primitives/MentionCombobox.tsx +0 -612
- package/src/primitives/MultiCombobox.tsx +0 -327
- package/src/primitives/PageHeader.tsx +0 -77
- package/src/primitives/Radio.tsx +0 -131
- package/src/primitives/SegmentedControl.tsx +0 -135
- package/src/primitives/Select.tsx +0 -779
- package/src/primitives/SignaturePad.tsx +0 -461
- package/src/primitives/Tag.tsx +0 -56
- package/src/primitives/Textarea.tsx +0 -62
- package/src/primitives/ToggleSwitch.tsx +0 -79
- package/src/primitives/Tooltip.tsx +0 -213
- package/src/primitives/index.ts +0 -53
- package/src/pwa/InstallPromptBanner.tsx +0 -132
- package/src/pwa/index.ts +0 -4
- package/src/pwa/manifest.template.json +0 -20
- package/src/pwa/registerSW.ts +0 -55
- package/src/riso/Halftone.tsx +0 -85
- package/src/riso/Misregister.tsx +0 -63
- package/src/riso/RisoStamp.tsx +0 -76
- package/src/riso/index.ts +0 -3
- package/src/sketch/HandUnderline.tsx +0 -53
- package/src/sketch/RoughArrow.tsx +0 -91
- package/src/sketch/RoughBox.tsx +0 -73
- package/src/sketch/StickyNote.tsx +0 -56
- package/src/sketch/index.ts +0 -4
- package/src/styles/base.css +0 -186
- package/src/styles/chrome-ambient.css +0 -235
- package/src/styles/chrome-bento.css +0 -192
- package/src/styles/chrome-editorial.css +0 -168
- package/src/styles/chrome-neobrutalism.css +0 -326
- package/src/styles/chrome-riso.css +0 -339
- package/src/styles/chrome-sketch.css +0 -362
- package/src/styles/chrome-swiss.css +0 -265
- package/src/styles/chrome-terminal.css +0 -272
- package/src/styles/fonts.css +0 -1034
- package/src/styles/index.css +0 -343
- package/src/styles/tokens.css +0 -941
- package/src/terminal/AsciiBox.tsx +0 -65
- package/src/terminal/BrailleSpinner.tsx +0 -46
- package/src/terminal/index.ts +0 -4
- package/src/theme/ThemeProvider.tsx +0 -99
- package/src/theme/index.ts +0 -6
- package/src/theme/inline-script.ts +0 -40
- package/src/theme/theme-context.ts +0 -7
- package/src/theme/types.ts +0 -39
- package/src/theme/useTheme.ts +0 -8
|
@@ -1,461 +0,0 @@
|
|
|
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
|
-
// Actively REQUEST the typed font. @font-face registration
|
|
160
|
-
// (fontsource CSS etc.) doesn't fetch the file until some
|
|
161
|
-
// rendered text uses it — and this component's only consumer
|
|
162
|
-
// of the font is the canvas, whose fillText silently falls
|
|
163
|
-
// back to the next family when the face isn't loaded yet.
|
|
164
|
-
// fonts.load() forces the fetch; fonts.ready is the safety
|
|
165
|
-
// net for browsers without it / faces not registered.
|
|
166
|
-
const kick =
|
|
167
|
-
typeof fonts.load === "function"
|
|
168
|
-
? fonts.load(`32px ${typedFontFamily}`).catch(() => [])
|
|
169
|
-
: Promise.resolve([]);
|
|
170
|
-
void kick
|
|
171
|
-
.then(() => fonts.ready)
|
|
172
|
-
.then(() => {
|
|
173
|
-
if (cancelled) return;
|
|
174
|
-
setFontReady(true);
|
|
175
|
-
});
|
|
176
|
-
return () => {
|
|
177
|
-
cancelled = true;
|
|
178
|
-
};
|
|
179
|
-
// typedFontFamily is config-stable per consumer; re-kicking on
|
|
180
|
-
// change is correct anyway.
|
|
181
|
-
}, [fontReady, typedFontFamily]);
|
|
182
|
-
|
|
183
|
-
// Mode switch — clear the canvas so a stale typed baseline
|
|
184
|
-
// doesn't hang around behind a drawn signature (or vice versa).
|
|
185
|
-
// MUST be declared BEFORE the typed-repaint + drawn-restore
|
|
186
|
-
// effects: effects run in declaration order, and v0.13.0 had
|
|
187
|
-
// this clear declared LAST — on switching to typed mode with a
|
|
188
|
-
// prefilled name the repaint painted first and this then wiped
|
|
189
|
-
// it (QA 2026-07-17: Type tab showed an empty pad until the
|
|
190
|
-
// signer edited the name). Skips the mount run so a parent-held
|
|
191
|
-
// captured value isn't nulled on first render.
|
|
192
|
-
const prevModeRef = useRef(mode);
|
|
193
|
-
useEffect(() => {
|
|
194
|
-
if (prevModeRef.current === mode) return;
|
|
195
|
-
prevModeRef.current = mode;
|
|
196
|
-
const canvas = canvasRef.current;
|
|
197
|
-
if (!canvas) return;
|
|
198
|
-
const ctx = canvas.getContext("2d");
|
|
199
|
-
if (!ctx) return;
|
|
200
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
201
|
-
dirtyRef.current = false;
|
|
202
|
-
onChange(null);
|
|
203
|
-
// Only fire when mode itself changes.
|
|
204
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
205
|
-
}, [mode]);
|
|
206
|
-
|
|
207
|
-
// Repaint on typedName / mode / fontReady change. Drawn mode
|
|
208
|
-
// doesn't repaint imperatively — strokes are appended as the user
|
|
209
|
-
// draws.
|
|
210
|
-
useEffect(() => {
|
|
211
|
-
if (mode !== "typed") return;
|
|
212
|
-
const canvas = canvasRef.current;
|
|
213
|
-
if (!canvas) return;
|
|
214
|
-
const ctx = canvas.getContext("2d");
|
|
215
|
-
if (!ctx) return;
|
|
216
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
217
|
-
if (!typedName.trim()) {
|
|
218
|
-
dirtyRef.current = false;
|
|
219
|
-
onChange(null);
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
if (!fontReady) return;
|
|
223
|
-
// Paint the name centred vertically, left-aligned horizontally.
|
|
224
|
-
// Auto-fit font size to width: start at height * 0.6 and shrink
|
|
225
|
-
// until measureText fits within `width - 24` (12px padding each
|
|
226
|
-
// side).
|
|
227
|
-
let fontSize = Math.round(height * 0.7);
|
|
228
|
-
ctx.textBaseline = "middle";
|
|
229
|
-
ctx.fillStyle = strokeColor;
|
|
230
|
-
const availableWidth = width - 24;
|
|
231
|
-
while (fontSize > 10) {
|
|
232
|
-
ctx.font = `${fontSize}px ${typedFontFamily}`;
|
|
233
|
-
if (ctx.measureText(typedName).width <= availableWidth) break;
|
|
234
|
-
fontSize -= 2;
|
|
235
|
-
}
|
|
236
|
-
ctx.fillText(typedName, 12, height / 2);
|
|
237
|
-
const dataURL = canvas.toDataURL("image/png");
|
|
238
|
-
dirtyRef.current = true;
|
|
239
|
-
onChange(dataURL);
|
|
240
|
-
// onChange is stable-by-contract from parent (React best
|
|
241
|
-
// practice); avoid feeding it into deps to prevent infinite
|
|
242
|
-
// loop.
|
|
243
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
244
|
-
}, [
|
|
245
|
-
mode,
|
|
246
|
-
typedName,
|
|
247
|
-
fontReady,
|
|
248
|
-
width,
|
|
249
|
-
height,
|
|
250
|
-
typedFontFamily,
|
|
251
|
-
strokeColor,
|
|
252
|
-
]);
|
|
253
|
-
|
|
254
|
-
// Restore a previously-captured value into the canvas on MOUNT
|
|
255
|
-
// only (parent phase round-trips remount the pad). Typed mode
|
|
256
|
-
// uses the repaint effect above; drawn mode needs to explicitly
|
|
257
|
-
// paint the dataURL back so subsequent strokes continue on top.
|
|
258
|
-
//
|
|
259
|
-
// Deliberately NOT re-run on mode switch: the clear effect wipes
|
|
260
|
-
// the canvas on switch, and re-painting the old capture here
|
|
261
|
-
// resurrected the TYPED baseline inside the drawn pad (v0.13.2
|
|
262
|
-
// regression — switching Type → Draw showed the handwriting-font
|
|
263
|
-
// bake as if it had been drawn).
|
|
264
|
-
const didRestoreRef = useRef(false);
|
|
265
|
-
useEffect(() => {
|
|
266
|
-
if (didRestoreRef.current) return;
|
|
267
|
-
didRestoreRef.current = true;
|
|
268
|
-
if (mode !== "drawn" || !value) return;
|
|
269
|
-
const canvas = canvasRef.current;
|
|
270
|
-
if (!canvas) return;
|
|
271
|
-
const ctx = canvas.getContext("2d");
|
|
272
|
-
if (!ctx) return;
|
|
273
|
-
const img = new Image();
|
|
274
|
-
img.onload = () => {
|
|
275
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
276
|
-
ctx.drawImage(img, 0, 0);
|
|
277
|
-
dirtyRef.current = true;
|
|
278
|
-
};
|
|
279
|
-
img.src = value;
|
|
280
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
281
|
-
}, []);
|
|
282
|
-
|
|
283
|
-
const clear = useCallback(() => {
|
|
284
|
-
const canvas = canvasRef.current;
|
|
285
|
-
if (canvas) {
|
|
286
|
-
const ctx = canvas.getContext("2d");
|
|
287
|
-
ctx?.clearRect(0, 0, canvas.width, canvas.height);
|
|
288
|
-
}
|
|
289
|
-
dirtyRef.current = false;
|
|
290
|
-
onChange(null);
|
|
291
|
-
if (mode === "typed") {
|
|
292
|
-
onTypedNameChange?.("");
|
|
293
|
-
}
|
|
294
|
-
}, [mode, onChange, onTypedNameChange]);
|
|
295
|
-
|
|
296
|
-
useImperativeHandle(ref, () => ({ clear }), [clear]);
|
|
297
|
-
|
|
298
|
-
// Drawn mode — pointer event handlers.
|
|
299
|
-
const onPointerDown = useCallback(
|
|
300
|
-
(e: ReactPointerEvent<HTMLCanvasElement>) => {
|
|
301
|
-
if (mode !== "drawn") return;
|
|
302
|
-
// touch-action: none on the canvas isn't enough on iOS
|
|
303
|
-
// Safari — need preventDefault too, otherwise the browser
|
|
304
|
-
// scrolls the page instead of drawing.
|
|
305
|
-
e.preventDefault();
|
|
306
|
-
const canvas = canvasRef.current;
|
|
307
|
-
if (!canvas) return;
|
|
308
|
-
const rect = canvas.getBoundingClientRect();
|
|
309
|
-
// Coordinate space: pointer client coords → canvas backing
|
|
310
|
-
// store coords. `rect` is CSS pixels; canvas.width is device
|
|
311
|
-
// pixels (or logical pixels when no DPR handling). Scale so
|
|
312
|
-
// strokes land at the visual point.
|
|
313
|
-
const scaleX = canvas.width / rect.width;
|
|
314
|
-
const scaleY = canvas.height / rect.height;
|
|
315
|
-
const x = (e.clientX - rect.left) * scaleX;
|
|
316
|
-
const y = (e.clientY - rect.top) * scaleY;
|
|
317
|
-
drawStateRef.current.drawing = true;
|
|
318
|
-
drawStateRef.current.lastX = x;
|
|
319
|
-
drawStateRef.current.lastY = y;
|
|
320
|
-
canvas.setPointerCapture(e.pointerId);
|
|
321
|
-
},
|
|
322
|
-
[mode],
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
const onPointerMove = useCallback(
|
|
326
|
-
(e: ReactPointerEvent<HTMLCanvasElement>) => {
|
|
327
|
-
if (mode !== "drawn") return;
|
|
328
|
-
if (!drawStateRef.current.drawing) return;
|
|
329
|
-
const canvas = canvasRef.current;
|
|
330
|
-
if (!canvas) return;
|
|
331
|
-
const ctx = canvas.getContext("2d");
|
|
332
|
-
if (!ctx) return;
|
|
333
|
-
const rect = canvas.getBoundingClientRect();
|
|
334
|
-
const scaleX = canvas.width / rect.width;
|
|
335
|
-
const scaleY = canvas.height / rect.height;
|
|
336
|
-
const x = (e.clientX - rect.left) * scaleX;
|
|
337
|
-
const y = (e.clientY - rect.top) * scaleY;
|
|
338
|
-
ctx.strokeStyle = strokeColor;
|
|
339
|
-
ctx.lineWidth = strokeWidth;
|
|
340
|
-
ctx.lineCap = "round";
|
|
341
|
-
ctx.lineJoin = "round";
|
|
342
|
-
ctx.beginPath();
|
|
343
|
-
ctx.moveTo(drawStateRef.current.lastX, drawStateRef.current.lastY);
|
|
344
|
-
// Simple line-to; a smoother quadratic curve is possible via
|
|
345
|
-
// buffering the last N points, but the default line-to
|
|
346
|
-
// already reads well at 2px stroke on human-scale pads.
|
|
347
|
-
ctx.lineTo(x, y);
|
|
348
|
-
ctx.stroke();
|
|
349
|
-
drawStateRef.current.lastX = x;
|
|
350
|
-
drawStateRef.current.lastY = y;
|
|
351
|
-
dirtyRef.current = true;
|
|
352
|
-
},
|
|
353
|
-
[mode, strokeColor, strokeWidth],
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
const onPointerUp = useCallback(
|
|
357
|
-
(e: ReactPointerEvent<HTMLCanvasElement>) => {
|
|
358
|
-
if (mode !== "drawn") return;
|
|
359
|
-
drawStateRef.current.drawing = false;
|
|
360
|
-
const canvas = canvasRef.current;
|
|
361
|
-
if (!canvas) return;
|
|
362
|
-
try {
|
|
363
|
-
canvas.releasePointerCapture(e.pointerId);
|
|
364
|
-
} catch {
|
|
365
|
-
// Some browsers throw when the pointer isn't captured
|
|
366
|
-
// (spurious pointerup); safe to ignore.
|
|
367
|
-
}
|
|
368
|
-
if (!dirtyRef.current) return;
|
|
369
|
-
const dataURL = canvas.toDataURL("image/png");
|
|
370
|
-
onChange(dataURL);
|
|
371
|
-
},
|
|
372
|
-
[mode, onChange],
|
|
373
|
-
);
|
|
374
|
-
|
|
375
|
-
// Wrapper + canvas styles. Inline (following the ck-* CSS var
|
|
376
|
-
// conventions) so consumers get a functional pad without needing
|
|
377
|
-
// to import a CSS file.
|
|
378
|
-
const wrapStyle: CSSProperties = useMemo(
|
|
379
|
-
() => ({
|
|
380
|
-
display: "flex",
|
|
381
|
-
flexDirection: "column",
|
|
382
|
-
gap: 8,
|
|
383
|
-
// fit-content sizes to the canvas's intrinsic `width` prop —
|
|
384
|
-
// which is exactly what overflowed narrow phones: the wrapper
|
|
385
|
-
// itself grew to 480px, so the canvas's own maxWidth:100%
|
|
386
|
-
// resolved against an already-overflowing parent. Cap the
|
|
387
|
-
// wrapper too; the flex-column children then shrink with it.
|
|
388
|
-
width: "fit-content",
|
|
389
|
-
maxWidth: "100%",
|
|
390
|
-
}),
|
|
391
|
-
[],
|
|
392
|
-
);
|
|
393
|
-
const canvasStyle: CSSProperties = useMemo(
|
|
394
|
-
() => ({
|
|
395
|
-
width,
|
|
396
|
-
// Never overflow a narrow container (phone signing surface) —
|
|
397
|
-
// the bitmap keeps its logical resolution and the pointer
|
|
398
|
-
// handlers already rescale via getBoundingClientRect, so CSS
|
|
399
|
-
// shrink is loss-free for drawing accuracy.
|
|
400
|
-
maxWidth: "100%",
|
|
401
|
-
height,
|
|
402
|
-
border: "1px solid var(--ck-border-subtle, #d4d4d8)",
|
|
403
|
-
borderRadius: "var(--ck-radius-sm, 6px)",
|
|
404
|
-
background: "var(--ck-bg-surface, #ffffff)",
|
|
405
|
-
// touch-action: none is the load-bearing bit for iOS Safari
|
|
406
|
-
// drawn mode — otherwise the page scrolls instead of drawing.
|
|
407
|
-
touchAction: "none",
|
|
408
|
-
cursor: mode === "drawn" ? "crosshair" : "default",
|
|
409
|
-
display: "block",
|
|
410
|
-
}),
|
|
411
|
-
[mode, width, height],
|
|
412
|
-
);
|
|
413
|
-
const typedInputStyle: CSSProperties = useMemo(
|
|
414
|
-
() => ({
|
|
415
|
-
padding: "6px 8px",
|
|
416
|
-
border: "1px solid var(--ck-border-subtle, #d4d4d8)",
|
|
417
|
-
borderRadius: "var(--ck-radius-sm, 6px)",
|
|
418
|
-
background: "var(--ck-bg-surface, #ffffff)",
|
|
419
|
-
color: "var(--ck-text-primary, #0b1220)",
|
|
420
|
-
fontSize: 14,
|
|
421
|
-
fontFamily: "inherit",
|
|
422
|
-
width,
|
|
423
|
-
maxWidth: "100%",
|
|
424
|
-
boxSizing: "border-box",
|
|
425
|
-
}),
|
|
426
|
-
[width],
|
|
427
|
-
);
|
|
428
|
-
|
|
429
|
-
return (
|
|
430
|
-
<div className={className} style={wrapStyle}>
|
|
431
|
-
{mode === "typed" ? (
|
|
432
|
-
<input
|
|
433
|
-
type="text"
|
|
434
|
-
value={typedName}
|
|
435
|
-
placeholder="Type your name"
|
|
436
|
-
onChange={(e) => onTypedNameChange?.(e.target.value)}
|
|
437
|
-
style={typedInputStyle}
|
|
438
|
-
aria-label="Type your signature"
|
|
439
|
-
/>
|
|
440
|
-
) : null}
|
|
441
|
-
<canvas
|
|
442
|
-
ref={canvasRef}
|
|
443
|
-
width={width}
|
|
444
|
-
height={height}
|
|
445
|
-
style={canvasStyle}
|
|
446
|
-
role="img"
|
|
447
|
-
aria-label={
|
|
448
|
-
mode === "typed"
|
|
449
|
-
? "Typed signature preview"
|
|
450
|
-
: "Draw your signature"
|
|
451
|
-
}
|
|
452
|
-
onPointerDown={onPointerDown}
|
|
453
|
-
onPointerMove={onPointerMove}
|
|
454
|
-
onPointerUp={onPointerUp}
|
|
455
|
-
onPointerCancel={onPointerUp}
|
|
456
|
-
onPointerLeave={onPointerUp}
|
|
457
|
-
/>
|
|
458
|
-
</div>
|
|
459
|
-
);
|
|
460
|
-
},
|
|
461
|
-
);
|
package/src/primitives/Tag.tsx
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
import { cn } from "../lib/cn";
|
|
3
|
-
|
|
4
|
-
// Small uppercase-mono pill — "PENDING", "LIVE", "Accepted", "3 docs".
|
|
5
|
-
// Same vocabulary as ck-tag in base.css but with explicit tone variants
|
|
6
|
-
// instead of inline color overrides. Use this when the tag is a status
|
|
7
|
-
// indicator; use the bare .ck-tag class when it's just a caption.
|
|
8
|
-
|
|
9
|
-
export type TagTone = "neutral" | "accent" | "success" | "warning" | "critical";
|
|
10
|
-
|
|
11
|
-
export interface TagProps {
|
|
12
|
-
tone?: TagTone;
|
|
13
|
-
// Renders as a filled pill (background tinted) instead of a bare
|
|
14
|
-
// colored label. Default false.
|
|
15
|
-
filled?: boolean;
|
|
16
|
-
children: ReactNode;
|
|
17
|
-
className?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const TONE_COLOR: Record<TagTone, string> = {
|
|
21
|
-
neutral: "var(--ck-text-tertiary)",
|
|
22
|
-
accent: "var(--ck-accent)",
|
|
23
|
-
success: "var(--ck-success)",
|
|
24
|
-
warning: "var(--ck-warning)",
|
|
25
|
-
critical: "var(--ck-critical)",
|
|
26
|
-
};
|
|
27
|
-
const TONE_BG: Record<TagTone, string> = {
|
|
28
|
-
neutral: "var(--ck-bg-muted)",
|
|
29
|
-
accent: "var(--ck-accent-muted)",
|
|
30
|
-
success: "var(--ck-success-muted)",
|
|
31
|
-
warning: "var(--ck-warning-muted)",
|
|
32
|
-
critical: "var(--ck-critical-muted)",
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export function Tag({ tone = "neutral", filled = false, children, className }: TagProps) {
|
|
36
|
-
return (
|
|
37
|
-
<span
|
|
38
|
-
className={cn("ck-tag", className)}
|
|
39
|
-
data-ck-tag
|
|
40
|
-
data-tone={tone}
|
|
41
|
-
data-filled={filled ? "true" : undefined}
|
|
42
|
-
style={
|
|
43
|
-
filled
|
|
44
|
-
? {
|
|
45
|
-
color: TONE_COLOR[tone],
|
|
46
|
-
background: TONE_BG[tone],
|
|
47
|
-
padding: "3px 8px",
|
|
48
|
-
borderRadius: "var(--ck-radius-sm)",
|
|
49
|
-
}
|
|
50
|
-
: { color: TONE_COLOR[tone] }
|
|
51
|
-
}
|
|
52
|
-
>
|
|
53
|
-
{children}
|
|
54
|
-
</span>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { forwardRef, useId, type TextareaHTMLAttributes, type ReactNode } from "react";
|
|
2
|
-
import { cn } from "../lib/cn";
|
|
3
|
-
|
|
4
|
-
// Same shape as Input — label + helper + error — for multiline.
|
|
5
|
-
|
|
6
|
-
export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
7
|
-
label?: ReactNode;
|
|
8
|
-
helper?: ReactNode;
|
|
9
|
-
error?: string | null;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
13
|
-
function Textarea({ label, helper, error, className, id, ...rest }, ref) {
|
|
14
|
-
const autoId = useId();
|
|
15
|
-
const tid = id ?? autoId;
|
|
16
|
-
return (
|
|
17
|
-
<div
|
|
18
|
-
className={cn("ck-textarea-field", className)}
|
|
19
|
-
style={{ display: "flex", flexDirection: "column", gap: 6 }}
|
|
20
|
-
>
|
|
21
|
-
{label && (
|
|
22
|
-
<label
|
|
23
|
-
htmlFor={tid}
|
|
24
|
-
className="ck-eyebrow"
|
|
25
|
-
style={{ color: "var(--ck-text-secondary)" }}
|
|
26
|
-
>
|
|
27
|
-
{label}
|
|
28
|
-
</label>
|
|
29
|
-
)}
|
|
30
|
-
<textarea
|
|
31
|
-
ref={ref}
|
|
32
|
-
id={tid}
|
|
33
|
-
aria-invalid={error ? true : undefined}
|
|
34
|
-
{...rest}
|
|
35
|
-
className="ck-textarea"
|
|
36
|
-
style={{
|
|
37
|
-
minHeight: 80,
|
|
38
|
-
padding: "8px 12px",
|
|
39
|
-
font: "400 13px/1.5 var(--ck-font-sans)",
|
|
40
|
-
background: "var(--ck-bg-surface)",
|
|
41
|
-
color: "var(--ck-text-primary)",
|
|
42
|
-
border: `1px solid ${error ? "var(--ck-critical)" : "var(--ck-border-strong)"}`,
|
|
43
|
-
borderRadius: "var(--ck-radius-sm)",
|
|
44
|
-
outline: "none",
|
|
45
|
-
resize: "vertical",
|
|
46
|
-
transition: "border-color var(--ck-dur-fast) var(--ck-ease)",
|
|
47
|
-
}}
|
|
48
|
-
/>
|
|
49
|
-
{(helper || error) && (
|
|
50
|
-
<div
|
|
51
|
-
style={{
|
|
52
|
-
font: "400 11px/1.4 var(--ck-font-sans)",
|
|
53
|
-
color: error ? "var(--ck-critical)" : "var(--ck-text-tertiary)",
|
|
54
|
-
}}
|
|
55
|
-
>
|
|
56
|
-
{error ?? helper}
|
|
57
|
-
</div>
|
|
58
|
-
)}
|
|
59
|
-
</div>
|
|
60
|
-
);
|
|
61
|
-
},
|
|
62
|
-
);
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { cn } from "../lib/cn";
|
|
2
|
-
|
|
3
|
-
// iOS-style track + knob toggle. Off-state track uses
|
|
4
|
-
// --ck-text-secondary (not --ck-border-subtle which is near-invisible
|
|
5
|
-
// on dark backgrounds — same lesson learned in the parent project).
|
|
6
|
-
// Knob is fixed white so it reads on either accent or gray track.
|
|
7
|
-
|
|
8
|
-
export interface ToggleSwitchProps {
|
|
9
|
-
checked: boolean;
|
|
10
|
-
onChange: (next: boolean) => void;
|
|
11
|
-
label?: React.ReactNode;
|
|
12
|
-
suffix?: React.ReactNode;
|
|
13
|
-
disabled?: boolean;
|
|
14
|
-
className?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function ToggleSwitch({
|
|
18
|
-
checked,
|
|
19
|
-
onChange,
|
|
20
|
-
label,
|
|
21
|
-
suffix,
|
|
22
|
-
disabled,
|
|
23
|
-
className,
|
|
24
|
-
}: ToggleSwitchProps) {
|
|
25
|
-
return (
|
|
26
|
-
<label
|
|
27
|
-
className={cn("ck-toggle", className)}
|
|
28
|
-
style={{
|
|
29
|
-
display: "inline-flex",
|
|
30
|
-
alignItems: "center",
|
|
31
|
-
gap: 8,
|
|
32
|
-
font: "400 13px/1 var(--ck-font-sans)",
|
|
33
|
-
color: "var(--ck-text-secondary)",
|
|
34
|
-
cursor: disabled ? "not-allowed" : "pointer",
|
|
35
|
-
opacity: disabled ? 0.5 : 1,
|
|
36
|
-
userSelect: "none",
|
|
37
|
-
}}
|
|
38
|
-
>
|
|
39
|
-
<button
|
|
40
|
-
type="button"
|
|
41
|
-
role="switch"
|
|
42
|
-
aria-checked={checked}
|
|
43
|
-
disabled={disabled}
|
|
44
|
-
onClick={() => !disabled && onChange(!checked)}
|
|
45
|
-
style={{
|
|
46
|
-
position: "relative",
|
|
47
|
-
width: 34,
|
|
48
|
-
height: 20,
|
|
49
|
-
borderRadius: 999,
|
|
50
|
-
border: "1px solid var(--ck-text-tertiary)",
|
|
51
|
-
background: checked ? "var(--ck-accent)" : "var(--ck-text-secondary)",
|
|
52
|
-
padding: 0,
|
|
53
|
-
cursor: disabled ? "not-allowed" : "pointer",
|
|
54
|
-
flexShrink: 0,
|
|
55
|
-
transition: "background var(--ck-dur-fast) var(--ck-ease)",
|
|
56
|
-
}}
|
|
57
|
-
>
|
|
58
|
-
<span
|
|
59
|
-
aria-hidden
|
|
60
|
-
style={{
|
|
61
|
-
position: "absolute",
|
|
62
|
-
top: 2,
|
|
63
|
-
left: checked ? 16 : 2,
|
|
64
|
-
width: 14,
|
|
65
|
-
height: 14,
|
|
66
|
-
background: "#FFFFFF",
|
|
67
|
-
borderRadius: "50%",
|
|
68
|
-
boxShadow: "0 1px 3px rgba(0,0,0,0.35)",
|
|
69
|
-
transition: "left var(--ck-dur-fast) var(--ck-ease)",
|
|
70
|
-
}}
|
|
71
|
-
/>
|
|
72
|
-
</button>
|
|
73
|
-
{label}
|
|
74
|
-
{suffix && (
|
|
75
|
-
<span style={{ color: "var(--ck-text-tertiary)" }}>{suffix}</span>
|
|
76
|
-
)}
|
|
77
|
-
</label>
|
|
78
|
-
);
|
|
79
|
-
}
|