@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosxai/ui",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
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
+ });
@@ -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";