@cosxai/ui 0.12.0 → 0.13.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosxai/ui",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "description": "COSX design system — React 19 component primitives shared across product-meta and other consumers",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -0,0 +1,139 @@
1
+ import { forwardRef, useCallback, useRef, useState, type HTMLAttributes, type ReactNode } from "react";
2
+ import { cn } from "../lib/cn";
3
+
4
+ // CopyField — read-only value field with an embedded Copy button.
5
+ //
6
+ // One bordered frame (same 36px field chrome as Input-with-addon):
7
+ // the value renders mono + ellipsized on the left, the Copy button
8
+ // sits INSIDE the frame flush right on a muted slab (mirrors Input's
9
+ // suffix addon). Clicking copies to the clipboard and flips the
10
+ // label to "Copied ✓" for a beat; clicking the value text selects it
11
+ // (manual-copy fallback for non-secure origins where the Clipboard
12
+ // API is unavailable).
13
+ //
14
+ // Use for share links, signing URLs, tokens, API keys — anywhere the
15
+ // user's next action is overwhelmingly "copy this". Compose with:
16
+ // dialog bodies (share link rows), sidebar token reveals, settings
17
+ // pages (API-key display).
18
+ //
19
+ // Forwards ref to the copy <button>. Spreads `...rest` on the frame
20
+ // so consumers can pass data-* / aria-* / style / className.
21
+
22
+ export interface CopyFieldProps extends Omit<HTMLAttributes<HTMLDivElement>, "onCopy"> {
23
+ // The string shown + copied. Rendered mono, single-line, ellipsized.
24
+ value: string;
25
+ // Copy-button label; flips to `copiedLabel` after a click.
26
+ copyLabel?: ReactNode;
27
+ copiedLabel?: ReactNode;
28
+ // How long the copied state lingers before flipping back (ms).
29
+ copiedForMs?: number;
30
+ // Notified after a successful clipboard write. Clipboard failures
31
+ // are silent by design — the value stays selectable in the field.
32
+ onCopied?: (value: string) => void;
33
+ }
34
+
35
+ export const CopyField = forwardRef<HTMLButtonElement, CopyFieldProps>(function CopyField(
36
+ {
37
+ value,
38
+ copyLabel = "Copy",
39
+ copiedLabel = "Copied ✓",
40
+ copiedForMs = 1500,
41
+ onCopied,
42
+ className,
43
+ style,
44
+ ...rest
45
+ },
46
+ ref,
47
+ ) {
48
+ const [copied, setCopied] = useState(false);
49
+ const valueRef = useRef<HTMLElement | null>(null);
50
+
51
+ const handleCopy = useCallback(() => {
52
+ void navigator.clipboard
53
+ ?.writeText(value)
54
+ .then(() => onCopied?.(value))
55
+ .catch(() => {
56
+ // Clipboard rejected (permissions / non-secure origin). The
57
+ // value stays visible + selectable in the field — no error UI.
58
+ });
59
+ setCopied(true);
60
+ window.setTimeout(() => setCopied(false), copiedForMs);
61
+ }, [value, copiedForMs, onCopied]);
62
+
63
+ // Click-to-select on the value — the manual-copy fallback.
64
+ const handleSelectAll = useCallback(() => {
65
+ const node = valueRef.current;
66
+ if (!node) return;
67
+ const range = document.createRange();
68
+ range.selectNodeContents(node);
69
+ const sel = window.getSelection();
70
+ sel?.removeAllRanges();
71
+ sel?.addRange(range);
72
+ }, []);
73
+
74
+ return (
75
+ <div
76
+ {...rest}
77
+ className={cn("ck-copy-field", className)}
78
+ style={{
79
+ display: "flex",
80
+ alignItems: "stretch",
81
+ height: 36,
82
+ background: "var(--ck-bg-surface)",
83
+ border: "1px solid var(--ck-border-strong)",
84
+ borderRadius: "var(--ck-radius-sm)",
85
+ overflow: "hidden",
86
+ minWidth: 0,
87
+ ...(style ?? {}),
88
+ }}
89
+ >
90
+ <code
91
+ ref={valueRef}
92
+ title={value}
93
+ onClick={handleSelectAll}
94
+ style={{
95
+ flex: 1,
96
+ minWidth: 0,
97
+ alignSelf: "center",
98
+ padding: "0 12px",
99
+ font: "400 12px/1.5 var(--ck-font-mono, ui-monospace, monospace)",
100
+ color: "var(--ck-text-primary)",
101
+ whiteSpace: "nowrap",
102
+ overflow: "hidden",
103
+ textOverflow: "ellipsis",
104
+ cursor: "text",
105
+ }}
106
+ >
107
+ {value}
108
+ </code>
109
+ <button
110
+ ref={ref}
111
+ type="button"
112
+ onClick={handleCopy}
113
+ disabled={copied}
114
+ className="ck-copy-field-btn"
115
+ style={{
116
+ flex: "0 0 auto",
117
+ display: "inline-flex",
118
+ alignItems: "center",
119
+ justifyContent: "center",
120
+ padding: "0 14px",
121
+ // Reserve the wider copied-label footprint so the value
122
+ // column doesn't shift mid-flash.
123
+ minWidth: 84,
124
+ border: "none",
125
+ borderLeft: "1px solid var(--ck-border-strong)",
126
+ background: "var(--ck-bg-muted)",
127
+ color: copied ? "var(--ck-text-secondary)" : "var(--ck-text-primary)",
128
+ font: "500 11px/1 var(--ck-font-sans)",
129
+ letterSpacing: "0.04em",
130
+ textTransform: "uppercase",
131
+ cursor: copied ? "default" : "pointer",
132
+ transition: "color var(--ck-dur-fast) var(--ck-ease)",
133
+ }}
134
+ >
135
+ {copied ? copiedLabel : copyLabel}
136
+ </button>
137
+ </div>
138
+ );
139
+ });
@@ -165,6 +165,30 @@ export const SignaturePad = forwardRef<SignaturePadHandle, SignaturePadProps>(
165
165
  };
166
166
  }, [fontReady]);
167
167
 
168
+ // Mode switch — clear the canvas so a stale typed baseline
169
+ // doesn't hang around behind a drawn signature (or vice versa).
170
+ // MUST be declared BEFORE the typed-repaint + drawn-restore
171
+ // effects: effects run in declaration order, and v0.13.0 had
172
+ // this clear declared LAST — on switching to typed mode with a
173
+ // prefilled name the repaint painted first and this then wiped
174
+ // it (QA 2026-07-17: Type tab showed an empty pad until the
175
+ // signer edited the name). Skips the mount run so a parent-held
176
+ // captured value isn't nulled on first render.
177
+ const prevModeRef = useRef(mode);
178
+ useEffect(() => {
179
+ if (prevModeRef.current === mode) return;
180
+ prevModeRef.current = mode;
181
+ const canvas = canvasRef.current;
182
+ if (!canvas) return;
183
+ const ctx = canvas.getContext("2d");
184
+ if (!ctx) return;
185
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
186
+ dirtyRef.current = false;
187
+ onChange(null);
188
+ // Only fire when mode itself changes.
189
+ // eslint-disable-next-line react-hooks/exhaustive-deps
190
+ }, [mode]);
191
+
168
192
  // Repaint on typedName / mode / fontReady change. Drawn mode
169
193
  // doesn't repaint imperatively — strokes are appended as the user
170
194
  // draws.
@@ -233,20 +257,6 @@ export const SignaturePad = forwardRef<SignaturePadHandle, SignaturePadProps>(
233
257
  // eslint-disable-next-line react-hooks/exhaustive-deps
234
258
  }, [mode]);
235
259
 
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
260
  const clear = useCallback(() => {
251
261
  const canvas = canvasRef.current;
252
262
  if (canvas) {
@@ -30,3 +30,5 @@ export { MentionCombobox, findActiveMentionStart, extractQuery } from "./Mention
30
30
  export type { MentionComboboxProps, MentionComboboxHandle } from "./MentionCombobox";
31
31
  export { SignaturePad } from "./SignaturePad";
32
32
  export type { SignaturePadProps, SignaturePadHandle } from "./SignaturePad";
33
+ export { CopyField } from "./CopyField";
34
+ export type { CopyFieldProps } from "./CopyField";