@cosxai/ui 0.12.0 → 0.13.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/package.json +1 -1
- package/src/primitives/CopyField.tsx +139 -0
- package/src/primitives/index.ts +2 -0
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
|
+
});
|
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";
|