@atercates/claude-deck 0.2.1
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/LICENSE +21 -0
- package/README.md +123 -0
- package/app/api/claude/hidden/route.ts +66 -0
- package/app/api/claude/projects/[name]/sessions/route.ts +71 -0
- package/app/api/claude/projects/route.ts +44 -0
- package/app/api/code-search/available/route.ts +12 -0
- package/app/api/code-search/route.ts +47 -0
- package/app/api/dev-servers/[id]/logs/route.ts +23 -0
- package/app/api/dev-servers/[id]/restart/route.ts +20 -0
- package/app/api/dev-servers/[id]/route.ts +51 -0
- package/app/api/dev-servers/[id]/stop/route.ts +20 -0
- package/app/api/dev-servers/detect/route.ts +39 -0
- package/app/api/dev-servers/route.ts +48 -0
- package/app/api/exec/route.ts +60 -0
- package/app/api/files/content/route.ts +76 -0
- package/app/api/files/route.ts +37 -0
- package/app/api/files/upload-temp/route.ts +41 -0
- package/app/api/git/check/route.ts +54 -0
- package/app/api/git/clone/route.ts +99 -0
- package/app/api/git/commit/route.ts +75 -0
- package/app/api/git/discard/route.ts +38 -0
- package/app/api/git/file-content/route.ts +64 -0
- package/app/api/git/history/[hash]/diff/route.ts +38 -0
- package/app/api/git/history/[hash]/route.ts +34 -0
- package/app/api/git/history/route.ts +27 -0
- package/app/api/git/multi-status/route.ts +46 -0
- package/app/api/git/pr/route.ts +164 -0
- package/app/api/git/push/route.ts +64 -0
- package/app/api/git/stage/route.ts +40 -0
- package/app/api/git/status/route.ts +51 -0
- package/app/api/git/unstage/route.ts +46 -0
- package/app/api/groups/[...path]/route.ts +136 -0
- package/app/api/groups/route.ts +93 -0
- package/app/api/orchestrate/spawn/route.ts +45 -0
- package/app/api/orchestrate/workers/[id]/route.ts +89 -0
- package/app/api/orchestrate/workers/route.ts +31 -0
- package/app/api/projects/[id]/detect/route.ts +27 -0
- package/app/api/projects/[id]/dev-servers/[dsId]/route.ts +66 -0
- package/app/api/projects/[id]/dev-servers/route.ts +51 -0
- package/app/api/projects/[id]/repositories/[repoId]/route.ts +67 -0
- package/app/api/projects/[id]/repositories/route.ts +74 -0
- package/app/api/projects/[id]/route.ts +108 -0
- package/app/api/projects/detect/route.ts +33 -0
- package/app/api/projects/route.ts +59 -0
- package/app/api/sessions/[id]/claude-session/route.ts +42 -0
- package/app/api/sessions/[id]/fork/route.ts +74 -0
- package/app/api/sessions/[id]/mcp-config/route.ts +34 -0
- package/app/api/sessions/[id]/messages/route.ts +60 -0
- package/app/api/sessions/[id]/pr/route.ts +188 -0
- package/app/api/sessions/[id]/preview/route.ts +42 -0
- package/app/api/sessions/[id]/route.ts +229 -0
- package/app/api/sessions/[id]/send-keys/route.ts +119 -0
- package/app/api/sessions/[id]/summarize/route.ts +331 -0
- package/app/api/sessions/init-script/route.ts +84 -0
- package/app/api/sessions/route.ts +209 -0
- package/app/api/sessions/status/route.ts +237 -0
- package/app/api/system/route.ts +9 -0
- package/app/api/tmux/kill-all/route.ts +57 -0
- package/app/api/tmux/rename/route.ts +30 -0
- package/app/globals.css +174 -0
- package/app/icon.svg +11 -0
- package/app/layout.tsx +122 -0
- package/app/page.tsx +629 -0
- package/components/ChatMessage.tsx +65 -0
- package/components/ChatView.tsx +276 -0
- package/components/ClaudeProjects/ClaudeProjectCard.tsx +195 -0
- package/components/ClaudeProjects/ClaudeProjectsSection.tsx +89 -0
- package/components/ClaudeProjects/ClaudeSessionCard.tsx +100 -0
- package/components/ClaudeProjects/index.ts +1 -0
- package/components/CodeSearch/CodeSearchResults.tsx +177 -0
- package/components/ConductorPanel.tsx +256 -0
- package/components/DevServers/DevServerCard.tsx +311 -0
- package/components/DevServers/DevServersSection.tsx +91 -0
- package/components/DevServers/ServerLogsModal.tsx +151 -0
- package/components/DevServers/StartServerDialog.tsx +359 -0
- package/components/DevServers/index.ts +4 -0
- package/components/DiffViewer/DiffModal.tsx +151 -0
- package/components/DiffViewer/UnifiedDiff.tsx +185 -0
- package/components/DiffViewer/index.tsx +2 -0
- package/components/DirectoryPicker.tsx +355 -0
- package/components/FileExplorer/FileEditor.tsx +276 -0
- package/components/FileExplorer/FileTabs.tsx +118 -0
- package/components/FileExplorer/FileTree.tsx +214 -0
- package/components/FileExplorer/HtmlRenderer.tsx +16 -0
- package/components/FileExplorer/MarkdownRenderer.tsx +18 -0
- package/components/FileExplorer/index.tsx +520 -0
- package/components/FilePicker.tsx +339 -0
- package/components/FolderPicker.tsx +201 -0
- package/components/GitDrawer/FileEditDialog.tsx +400 -0
- package/components/GitDrawer/index.tsx +464 -0
- package/components/GitPanel/CommitForm.tsx +205 -0
- package/components/GitPanel/CommitHistory.tsx +174 -0
- package/components/GitPanel/CommitItem.tsx +196 -0
- package/components/GitPanel/FileChanges.tsx +414 -0
- package/components/GitPanel/GitPanelTabs.tsx +39 -0
- package/components/GitPanel/index.tsx +817 -0
- package/components/MessageInput.tsx +82 -0
- package/components/NewClaudeSessionDialog.tsx +166 -0
- package/components/NewSessionDialog/AdvancedSettings.tsx +78 -0
- package/components/NewSessionDialog/AgentSelector.tsx +37 -0
- package/components/NewSessionDialog/CreatingOverlay.tsx +94 -0
- package/components/NewSessionDialog/NewSessionDialog.types.ts +136 -0
- package/components/NewSessionDialog/ProjectSelector.tsx +146 -0
- package/components/NewSessionDialog/WorkingDirectoryInput.tsx +55 -0
- package/components/NewSessionDialog/WorktreeSection.tsx +92 -0
- package/components/NewSessionDialog/hooks/useNewSessionForm.ts +370 -0
- package/components/NewSessionDialog/index.tsx +106 -0
- package/components/NotificationSettings.tsx +127 -0
- package/components/PRCreationModal.tsx +272 -0
- package/components/Pane/DesktopTabBar.tsx +353 -0
- package/components/Pane/MobileTabBar.tsx +210 -0
- package/components/Pane/OpenInVSCode.tsx +69 -0
- package/components/Pane/PaneSkeletons.tsx +57 -0
- package/components/Pane/index.tsx +558 -0
- package/components/PaneLayout.tsx +60 -0
- package/components/Projects/DevServersSection.tsx +140 -0
- package/components/Projects/DirectoryField.tsx +92 -0
- package/components/Projects/NewProjectDialog.tsx +188 -0
- package/components/Projects/NewProjectDialog.types.ts +46 -0
- package/components/Projects/ProjectCard.tsx +276 -0
- package/components/Projects/ProjectSettingsDialog.tsx +811 -0
- package/components/Projects/hooks/useNewProjectForm.ts +249 -0
- package/components/Projects/index.ts +3 -0
- package/components/Providers.tsx +49 -0
- package/components/QuickSwitcher.tsx +306 -0
- package/components/SessionList/KillAllConfirm.tsx +46 -0
- package/components/SessionList/SelectionToolbar.tsx +164 -0
- package/components/SessionList/SessionList.types.ts +37 -0
- package/components/SessionList/SessionListHeader.tsx +71 -0
- package/components/SessionList/hooks/useSessionListMutations.ts +269 -0
- package/components/SessionList/index.tsx +189 -0
- package/components/ShellDrawer/index.tsx +106 -0
- package/components/SidebarFooter.tsx +55 -0
- package/components/Terminal/KeybarToggleButton.tsx +45 -0
- package/components/Terminal/ScrollToBottomButton.tsx +32 -0
- package/components/Terminal/SearchBar.tsx +71 -0
- package/components/Terminal/TerminalToolbar.tsx +551 -0
- package/components/Terminal/VirtualKeyboard.tsx +711 -0
- package/components/Terminal/constants.ts +20 -0
- package/components/Terminal/hooks/index.ts +5 -0
- package/components/Terminal/hooks/resize-handlers.ts +140 -0
- package/components/Terminal/hooks/terminal-init.ts +151 -0
- package/components/Terminal/hooks/touch-scroll.ts +155 -0
- package/components/Terminal/hooks/useTerminalConnection.ts +282 -0
- package/components/Terminal/hooks/useTerminalConnection.types.ts +39 -0
- package/components/Terminal/hooks/useTerminalSearch.ts +103 -0
- package/components/Terminal/hooks/websocket-connection.ts +274 -0
- package/components/Terminal/index.tsx +320 -0
- package/components/ThemeToggle.tsx +168 -0
- package/components/TmuxSessions.tsx +132 -0
- package/components/ToolCallDisplay.tsx +71 -0
- package/components/WorkerCard.tsx +245 -0
- package/components/a/ABadge.tsx +115 -0
- package/components/a/AButton.tsx +163 -0
- package/components/a/ADialog.tsx +93 -0
- package/components/a/ADropdownMenu.tsx +279 -0
- package/components/a/AIconButton.tsx +190 -0
- package/components/a/ASheet.tsx +150 -0
- package/components/a/ATooltip.tsx +77 -0
- package/components/a/index.ts +64 -0
- package/components/mobile/SwipeSidebar.tsx +122 -0
- package/components/ui/badge.tsx +41 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/context-menu.tsx +197 -0
- package/components/ui/dialog.tsx +143 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/input.tsx +21 -0
- package/components/ui/scroll-area.tsx +52 -0
- package/components/ui/select.tsx +159 -0
- package/components/ui/skeleton.tsx +111 -0
- package/components/ui/switch.tsx +31 -0
- package/components/ui/textarea.tsx +21 -0
- package/components/ui/tooltip.tsx +32 -0
- package/components/views/DesktopView.tsx +244 -0
- package/components/views/MobileView.tsx +110 -0
- package/components/views/types.ts +75 -0
- package/contexts/PaneContext.tsx +336 -0
- package/data/claude/index.ts +9 -0
- package/data/claude/keys.ts +6 -0
- package/data/claude/queries.ts +120 -0
- package/data/claude/useClaudeUpdates.ts +37 -0
- package/data/code-search/index.ts +2 -0
- package/data/code-search/keys.ts +7 -0
- package/data/code-search/queries.ts +61 -0
- package/data/dev-servers/index.ts +8 -0
- package/data/dev-servers/keys.ts +4 -0
- package/data/dev-servers/queries.ts +104 -0
- package/data/files/index.ts +3 -0
- package/data/files/keys.ts +4 -0
- package/data/files/queries.ts +25 -0
- package/data/git/keys.ts +15 -0
- package/data/git/queries.ts +395 -0
- package/data/groups/index.ts +1 -0
- package/data/groups/mutations.ts +95 -0
- package/data/projects/index.ts +10 -0
- package/data/projects/keys.ts +4 -0
- package/data/projects/queries.ts +193 -0
- package/data/repositories/index.ts +7 -0
- package/data/repositories/keys.ts +5 -0
- package/data/repositories/queries.ts +122 -0
- package/data/sessions/index.ts +12 -0
- package/data/sessions/keys.ts +8 -0
- package/data/sessions/queries.ts +218 -0
- package/data/statuses/index.ts +1 -0
- package/data/statuses/queries.ts +69 -0
- package/hooks/useCopyToClipboard.ts +48 -0
- package/hooks/useDevServersManager.ts +73 -0
- package/hooks/useDirectoryBrowser.ts +90 -0
- package/hooks/useDrawerAnimation.ts +27 -0
- package/hooks/useFileDrop.ts +87 -0
- package/hooks/useFileEditor.ts +184 -0
- package/hooks/useGroups.ts +37 -0
- package/hooks/useHomePath.ts +34 -0
- package/hooks/useKeyRepeat.ts +55 -0
- package/hooks/useKeybarVisibility.ts +42 -0
- package/hooks/useNotifications.ts +257 -0
- package/hooks/useProjects.ts +53 -0
- package/hooks/useSessionStatuses.ts +30 -0
- package/hooks/useSessions.ts +86 -0
- package/hooks/useSpeechRecognition.ts +124 -0
- package/hooks/useViewport.ts +32 -0
- package/hooks/useViewportHeight.ts +50 -0
- package/lib/async-operations.ts +35 -0
- package/lib/banner.ts +81 -0
- package/lib/claude/jsonl-cache.ts +86 -0
- package/lib/claude/jsonl-reader.ts +271 -0
- package/lib/claude/process-manager.ts +278 -0
- package/lib/claude/stream-parser.ts +173 -0
- package/lib/claude/types.ts +154 -0
- package/lib/claude/watcher.ts +71 -0
- package/lib/client/session-registry.ts +111 -0
- package/lib/code-search.ts +121 -0
- package/lib/db/index.ts +48 -0
- package/lib/db/migrations.ts +45 -0
- package/lib/db/queries.ts +460 -0
- package/lib/db/schema.ts +114 -0
- package/lib/db/types.ts +92 -0
- package/lib/db.ts +2 -0
- package/lib/dev-servers.ts +509 -0
- package/lib/diff-parser.ts +221 -0
- package/lib/env-setup.ts +285 -0
- package/lib/file-upload.ts +34 -0
- package/lib/file-utils.ts +50 -0
- package/lib/files.ts +207 -0
- package/lib/git-history.ts +294 -0
- package/lib/git-status.ts +391 -0
- package/lib/git.ts +257 -0
- package/lib/mcp-config.ts +81 -0
- package/lib/multi-repo-git.ts +179 -0
- package/lib/notifications.ts +219 -0
- package/lib/orchestration.ts +448 -0
- package/lib/panes.ts +232 -0
- package/lib/ports.ts +97 -0
- package/lib/pr-generation.ts +307 -0
- package/lib/pr.ts +234 -0
- package/lib/projects.ts +578 -0
- package/lib/providers/registry.ts +70 -0
- package/lib/providers.ts +121 -0
- package/lib/query-client.ts +14 -0
- package/lib/rangeSelectionUtils.ts +65 -0
- package/lib/status-detector.ts +375 -0
- package/lib/terminal-themes.ts +265 -0
- package/lib/theme-config.ts +327 -0
- package/lib/utils.ts +6 -0
- package/lib/worktrees.ts +262 -0
- package/mcp/orchestration-server.ts +438 -0
- package/package.json +139 -0
- package/postcss.config.mjs +7 -0
- package/public/icon.svg +10 -0
- package/public/icons/icon-128x128.png +0 -0
- package/public/icons/icon-144x144.png +0 -0
- package/public/icons/icon-152x152.png +0 -0
- package/public/icons/icon-192x192.png +0 -0
- package/public/icons/icon-384x384.png +0 -0
- package/public/icons/icon-512x512.png +0 -0
- package/public/icons/icon-72x72.png +0 -0
- package/public/icons/icon-96x96.png +0 -0
- package/public/manifest.json +61 -0
- package/public/sw.js +64 -0
- package/scripts/agent-os +91 -0
- package/scripts/install.sh +48 -0
- package/scripts/lib/ai-clis.sh +132 -0
- package/scripts/lib/commands.sh +487 -0
- package/scripts/lib/common.sh +89 -0
- package/scripts/lib/prerequisites.sh +462 -0
- package/scripts/setup.sh +134 -0
- package/server.ts +155 -0
- package/stores/fileOpen.ts +26 -0
- package/stores/index.ts +1 -0
- package/stores/initialPrompt.ts +24 -0
- package/stores/sessionSelection.ts +48 -0
- package/styles/themes.css +603 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADropdownMenu - Declarative dropdown menu component
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```tsx
|
|
6
|
+
* <ADropdownMenu
|
|
7
|
+
* trigger={<Button><MoreHorizontal /></Button>}
|
|
8
|
+
* items={[
|
|
9
|
+
* menuItem('Edit', onEdit, { icon: Pencil }),
|
|
10
|
+
* toggleItem('Full Width', isFullWidth, onToggle),
|
|
11
|
+
* separator(),
|
|
12
|
+
* menuItem('Delete', onDelete, { icon: Trash2, variant: 'destructive' }),
|
|
13
|
+
* ]}
|
|
14
|
+
* />
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
"use client";
|
|
19
|
+
|
|
20
|
+
import type { ReactNode } from "react";
|
|
21
|
+
import type { LucideIcon } from "lucide-react";
|
|
22
|
+
import {
|
|
23
|
+
DropdownMenu,
|
|
24
|
+
DropdownMenuContent,
|
|
25
|
+
DropdownMenuItem,
|
|
26
|
+
DropdownMenuSeparator,
|
|
27
|
+
DropdownMenuSub,
|
|
28
|
+
DropdownMenuSubContent,
|
|
29
|
+
DropdownMenuSubTrigger,
|
|
30
|
+
DropdownMenuTrigger,
|
|
31
|
+
DropdownMenuPortal,
|
|
32
|
+
} from "@/components/ui/dropdown-menu";
|
|
33
|
+
import {
|
|
34
|
+
Tooltip,
|
|
35
|
+
TooltipContent,
|
|
36
|
+
TooltipTrigger,
|
|
37
|
+
} from "@/components/ui/tooltip";
|
|
38
|
+
import { Button } from "@/components/ui/button";
|
|
39
|
+
import { Switch } from "@/components/ui/switch";
|
|
40
|
+
import { cn } from "@/lib/utils";
|
|
41
|
+
|
|
42
|
+
// Types
|
|
43
|
+
|
|
44
|
+
interface BaseItem {
|
|
45
|
+
key?: string;
|
|
46
|
+
visible?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface MenuItemConfig extends BaseItem {
|
|
50
|
+
type: "item";
|
|
51
|
+
label: string;
|
|
52
|
+
onClick: () => void;
|
|
53
|
+
icon?: LucideIcon;
|
|
54
|
+
disabled?: boolean;
|
|
55
|
+
variant?: "default" | "destructive";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ToggleItemConfig extends BaseItem {
|
|
59
|
+
type: "toggle";
|
|
60
|
+
label: string;
|
|
61
|
+
checked: boolean;
|
|
62
|
+
onChange: () => void;
|
|
63
|
+
icon?: LucideIcon;
|
|
64
|
+
disabled?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface SubmenuItemConfig extends BaseItem {
|
|
68
|
+
type: "submenu";
|
|
69
|
+
label: string;
|
|
70
|
+
icon?: LucideIcon;
|
|
71
|
+
items: DropdownItemConfig[];
|
|
72
|
+
disabled?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface SeparatorItemConfig extends BaseItem {
|
|
76
|
+
type: "separator";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface CustomItemConfig extends BaseItem {
|
|
80
|
+
type: "custom";
|
|
81
|
+
render: () => ReactNode;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type DropdownItemConfig =
|
|
85
|
+
| MenuItemConfig
|
|
86
|
+
| ToggleItemConfig
|
|
87
|
+
| SubmenuItemConfig
|
|
88
|
+
| SeparatorItemConfig
|
|
89
|
+
| CustomItemConfig;
|
|
90
|
+
|
|
91
|
+
type FalsyItem = false | null | undefined | "" | 0;
|
|
92
|
+
|
|
93
|
+
export interface ADropdownMenuProps {
|
|
94
|
+
trigger?: ReactNode;
|
|
95
|
+
icon?: LucideIcon;
|
|
96
|
+
items: (DropdownItemConfig | FalsyItem)[];
|
|
97
|
+
align?: "start" | "center" | "end";
|
|
98
|
+
minWidth?: string;
|
|
99
|
+
className?: string;
|
|
100
|
+
tooltip?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function filterVisibleItems(
|
|
104
|
+
items: (DropdownItemConfig | FalsyItem)[]
|
|
105
|
+
): DropdownItemConfig[] {
|
|
106
|
+
return items.filter(
|
|
107
|
+
(item): item is DropdownItemConfig => !!item && item.visible !== false
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Renderers
|
|
112
|
+
|
|
113
|
+
function ToggleItem({ item }: { item: ToggleItemConfig }) {
|
|
114
|
+
const Icon = item.icon;
|
|
115
|
+
return (
|
|
116
|
+
<div className="flex min-h-[32px] items-center justify-between px-2 py-1.5">
|
|
117
|
+
<div className="flex items-center gap-2">
|
|
118
|
+
{Icon && <Icon className="text-muted-foreground h-4 w-4" />}
|
|
119
|
+
<span className="text-sm">{item.label}</span>
|
|
120
|
+
</div>
|
|
121
|
+
<Switch
|
|
122
|
+
checked={item.checked}
|
|
123
|
+
onCheckedChange={item.onChange}
|
|
124
|
+
className="scale-75"
|
|
125
|
+
disabled={item.disabled}
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function MenuItem({ item }: { item: MenuItemConfig }) {
|
|
132
|
+
const Icon = item.icon;
|
|
133
|
+
return (
|
|
134
|
+
<DropdownMenuItem
|
|
135
|
+
onClick={item.onClick}
|
|
136
|
+
disabled={item.disabled}
|
|
137
|
+
className={cn(
|
|
138
|
+
item.variant === "destructive" &&
|
|
139
|
+
"text-destructive focus:text-destructive"
|
|
140
|
+
)}
|
|
141
|
+
>
|
|
142
|
+
{Icon && <Icon className="mr-2 h-4 w-4" />}
|
|
143
|
+
{item.label}
|
|
144
|
+
</DropdownMenuItem>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function SubmenuItem({ item }: { item: SubmenuItemConfig }) {
|
|
149
|
+
const Icon = item.icon;
|
|
150
|
+
const visibleItems = filterVisibleItems(item.items);
|
|
151
|
+
|
|
152
|
+
if (visibleItems.length === 0) return null;
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<DropdownMenuSub>
|
|
156
|
+
<DropdownMenuSubTrigger disabled={item.disabled}>
|
|
157
|
+
{Icon && <Icon className="mr-2 h-4 w-4" />}
|
|
158
|
+
{item.label}
|
|
159
|
+
</DropdownMenuSubTrigger>
|
|
160
|
+
<DropdownMenuPortal>
|
|
161
|
+
<DropdownMenuSubContent>
|
|
162
|
+
{visibleItems.map((subItem, index) => (
|
|
163
|
+
<DropdownItemRenderer
|
|
164
|
+
key={subItem.key ?? `sub-${index}`}
|
|
165
|
+
item={subItem}
|
|
166
|
+
/>
|
|
167
|
+
))}
|
|
168
|
+
</DropdownMenuSubContent>
|
|
169
|
+
</DropdownMenuPortal>
|
|
170
|
+
</DropdownMenuSub>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function DropdownItemRenderer({ item }: { item: DropdownItemConfig }) {
|
|
175
|
+
switch (item.type) {
|
|
176
|
+
case "item":
|
|
177
|
+
return <MenuItem item={item} />;
|
|
178
|
+
case "toggle":
|
|
179
|
+
return <ToggleItem item={item} />;
|
|
180
|
+
case "submenu":
|
|
181
|
+
return <SubmenuItem item={item} />;
|
|
182
|
+
case "separator":
|
|
183
|
+
return <DropdownMenuSeparator />;
|
|
184
|
+
case "custom":
|
|
185
|
+
return <>{item.render()}</>;
|
|
186
|
+
default:
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Main Component
|
|
192
|
+
|
|
193
|
+
export function ADropdownMenu({
|
|
194
|
+
trigger,
|
|
195
|
+
icon: Icon,
|
|
196
|
+
items,
|
|
197
|
+
align = "end",
|
|
198
|
+
minWidth = "180px",
|
|
199
|
+
className,
|
|
200
|
+
tooltip,
|
|
201
|
+
}: ADropdownMenuProps) {
|
|
202
|
+
const visibleItems = filterVisibleItems(items);
|
|
203
|
+
|
|
204
|
+
const resolvedTrigger =
|
|
205
|
+
trigger ??
|
|
206
|
+
(Icon ? (
|
|
207
|
+
<Button variant="ghost" size="icon-sm">
|
|
208
|
+
<Icon className="h-4 w-4" />
|
|
209
|
+
</Button>
|
|
210
|
+
) : null);
|
|
211
|
+
|
|
212
|
+
const triggerElement = tooltip ? (
|
|
213
|
+
<Tooltip>
|
|
214
|
+
<TooltipTrigger asChild>
|
|
215
|
+
<DropdownMenuTrigger asChild>{resolvedTrigger}</DropdownMenuTrigger>
|
|
216
|
+
</TooltipTrigger>
|
|
217
|
+
<TooltipContent>{tooltip}</TooltipContent>
|
|
218
|
+
</Tooltip>
|
|
219
|
+
) : (
|
|
220
|
+
<DropdownMenuTrigger asChild>{resolvedTrigger}</DropdownMenuTrigger>
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<DropdownMenu>
|
|
225
|
+
{triggerElement}
|
|
226
|
+
<DropdownMenuContent
|
|
227
|
+
align={align}
|
|
228
|
+
className={className}
|
|
229
|
+
style={{ minWidth }}
|
|
230
|
+
>
|
|
231
|
+
{visibleItems.map((item, index) => (
|
|
232
|
+
<DropdownItemRenderer key={item.key ?? index} item={item} />
|
|
233
|
+
))}
|
|
234
|
+
</DropdownMenuContent>
|
|
235
|
+
</DropdownMenu>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Factory Functions
|
|
240
|
+
|
|
241
|
+
export function menuItem(
|
|
242
|
+
label: string,
|
|
243
|
+
onClick: () => void,
|
|
244
|
+
options?: Partial<Omit<MenuItemConfig, "type" | "label" | "onClick">>
|
|
245
|
+
): MenuItemConfig {
|
|
246
|
+
return { type: "item", label, onClick, ...options };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function toggleItem(
|
|
250
|
+
label: string,
|
|
251
|
+
checked: boolean,
|
|
252
|
+
onChange: () => void,
|
|
253
|
+
options?: Partial<
|
|
254
|
+
Omit<ToggleItemConfig, "type" | "label" | "checked" | "onChange">
|
|
255
|
+
>
|
|
256
|
+
): ToggleItemConfig {
|
|
257
|
+
return { type: "toggle", label, checked, onChange, ...options };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function submenuItem(
|
|
261
|
+
label: string,
|
|
262
|
+
items: DropdownItemConfig[],
|
|
263
|
+
options?: Partial<Omit<SubmenuItemConfig, "type" | "label" | "items">>
|
|
264
|
+
): SubmenuItemConfig {
|
|
265
|
+
return { type: "submenu", label, items, ...options };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function separator(
|
|
269
|
+
options?: Partial<Omit<SeparatorItemConfig, "type">>
|
|
270
|
+
): SeparatorItemConfig {
|
|
271
|
+
return { type: "separator", ...options };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function customItem(
|
|
275
|
+
render: () => ReactNode,
|
|
276
|
+
options?: Partial<Omit<CustomItemConfig, "type" | "render">>
|
|
277
|
+
): CustomItemConfig {
|
|
278
|
+
return { type: "custom", render, ...options };
|
|
279
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIconButton - Icon button component for the design system
|
|
3
|
+
*
|
|
4
|
+
* A consistent icon button with optional tooltip, badge, and dot indicator support.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* // Simple icon button
|
|
9
|
+
* <AIconButton icon={Settings} onClick={handleClick} />
|
|
10
|
+
*
|
|
11
|
+
* // With tooltip
|
|
12
|
+
* <AIconButton icon={MessageSquare} tooltip="Comments" onClick={handleClick} />
|
|
13
|
+
*
|
|
14
|
+
* // With badge (for notifications)
|
|
15
|
+
* <AIconButton icon={Bell} tooltip="Notifications" badge={5} onClick={handleClick} />
|
|
16
|
+
*
|
|
17
|
+
* // With dot indicator (for active state)
|
|
18
|
+
* <AIconButton icon={Share2} tooltip="Share" dot dotColor="blue" onClick={handleClick} />
|
|
19
|
+
*
|
|
20
|
+
* // Active/selected state
|
|
21
|
+
* <AIconButton icon={MessageSquare} active onClick={handleClick} />
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
"use client";
|
|
26
|
+
|
|
27
|
+
import { forwardRef } from "react";
|
|
28
|
+
import type { LucideIcon } from "lucide-react";
|
|
29
|
+
import { Button } from "@/components/ui/button";
|
|
30
|
+
import { Badge } from "@/components/ui/badge";
|
|
31
|
+
import { ATooltip } from "@/components/a/ATooltip";
|
|
32
|
+
import { cn } from "@/lib/utils";
|
|
33
|
+
|
|
34
|
+
export type AIconButtonSize = "sm" | "md" | "lg";
|
|
35
|
+
export type AIconButtonHighlight =
|
|
36
|
+
| "blue"
|
|
37
|
+
| "green"
|
|
38
|
+
| "red"
|
|
39
|
+
| "orange"
|
|
40
|
+
| "purple";
|
|
41
|
+
|
|
42
|
+
export interface AIconButtonProps extends Omit<
|
|
43
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
44
|
+
"children"
|
|
45
|
+
> {
|
|
46
|
+
/** Lucide icon component */
|
|
47
|
+
icon: LucideIcon;
|
|
48
|
+
/** Optional tooltip text */
|
|
49
|
+
tooltip?: string;
|
|
50
|
+
/** Optional keyboard shortcut hint for tooltip */
|
|
51
|
+
shortcut?: string;
|
|
52
|
+
/** Tooltip position */
|
|
53
|
+
tooltipSide?: "top" | "right" | "bottom" | "left";
|
|
54
|
+
/** Optional badge count (shows red notification badge) */
|
|
55
|
+
badge?: number;
|
|
56
|
+
/** Show a small dot indicator */
|
|
57
|
+
dot?: boolean;
|
|
58
|
+
/** Dot color (default: "purple") */
|
|
59
|
+
dotColor?: AIconButtonHighlight;
|
|
60
|
+
/** Whether the button is in active/selected state */
|
|
61
|
+
active?: boolean;
|
|
62
|
+
/** Highlight color for the icon */
|
|
63
|
+
highlight?: AIconButtonHighlight;
|
|
64
|
+
/** Button size variant */
|
|
65
|
+
size?: AIconButtonSize;
|
|
66
|
+
/** Button variant */
|
|
67
|
+
variant?: "ghost" | "default" | "muted";
|
|
68
|
+
/** Additional className for the icon */
|
|
69
|
+
iconClassName?: string;
|
|
70
|
+
/** Additional className */
|
|
71
|
+
className?: string;
|
|
72
|
+
/** Accessible label (required if no tooltip) */
|
|
73
|
+
"aria-label"?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const SIZE_CLASSES: Record<AIconButtonSize, { button: string; icon: string }> =
|
|
77
|
+
{
|
|
78
|
+
sm: { button: "h-7 w-7", icon: "h-3.5 w-3.5" },
|
|
79
|
+
md: { button: "h-8 w-8", icon: "h-4 w-4" },
|
|
80
|
+
lg: { button: "h-9 w-9", icon: "h-5 w-5" },
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const VARIANT_CLASSES = {
|
|
84
|
+
ghost: "bg-transparent hover:bg-muted/60",
|
|
85
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
86
|
+
muted: "bg-muted/40 hover:bg-muted/60",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const HIGHLIGHT_CLASSES: Record<AIconButtonHighlight, string> = {
|
|
90
|
+
blue: "text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300",
|
|
91
|
+
green:
|
|
92
|
+
"text-green-600 hover:text-green-700 dark:text-green-400 dark:hover:text-green-300",
|
|
93
|
+
red: "text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300",
|
|
94
|
+
orange:
|
|
95
|
+
"text-orange-600 hover:text-orange-700 dark:text-orange-400 dark:hover:text-orange-300",
|
|
96
|
+
purple:
|
|
97
|
+
"text-purple-600 hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-300",
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const DOT_COLORS: Record<AIconButtonHighlight, string> = {
|
|
101
|
+
blue: "bg-blue-500 dark:bg-blue-400",
|
|
102
|
+
green: "bg-green-500 dark:bg-green-400",
|
|
103
|
+
red: "bg-red-500 dark:bg-red-400",
|
|
104
|
+
orange: "bg-orange-500 dark:bg-orange-400",
|
|
105
|
+
purple: "bg-purple-500 dark:bg-purple-400",
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const AIconButton = forwardRef<HTMLButtonElement, AIconButtonProps>(
|
|
109
|
+
(
|
|
110
|
+
{
|
|
111
|
+
icon: Icon,
|
|
112
|
+
tooltip,
|
|
113
|
+
shortcut,
|
|
114
|
+
tooltipSide = "bottom",
|
|
115
|
+
badge,
|
|
116
|
+
dot = false,
|
|
117
|
+
dotColor = "purple",
|
|
118
|
+
active = false,
|
|
119
|
+
highlight,
|
|
120
|
+
size = "md",
|
|
121
|
+
variant = "ghost",
|
|
122
|
+
iconClassName,
|
|
123
|
+
className,
|
|
124
|
+
disabled,
|
|
125
|
+
"aria-label": ariaLabel,
|
|
126
|
+
...props
|
|
127
|
+
},
|
|
128
|
+
ref
|
|
129
|
+
) => {
|
|
130
|
+
const sizeClasses = SIZE_CLASSES[size];
|
|
131
|
+
|
|
132
|
+
const button = (
|
|
133
|
+
<Button
|
|
134
|
+
ref={ref}
|
|
135
|
+
variant="ghost"
|
|
136
|
+
size="sm"
|
|
137
|
+
disabled={disabled}
|
|
138
|
+
aria-label={ariaLabel || tooltip}
|
|
139
|
+
className={cn(
|
|
140
|
+
"relative p-0",
|
|
141
|
+
sizeClasses.button,
|
|
142
|
+
active ? VARIANT_CLASSES.default : VARIANT_CLASSES[variant],
|
|
143
|
+
highlight && !active && HIGHLIGHT_CLASSES[highlight],
|
|
144
|
+
disabled && "cursor-not-allowed opacity-50",
|
|
145
|
+
className
|
|
146
|
+
)}
|
|
147
|
+
{...props}
|
|
148
|
+
>
|
|
149
|
+
<Icon className={cn(sizeClasses.icon, iconClassName)} />
|
|
150
|
+
|
|
151
|
+
{/* Badge with count */}
|
|
152
|
+
{badge !== undefined && badge > 0 && (
|
|
153
|
+
<Badge
|
|
154
|
+
variant="destructive"
|
|
155
|
+
className="absolute -top-1 -right-1 h-4 min-w-4 px-1 text-[10px] font-medium"
|
|
156
|
+
>
|
|
157
|
+
{badge > 99 ? "99+" : badge}
|
|
158
|
+
</Badge>
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
{/* Dot indicator */}
|
|
162
|
+
{dot && !badge && (
|
|
163
|
+
<div
|
|
164
|
+
className={cn(
|
|
165
|
+
"absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full",
|
|
166
|
+
DOT_COLORS[dotColor]
|
|
167
|
+
)}
|
|
168
|
+
/>
|
|
169
|
+
)}
|
|
170
|
+
</Button>
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
if (tooltip) {
|
|
174
|
+
return (
|
|
175
|
+
<ATooltip
|
|
176
|
+
content={tooltip}
|
|
177
|
+
shortcut={shortcut}
|
|
178
|
+
side={tooltipSide}
|
|
179
|
+
disabled={disabled}
|
|
180
|
+
>
|
|
181
|
+
{button}
|
|
182
|
+
</ATooltip>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return button;
|
|
187
|
+
}
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
AIconButton.displayName = "AIconButton";
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASheet - Design system Sheet component
|
|
3
|
+
*
|
|
4
|
+
* A slide-out panel that follows ClaudeDeck design guidelines:
|
|
5
|
+
* - No strong borders, uses subtle shadows instead
|
|
6
|
+
* - Consistent styling across the app
|
|
7
|
+
* - Wraps Radix Sheet primitive
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
"use client";
|
|
11
|
+
|
|
12
|
+
import * as React from "react";
|
|
13
|
+
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
|
14
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
15
|
+
import { X } from "lucide-react";
|
|
16
|
+
import { cn } from "@/lib/utils";
|
|
17
|
+
|
|
18
|
+
const ASheet = SheetPrimitive.Root;
|
|
19
|
+
|
|
20
|
+
const ASheetTrigger = SheetPrimitive.Trigger;
|
|
21
|
+
|
|
22
|
+
const ASheetClose = SheetPrimitive.Close;
|
|
23
|
+
|
|
24
|
+
const ASheetPortal = SheetPrimitive.Portal;
|
|
25
|
+
|
|
26
|
+
const ASheetOverlay = React.forwardRef<
|
|
27
|
+
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
|
28
|
+
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
|
29
|
+
>(({ className, ...props }, ref) => (
|
|
30
|
+
<SheetPrimitive.Overlay
|
|
31
|
+
className={cn(
|
|
32
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/60",
|
|
33
|
+
className
|
|
34
|
+
)}
|
|
35
|
+
{...props}
|
|
36
|
+
ref={ref}
|
|
37
|
+
/>
|
|
38
|
+
));
|
|
39
|
+
ASheetOverlay.displayName = "ASheetOverlay";
|
|
40
|
+
|
|
41
|
+
const aSheetVariants = cva(
|
|
42
|
+
"fixed z-50 flex flex-col bg-background shadow-xl transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
|
43
|
+
{
|
|
44
|
+
variants: {
|
|
45
|
+
side: {
|
|
46
|
+
top: "inset-x-0 top-0 data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
|
47
|
+
bottom:
|
|
48
|
+
"inset-x-0 bottom-0 data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
|
49
|
+
left: "inset-y-0 left-0 h-full w-3/4 data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
|
50
|
+
right:
|
|
51
|
+
"inset-y-0 right-0 h-full w-3/4 data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
defaultVariants: {
|
|
55
|
+
side: "right",
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
interface ASheetContentProps
|
|
61
|
+
extends
|
|
62
|
+
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
|
63
|
+
VariantProps<typeof aSheetVariants> {
|
|
64
|
+
/** Hide the default close button */
|
|
65
|
+
hideCloseButton?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const ASheetContent = React.forwardRef<
|
|
69
|
+
React.ElementRef<typeof SheetPrimitive.Content>,
|
|
70
|
+
ASheetContentProps
|
|
71
|
+
>(
|
|
72
|
+
(
|
|
73
|
+
{ side = "right", className, children, hideCloseButton = false, ...props },
|
|
74
|
+
ref
|
|
75
|
+
) => (
|
|
76
|
+
<ASheetPortal>
|
|
77
|
+
<ASheetOverlay />
|
|
78
|
+
<SheetPrimitive.Content
|
|
79
|
+
ref={ref}
|
|
80
|
+
className={cn(aSheetVariants({ side }), className)}
|
|
81
|
+
{...props}
|
|
82
|
+
>
|
|
83
|
+
{children}
|
|
84
|
+
{!hideCloseButton && (
|
|
85
|
+
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none">
|
|
86
|
+
<X className="h-4 w-4" />
|
|
87
|
+
<span className="sr-only">Close</span>
|
|
88
|
+
</SheetPrimitive.Close>
|
|
89
|
+
)}
|
|
90
|
+
</SheetPrimitive.Content>
|
|
91
|
+
</ASheetPortal>
|
|
92
|
+
)
|
|
93
|
+
);
|
|
94
|
+
ASheetContent.displayName = "ASheetContent";
|
|
95
|
+
|
|
96
|
+
const ASheetHeader = ({
|
|
97
|
+
className,
|
|
98
|
+
...props
|
|
99
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
100
|
+
<div className={cn("flex flex-col space-y-1.5 p-4", className)} {...props} />
|
|
101
|
+
);
|
|
102
|
+
ASheetHeader.displayName = "ASheetHeader";
|
|
103
|
+
|
|
104
|
+
const ASheetFooter = ({
|
|
105
|
+
className,
|
|
106
|
+
...props
|
|
107
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
108
|
+
<div
|
|
109
|
+
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
|
110
|
+
{...props}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
ASheetFooter.displayName = "ASheetFooter";
|
|
114
|
+
|
|
115
|
+
const ASheetTitle = React.forwardRef<
|
|
116
|
+
React.ElementRef<typeof SheetPrimitive.Title>,
|
|
117
|
+
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
|
118
|
+
>(({ className, ...props }, ref) => (
|
|
119
|
+
<SheetPrimitive.Title
|
|
120
|
+
ref={ref}
|
|
121
|
+
className={cn("text-foreground text-base font-semibold", className)}
|
|
122
|
+
{...props}
|
|
123
|
+
/>
|
|
124
|
+
));
|
|
125
|
+
ASheetTitle.displayName = "ASheetTitle";
|
|
126
|
+
|
|
127
|
+
const ASheetDescription = React.forwardRef<
|
|
128
|
+
React.ElementRef<typeof SheetPrimitive.Description>,
|
|
129
|
+
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
|
130
|
+
>(({ className, ...props }, ref) => (
|
|
131
|
+
<SheetPrimitive.Description
|
|
132
|
+
ref={ref}
|
|
133
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
134
|
+
{...props}
|
|
135
|
+
/>
|
|
136
|
+
));
|
|
137
|
+
ASheetDescription.displayName = "ASheetDescription";
|
|
138
|
+
|
|
139
|
+
export {
|
|
140
|
+
ASheet,
|
|
141
|
+
ASheetPortal,
|
|
142
|
+
ASheetOverlay,
|
|
143
|
+
ASheetTrigger,
|
|
144
|
+
ASheetClose,
|
|
145
|
+
ASheetContent,
|
|
146
|
+
ASheetHeader,
|
|
147
|
+
ASheetFooter,
|
|
148
|
+
ASheetTitle,
|
|
149
|
+
ASheetDescription,
|
|
150
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ATooltip - Simple tooltip wrapper for the design system
|
|
3
|
+
*
|
|
4
|
+
* Wraps Radix tooltip primitives into a single, easy-to-use component.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* <ATooltip content="Edit document">
|
|
9
|
+
* <Button>Edit</Button>
|
|
10
|
+
* </ATooltip>
|
|
11
|
+
*
|
|
12
|
+
* // With shortcut hint
|
|
13
|
+
* <ATooltip content="Save" shortcut="Cmd+S">
|
|
14
|
+
* <Button>Save</Button>
|
|
15
|
+
* </ATooltip>
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
"use client";
|
|
20
|
+
|
|
21
|
+
import { type ReactNode } from "react";
|
|
22
|
+
import {
|
|
23
|
+
Tooltip,
|
|
24
|
+
TooltipContent,
|
|
25
|
+
TooltipTrigger,
|
|
26
|
+
} from "@/components/ui/tooltip";
|
|
27
|
+
import { cn } from "@/lib/utils";
|
|
28
|
+
|
|
29
|
+
export interface ATooltipProps {
|
|
30
|
+
/** The element that triggers the tooltip */
|
|
31
|
+
children: ReactNode;
|
|
32
|
+
/** Tooltip content - can be a string or ReactNode */
|
|
33
|
+
content: ReactNode;
|
|
34
|
+
/** Optional keyboard shortcut hint (e.g., "Cmd+S") */
|
|
35
|
+
shortcut?: string;
|
|
36
|
+
/** Side of the trigger to render the tooltip (default: "top") */
|
|
37
|
+
side?: "top" | "right" | "bottom" | "left";
|
|
38
|
+
/** Alignment of the tooltip relative to trigger (default: "center") */
|
|
39
|
+
align?: "start" | "center" | "end";
|
|
40
|
+
/** Delay in ms before showing tooltip (default: 300) */
|
|
41
|
+
delayDuration?: number;
|
|
42
|
+
/** Whether the tooltip is disabled */
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
/** Additional className for the tooltip content */
|
|
45
|
+
className?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function ATooltip({
|
|
49
|
+
children,
|
|
50
|
+
content,
|
|
51
|
+
shortcut,
|
|
52
|
+
side = "top",
|
|
53
|
+
align = "center",
|
|
54
|
+
delayDuration = 300,
|
|
55
|
+
disabled = false,
|
|
56
|
+
className,
|
|
57
|
+
}: ATooltipProps) {
|
|
58
|
+
if (disabled || !content) {
|
|
59
|
+
return <>{children}</>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Tooltip delayDuration={delayDuration}>
|
|
64
|
+
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
|
65
|
+
<TooltipContent side={side} align={align} className={cn(className)}>
|
|
66
|
+
<div className="flex items-center gap-2">
|
|
67
|
+
<span>{content}</span>
|
|
68
|
+
{shortcut && (
|
|
69
|
+
<kbd className="bg-muted text-muted-foreground rounded px-1.5 py-0.5 text-[10px] font-medium">
|
|
70
|
+
{shortcut}
|
|
71
|
+
</kbd>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
</TooltipContent>
|
|
75
|
+
</Tooltip>
|
|
76
|
+
);
|
|
77
|
+
}
|