@plannotator/ui 0.22.0 → 0.24.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/components/AnnotationPanel.tsx +68 -37
- package/components/AnnotationToolbar.tsx +1 -1
- package/components/BlockRenderer.tsx +1 -1
- package/components/EditorAnnotationCard.tsx +13 -10
- package/components/ImageAnnotator/index.tsx +1 -1
- package/components/OpenInAppButton.tsx +25 -29
- package/components/PopoutDialog.tsx +46 -32
- package/components/Popover.tsx +18 -12
- package/components/SearchableSelect.tsx +4 -7
- package/components/Tooltip.tsx +50 -20
- package/components/Viewer.tsx +23 -9
- package/components/blocks/CodeBlock.tsx +2 -2
- package/components/blocks/HtmlBlock.tsx +1 -1
- package/components/blocks/TableBlock.tsx +1 -1
- package/components/blocks/TablePopout.tsx +1 -1
- package/components/ui/badge.tsx +15 -10
- package/components/ui/button.tsx +10 -10
- package/components/ui/dialog.tsx +17 -17
- package/components/ui/dropdown-menu.tsx +80 -66
- package/components/ui/tabs.tsx +11 -11
- package/hooks/useAnnotationHighlighter.ts +44 -6
- package/hooks/useCodeAnnotationDraft.ts +0 -5
- package/hooks/usePinpoint.ts +2 -2
- package/package.json +5 -12
- package/styles.css +1 -1
- package/theme.css +17 -0
- package/utils/parser.ts +1 -1
- package/utils/sharing.ts +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { AnnotationType, type Annotation, type Block, type CodeAnnotation, type EditorAnnotation } from '../types';
|
|
3
3
|
import { isCurrentUser } from '../utils/identity';
|
|
4
4
|
import { ImageThumbnail } from './ImageThumbnail';
|
|
5
5
|
import { EditorAnnotationCard } from './EditorAnnotationCard';
|
|
@@ -73,12 +73,18 @@ interface PanelProps {
|
|
|
73
73
|
/** Committed direct edits to one or more documents. Rendered as pinned cards
|
|
74
74
|
* above the annotation timeline with expandable unified diffs. */
|
|
75
75
|
directEdits?: DirectEditsPanelItem[] | null;
|
|
76
|
+
/** Host slot rendered at the foot of each plan-annotation card (e.g. reply/
|
|
77
|
+
* resolve UI). The panel stays presentation-only; clicks inside the slot
|
|
78
|
+
* do not select the card. Default: nothing rendered. */
|
|
79
|
+
renderCardFooter?: (annotation: Annotation) => React.ReactNode;
|
|
80
|
+
/** Hide every mutation affordance (delete/edit buttons on all card kinds).
|
|
81
|
+
* Selection and scrolling still work. Default false — today's behavior. */
|
|
82
|
+
readOnly?: boolean;
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
79
86
|
isOpen,
|
|
80
87
|
annotations,
|
|
81
|
-
blocks,
|
|
82
88
|
onSelect,
|
|
83
89
|
onDelete,
|
|
84
90
|
onEdit,
|
|
@@ -97,6 +103,8 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
97
103
|
otherFileAnnotations,
|
|
98
104
|
onOtherFileAnnotationsClick,
|
|
99
105
|
directEdits = null,
|
|
106
|
+
renderCardFooter,
|
|
107
|
+
readOnly = false,
|
|
100
108
|
}) => {
|
|
101
109
|
const isMobile = useIsMobile();
|
|
102
110
|
const [copiedText, setCopiedText] = useState(false);
|
|
@@ -195,6 +203,8 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
195
203
|
onSelect={() => onSelect(entry.annotation.id)}
|
|
196
204
|
onDelete={() => onDelete(entry.annotation.id)}
|
|
197
205
|
onEdit={onEdit ? (updates: Partial<Annotation>) => onEdit(entry.annotation.id, updates) : undefined}
|
|
206
|
+
readOnly={readOnly}
|
|
207
|
+
footer={renderCardFooter?.(entry.annotation)}
|
|
198
208
|
/>
|
|
199
209
|
) : (
|
|
200
210
|
<CodeAnnotationCard
|
|
@@ -205,6 +215,7 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
205
215
|
onSelect={() => onSelectCodeAnnotation?.(entry.annotation.id)}
|
|
206
216
|
onDelete={() => onDeleteCodeAnnotation?.(entry.annotation.id)}
|
|
207
217
|
onEdit={onEditCodeAnnotation ? (updates: Partial<CodeAnnotation>) => onEditCodeAnnotation(entry.annotation.id, updates) : undefined}
|
|
218
|
+
readOnly={readOnly}
|
|
208
219
|
/>
|
|
209
220
|
)
|
|
210
221
|
))}
|
|
@@ -221,7 +232,7 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
221
232
|
<EditorAnnotationCard
|
|
222
233
|
key={ann.id}
|
|
223
234
|
annotation={ann}
|
|
224
|
-
onDelete={() => onDeleteEditorAnnotation?.(ann.id)}
|
|
235
|
+
onDelete={readOnly ? undefined : () => onDeleteEditorAnnotation?.(ann.id)}
|
|
225
236
|
/>
|
|
226
237
|
))}
|
|
227
238
|
</>
|
|
@@ -418,7 +429,9 @@ const AnnotationCard: React.FC<{
|
|
|
418
429
|
onSelect: () => void;
|
|
419
430
|
onDelete: () => void;
|
|
420
431
|
onEdit?: (updates: Partial<Annotation>) => void;
|
|
421
|
-
|
|
432
|
+
readOnly?: boolean;
|
|
433
|
+
footer?: React.ReactNode;
|
|
434
|
+
}> = ({ annotation, isSelected, isMe, onSelect, onDelete, onEdit, readOnly = false, footer }) => {
|
|
422
435
|
const [isEditing, setIsEditing] = useState(false);
|
|
423
436
|
const [editText, setEditText] = useState(annotation.text || '');
|
|
424
437
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
@@ -517,26 +530,28 @@ const AnnotationCard: React.FC<{
|
|
|
517
530
|
<span className="text-[10px] text-muted-foreground/50 truncate">
|
|
518
531
|
{annotation.author ? `${annotation.author}${isMe ? ' (me)' : ''} · ` : ''}{formatTimestamp(annotation.createdA)}
|
|
519
532
|
</span>
|
|
520
|
-
|
|
521
|
-
|
|
533
|
+
{!readOnly && (
|
|
534
|
+
<div className="ml-auto flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 [@media(hover:none)]:opacity-100">
|
|
535
|
+
{onEdit && annotation.type !== AnnotationType.DELETION && !isEditing && (
|
|
536
|
+
<button
|
|
537
|
+
type="button"
|
|
538
|
+
onClick={handleStartEdit}
|
|
539
|
+
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-foreground"
|
|
540
|
+
title="Edit annotation"
|
|
541
|
+
>
|
|
542
|
+
<PencilIcon />
|
|
543
|
+
</button>
|
|
544
|
+
)}
|
|
522
545
|
<button
|
|
523
546
|
type="button"
|
|
524
|
-
onClick={
|
|
525
|
-
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-
|
|
526
|
-
title="
|
|
547
|
+
onClick={(e: React.MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); onDelete(); }}
|
|
548
|
+
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-destructive"
|
|
549
|
+
title="Delete annotation"
|
|
527
550
|
>
|
|
528
|
-
<
|
|
551
|
+
<TrashCardIcon />
|
|
529
552
|
</button>
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
type="button"
|
|
533
|
-
onClick={(e: React.MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); onDelete(); }}
|
|
534
|
-
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-destructive"
|
|
535
|
-
title="Delete annotation"
|
|
536
|
-
>
|
|
537
|
-
<TrashCardIcon />
|
|
538
|
-
</button>
|
|
539
|
-
</div>
|
|
553
|
+
</div>
|
|
554
|
+
)}
|
|
540
555
|
</div>
|
|
541
556
|
|
|
542
557
|
{/* Global Comment - show text directly */}
|
|
@@ -585,6 +600,19 @@ const AnnotationCard: React.FC<{
|
|
|
585
600
|
))}
|
|
586
601
|
</div>
|
|
587
602
|
)}
|
|
603
|
+
|
|
604
|
+
{/* Host footer slot (reply/resolve UI etc.) — interactions inside it
|
|
605
|
+
must not toggle card selection. */}
|
|
606
|
+
{footer != null && footer !== false && (
|
|
607
|
+
<div
|
|
608
|
+
data-annotation-card-footer="true"
|
|
609
|
+
className="mt-2"
|
|
610
|
+
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
|
611
|
+
onKeyDown={(e: React.KeyboardEvent) => e.stopPropagation()}
|
|
612
|
+
>
|
|
613
|
+
{footer}
|
|
614
|
+
</div>
|
|
615
|
+
)}
|
|
588
616
|
</div>
|
|
589
617
|
);
|
|
590
618
|
};
|
|
@@ -596,7 +624,8 @@ const CodeAnnotationCard: React.FC<{
|
|
|
596
624
|
onSelect: () => void;
|
|
597
625
|
onDelete: () => void;
|
|
598
626
|
onEdit?: (updates: Partial<CodeAnnotation>) => void;
|
|
599
|
-
|
|
627
|
+
readOnly?: boolean;
|
|
628
|
+
}> = ({ annotation, isSelected, isMe, onSelect, onDelete, onEdit, readOnly = false }) => {
|
|
600
629
|
const [isEditing, setIsEditing] = useState(false);
|
|
601
630
|
const [editText, setEditText] = useState(annotation.text || '');
|
|
602
631
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
@@ -650,26 +679,28 @@ const CodeAnnotationCard: React.FC<{
|
|
|
650
679
|
<span className="text-[10px] text-muted-foreground/50 truncate">
|
|
651
680
|
{annotation.author ? `${annotation.author}${isMe ? ' (me)' : ''} · ` : ''}{formatTimestamp(annotation.createdAt)}
|
|
652
681
|
</span>
|
|
653
|
-
|
|
654
|
-
|
|
682
|
+
{!readOnly && (
|
|
683
|
+
<div className="ml-auto flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 [@media(hover:none)]:opacity-100">
|
|
684
|
+
{onEdit && !isEditing && (
|
|
685
|
+
<button
|
|
686
|
+
type="button"
|
|
687
|
+
onClick={(e) => { e.stopPropagation(); setIsEditing(true); }}
|
|
688
|
+
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-foreground"
|
|
689
|
+
title="Edit annotation"
|
|
690
|
+
>
|
|
691
|
+
<PencilIcon />
|
|
692
|
+
</button>
|
|
693
|
+
)}
|
|
655
694
|
<button
|
|
656
695
|
type="button"
|
|
657
|
-
onClick={(e) => { e.stopPropagation();
|
|
658
|
-
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-
|
|
659
|
-
title="
|
|
696
|
+
onClick={(e) => { e.stopPropagation(); onDelete(); }}
|
|
697
|
+
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-destructive"
|
|
698
|
+
title="Delete annotation"
|
|
660
699
|
>
|
|
661
|
-
<
|
|
700
|
+
<TrashCardIcon />
|
|
662
701
|
</button>
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
type="button"
|
|
666
|
-
onClick={(e) => { e.stopPropagation(); onDelete(); }}
|
|
667
|
-
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-destructive"
|
|
668
|
-
title="Delete annotation"
|
|
669
|
-
>
|
|
670
|
-
<TrashCardIcon />
|
|
671
|
-
</button>
|
|
672
|
-
</div>
|
|
702
|
+
</div>
|
|
703
|
+
)}
|
|
673
704
|
</div>
|
|
674
705
|
|
|
675
706
|
{/* File / line meta */}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect, useRef, useMemo
|
|
1
|
+
import React, { useState, useEffect, useRef, useMemo } from "react";
|
|
2
2
|
import { AnnotationType } from "../types";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
4
|
import { useDismissOnOutsideAndEscape } from "../hooks/useDismissOnOutsideAndEscape";
|
|
@@ -14,7 +14,8 @@ type EditorAnnotationVariant = 'plan' | 'code-review';
|
|
|
14
14
|
|
|
15
15
|
interface EditorAnnotationCardProps {
|
|
16
16
|
annotation: EditorAnnotation;
|
|
17
|
-
|
|
17
|
+
/** Omit to render the card read-only (no delete affordance). */
|
|
18
|
+
onDelete?: () => void;
|
|
18
19
|
variant?: EditorAnnotationVariant;
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -38,15 +39,17 @@ export const EditorAnnotationCard: React.FC<EditorAnnotationCardProps> = ({ anno
|
|
|
38
39
|
<span className="text-[10px] font-mono text-muted-foreground/50 truncate" title={annotation.filePath}>
|
|
39
40
|
{annotation.filePath}:{lineRange}
|
|
40
41
|
</span>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
{onDelete && (
|
|
43
|
+
<button
|
|
44
|
+
onClick={(e) => { e.stopPropagation(); onDelete(); }}
|
|
45
|
+
className="ml-auto relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] opacity-0 group-hover:opacity-100 [@media(hover:none)]:opacity-100 hover:text-destructive"
|
|
46
|
+
title="Delete annotation"
|
|
47
|
+
>
|
|
48
|
+
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
49
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
50
|
+
</svg>
|
|
51
|
+
</button>
|
|
52
|
+
)}
|
|
50
53
|
</div>
|
|
51
54
|
|
|
52
55
|
{/* Selected text */}
|
|
@@ -2,7 +2,7 @@ import React, { useState, useCallback, useEffect, useRef } from 'react';
|
|
|
2
2
|
import { Canvas } from './Canvas';
|
|
3
3
|
import { Toolbar } from './Toolbar';
|
|
4
4
|
import { renderStroke } from './utils';
|
|
5
|
-
import type { Point,
|
|
5
|
+
import type { Point, AnnotatorState } from './types';
|
|
6
6
|
import { DEFAULT_STATE } from './types';
|
|
7
7
|
|
|
8
8
|
interface ImageAnnotatorProps {
|
|
@@ -212,22 +212,24 @@ export const OpenInAppButton: React.FC<OpenInAppButtonProps> = ({
|
|
|
212
212
|
)}
|
|
213
213
|
{/* Chevron (with primary) or standalone overflow (copy-only). */}
|
|
214
214
|
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
|
|
215
|
-
<DropdownMenuTrigger
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
215
|
+
<DropdownMenuTrigger
|
|
216
|
+
render={
|
|
217
|
+
<button
|
|
218
|
+
type="button"
|
|
219
|
+
disabled={isDisabled}
|
|
220
|
+
className={`text-xs flex items-center py-1 transition-colors text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed ${
|
|
221
|
+
openable ? 'px-1 border-l border-border/50' : 'px-1.5'
|
|
222
|
+
}`}
|
|
223
|
+
title={openable ? 'Open in…' : 'File actions'}
|
|
224
|
+
aria-label={openable ? 'Choose app to open in' : 'File actions'}
|
|
225
|
+
/>
|
|
226
|
+
}
|
|
227
|
+
>
|
|
228
|
+
{openable ? (
|
|
229
|
+
<ChevronDown className="w-3 h-3" />
|
|
230
|
+
) : (
|
|
231
|
+
<MoreHorizontal className="w-3.5 h-3.5" />
|
|
232
|
+
)}
|
|
231
233
|
</DropdownMenuTrigger>
|
|
232
234
|
<DropdownMenuContent
|
|
233
235
|
align="end"
|
|
@@ -235,7 +237,7 @@ export const OpenInAppButton: React.FC<OpenInAppButtonProps> = ({
|
|
|
235
237
|
className="min-w-[12rem]"
|
|
236
238
|
// Don't snap focus (and its focus ring) back onto the trigger when
|
|
237
239
|
// the menu closes — that left-edge bar reads as a stray artifact.
|
|
238
|
-
|
|
240
|
+
finalFocus={false}
|
|
239
241
|
>
|
|
240
242
|
{openable &&
|
|
241
243
|
grouped.map((group, gi) => (
|
|
@@ -244,10 +246,8 @@ export const OpenInAppButton: React.FC<OpenInAppButtonProps> = ({
|
|
|
244
246
|
{group.map((app) => (
|
|
245
247
|
<DropdownMenuItem
|
|
246
248
|
key={app.id}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
selectApp(app.id);
|
|
250
|
-
}}
|
|
249
|
+
closeOnClick={false}
|
|
250
|
+
onClick={() => selectApp(app.id)}
|
|
251
251
|
className="text-xs"
|
|
252
252
|
>
|
|
253
253
|
<AppIcon id={app.icon} className="w-4 h-4" />
|
|
@@ -260,10 +260,8 @@ export const OpenInAppButton: React.FC<OpenInAppButtonProps> = ({
|
|
|
260
260
|
{openable && (filePath || diffText) && <DropdownMenuSeparator />}
|
|
261
261
|
{filePath && (
|
|
262
262
|
<DropdownMenuItem
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
void copyText(filePath);
|
|
266
|
-
}}
|
|
263
|
+
closeOnClick={false}
|
|
264
|
+
onClick={() => void copyText(filePath)}
|
|
267
265
|
className="text-xs"
|
|
268
266
|
>
|
|
269
267
|
<Copy className="w-4 h-4" />
|
|
@@ -272,10 +270,8 @@ export const OpenInAppButton: React.FC<OpenInAppButtonProps> = ({
|
|
|
272
270
|
)}
|
|
273
271
|
{diffText && (
|
|
274
272
|
<DropdownMenuItem
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
void copyText(diffText);
|
|
278
|
-
}}
|
|
273
|
+
closeOnClick={false}
|
|
274
|
+
onClick={() => void copyText(diffText)}
|
|
279
275
|
className="text-xs"
|
|
280
276
|
>
|
|
281
277
|
<Copy className="w-4 h-4" />
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useCallback } from 'react';
|
|
2
|
-
import
|
|
2
|
+
import { Dialog } from '@base-ui/react/dialog';
|
|
3
3
|
|
|
4
4
|
const ANNOTATION_SELECTORS = [
|
|
5
5
|
'.annotation-toolbar',
|
|
@@ -14,7 +14,7 @@ interface PopoutDialogProps {
|
|
|
14
14
|
container?: HTMLElement | null;
|
|
15
15
|
className?: string;
|
|
16
16
|
children: React.ReactNode;
|
|
17
|
-
/** Extra data attributes to spread onto
|
|
17
|
+
/** Extra data attributes to spread onto the dialog popup */
|
|
18
18
|
dataAttributes?: Record<string, string>;
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -36,55 +36,69 @@ export const PopoutDialog: React.FC<PopoutDialogProps> = ({
|
|
|
36
36
|
[onClose],
|
|
37
37
|
);
|
|
38
38
|
|
|
39
|
-
const
|
|
40
|
-
(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
const handleOpenChange = useCallback(
|
|
40
|
+
(next: boolean, eventDetails: Dialog.Root.ChangeEventDetails) => {
|
|
41
|
+
if (next) return;
|
|
42
|
+
// Interactions with annotation UI (toolbars/popovers portal outside the
|
|
43
|
+
// dialog) must not dismiss the popout — the Radix version guarded this
|
|
44
|
+
// via onInteractOutside preventDefault; Base UI expresses it as
|
|
45
|
+
// cancelling the outside-press/focus-out close reason.
|
|
46
|
+
if (eventDetails.reason === 'outside-press' || eventDetails.reason === 'focus-out') {
|
|
47
|
+
// For focus events the annotation UI may sit on either side of the
|
|
48
|
+
// event (target = element losing focus, relatedTarget = gaining), so
|
|
49
|
+
// guard both.
|
|
50
|
+
const event = eventDetails.event as Event | undefined;
|
|
51
|
+
const candidates = [
|
|
52
|
+
event?.target as Element | null,
|
|
53
|
+
(event as FocusEvent | undefined)?.relatedTarget as Element | null,
|
|
54
|
+
];
|
|
55
|
+
if (
|
|
56
|
+
candidates.some(
|
|
57
|
+
(el) => el instanceof Element && ANNOTATION_SELECTORS.some((sel) => el.closest(sel)),
|
|
58
|
+
)
|
|
59
|
+
) {
|
|
60
|
+
eventDetails.cancel();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
45
63
|
}
|
|
64
|
+
onClose();
|
|
46
65
|
},
|
|
47
|
-
[],
|
|
66
|
+
[onClose],
|
|
48
67
|
);
|
|
49
68
|
|
|
50
69
|
return (
|
|
51
|
-
<Dialog.Root
|
|
52
|
-
open={open}
|
|
53
|
-
onOpenChange={(next) => { if (!next) onClose(); }}
|
|
54
|
-
modal={false}
|
|
55
|
-
>
|
|
70
|
+
<Dialog.Root open={open} onOpenChange={handleOpenChange} modal={false}>
|
|
56
71
|
<Dialog.Portal container={container ?? undefined}>
|
|
57
|
-
{/*
|
|
58
|
-
div for the
|
|
59
|
-
|
|
60
|
-
interactive. */}
|
|
72
|
+
{/* Non-modal dialogs render no library backdrop, so we use a plain
|
|
73
|
+
div for the dark scrim + blur while keeping annotation toolbars
|
|
74
|
+
(which portal outside the dialog) interactive. */}
|
|
61
75
|
<div
|
|
62
76
|
className="fixed inset-0 z-50 bg-black/50 backdrop-blur-[2px]"
|
|
63
77
|
onClick={handleBackdropClick}
|
|
64
78
|
aria-hidden="true"
|
|
65
79
|
/>
|
|
66
|
-
<Dialog.
|
|
80
|
+
<Dialog.Popup
|
|
67
81
|
className={`fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2 bg-background border border-border rounded-xl shadow-2xl flex flex-col overflow-hidden ${className ?? 'w-[calc(100vw-4rem)] max-w-[min(calc(100vw-4rem),1500px)] max-h-[calc(100vh-4rem)]'}`}
|
|
68
82
|
data-popout="true"
|
|
69
|
-
|
|
70
|
-
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
71
|
-
onInteractOutside={handleInteractOutside}
|
|
83
|
+
initialFocus={false}
|
|
72
84
|
{...dataAttributes}
|
|
73
85
|
>
|
|
74
86
|
<Dialog.Title className="sr-only">{title}</Dialog.Title>
|
|
75
|
-
<Dialog.Close
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
<Dialog.Close
|
|
88
|
+
render={
|
|
89
|
+
<button
|
|
90
|
+
className="absolute top-3 right-3 z-20 p-1.5 rounded-md text-muted-foreground/70 hover:bg-muted hover:text-foreground transition-colors"
|
|
91
|
+
aria-label="Close"
|
|
92
|
+
/>
|
|
93
|
+
}
|
|
94
|
+
>
|
|
95
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
96
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
97
|
+
</svg>
|
|
84
98
|
</Dialog.Close>
|
|
85
99
|
|
|
86
100
|
{children}
|
|
87
|
-
</Dialog.
|
|
101
|
+
</Dialog.Popup>
|
|
88
102
|
</Dialog.Portal>
|
|
89
103
|
</Dialog.Root>
|
|
90
104
|
);
|
package/components/Popover.tsx
CHANGED
|
@@ -1,27 +1,33 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Popover as PopoverPrimitive } from '@base-ui/react/popover';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
|
|
4
4
|
const Popover = PopoverPrimitive.Root;
|
|
5
5
|
const PopoverTrigger = PopoverPrimitive.Trigger;
|
|
6
|
-
const PopoverAnchor = PopoverPrimitive.Anchor;
|
|
7
6
|
|
|
8
7
|
const PopoverContent = React.forwardRef<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
HTMLDivElement,
|
|
9
|
+
PopoverPrimitive.Popup.Props &
|
|
10
|
+
Pick<PopoverPrimitive.Positioner.Props, 'side' | 'sideOffset' | 'align' | 'alignOffset'>
|
|
11
|
+
>(({ className, side, align = 'center', sideOffset = 6, alignOffset, children, ...props }, ref) => (
|
|
12
12
|
<PopoverPrimitive.Portal>
|
|
13
|
-
<PopoverPrimitive.
|
|
14
|
-
|
|
13
|
+
<PopoverPrimitive.Positioner
|
|
14
|
+
side={side}
|
|
15
15
|
align={align}
|
|
16
16
|
sideOffset={sideOffset}
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
alignOffset={alignOffset}
|
|
18
|
+
className="isolate z-[100]"
|
|
19
19
|
>
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
<PopoverPrimitive.Popup
|
|
21
|
+
ref={ref}
|
|
22
|
+
className={`bg-popover text-popover-foreground border border-border/50 rounded-lg shadow-xl z-[100] outline-none origin-[var(--transform-origin)] transition-[opacity,scale] duration-150 ease-out data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95 ${className ?? ''}`}
|
|
23
|
+
{...props}
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
</PopoverPrimitive.Popup>
|
|
27
|
+
</PopoverPrimitive.Positioner>
|
|
22
28
|
</PopoverPrimitive.Portal>
|
|
23
29
|
));
|
|
24
30
|
|
|
25
31
|
PopoverContent.displayName = 'PopoverContent';
|
|
26
32
|
|
|
27
|
-
export { Popover, PopoverTrigger, PopoverContent
|
|
33
|
+
export { Popover, PopoverTrigger, PopoverContent };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useMemo, useRef, useCallback, type ReactNode } from 'react';
|
|
1
|
+
import React, { useState, useMemo, useRef, useCallback, type ReactElement, type ReactNode } from 'react';
|
|
2
2
|
import { Popover, PopoverTrigger, PopoverContent } from './Popover';
|
|
3
3
|
|
|
4
4
|
interface SearchableSelectProps<T extends { id: string }> {
|
|
@@ -7,7 +7,7 @@ interface SearchableSelectProps<T extends { id: string }> {
|
|
|
7
7
|
onSelect: (item: T) => void;
|
|
8
8
|
filterFn: (item: T, query: string) => boolean;
|
|
9
9
|
renderItem: (item: T, state: { isSelected: boolean; isFocused: boolean }) => ReactNode;
|
|
10
|
-
renderTrigger: (state: { open: boolean }) =>
|
|
10
|
+
renderTrigger: (state: { open: boolean }) => ReactElement;
|
|
11
11
|
headerContent?: ReactNode;
|
|
12
12
|
placeholder?: string;
|
|
13
13
|
emptyMessage?: string;
|
|
@@ -85,14 +85,11 @@ export function SearchableSelect<T extends { id: string }>({
|
|
|
85
85
|
|
|
86
86
|
return (
|
|
87
87
|
<Popover open={open} onOpenChange={handleOpenChange}>
|
|
88
|
-
<PopoverTrigger
|
|
88
|
+
<PopoverTrigger render={renderTrigger({ open })} />
|
|
89
89
|
<PopoverContent
|
|
90
90
|
align={align}
|
|
91
91
|
className={`${width} p-0`}
|
|
92
|
-
|
|
93
|
-
e.preventDefault();
|
|
94
|
-
inputRef.current?.focus();
|
|
95
|
-
}}
|
|
92
|
+
initialFocus={inputRef}
|
|
96
93
|
>
|
|
97
94
|
{/* Search row */}
|
|
98
95
|
<div className="flex items-center gap-2 border-b border-border/50 px-3 py-2">
|
package/components/Tooltip.tsx
CHANGED
|
@@ -1,11 +1,39 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import
|
|
2
|
+
import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* TooltipProvider keeps the Radix-era prop names (`delayDuration`,
|
|
6
|
+
* `skipDelayDuration`, `disableHoverableContent`) as its public API and maps
|
|
7
|
+
* them onto Base UI's Provider (`delay`, `timeout`) — call sites in the apps
|
|
8
|
+
* stay unchanged. `disableHoverableContent` has no provider-level equivalent
|
|
9
|
+
* in Base UI (it moved to per-root `disableHoverablePopup`), so it is carried
|
|
10
|
+
* via context and applied to every Tooltip root under the provider.
|
|
11
|
+
*/
|
|
12
|
+
const DisableHoverablePopupContext = React.createContext(false);
|
|
13
|
+
|
|
14
|
+
interface TooltipProviderProps {
|
|
15
|
+
delayDuration?: number;
|
|
16
|
+
skipDelayDuration?: number;
|
|
17
|
+
disableHoverableContent?: boolean;
|
|
18
|
+
children?: React.ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const TooltipProvider: React.FC<TooltipProviderProps> = ({
|
|
22
|
+
delayDuration,
|
|
23
|
+
skipDelayDuration,
|
|
24
|
+
disableHoverableContent = false,
|
|
25
|
+
children,
|
|
26
|
+
}) => (
|
|
27
|
+
<BaseTooltip.Provider delay={delayDuration} timeout={skipDelayDuration}>
|
|
28
|
+
<DisableHoverablePopupContext.Provider value={disableHoverableContent}>
|
|
29
|
+
{children}
|
|
30
|
+
</DisableHoverablePopupContext.Provider>
|
|
31
|
+
</BaseTooltip.Provider>
|
|
32
|
+
);
|
|
5
33
|
|
|
6
34
|
interface TooltipProps {
|
|
7
35
|
content: React.ReactNode;
|
|
8
|
-
children: React.
|
|
36
|
+
children: React.ReactElement;
|
|
9
37
|
side?: 'top' | 'right' | 'bottom' | 'left';
|
|
10
38
|
align?: 'start' | 'center' | 'end';
|
|
11
39
|
delayDuration?: number;
|
|
@@ -26,20 +54,22 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
|
|
26
54
|
delayDuration,
|
|
27
55
|
sideOffset = 8,
|
|
28
56
|
wide = false,
|
|
29
|
-
}) =>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
<
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
align={align}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
57
|
+
}) => {
|
|
58
|
+
const disableHoverablePopup = React.useContext(DisableHoverablePopupContext);
|
|
59
|
+
return (
|
|
60
|
+
<BaseTooltip.Root disableHoverablePopup={disableHoverablePopup}>
|
|
61
|
+
<BaseTooltip.Trigger render={children} delay={delayDuration} />
|
|
62
|
+
<BaseTooltip.Portal>
|
|
63
|
+
<BaseTooltip.Positioner side={side} align={align} sideOffset={sideOffset} className="isolate z-50">
|
|
64
|
+
<BaseTooltip.Popup
|
|
65
|
+
className={`z-50 px-2 py-1 text-xs bg-popover text-popover-foreground border border-border rounded shadow-md origin-[var(--transform-origin)] transition-[opacity,scale] duration-150 ease-out data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95 ${
|
|
66
|
+
wide ? 'max-w-[260px] leading-snug whitespace-normal' : 'whitespace-nowrap'
|
|
67
|
+
}`}
|
|
68
|
+
>
|
|
69
|
+
{content}
|
|
70
|
+
</BaseTooltip.Popup>
|
|
71
|
+
</BaseTooltip.Positioner>
|
|
72
|
+
</BaseTooltip.Portal>
|
|
73
|
+
</BaseTooltip.Root>
|
|
74
|
+
);
|
|
75
|
+
};
|