@cosxai/ui 0.25.0 → 1.0.0-alpha.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.
Files changed (133) hide show
  1. package/CHANGELOG.md +577 -0
  2. package/NOTICE +11 -0
  3. package/README.md +52 -0
  4. package/package.json +33 -7
  5. package/src/fonts.css +20 -0
  6. package/src/index.ts +8 -25
  7. package/src/lib/cn.ts +23 -6
  8. package/src/styles.css +17 -0
  9. package/src/theme.css +132 -0
  10. package/src/tokens/colors.css +112 -0
  11. package/src/tokens/compat.css +36 -0
  12. package/src/tokens/elevation.css +14 -0
  13. package/src/tokens/motion.css +42 -0
  14. package/src/tokens/spacing.css +55 -0
  15. package/src/tokens/typography-cjk.css +42 -0
  16. package/src/tokens/typography.css +81 -0
  17. package/src/actionbar/ActionBar.tsx +0 -837
  18. package/src/actionbar/ActionBarButton.tsx +0 -111
  19. package/src/actionbar/ActionBarMenuGroup.tsx +0 -116
  20. package/src/actionbar/ActionBarModeHandle.tsx +0 -171
  21. package/src/actionbar/ActionBarProvider.tsx +0 -91
  22. package/src/actionbar/actionbar-context.ts +0 -45
  23. package/src/actionbar/index.ts +0 -28
  24. package/src/actionbar/types.ts +0 -147
  25. package/src/actionbar/useActionBarItems.ts +0 -60
  26. package/src/actionbar/useActionBarMode.ts +0 -144
  27. package/src/actionbar/useActionBarPanel.ts +0 -56
  28. package/src/actionbar/useActionBarStatusDot.ts +0 -49
  29. package/src/actionbar/useActionBarToast.ts +0 -33
  30. package/src/ambient/AmbientBackdrop.tsx +0 -74
  31. package/src/ambient/CommandInput.tsx +0 -107
  32. package/src/ambient/SuperbarStrip.tsx +0 -36
  33. package/src/ambient/index.ts +0 -6
  34. package/src/bento/BentoCell.tsx +0 -66
  35. package/src/bento/BentoGrid.tsx +0 -42
  36. package/src/bento/index.ts +0 -2
  37. package/src/command/CommandPalette.tsx +0 -329
  38. package/src/command/CommandProvider.tsx +0 -57
  39. package/src/command/command-context.ts +0 -12
  40. package/src/command/index.ts +0 -6
  41. package/src/command/rank.ts +0 -50
  42. package/src/command/types.ts +0 -37
  43. package/src/command/useCommandSource.ts +0 -37
  44. package/src/dialogs/DialogsProvider.tsx +0 -216
  45. package/src/dialogs/Modal.tsx +0 -260
  46. package/src/dialogs/Toast.tsx +0 -85
  47. package/src/dialogs/dialogs-context.ts +0 -6
  48. package/src/dialogs/index.ts +0 -10
  49. package/src/dialogs/types.ts +0 -37
  50. package/src/dialogs/useDialogs.ts +0 -8
  51. package/src/editorial/EditorialSpotlight.tsx +0 -63
  52. package/src/editorial/Folio.tsx +0 -52
  53. package/src/editorial/PlateMarker.tsx +0 -33
  54. package/src/editorial/RomanSection.tsx +0 -65
  55. package/src/editorial/RunningMarginalia.tsx +0 -65
  56. package/src/editorial/index.ts +0 -10
  57. package/src/hooks/index.ts +0 -5
  58. package/src/hooks/useKeyboardHotkey.ts +0 -93
  59. package/src/hooks/useReducedMotion.ts +0 -20
  60. package/src/hooks/useViewport.ts +0 -61
  61. package/src/layout/Breadcrumb.tsx +0 -74
  62. package/src/layout/LeftNavRail.tsx +0 -139
  63. package/src/layout/MobileTabBar.tsx +0 -101
  64. package/src/layout/NavItem.tsx +0 -128
  65. package/src/layout/NavSearchTrigger.tsx +0 -88
  66. package/src/layout/NavSection.tsx +0 -40
  67. package/src/layout/RightSidebarPanel.tsx +0 -111
  68. package/src/layout/Shell.tsx +0 -91
  69. package/src/layout/SidePanel.tsx +0 -316
  70. package/src/layout/StickyBanner.tsx +0 -83
  71. package/src/layout/Topbar.tsx +0 -68
  72. package/src/layout/index.ts +0 -24
  73. package/src/layout/useNavRailState.ts +0 -69
  74. package/src/lib/time-utils.ts +0 -44
  75. package/src/neobrutalism/Marquee.tsx +0 -81
  76. package/src/neobrutalism/Sticker.tsx +0 -71
  77. package/src/neobrutalism/index.ts +0 -4
  78. package/src/primitives/Avatar.tsx +0 -53
  79. package/src/primitives/Button.tsx +0 -67
  80. package/src/primitives/Card.tsx +0 -41
  81. package/src/primitives/Checkbox.tsx +0 -87
  82. package/src/primitives/Chip.tsx +0 -191
  83. package/src/primitives/Combobox.tsx +0 -507
  84. package/src/primitives/CopyField.tsx +0 -139
  85. package/src/primitives/CountBadge.tsx +0 -50
  86. package/src/primitives/Input.tsx +0 -204
  87. package/src/primitives/Kbd.tsx +0 -45
  88. package/src/primitives/MentionCombobox.tsx +0 -612
  89. package/src/primitives/MultiCombobox.tsx +0 -327
  90. package/src/primitives/PageHeader.tsx +0 -77
  91. package/src/primitives/Radio.tsx +0 -131
  92. package/src/primitives/SegmentedControl.tsx +0 -135
  93. package/src/primitives/Select.tsx +0 -779
  94. package/src/primitives/SignaturePad.tsx +0 -461
  95. package/src/primitives/Tag.tsx +0 -56
  96. package/src/primitives/Textarea.tsx +0 -62
  97. package/src/primitives/ToggleSwitch.tsx +0 -79
  98. package/src/primitives/Tooltip.tsx +0 -213
  99. package/src/primitives/index.ts +0 -53
  100. package/src/pwa/InstallPromptBanner.tsx +0 -132
  101. package/src/pwa/index.ts +0 -4
  102. package/src/pwa/manifest.template.json +0 -20
  103. package/src/pwa/registerSW.ts +0 -55
  104. package/src/riso/Halftone.tsx +0 -85
  105. package/src/riso/Misregister.tsx +0 -63
  106. package/src/riso/RisoStamp.tsx +0 -76
  107. package/src/riso/index.ts +0 -3
  108. package/src/sketch/HandUnderline.tsx +0 -53
  109. package/src/sketch/RoughArrow.tsx +0 -91
  110. package/src/sketch/RoughBox.tsx +0 -73
  111. package/src/sketch/StickyNote.tsx +0 -56
  112. package/src/sketch/index.ts +0 -4
  113. package/src/styles/base.css +0 -186
  114. package/src/styles/chrome-ambient.css +0 -235
  115. package/src/styles/chrome-bento.css +0 -192
  116. package/src/styles/chrome-editorial.css +0 -168
  117. package/src/styles/chrome-neobrutalism.css +0 -326
  118. package/src/styles/chrome-riso.css +0 -339
  119. package/src/styles/chrome-sketch.css +0 -362
  120. package/src/styles/chrome-swiss.css +0 -265
  121. package/src/styles/chrome-terminal.css +0 -272
  122. package/src/styles/fonts.css +0 -1034
  123. package/src/styles/index.css +0 -343
  124. package/src/styles/tokens.css +0 -941
  125. package/src/terminal/AsciiBox.tsx +0 -65
  126. package/src/terminal/BrailleSpinner.tsx +0 -46
  127. package/src/terminal/index.ts +0 -4
  128. package/src/theme/ThemeProvider.tsx +0 -99
  129. package/src/theme/index.ts +0 -6
  130. package/src/theme/inline-script.ts +0 -40
  131. package/src/theme/theme-context.ts +0 -7
  132. package/src/theme/types.ts +0 -39
  133. 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
- );
@@ -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
- }