@ui-annotate/react-vite 0.1.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.
Files changed (100) hide show
  1. package/dist/code-open.d.ts +17 -0
  2. package/dist/code-open.js +82 -0
  3. package/dist/index.d.ts +8 -0
  4. package/dist/index.js +4 -0
  5. package/dist/inspector-transform.d.ts +59 -0
  6. package/dist/inspector-transform.js +218 -0
  7. package/dist/protocol/constants.d.ts +7 -0
  8. package/dist/protocol/constants.js +31 -0
  9. package/dist/protocol/ids.d.ts +3 -0
  10. package/dist/protocol/ids.js +15 -0
  11. package/dist/protocol/index.d.ts +4 -0
  12. package/dist/protocol/index.js +4 -0
  13. package/dist/protocol/paths.d.ts +3 -0
  14. package/dist/protocol/paths.js +36 -0
  15. package/dist/protocol/task-model.d.ts +35 -0
  16. package/dist/protocol/task-model.js +68 -0
  17. package/dist/runtime/app/AnnotateToolbar.d.ts +9 -0
  18. package/dist/runtime/app/AnnotateToolbar.js +14 -0
  19. package/dist/runtime/app/AnnotateWindows.d.ts +28 -0
  20. package/dist/runtime/app/AnnotateWindows.js +8 -0
  21. package/dist/runtime/app/UiAnnotate.d.ts +5 -0
  22. package/dist/runtime/app/UiAnnotate.js +540 -0
  23. package/dist/runtime/index.d.ts +5 -0
  24. package/dist/runtime/index.js +4 -0
  25. package/dist/runtime/inspector/target-inspection.d.ts +10 -0
  26. package/dist/runtime/inspector/target-inspection.js +337 -0
  27. package/dist/runtime/layout/annotate-storage.d.ts +9 -0
  28. package/dist/runtime/layout/annotate-storage.js +134 -0
  29. package/dist/runtime/layout/use-annotate-layout.d.ts +17 -0
  30. package/dist/runtime/layout/use-annotate-layout.js +147 -0
  31. package/dist/runtime/overlay/SelectionOverlay.d.ts +7 -0
  32. package/dist/runtime/overlay/SelectionOverlay.js +95 -0
  33. package/dist/runtime/shared/annotate-constants.d.ts +13 -0
  34. package/dist/runtime/shared/annotate-constants.js +13 -0
  35. package/dist/runtime/shared/annotate-types.d.ts +36 -0
  36. package/dist/runtime/shared/annotate-types.js +1 -0
  37. package/dist/runtime/shared/clipboard.d.ts +1 -0
  38. package/dist/runtime/shared/clipboard.js +33 -0
  39. package/dist/runtime/style.css +206 -0
  40. package/dist/runtime/task/annotate-task.d.ts +16 -0
  41. package/dist/runtime/task/annotate-task.js +85 -0
  42. package/dist/runtime/task/use-annotate-task.d.ts +16 -0
  43. package/dist/runtime/task/use-annotate-task.js +115 -0
  44. package/dist/runtime/windows/AnnotateSettingsWindow.d.ts +6 -0
  45. package/dist/runtime/windows/AnnotateSettingsWindow.js +5 -0
  46. package/dist/runtime/windows/AnnotateWindow.d.ts +21 -0
  47. package/dist/runtime/windows/AnnotateWindow.js +83 -0
  48. package/dist/runtime/windows/AnnotateWindowFrame.d.ts +26 -0
  49. package/dist/runtime/windows/AnnotateWindowFrame.js +56 -0
  50. package/dist/runtime/windows/TargetTraceTree.d.ts +12 -0
  51. package/dist/runtime/windows/TargetTraceTree.js +163 -0
  52. package/dist/runtime/windows/window-shared.d.ts +14 -0
  53. package/dist/runtime/windows/window-shared.js +41 -0
  54. package/dist/task-api.d.ts +15 -0
  55. package/dist/task-api.js +239 -0
  56. package/dist/ui/components/accordion.d.ts +7 -0
  57. package/dist/ui/components/accordion.js +18 -0
  58. package/dist/ui/components/alert-dialog.d.ts +18 -0
  59. package/dist/ui/components/alert-dialog.js +41 -0
  60. package/dist/ui/components/alert.d.ts +9 -0
  61. package/dist/ui/components/alert.js +24 -0
  62. package/dist/ui/components/badge.d.ts +9 -0
  63. package/dist/ui/components/badge.js +24 -0
  64. package/dist/ui/components/breadcrumb.d.ts +11 -0
  65. package/dist/ui/components/breadcrumb.js +27 -0
  66. package/dist/ui/components/button.d.ts +10 -0
  67. package/dist/ui/components/button.js +31 -0
  68. package/dist/ui/components/card.d.ts +9 -0
  69. package/dist/ui/components/card.js +24 -0
  70. package/dist/ui/components/dropdown-menu.d.ts +11 -0
  71. package/dist/ui/components/dropdown-menu.js +21 -0
  72. package/dist/ui/components/input.d.ts +3 -0
  73. package/dist/ui/components/input.js +6 -0
  74. package/dist/ui/components/scroll-area.d.ts +5 -0
  75. package/dist/ui/components/scroll-area.js +12 -0
  76. package/dist/ui/components/separator.d.ts +4 -0
  77. package/dist/ui/components/separator.js +8 -0
  78. package/dist/ui/components/switch.d.ts +6 -0
  79. package/dist/ui/components/switch.js +7 -0
  80. package/dist/ui/components/table.d.ts +10 -0
  81. package/dist/ui/components/table.js +27 -0
  82. package/dist/ui/components/tabs.d.ts +11 -0
  83. package/dist/ui/components/tabs.js +28 -0
  84. package/dist/ui/components/textarea.d.ts +3 -0
  85. package/dist/ui/components/textarea.js +6 -0
  86. package/dist/ui/components/toggle-group.d.ts +9 -0
  87. package/dist/ui/components/toggle-group.js +22 -0
  88. package/dist/ui/components/toggle.d.ts +9 -0
  89. package/dist/ui/components/toggle.js +25 -0
  90. package/dist/ui/components/tooltip.d.ts +7 -0
  91. package/dist/ui/components/tooltip.js +18 -0
  92. package/dist/ui/index.d.ts +2 -0
  93. package/dist/ui/index.js +2 -0
  94. package/dist/ui/lib/utils.d.ts +2 -0
  95. package/dist/ui/lib/utils.js +5 -0
  96. package/dist/ui/portal/portal-container.d.ts +13 -0
  97. package/dist/ui/portal/portal-container.js +12 -0
  98. package/dist/ui-annotate-plugin.d.ts +28 -0
  99. package/dist/ui-annotate-plugin.js +227 -0
  100. package/package.json +55 -0
@@ -0,0 +1,83 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
3
+ import { CheckIcon, CopyIcon, PlusIcon, RotateCcwIcon, Trash2Icon, XIcon } from "lucide-react";
4
+ import { ANNOTATE_TASK_FILE_NAME } from "../../protocol/constants.js";
5
+ import { Button } from "../../ui/components/button.js";
6
+ import { ScrollArea } from "../../ui/components/scroll-area.js";
7
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/components/tabs.js";
8
+ import { Textarea } from "../../ui/components/textarea.js";
9
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/components/tooltip.js";
10
+ import { cn } from "../../ui/lib/utils.js";
11
+ import { writeClipboardText } from "../shared/clipboard.js";
12
+ import { TargetTraceTree } from "./TargetTraceTree.js";
13
+ import { EmptyState, ErrorState, SaveStatusText } from "./window-shared.js";
14
+ export function AnnotateWindow({ activeTargetId, saveError, saveStatus, targets, taskError, taskStatus, getTraceNodeElement, onActiveTargetChange, onAddComment, onChangeComment, onDeleteComment, onDeleteTarget, onHoverTraceNode, onOpenPath, onResetTargets }) {
15
+ const [editingComment, setEditingComment] = useState(null);
16
+ const selectedTarget = targets.find((target) => target.id === activeTargetId) ?? targets[0] ?? null;
17
+ if (taskStatus === "loading") {
18
+ return _jsx(EmptyState, { children: "Loading annotate task." });
19
+ }
20
+ if (taskStatus === "error") {
21
+ return _jsx(ErrorState, { children: taskError });
22
+ }
23
+ return (_jsxs("div", { className: "flex h-full min-w-0 flex-col gap-3 overflow-hidden bg-background", children: [_jsx(AnnotateTaskPathInfo, { path: ANNOTATE_TASK_FILE_NAME, targetCount: targets.length, onResetTargets: onResetTargets }), selectedTarget ? (_jsxs(Tabs, { className: "min-h-0 flex-1 overflow-hidden", value: selectedTarget.id, onValueChange: onActiveTargetChange, children: [_jsx("div", { className: "shrink-0 border-b border-border/50 px-3 pb-2", children: _jsx(TabsList, { className: "w-full grid", style: { gridTemplateColumns: `repeat(${targets.length}, 1fr)` }, children: targets.map((target) => (_jsx(TabsTrigger, { value: target.id, children: target.id }, target.id))) }) }), targets.map((target) => (_jsx(TabsContent, { value: target.id, className: "min-h-0 data-[state=active]:flex data-[state=active]:flex-1 data-[state=active]:flex-col", children: _jsx(ScrollArea, { className: "min-h-0 flex-1", children: _jsxs("div", { className: "flex flex-col gap-4 p-3", children: [_jsx(TargetComments, { editingComment: editingComment, target: target, onAddComment: () => {
24
+ setEditingComment({ targetId: target.id, index: target.comments.length });
25
+ onAddComment(target.id);
26
+ }, onBlurComment: () => setEditingComment(null), onChangeComment: (index, value) => onChangeComment(target.id, index, value), onDeleteComment: (index) => onDeleteComment(target.id, index), onEditComment: (index) => setEditingComment({ targetId: target.id, index }) }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("span", { className: "text-xs font-semibold uppercase text-muted-foreground", children: "Trace" }), _jsx(TargetTraceTree, { target: target, getTraceNodeElement: getTraceNodeElement(target.id), onHoverTraceNode: (node) => onHoverTraceNode(target.id, node), onOpenPath: onOpenPath })] }), _jsx("div", { className: "pt-2 pb-4", children: _jsxs(Button, { type: "button", variant: "secondary", className: "w-full", onClick: () => onDeleteTarget(target.id), children: [_jsx(Trash2Icon, { "data-icon": "inline-start", "aria-hidden": "true" }), "Delete target"] }) })] }) }) }, target.id)))] })) : (_jsx("div", { className: "min-h-0 flex-1", children: _jsx(EmptyState, { children: "Click a component in annotate mode to add a target." }) })), _jsxs("div", { className: "shrink-0 px-3 pb-3", children: [_jsx(SaveStatusText, { status: saveStatus }), saveError ? _jsx("div", { className: "mt-2", children: _jsx(ErrorState, { children: saveError }) }) : null] })] }));
27
+ }
28
+ function AnnotateTaskPathInfo({ path, targetCount, onResetTargets }) {
29
+ const [copied, setCopied] = useState(false);
30
+ const resetCopiedTimerRef = useRef(null);
31
+ useEffect(() => {
32
+ return () => {
33
+ if (resetCopiedTimerRef.current !== null) {
34
+ window.clearTimeout(resetCopiedTimerRef.current);
35
+ }
36
+ };
37
+ }, []);
38
+ async function handleCopyPath() {
39
+ if (!(await writeClipboardText(path))) {
40
+ return;
41
+ }
42
+ setCopied(true);
43
+ if (resetCopiedTimerRef.current !== null) {
44
+ window.clearTimeout(resetCopiedTimerRef.current);
45
+ }
46
+ resetCopiedTimerRef.current = window.setTimeout(() => {
47
+ setCopied(false);
48
+ resetCopiedTimerRef.current = null;
49
+ }, 1200);
50
+ }
51
+ return (_jsx("div", { className: "shrink-0 border-b border-border/50 px-3 py-2", children: _jsxs("div", { className: "flex min-w-0 items-center gap-2 text-xs", children: [_jsx("span", { className: "shrink-0 font-semibold uppercase text-muted-foreground", children: "Task file" }), _jsx("code", { className: "min-w-0 flex-1 truncate rounded bg-muted/60 px-2 py-1 font-mono text-foreground", children: path }), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-7 shrink-0", "aria-label": "Copy annotate task path", onClick: handleCopyPath, children: copied ? (_jsx(CheckIcon, { "data-icon": "inline-start", "aria-hidden": "true" })) : (_jsx(CopyIcon, { "data-icon": "inline-start", "aria-hidden": "true" })) }) }), _jsx(TooltipContent, { side: "bottom", children: copied ? "Copied" : "Copy path" })] }), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-7 shrink-0", "aria-label": "Reset annotate task targets", disabled: targetCount === 0, onClick: onResetTargets, children: _jsx(RotateCcwIcon, { "data-icon": "inline-start", "aria-hidden": "true" }) }) }), _jsx(TooltipContent, { side: "bottom", children: "Reset targets" })] })] }) }));
52
+ }
53
+ function TargetComments({ editingComment, target, onAddComment, onBlurComment, onChangeComment, onDeleteComment, onEditComment }) {
54
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("span", { className: "text-xs font-semibold uppercase text-muted-foreground", children: "Comments" }), _jsxs("div", { className: "overflow-hidden rounded-md border bg-background", children: [target.comments.length === 0 ? (_jsx("div", { className: "flex min-h-11 items-center px-3 text-sm text-muted-foreground", children: "No comments yet." })) : (_jsx("div", { className: "divide-y", children: target.comments.map((comment, index) => {
55
+ const isEditing = editingComment?.targetId === target.id && editingComment.index === index;
56
+ return (_jsxs("div", { className: "flex min-w-0 items-start gap-1 px-2 py-1.5", children: [isEditing ? (_jsx(AutoGrowCommentTextarea, { value: comment, onBlur: onBlurComment, onChange: (value) => onChangeComment(index, value) })) : (_jsx("button", { type: "button", className: cn("min-w-0 flex-1 rounded-sm px-1.5 py-1 text-left text-sm hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none", "whitespace-pre-wrap break-words"), onClick: () => onEditComment(index), children: comment.trim() ? comment : "Empty comment" })), _jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-8 shrink-0", "aria-label": `Delete ${target.id} comment`, onClick: () => onDeleteComment(index), children: _jsx(XIcon, { "data-icon": "inline-start", "aria-hidden": "true" }) })] }, index));
57
+ }) })), _jsxs(Button, { className: "h-10 w-full rounded-none border-0 border-t border-dashed text-muted-foreground hover:text-foreground", type: "button", variant: "ghost", onClick: onAddComment, children: [_jsx(PlusIcon, { "data-icon": "inline-start", "aria-hidden": "true" }), "Add comment"] })] })] }));
58
+ }
59
+ function AutoGrowCommentTextarea({ value, onBlur, onChange }) {
60
+ const textareaRef = useRef(null);
61
+ useLayoutEffect(() => {
62
+ const textarea = textareaRef.current;
63
+ if (!textarea) {
64
+ return;
65
+ }
66
+ textarea.style.height = "0px";
67
+ textarea.style.height = `${textarea.scrollHeight}px`;
68
+ }, [value]);
69
+ function handleChange(event) {
70
+ onChange(event.target.value);
71
+ }
72
+ function handleBlur(_event) {
73
+ onBlur();
74
+ }
75
+ function handleKeyDown(event) {
76
+ if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) {
77
+ return;
78
+ }
79
+ event.preventDefault();
80
+ event.currentTarget.blur();
81
+ }
82
+ return (_jsx(Textarea, { ref: textareaRef, className: "min-h-8 flex-1 resize-none overflow-hidden py-1.5 text-sm", value: value, placeholder: "Describe the UI change for this target.", rows: 1, autoFocus: true, onBlur: handleBlur, onChange: handleChange, onKeyDown: handleKeyDown }));
83
+ }
@@ -0,0 +1,26 @@
1
+ import { type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
2
+ import { type LucideIcon } from "lucide-react";
3
+ export type AnnotateWindowFrameState = {
4
+ open: boolean;
5
+ x: number;
6
+ y: number;
7
+ width: number;
8
+ height: number;
9
+ zIndex: number;
10
+ };
11
+ export type AnnotateWindowResizeDirection = {
12
+ horizontal: -1 | 0 | 1;
13
+ vertical: -1 | 0 | 1;
14
+ };
15
+ export declare function AnnotateWindowFrame<TId extends string>({ children, floating, icon: Icon, id, label, state, title, onBeginMove, onBeginResize, onClose }: {
16
+ children: ReactNode;
17
+ floating?: boolean;
18
+ icon: LucideIcon;
19
+ id: TId;
20
+ label: string;
21
+ state: AnnotateWindowFrameState;
22
+ title: string;
23
+ onBeginMove(id: TId, event: ReactPointerEvent<HTMLElement>): void;
24
+ onBeginResize(id: TId, direction: AnnotateWindowResizeDirection, event: ReactPointerEvent<HTMLDivElement>): void;
25
+ onClose(id: TId): void;
26
+ }): ReactNode;
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { XIcon } from "lucide-react";
3
+ import { Button } from "../../ui/components/button.js";
4
+ import { cn } from "../../ui/lib/utils.js";
5
+ const resizeHandles = [
6
+ {
7
+ className: "top-0 left-3 right-3 h-2 cursor-ns-resize",
8
+ direction: { horizontal: 0, vertical: -1 }
9
+ },
10
+ {
11
+ className: "right-0 top-3 bottom-3 w-2 cursor-ew-resize",
12
+ direction: { horizontal: 1, vertical: 0 }
13
+ },
14
+ {
15
+ className: "bottom-0 left-3 right-3 h-2 cursor-ns-resize",
16
+ direction: { horizontal: 0, vertical: 1 }
17
+ },
18
+ {
19
+ className: "left-0 top-3 bottom-3 w-2 cursor-ew-resize",
20
+ direction: { horizontal: -1, vertical: 0 }
21
+ },
22
+ {
23
+ className: "top-0 left-0 size-3 cursor-nwse-resize",
24
+ direction: { horizontal: -1, vertical: -1 }
25
+ },
26
+ {
27
+ className: "top-0 right-0 size-3 cursor-nesw-resize",
28
+ direction: { horizontal: 1, vertical: -1 }
29
+ },
30
+ {
31
+ className: "bottom-0 right-0 size-3 cursor-nwse-resize",
32
+ direction: { horizontal: 1, vertical: 1 }
33
+ },
34
+ {
35
+ className: "bottom-0 left-0 size-3 cursor-nesw-resize",
36
+ direction: { horizontal: -1, vertical: 1 }
37
+ }
38
+ ];
39
+ export function AnnotateWindowFrame({ children, floating = true, icon: Icon, id, label, state, title, onBeginMove, onBeginResize, onClose }) {
40
+ const style = floating
41
+ ? {
42
+ left: state.x,
43
+ top: state.y,
44
+ width: state.width,
45
+ height: state.height,
46
+ zIndex: state.zIndex
47
+ }
48
+ : {
49
+ width: state.width,
50
+ height: state.height
51
+ };
52
+ return (_jsxs("section", { className: cn("pointer-events-auto flex flex-col overflow-hidden rounded-xl border bg-card/95 text-card-foreground shadow-xl backdrop-blur", floating ? "fixed z-20" : "relative max-w-full"), style: style, "aria-label": label, children: [_jsxs("header", { className: "flex min-h-9 cursor-move select-none items-center justify-between gap-2 border-b bg-muted/30 py-0.5 pl-2.5 pr-1.5", onPointerDown: (event) => onBeginMove(id, event), children: [_jsxs("div", { className: "flex min-w-0 items-center gap-1.5 text-xs font-semibold text-foreground", children: [_jsx(Icon, { size: 16, "aria-hidden": "true" }), _jsx("span", { className: "min-w-0 truncate", children: title })] }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-6" // 覆盖为 size-6(24px),不再被 size="icon" 默认的 size-9(36px)撑开
53
+ , "aria-label": `Close ${label}`, onPointerDown: (event) => event.stopPropagation(), onClick: () => onClose(id), children: _jsx(XIcon, { "data-icon": "inline-start", "aria-hidden": "true" }) })] }), _jsx("div", { className: "min-h-0 flex-1 p-3", children: children }), floating
54
+ ? resizeHandles.map((handle) => (_jsx("div", { className: cn("absolute touch-none", handle.className), "aria-hidden": "true", onPointerDown: (event) => onBeginResize(id, handle.direction, event) }, `${handle.direction.horizontal}:${handle.direction.vertical}`)))
55
+ : null] }));
56
+ }
@@ -0,0 +1,12 @@
1
+ import { type ReactNode } from "react";
2
+ import { type TraceFrame, type TraceNodeRef } from "../../protocol/task-model.js";
3
+ export type TargetTraceTreeTarget = {
4
+ trace?: TraceFrame[];
5
+ };
6
+ export type TraceNodeElementResolver = (node: TraceNodeRef) => Element | null;
7
+ export declare function TargetTraceTree({ target, getTraceNodeElement, onHoverTraceNode, onOpenPath }: {
8
+ target: TargetTraceTreeTarget;
9
+ getTraceNodeElement?: TraceNodeElementResolver;
10
+ onHoverTraceNode?(node: TraceNodeRef | null): void;
11
+ onOpenPath(path: string): void;
12
+ }): ReactNode;
@@ -0,0 +1,163 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { CheckIcon, ComponentIcon, CopyIcon, FileCodeIcon, MoreHorizontalIcon, SquareCodeIcon } from "lucide-react";
4
+ import { Button } from "../../ui/components/button.js";
5
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger } from "../../ui/components/dropdown-menu.js";
6
+ import { cn } from "../../ui/lib/utils.js";
7
+ import { writeClipboardText } from "../shared/clipboard.js";
8
+ export function TargetTraceTree({ target, getTraceNodeElement, onHoverTraceNode, onOpenPath }) {
9
+ const sections = useMemo(() => createStackSections(target), [target.trace]);
10
+ const modifierPressed = useModifierPressed();
11
+ if (sections.length === 0) {
12
+ return _jsx("p", { className: "text-sm text-muted-foreground", children: "No trace detected." });
13
+ }
14
+ return (_jsx("div", { className: "flex flex-col gap-2", "aria-label": "Target trace", children: sections.map((section) => (_jsx(StackSectionView, { section: section, getTraceNodeElement: getTraceNodeElement, onHoverTraceNode: onHoverTraceNode, onOpenPath: onOpenPath, modifierPressed: modifierPressed }, section.key))) }));
15
+ }
16
+ function StackSectionView({ section, getTraceNodeElement, onHoverTraceNode, onOpenPath, modifierPressed }) {
17
+ const fileNode = section.frameIndex === undefined
18
+ ? null
19
+ : {
20
+ kind: "file",
21
+ frameIndex: section.frameIndex
22
+ };
23
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "group/trace flex w-full min-w-0 items-start gap-1 rounded-md hover:bg-accent hover:text-accent-foreground", onPointerEnter: () => onHoverTraceNode?.(fileNode), onPointerLeave: () => onHoverTraceNode?.(null), children: [_jsxs("button", { type: "button", className: cn("flex min-w-0 flex-1 items-start gap-2 rounded-md px-2 pt-1.5 text-left focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none", modifierPressed ? "cursor-pointer" : "cursor-default"), title: `${section.relativePath}\nCtrl/Cmd click to open`, onClick: (event) => openOnModifierClick(event, section.path, onOpenPath), children: [_jsx(FileCodeIcon, { className: "mt-0.5 size-3.5 shrink-0 text-muted-foreground", "aria-hidden": "true" }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate font-mono text-xs font-semibold", children: section.fileName }), _jsx("span", { className: "block truncate text-[11px] leading-4 text-muted-foreground", children: section.relativePath })] })] }), fileNode ? (_jsx(TraceNodeActionsMenu, { ariaLabel: `Open ${section.fileName} trace actions`, copyText: section.copyText, contentCopyHeader: section.contentCopyHeader, node: fileNode, getTraceNodeElement: getTraceNodeElement })) : null] }), section.nodes.length > 0 ? (_jsx("ul", { className: "ml-3 border-l border-border/80", children: _jsx(StackNodeView, { nodes: section.nodes, index: 0, getTraceNodeElement: getTraceNodeElement, onHoverTraceNode: onHoverTraceNode, onOpenPath: onOpenPath, modifierPressed: modifierPressed }) })) : null] }));
24
+ }
25
+ function StackNodeView({ nodes, index, getTraceNodeElement, onHoverTraceNode, onOpenPath, modifierPressed }) {
26
+ const node = nodes[index];
27
+ if (!node) {
28
+ return null;
29
+ }
30
+ const Icon = node.kind === "native" ? SquareCodeIcon : ComponentIcon;
31
+ const callNode = node.frameIndex === undefined || node.callIndex === undefined
32
+ ? null
33
+ : {
34
+ kind: "call",
35
+ frameIndex: node.frameIndex,
36
+ callIndex: node.callIndex
37
+ };
38
+ return (_jsxs("li", { className: "flex flex-col", children: [_jsxs("div", { className: "group/trace flex min-w-0 items-center gap-1 rounded-md hover:bg-accent hover:text-accent-foreground", onPointerEnter: () => onHoverTraceNode?.(callNode), onPointerLeave: () => onHoverTraceNode?.(null), children: [_jsxs("button", { type: "button", className: cn("flex min-w-0 flex-1 items-center gap-1.5 rounded-md pt-1 pr-2 pl-0 text-left text-xs focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none", modifierPressed ? "cursor-pointer" : "cursor-default", node.kind === "native" ? "font-mono text-muted-foreground" : "font-medium text-foreground"), title: `${node.path}\nCtrl/Cmd click to open`, onClick: (event) => openOnModifierClick(event, node.path, onOpenPath), children: [_jsxs("span", { className: "relative h-5 w-[30px] shrink-0 text-muted-foreground", "aria-hidden": "true", children: [index > 0 ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "absolute top-0 bottom-1/2 left-0 border-l border-border/80" }), _jsx("span", { className: "absolute top-1/2 left-0 w-3 border-t border-border/80" })] })) : null, _jsx(Icon, { className: "absolute top-1/2 left-4 size-3.5 -translate-y-1/2 text-muted-foreground" })] }), _jsx("span", { className: "min-w-0 truncate", children: node.name })] }), callNode ? (_jsx(TraceNodeActionsMenu, { ariaLabel: `Open ${node.name} trace actions`, copyText: node.copyText, contentCopyHeader: node.contentCopyHeader, node: callNode, getTraceNodeElement: getTraceNodeElement })) : null] }), index < nodes.length - 1 ? (_jsx("ul", { className: "ml-[23px]", children: _jsx(StackNodeView, { nodes: nodes, index: index + 1, getTraceNodeElement: getTraceNodeElement, onHoverTraceNode: onHoverTraceNode, onOpenPath: onOpenPath, modifierPressed: modifierPressed }) })) : null] }));
39
+ }
40
+ function useModifierPressed() {
41
+ const [modifierPressed, setModifierPressed] = useState(false);
42
+ useEffect(() => {
43
+ function updateModifierState(event) {
44
+ setModifierPressed(event.ctrlKey || event.metaKey);
45
+ }
46
+ function resetModifierState() {
47
+ setModifierPressed(false);
48
+ }
49
+ window.addEventListener("keydown", updateModifierState);
50
+ window.addEventListener("keyup", updateModifierState);
51
+ window.addEventListener("blur", resetModifierState);
52
+ return () => {
53
+ window.removeEventListener("keydown", updateModifierState);
54
+ window.removeEventListener("keyup", updateModifierState);
55
+ window.removeEventListener("blur", resetModifierState);
56
+ };
57
+ }, []);
58
+ return modifierPressed;
59
+ }
60
+ function TraceNodeActionsMenu({ ariaLabel, copyText, contentCopyHeader, node, getTraceNodeElement }) {
61
+ const [copiedAction, setCopiedAction] = useState(null);
62
+ const [contentElementAvailable, setContentElementAvailable] = useState(true);
63
+ function handleOpenChange(open) {
64
+ if (open) {
65
+ setContentElementAvailable(Boolean(getTraceNodeElement?.(node)));
66
+ }
67
+ }
68
+ async function copyToClipboard(action, text) {
69
+ const copiedToClipboard = await writeClipboardText(text);
70
+ if (!copiedToClipboard) {
71
+ return;
72
+ }
73
+ setCopiedAction(action);
74
+ window.setTimeout(() => {
75
+ setCopiedAction((current) => current === action ? null : current);
76
+ }, 1200);
77
+ }
78
+ async function handleCopyPath(event) {
79
+ event.preventDefault();
80
+ event.stopPropagation();
81
+ await copyToClipboard("path", copyText);
82
+ }
83
+ async function handleCopyContent(event) {
84
+ event.preventDefault();
85
+ event.stopPropagation();
86
+ const element = getTraceNodeElement?.(node) ?? null;
87
+ if (!element) {
88
+ setContentElementAvailable(false);
89
+ return;
90
+ }
91
+ await copyToClipboard("content", formatVisibleContentCopyText(contentCopyHeader, getVisibleElementContent(element)));
92
+ }
93
+ return (_jsxs(DropdownMenu, { onOpenChange: handleOpenChange, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "pointer-events-none size-7 shrink-0 opacity-0 group-hover/trace:pointer-events-auto group-hover/trace:opacity-60 focus-visible:pointer-events-auto focus-visible:opacity-100 data-[state=open]:pointer-events-auto data-[state=open]:opacity-100", title: "More", "aria-label": ariaLabel, children: _jsx(MoreHorizontalIcon, { "data-icon": "inline-start", "aria-hidden": "true" }) }) }), _jsx(DropdownMenuContent, { align: "end", children: _jsxs(DropdownMenuGroup, { children: [_jsxs(DropdownMenuItem, { onSelect: handleCopyPath, children: [copiedAction === "path" ? (_jsx(CheckIcon, { "data-icon": "inline-start", "aria-hidden": "true" })) : (_jsx(CopyIcon, { "data-icon": "inline-start", "aria-hidden": "true" })), "Copy path"] }), _jsxs(DropdownMenuItem, { disabled: !contentElementAvailable, title: contentElementAvailable ? undefined : "No DOM match", onSelect: handleCopyContent, children: [copiedAction === "content" ? (_jsx(CheckIcon, { "data-icon": "inline-start", "aria-hidden": "true" })) : (_jsx(CopyIcon, { "data-icon": "inline-start", "aria-hidden": "true" })), "Copy InnerText"] })] }) })] }));
94
+ }
95
+ function getVisibleElementContent(element) {
96
+ if (element instanceof HTMLElement) {
97
+ return element.innerText.trim();
98
+ }
99
+ return (element.textContent ?? "").trim();
100
+ }
101
+ function formatVisibleContentCopyText(header, content) {
102
+ return `${header}\n\`\`\`interText\n${content}\n\`\`\``;
103
+ }
104
+ function openOnModifierClick(event, path, onOpenPath) {
105
+ if (!event.ctrlKey && !event.metaKey) {
106
+ return;
107
+ }
108
+ event.preventDefault();
109
+ event.stopPropagation();
110
+ onOpenPath(path);
111
+ }
112
+ function createStackSections(target) {
113
+ return target.trace ? createStackSectionsFromTrace(target.trace) : [];
114
+ }
115
+ function createStackSectionsFromTrace(trace) {
116
+ return trace
117
+ .map((frame, frameIndex) => {
118
+ const calls = frame.calls.map((call, callIndex) => {
119
+ const sourcePath = `${frame.file}:${call.line}`;
120
+ const path = `${sourcePath}:1`;
121
+ return {
122
+ key: `node:${frameIndex}:${callIndex}:${path}:${call.component}`,
123
+ name: call.component,
124
+ path,
125
+ copyText: formatTraceCallCopyText(sourcePath, call.component),
126
+ contentCopyHeader: formatTraceCallCopyText(sourcePath, call.component),
127
+ frameIndex,
128
+ callIndex,
129
+ kind: isNativeElementName(call.component) ? "native" : "component"
130
+ };
131
+ });
132
+ const firstCall = frame.calls[0];
133
+ const path = firstCall ? `${frame.file}:${firstCall.line}:1` : `${frame.file}:1:1`;
134
+ const copyText = formatTraceFileCopyText(frame.file);
135
+ const contentCopyHeader = firstCall
136
+ ? formatTraceCallCopyText(`${frame.file}:${firstCall.line}`, firstCall.component)
137
+ : frame.file;
138
+ return {
139
+ key: `section:${frameIndex}:${path}`,
140
+ fileName: getFileName(frame.file),
141
+ relativePath: frame.file,
142
+ path,
143
+ copyText,
144
+ contentCopyHeader,
145
+ frameIndex,
146
+ nodes: calls
147
+ };
148
+ })
149
+ .filter((section) => section.nodes.length > 0);
150
+ }
151
+ function getFileName(relativePath) {
152
+ return relativePath.split("/").filter(Boolean).at(-1) ?? relativePath;
153
+ }
154
+ function formatTraceFileCopyText(path) {
155
+ return path;
156
+ }
157
+ function formatTraceCallCopyText(path, component) {
158
+ return `${path} ${component}`;
159
+ }
160
+ function isNativeElementName(name) {
161
+ const leafName = name.split(".").at(-1) ?? name;
162
+ return /^[a-z][a-z0-9-]*$/.test(leafName);
163
+ }
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from "react";
2
+ export type TaskStatus = "idle" | "loading" | "ready" | "error";
3
+ export type SaveStatus = "idle" | "dirty" | "saving" | "saved" | "error";
4
+ export declare function EmptyState({ children }: {
5
+ children: ReactNode;
6
+ }): ReactNode;
7
+ export declare function ErrorState({ children }: {
8
+ children: ReactNode;
9
+ }): ReactNode;
10
+ export declare function SaveStatusText({ status }: {
11
+ status: SaveStatus;
12
+ }): ReactNode;
13
+ export declare function getSaveStatusClass(status: SaveStatus): string;
14
+ export declare function getSaveStatusText(status: SaveStatus): string;
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cn } from "../../ui/lib/utils.js";
3
+ export function EmptyState({ children }) {
4
+ return _jsx("div", { className: "p-4 text-sm leading-relaxed text-muted-foreground", children: children });
5
+ }
6
+ export function ErrorState({ children }) {
7
+ if (!children) {
8
+ return null;
9
+ }
10
+ return (_jsx("div", { className: "rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive", children: children }));
11
+ }
12
+ export function SaveStatusText({ status }) {
13
+ return (_jsx("span", { className: cn("shrink-0 text-xs font-medium", getSaveStatusClass(status)), children: getSaveStatusText(status) }));
14
+ }
15
+ export function getSaveStatusClass(status) {
16
+ switch (status) {
17
+ case "dirty":
18
+ case "saving":
19
+ return "text-amber-600";
20
+ case "saved":
21
+ return "text-primary";
22
+ case "error":
23
+ return "text-destructive";
24
+ default:
25
+ return "text-muted-foreground";
26
+ }
27
+ }
28
+ export function getSaveStatusText(status) {
29
+ switch (status) {
30
+ case "dirty":
31
+ return "Unsaved";
32
+ case "saving":
33
+ return "Saving";
34
+ case "saved":
35
+ return "Saved";
36
+ case "error":
37
+ return "Save failed";
38
+ default:
39
+ return "Idle";
40
+ }
41
+ }
@@ -0,0 +1,15 @@
1
+ import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
2
+ import { type AnnotateTaskFile } from "./protocol/index.js";
3
+ export type AnnotateApiServerOptions = {
4
+ root?: string;
5
+ };
6
+ export type AnnotateApiServerResult = {
7
+ server: ReturnType<typeof createServer>;
8
+ root: string;
9
+ };
10
+ export declare function resolveAnnotateTaskPath(root: string): string;
11
+ export declare function readAnnotateTask(root: string): Promise<AnnotateTaskFile>;
12
+ export declare function writeAnnotateTask(root: string, task: unknown): Promise<AnnotateTaskFile>;
13
+ export declare function validateAnnotateTaskFile(input: unknown): AnnotateTaskFile;
14
+ export declare function createAnnotateApiServer(options?: AnnotateApiServerOptions): AnnotateApiServerResult;
15
+ export declare function handleUiAnnotateRequest(root: string, request: IncomingMessage, response: ServerResponse): Promise<boolean>;