@opengeni/react 0.34.1 → 0.34.3
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/dist/{chunk-A2WITZTI.js → chunk-4NMH6UJ3.js} +2 -2
- package/dist/{chunk-GZ4KR67U.js → chunk-ALA3UGWB.js} +2 -2
- package/dist/chunk-ATSTR5P3.js +138 -0
- package/dist/chunk-ATSTR5P3.js.map +1 -0
- package/dist/{chunk-KZRUYCL4.js → chunk-FT2TXNGZ.js} +5 -2
- package/dist/chunk-FT2TXNGZ.js.map +1 -0
- package/dist/{chunk-4QOQ2DSR.js → chunk-LAKLF4PA.js} +14 -1
- package/dist/chunk-LAKLF4PA.js.map +1 -0
- package/dist/{chunk-TCCE5ZU5.js → chunk-MEGI4FS6.js} +38 -9
- package/dist/chunk-MEGI4FS6.js.map +1 -0
- package/dist/{chunk-MS3FBZ6A.js → chunk-PVW56EVR.js} +105 -62
- package/dist/chunk-PVW56EVR.js.map +1 -0
- package/dist/{chunk-L7R5RK6A.js → chunk-QRYJCWGD.js} +1199 -897
- package/dist/chunk-QRYJCWGD.js.map +1 -0
- package/dist/components/chat-composer.d.ts +2 -1
- package/dist/components/composer.d.ts +8 -2
- package/dist/components/copy-button.d.ts +4 -2
- package/dist/components/tip-follow.d.ts +3 -0
- package/dist/composer.d.ts +2 -1
- package/dist/composer.js +4 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +106 -101
- package/dist/index.js.map +1 -1
- package/dist/lib/format.d.ts +2 -0
- package/dist/machines.js +2 -2
- package/dist/session-ui.js +4 -3
- package/dist/session.js +4 -4
- package/dist/timeline/index.d.ts +1 -1
- package/dist/timeline/parsers.d.ts +5 -0
- package/dist/timeline/projection.d.ts +1 -12
- package/dist/timeline/tool-display-name.d.ts +17 -6
- package/dist/workstream-control-event.d.ts +6 -0
- package/package.json +1 -1
- package/src/components/chat-composer.tsx +1 -1
- package/src/components/composer-transcription-control.tsx +70 -57
- package/src/components/composer.tsx +197 -126
- package/src/components/copy-button.tsx +27 -8
- package/src/components/message-timeline.tsx +236 -39
- package/src/components/tip-follow.ts +15 -0
- package/src/composer.ts +1 -1
- package/src/hooks/use-composer.ts +7 -1
- package/src/hooks/use-workspace-capture.ts +6 -0
- package/src/index.ts +2 -1
- package/src/lib/format.ts +14 -0
- package/src/timeline/index.ts +2 -0
- package/src/timeline/parsers.ts +18 -1
- package/src/timeline/projection.ts +50 -20
- package/src/timeline/registry.ts +15 -2
- package/src/timeline/shared.tsx +30 -28
- package/src/timeline/tool-display-name.ts +30 -11
- package/src/timeline/tool-renderers.tsx +347 -16
- package/src/timeline/turn-summary.tsx +5 -2
- package/src/workstream-control-event.ts +6 -0
- package/dist/chunk-4QOQ2DSR.js.map +0 -1
- package/dist/chunk-KZRUYCL4.js.map +0 -1
- package/dist/chunk-L7R5RK6A.js.map +0 -1
- package/dist/chunk-MS3FBZ6A.js.map +0 -1
- package/dist/chunk-TCCE5ZU5.js.map +0 -1
- /package/dist/{chunk-A2WITZTI.js.map → chunk-4NMH6UJ3.js.map} +0 -0
- /package/dist/{chunk-GZ4KR67U.js.map → chunk-ALA3UGWB.js.map} +0 -0
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "lucide-react";
|
|
14
14
|
import { AnimatePresence, motion } from "motion/react";
|
|
15
15
|
import {
|
|
16
|
+
cloneElement,
|
|
16
17
|
createContext,
|
|
17
18
|
forwardRef,
|
|
18
19
|
useCallback,
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
type ComponentPropsWithoutRef,
|
|
29
30
|
type DragEvent,
|
|
30
31
|
type KeyboardEvent,
|
|
32
|
+
type ReactElement,
|
|
31
33
|
type ReactNode,
|
|
32
34
|
type Ref,
|
|
33
35
|
type RefObject,
|
|
@@ -45,10 +47,23 @@ import {
|
|
|
45
47
|
import { cn } from "../lib/cn";
|
|
46
48
|
import { composerSubmissionErrorMessage, formatBytes, formatRelativeTime } from "../lib/format";
|
|
47
49
|
import type { PickerModelRow } from "../model-policy";
|
|
50
|
+
import { OPEN_WORKSTREAM_CONTROL_EVENT } from "../workstream-control-event";
|
|
48
51
|
import { CommandPalette as CommandPaletteView } from "./command-palette";
|
|
49
52
|
import { ModelPicker as ModelPickerView } from "./model-picker";
|
|
53
|
+
import { TooltipProvider } from "./tooltip";
|
|
50
54
|
|
|
51
|
-
export
|
|
55
|
+
export { OPEN_WORKSTREAM_CONTROL_EVENT };
|
|
56
|
+
|
|
57
|
+
function ComposerTip({
|
|
58
|
+
tip,
|
|
59
|
+
children,
|
|
60
|
+
}: {
|
|
61
|
+
tip: string;
|
|
62
|
+
children: ReactElement<{ title?: string }>;
|
|
63
|
+
}) {
|
|
64
|
+
if (!tip) return children;
|
|
65
|
+
return cloneElement(children, { title: tip });
|
|
66
|
+
}
|
|
52
67
|
|
|
53
68
|
export type ComposerDelivery = Pick<
|
|
54
69
|
ComposerState,
|
|
@@ -225,6 +240,31 @@ export type UseChatComposerControllerOptions = {
|
|
|
225
240
|
messages?: Partial<ChatComposerMessages> | undefined;
|
|
226
241
|
};
|
|
227
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Autosize a composer textarea up to `maxPx` without the classic height→0
|
|
245
|
+
* measure collapse (that flash expands the timeline flex sibling and yanks
|
|
246
|
+
* tip-follow while a stream is pinned).
|
|
247
|
+
*/
|
|
248
|
+
export function applyComposerTextareaHeight(
|
|
249
|
+
textarea: HTMLTextAreaElement,
|
|
250
|
+
maxPx: number = 220,
|
|
251
|
+
): void {
|
|
252
|
+
// Grow path: content already overflows — raise height, no measure collapse.
|
|
253
|
+
if (textarea.scrollHeight > textarea.clientHeight + 1) {
|
|
254
|
+
textarea.style.height = `${Math.min(textarea.scrollHeight, maxPx)}px`;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// Shrink / steady: `auto` measures natural height without flashing empty.
|
|
258
|
+
const before = textarea.offsetHeight;
|
|
259
|
+
textarea.style.height = "auto";
|
|
260
|
+
const nextPx = Math.min(textarea.scrollHeight, maxPx);
|
|
261
|
+
if (Math.abs(nextPx - before) < 1) {
|
|
262
|
+
textarea.style.height = `${before}px`;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
textarea.style.height = `${nextPx}px`;
|
|
266
|
+
}
|
|
267
|
+
|
|
228
268
|
/**
|
|
229
269
|
* Headless interaction layer for a chat composer. It is the single owner of
|
|
230
270
|
* delivery guards, keyboard routing, command interception, drag/drop, focus,
|
|
@@ -347,13 +387,33 @@ export function useChatComposerController({
|
|
|
347
387
|
[],
|
|
348
388
|
);
|
|
349
389
|
|
|
390
|
+
// Autosize without collapsing the composer to 0 on every keystroke. The old
|
|
391
|
+
// height→0→content pattern briefly expanded the timeline scroller and yanked
|
|
392
|
+
// tip-follow while streams were live (pin/unpin flicker when typing fast).
|
|
393
|
+
const resizeInputRafRef = useRef<number | null>(null);
|
|
350
394
|
const resizeInput = useCallback(() => {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
395
|
+
if (resizeInputRafRef.current !== null) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
resizeInputRafRef.current = requestAnimationFrame(() => {
|
|
399
|
+
resizeInputRafRef.current = null;
|
|
400
|
+
const textarea = textareaRef.current;
|
|
401
|
+
if (!textarea) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
applyComposerTextareaHeight(textarea, 220);
|
|
405
|
+
});
|
|
355
406
|
}, []);
|
|
356
407
|
useEffect(() => resizeInput(), [delivery.value, resizeInput]);
|
|
408
|
+
useEffect(
|
|
409
|
+
() => () => {
|
|
410
|
+
if (resizeInputRafRef.current !== null) {
|
|
411
|
+
cancelAnimationFrame(resizeInputRafRef.current);
|
|
412
|
+
resizeInputRafRef.current = null;
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
[],
|
|
416
|
+
);
|
|
357
417
|
|
|
358
418
|
const handlers = useMemo(
|
|
359
419
|
() => ({
|
|
@@ -630,18 +690,23 @@ export const Root = forwardRef<HTMLDivElement, ComposerRootProps>(function Compo
|
|
|
630
690
|
{ controller, children, className, style, ...props },
|
|
631
691
|
forwardedRef,
|
|
632
692
|
) {
|
|
693
|
+
// Provider stays on Root so optional Radix tip surfaces (voice input) work.
|
|
694
|
+
// Ordinary composer actions use native `title` via ComposerTip to avoid
|
|
695
|
+
// pulling Popper into every tip hotspot.
|
|
633
696
|
return (
|
|
634
697
|
<ComposerContext.Provider value={controller}>
|
|
635
|
-
<
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
698
|
+
<TooltipProvider delayDuration={300}>
|
|
699
|
+
<div
|
|
700
|
+
{...props}
|
|
701
|
+
ref={mergeRefs(controller.rootRef, forwardedRef)}
|
|
702
|
+
data-og-composer-id={controller.id}
|
|
703
|
+
className={cn("og-root", className)}
|
|
704
|
+
style={{ paddingBottom: "env(safe-area-inset-bottom)", ...style }}
|
|
705
|
+
>
|
|
706
|
+
{children}
|
|
707
|
+
<ComposerAnnouncements />
|
|
708
|
+
</div>
|
|
709
|
+
</TooltipProvider>
|
|
645
710
|
</ComposerContext.Provider>
|
|
646
711
|
);
|
|
647
712
|
});
|
|
@@ -919,6 +984,7 @@ export const AttachButton = forwardRef<HTMLButtonElement, ComposerAttachButtonPr
|
|
|
919
984
|
) {
|
|
920
985
|
const controller = useComposerController();
|
|
921
986
|
if (!controller.attachments) return null;
|
|
987
|
+
const tip = title ?? controller.messages.attachFiles;
|
|
922
988
|
return (
|
|
923
989
|
<>
|
|
924
990
|
<input
|
|
@@ -929,23 +995,24 @@ export const AttachButton = forwardRef<HTMLButtonElement, ComposerAttachButtonPr
|
|
|
929
995
|
className="hidden"
|
|
930
996
|
onChange={controller.handleFileChange}
|
|
931
997
|
/>
|
|
932
|
-
<
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
998
|
+
<ComposerTip tip={tip}>
|
|
999
|
+
<button
|
|
1000
|
+
{...props}
|
|
1001
|
+
ref={ref}
|
|
1002
|
+
type="button"
|
|
1003
|
+
disabled={controller.disabled}
|
|
1004
|
+
onClick={() => controller.fileInputRef.current?.click()}
|
|
1005
|
+
aria-label={ariaLabel ?? tip}
|
|
1006
|
+
className={cn(
|
|
1007
|
+
"inline-flex size-8 items-center justify-center rounded-og-md",
|
|
1008
|
+
"text-og-fg-muted transition-colors duration-150 hover:bg-og-surface-2 hover:text-og-fg",
|
|
1009
|
+
"disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:size-11",
|
|
1010
|
+
className,
|
|
1011
|
+
)}
|
|
1012
|
+
>
|
|
1013
|
+
{children ?? <PaperclipIcon className="size-4" />}
|
|
1014
|
+
</button>
|
|
1015
|
+
</ComposerTip>
|
|
949
1016
|
</>
|
|
950
1017
|
);
|
|
951
1018
|
},
|
|
@@ -1000,30 +1067,32 @@ export const PauseButton = forwardRef<HTMLButtonElement, ComposerPauseButtonProp
|
|
|
1000
1067
|
const controller = useComposerController();
|
|
1001
1068
|
if (!controller.effectiveControl || controller.paused || !controller.hasControl) return null;
|
|
1002
1069
|
const busy = controller.pausing || controller.resuming;
|
|
1070
|
+
const tip = title ?? controller.messages.pauseTitle;
|
|
1003
1071
|
return (
|
|
1004
|
-
<
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1072
|
+
<ComposerTip tip={tip}>
|
|
1073
|
+
<button
|
|
1074
|
+
{...props}
|
|
1075
|
+
ref={mergeRefs(controller.pauseButtonRef, ref)}
|
|
1076
|
+
type="button"
|
|
1077
|
+
onClick={() => void controller.pause()}
|
|
1078
|
+
disabled={busy}
|
|
1079
|
+
aria-label={ariaLabel ?? controller.messages.pauseAriaLabel}
|
|
1080
|
+
className={cn(
|
|
1081
|
+
"inline-flex size-8 items-center justify-center rounded-og-md border border-og-border pointer-coarse:size-11",
|
|
1082
|
+
"bg-og-surface-2 text-og-fg-muted transition-colors duration-150",
|
|
1083
|
+
"hover:border-og-status-waiting/50 hover:text-og-status-waiting",
|
|
1084
|
+
"disabled:opacity-50",
|
|
1085
|
+
className,
|
|
1086
|
+
)}
|
|
1087
|
+
>
|
|
1088
|
+
{children ??
|
|
1089
|
+
(busy ? (
|
|
1090
|
+
<LoaderCircleIcon className="size-3.5 animate-og-spin" />
|
|
1091
|
+
) : (
|
|
1092
|
+
<PauseIcon className="size-3.5 fill-current" />
|
|
1093
|
+
))}
|
|
1094
|
+
</button>
|
|
1095
|
+
</ComposerTip>
|
|
1027
1096
|
);
|
|
1028
1097
|
},
|
|
1029
1098
|
);
|
|
@@ -1039,41 +1108,41 @@ export const SendButton = forwardRef<HTMLButtonElement, ComposerSendButtonProps>
|
|
|
1039
1108
|
ref,
|
|
1040
1109
|
) {
|
|
1041
1110
|
const controller = useComposerController();
|
|
1111
|
+
const tip =
|
|
1112
|
+
title ??
|
|
1113
|
+
(controller.paused ? controller.messages.sendAndResumeTitle : controller.messages.sendTitle);
|
|
1042
1114
|
return (
|
|
1043
|
-
<
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
(
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
<ArrowUpIcon className="size-4" />
|
|
1075
|
-
))}
|
|
1076
|
-
</button>
|
|
1115
|
+
<ComposerTip tip={tip}>
|
|
1116
|
+
<button
|
|
1117
|
+
{...props}
|
|
1118
|
+
ref={ref}
|
|
1119
|
+
type="button"
|
|
1120
|
+
onClick={() => void controller.submit("queue")}
|
|
1121
|
+
disabled={!controller.canSubmit}
|
|
1122
|
+
data-og-tip={tip}
|
|
1123
|
+
aria-label={
|
|
1124
|
+
ariaLabel ??
|
|
1125
|
+
(controller.paused
|
|
1126
|
+
? controller.messages.sendAndResumeAriaLabel
|
|
1127
|
+
: controller.messages.sendMessageAriaLabel)
|
|
1128
|
+
}
|
|
1129
|
+
className={cn(
|
|
1130
|
+
"inline-flex size-8 items-center justify-center rounded-og-md pointer-coarse:size-11",
|
|
1131
|
+
"bg-og-accent text-og-accent-fg shadow-og-sm",
|
|
1132
|
+
"transition-[background-color,transform,opacity] duration-150 ease-og-spring",
|
|
1133
|
+
"hover:bg-og-accent-strong active:scale-95",
|
|
1134
|
+
"disabled:cursor-not-allowed disabled:bg-og-surface-3 disabled:text-og-fg-subtle disabled:shadow-none",
|
|
1135
|
+
className,
|
|
1136
|
+
)}
|
|
1137
|
+
>
|
|
1138
|
+
{children ??
|
|
1139
|
+
(controller.sending ? (
|
|
1140
|
+
<LoaderCircleIcon className="size-4 animate-og-spin" />
|
|
1141
|
+
) : (
|
|
1142
|
+
<ArrowUpIcon className="size-4" />
|
|
1143
|
+
))}
|
|
1144
|
+
</button>
|
|
1145
|
+
</ComposerTip>
|
|
1077
1146
|
);
|
|
1078
1147
|
},
|
|
1079
1148
|
);
|
|
@@ -1275,27 +1344,30 @@ function WorkstreamPausedStrip({
|
|
|
1275
1344
|
{broaderOptions.length > 0 ? (
|
|
1276
1345
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
1277
1346
|
{broaderOptions.map((option) => (
|
|
1278
|
-
<
|
|
1347
|
+
<ComposerTip
|
|
1279
1348
|
key={`${option.scope}-${option.targetId ?? "selected"}`}
|
|
1280
|
-
|
|
1281
|
-
disabled={busy}
|
|
1282
|
-
title={option.impactCopy}
|
|
1283
|
-
onClick={() => onResumeOption(option)}
|
|
1284
|
-
className="rounded-og-md border border-og-border bg-og-surface-1 px-2 py-1 text-og-xs text-og-fg-muted hover:bg-og-surface-2 hover:text-og-fg pointer-coarse:min-h-10"
|
|
1349
|
+
tip={option.impactCopy}
|
|
1285
1350
|
>
|
|
1286
|
-
<
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1351
|
+
<button
|
|
1352
|
+
type="button"
|
|
1353
|
+
disabled={busy}
|
|
1354
|
+
onClick={() => onResumeOption(option)}
|
|
1355
|
+
className="rounded-og-md border border-og-border bg-og-surface-1 px-2 py-1 text-og-xs text-og-fg-muted hover:bg-og-surface-2 hover:text-og-fg pointer-coarse:min-h-10"
|
|
1356
|
+
>
|
|
1357
|
+
<span className="block font-medium">
|
|
1358
|
+
{option.scope === "workspace"
|
|
1359
|
+
? messages.resumeWorkspace
|
|
1360
|
+
: messages.resumeFromSession}
|
|
1361
|
+
</span>
|
|
1362
|
+
<span className="block text-[10px] text-og-fg-subtle">
|
|
1363
|
+
{option.selectedStateAfter === "active"
|
|
1364
|
+
? messages.sessionCanRun
|
|
1365
|
+
: messages.stillPausedBy(
|
|
1366
|
+
option.remainingPrimaryBlocker?.displayName ?? messages.narrowerPause,
|
|
1367
|
+
)}
|
|
1368
|
+
</span>
|
|
1369
|
+
</button>
|
|
1370
|
+
</ComposerTip>
|
|
1299
1371
|
))}
|
|
1300
1372
|
</div>
|
|
1301
1373
|
) : null}
|
|
@@ -1453,29 +1525,28 @@ function AttachmentChips({
|
|
|
1453
1525
|
)}
|
|
1454
1526
|
<div className="min-w-0 flex-1">
|
|
1455
1527
|
<div className="truncate font-medium text-og-fg">{attachment.name}</div>
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
"truncate text-og-xs"
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
{statusText}
|
|
1464
|
-
</div>
|
|
1528
|
+
{failed ? (
|
|
1529
|
+
<ComposerTip tip={statusText}>
|
|
1530
|
+
<div className="truncate text-og-xs text-og-status-failed">{statusText}</div>
|
|
1531
|
+
</ComposerTip>
|
|
1532
|
+
) : (
|
|
1533
|
+
<div className="truncate text-og-xs text-og-fg-subtle">{statusText}</div>
|
|
1534
|
+
)}
|
|
1465
1535
|
</div>
|
|
1466
1536
|
{attachment.status === "uploading" ? (
|
|
1467
1537
|
<LoaderCircleIcon className="size-3.5 shrink-0 animate-og-spin" />
|
|
1468
1538
|
) : null}
|
|
1469
1539
|
{failed && onRetry ? (
|
|
1470
|
-
<
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1540
|
+
<ComposerTip tip={messages.retryUpload}>
|
|
1541
|
+
<button
|
|
1542
|
+
type="button"
|
|
1543
|
+
onClick={() => onRetry(attachment.id)}
|
|
1544
|
+
className="shrink-0 rounded-og-xs p-1 text-og-fg-muted hover:bg-og-surface-1 hover:text-og-fg pointer-coarse:size-10"
|
|
1545
|
+
aria-label={messages.retryAttachment(attachment.name)}
|
|
1546
|
+
>
|
|
1547
|
+
<RotateCwIcon className="size-3.5" />
|
|
1548
|
+
</button>
|
|
1549
|
+
</ComposerTip>
|
|
1479
1550
|
) : null}
|
|
1480
1551
|
<button
|
|
1481
1552
|
type="button"
|
|
@@ -14,7 +14,7 @@ export type CopyButtonProps = {
|
|
|
14
14
|
className?: string | undefined;
|
|
15
15
|
/**
|
|
16
16
|
* `always` — visible control.
|
|
17
|
-
* `group-hover` — parent must use `group/copy`; fades in on hover/focus.
|
|
17
|
+
* `group-hover` — parent must use `group/copy`; fades in on hover/focus-visible.
|
|
18
18
|
*/
|
|
19
19
|
reveal?: "always" | "group-hover" | undefined;
|
|
20
20
|
};
|
|
@@ -30,6 +30,7 @@ export function CopyButton({
|
|
|
30
30
|
reveal = "group-hover",
|
|
31
31
|
}: CopyButtonProps) {
|
|
32
32
|
const [copied, setCopied] = useState(false);
|
|
33
|
+
const [tipOpen, setTipOpen] = useState(false);
|
|
33
34
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
34
35
|
|
|
35
36
|
useEffect(() => {
|
|
@@ -49,19 +50,34 @@ export function CopyButton({
|
|
|
49
50
|
return;
|
|
50
51
|
}
|
|
51
52
|
setCopied(true);
|
|
53
|
+
setTipOpen(true);
|
|
54
|
+
// Pointer activation focuses the button and leaves `group-focus-within`
|
|
55
|
+
// stuck after the cursor leaves — chrome never "unhovers". Keyboard
|
|
56
|
+
// activation (`detail === 0`) keeps focus for a11y.
|
|
57
|
+
if (event.detail > 0) {
|
|
58
|
+
event.currentTarget.blur();
|
|
59
|
+
}
|
|
52
60
|
if (timerRef.current !== null) {
|
|
53
61
|
clearTimeout(timerRef.current);
|
|
54
62
|
}
|
|
55
63
|
timerRef.current = setTimeout(() => {
|
|
56
64
|
timerRef.current = null;
|
|
57
65
|
setCopied(false);
|
|
66
|
+
setTipOpen(false);
|
|
58
67
|
}, COPIED_MS);
|
|
59
68
|
};
|
|
60
69
|
|
|
61
70
|
const tip = copied ? "Copied" : label;
|
|
62
71
|
|
|
63
72
|
return (
|
|
64
|
-
<Tooltip
|
|
73
|
+
<Tooltip
|
|
74
|
+
open={copied ? true : tipOpen}
|
|
75
|
+
onOpenChange={(next) => {
|
|
76
|
+
if (!copied) {
|
|
77
|
+
setTipOpen(next);
|
|
78
|
+
}
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
65
81
|
<TooltipTrigger asChild>
|
|
66
82
|
<button
|
|
67
83
|
type="button"
|
|
@@ -99,28 +115,31 @@ export function CopyHoverFrame({
|
|
|
99
115
|
copyText,
|
|
100
116
|
label,
|
|
101
117
|
align = "end",
|
|
118
|
+
trailing,
|
|
102
119
|
}: {
|
|
103
120
|
children: ReactNode;
|
|
104
121
|
className?: string | undefined;
|
|
105
122
|
copyText: string;
|
|
106
123
|
label: string;
|
|
107
124
|
align?: "start" | "end" | undefined;
|
|
125
|
+
/** Optional footer meta (e.g. sent/finished clock) — immediately after the copy control. */
|
|
126
|
+
trailing?: ReactNode | undefined;
|
|
108
127
|
}) {
|
|
109
128
|
if (copyText.trim().length === 0) {
|
|
110
129
|
return <div className={className}>{children}</div>;
|
|
111
130
|
}
|
|
112
131
|
return (
|
|
113
|
-
<div className={cn("group/copy
|
|
132
|
+
<div className={cn("group/copy", className)}>
|
|
114
133
|
{children}
|
|
134
|
+
{/* Sit under the body — top-right overlay collided with the first line. */}
|
|
115
135
|
<div
|
|
116
136
|
className={cn(
|
|
117
|
-
"
|
|
118
|
-
align === "end" ? "
|
|
137
|
+
"mt-1 flex h-7 items-center gap-1.5",
|
|
138
|
+
align === "end" ? "justify-end" : "justify-start",
|
|
119
139
|
)}
|
|
120
140
|
>
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
</div>
|
|
141
|
+
<CopyButton text={copyText} label={label} reveal="group-hover" />
|
|
142
|
+
{trailing}
|
|
124
143
|
</div>
|
|
125
144
|
</div>
|
|
126
145
|
);
|