@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
|
@@ -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) {
|
package/src/primitives/index.ts
CHANGED
|
@@ -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";
|