@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,711 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useRef, useEffect, memo } from "react";
|
|
4
|
+
import {
|
|
5
|
+
ChevronUp,
|
|
6
|
+
ChevronDown,
|
|
7
|
+
ChevronLeft,
|
|
8
|
+
ChevronRight,
|
|
9
|
+
ImagePlus,
|
|
10
|
+
Mic,
|
|
11
|
+
MicOff,
|
|
12
|
+
Clipboard,
|
|
13
|
+
X,
|
|
14
|
+
Send,
|
|
15
|
+
} from "lucide-react";
|
|
16
|
+
import { cn } from "@/lib/utils";
|
|
17
|
+
import { useSpeechRecognition } from "@/hooks/useSpeechRecognition";
|
|
18
|
+
import { useKeyRepeat } from "@/hooks/useKeyRepeat";
|
|
19
|
+
|
|
20
|
+
// ANSI escape sequences
|
|
21
|
+
const SPECIAL_KEYS = {
|
|
22
|
+
UP: "\x1b[A",
|
|
23
|
+
DOWN: "\x1b[B",
|
|
24
|
+
LEFT: "\x1b[D",
|
|
25
|
+
RIGHT: "\x1b[C",
|
|
26
|
+
ENTER: "\r",
|
|
27
|
+
ESC: "\x1b",
|
|
28
|
+
TAB: "\t",
|
|
29
|
+
BACKSPACE: "\x7f",
|
|
30
|
+
CTRL_C: "\x03",
|
|
31
|
+
CTRL_D: "\x04",
|
|
32
|
+
CTRL_Z: "\x1a",
|
|
33
|
+
CTRL_L: "\x0c",
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
// Keyboard layouts
|
|
37
|
+
const ROWS = {
|
|
38
|
+
numbers: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
|
|
39
|
+
numbersShift: ["!", "@", "#", "$", "%", "^", "&", "*", "(", ")"],
|
|
40
|
+
row1: ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"],
|
|
41
|
+
row2: ["a", "s", "d", "f", "g", "h", "j", "k", "l"],
|
|
42
|
+
row3: ["z", "x", "c", "v", "b", "n", "m"],
|
|
43
|
+
symbols: ["-", "/", ":", ";", "(", ")", "$", "&", "@", '"'],
|
|
44
|
+
symbolsMore: [".", ",", "?", "!", "'", "`", "~", "=", "+", "*"],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type KeyboardMode = "quick" | "abc" | "num";
|
|
48
|
+
|
|
49
|
+
interface VirtualKeyboardProps {
|
|
50
|
+
onKeyPress: (key: string) => void;
|
|
51
|
+
onImagePick?: () => void;
|
|
52
|
+
visible?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Track last touch time globally to prevent duplicate events from touch->mouse emulation
|
|
56
|
+
let lastTouchTime = 0;
|
|
57
|
+
|
|
58
|
+
// Event delegation handler - finds the key from data attribute and fires callback
|
|
59
|
+
function createKeyboardHandler(onKey: (key: string) => void) {
|
|
60
|
+
const handleEvent = (e: TouchEvent | MouseEvent) => {
|
|
61
|
+
// Find the button with data-key attribute
|
|
62
|
+
const target = e.target as HTMLElement;
|
|
63
|
+
const button = target.closest("[data-key]") as HTMLElement | null;
|
|
64
|
+
if (!button) return;
|
|
65
|
+
|
|
66
|
+
const key = button.getAttribute("data-key");
|
|
67
|
+
if (!key) return;
|
|
68
|
+
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
|
|
71
|
+
// Prevent duplicate from touch->mouse emulation
|
|
72
|
+
if (e.type === "touchstart") {
|
|
73
|
+
lastTouchTime = Date.now();
|
|
74
|
+
} else if (e.type === "mousedown" && Date.now() - lastTouchTime < 500) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
onKey(key);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return handleEvent;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Simple key button - no individual handlers, uses event delegation
|
|
85
|
+
// Memoized to prevent re-renders when parent state changes (like shift)
|
|
86
|
+
const Key = memo(function Key({
|
|
87
|
+
char,
|
|
88
|
+
dataKey,
|
|
89
|
+
className,
|
|
90
|
+
}: {
|
|
91
|
+
char: string;
|
|
92
|
+
dataKey?: string;
|
|
93
|
+
className?: string;
|
|
94
|
+
}) {
|
|
95
|
+
return (
|
|
96
|
+
<button
|
|
97
|
+
data-key={dataKey ?? char}
|
|
98
|
+
className={cn(
|
|
99
|
+
"flex h-[44px] flex-1 touch-manipulation items-center justify-center rounded-md text-sm font-medium",
|
|
100
|
+
"bg-secondary text-secondary-foreground",
|
|
101
|
+
"active:bg-primary active:text-primary-foreground",
|
|
102
|
+
"min-w-[32px] select-none",
|
|
103
|
+
className
|
|
104
|
+
)}
|
|
105
|
+
>
|
|
106
|
+
{char}
|
|
107
|
+
</button>
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Fast button for special keys (uses event delegation via data-key)
|
|
112
|
+
function FastKey({
|
|
113
|
+
dataKey,
|
|
114
|
+
className,
|
|
115
|
+
children,
|
|
116
|
+
}: {
|
|
117
|
+
dataKey: string;
|
|
118
|
+
className?: string;
|
|
119
|
+
children: React.ReactNode;
|
|
120
|
+
}) {
|
|
121
|
+
return (
|
|
122
|
+
<button data-key={dataKey} className={className}>
|
|
123
|
+
{children}
|
|
124
|
+
</button>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Fast button with direct handler (for shortcuts bar which is outside main keyboard delegation)
|
|
129
|
+
function FastButton({
|
|
130
|
+
onPress,
|
|
131
|
+
className,
|
|
132
|
+
children,
|
|
133
|
+
}: {
|
|
134
|
+
onPress: () => void;
|
|
135
|
+
className?: string;
|
|
136
|
+
children: React.ReactNode;
|
|
137
|
+
}) {
|
|
138
|
+
const handleTouchStart = (e: React.TouchEvent) => {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
lastTouchTime = Date.now();
|
|
141
|
+
onPress();
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const handleMouseDown = (e: React.MouseEvent) => {
|
|
145
|
+
if (Date.now() - lastTouchTime < 500) return;
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
onPress();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<button
|
|
152
|
+
onTouchStart={handleTouchStart}
|
|
153
|
+
onMouseDown={handleMouseDown}
|
|
154
|
+
onContextMenu={(e) => e.preventDefault()}
|
|
155
|
+
className={className}
|
|
156
|
+
>
|
|
157
|
+
{children}
|
|
158
|
+
</button>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Paste modal for when clipboard API isn't available
|
|
163
|
+
function PasteModal({
|
|
164
|
+
open,
|
|
165
|
+
onClose,
|
|
166
|
+
onPaste,
|
|
167
|
+
}: {
|
|
168
|
+
open: boolean;
|
|
169
|
+
onClose: () => void;
|
|
170
|
+
onPaste: (text: string) => void;
|
|
171
|
+
}) {
|
|
172
|
+
const [text, setText] = useState("");
|
|
173
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
174
|
+
|
|
175
|
+
// Focus input when modal opens
|
|
176
|
+
useCallback(() => {
|
|
177
|
+
if (open && inputRef.current) {
|
|
178
|
+
inputRef.current.focus();
|
|
179
|
+
}
|
|
180
|
+
}, [open]);
|
|
181
|
+
|
|
182
|
+
const handleSend = () => {
|
|
183
|
+
if (text) {
|
|
184
|
+
onPaste(text);
|
|
185
|
+
setText("");
|
|
186
|
+
onClose();
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
if (!open) return null;
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<div
|
|
194
|
+
className="fixed inset-0 z-50 flex items-end justify-center bg-black/50"
|
|
195
|
+
onClick={onClose}
|
|
196
|
+
>
|
|
197
|
+
<div
|
|
198
|
+
className="bg-background w-full max-w-lg rounded-t-xl p-4 pb-[calc(1rem+env(safe-area-inset-bottom))]"
|
|
199
|
+
onClick={(e) => e.stopPropagation()}
|
|
200
|
+
>
|
|
201
|
+
<div className="mb-3 flex items-center justify-between">
|
|
202
|
+
<span className="text-sm font-medium">Paste text</span>
|
|
203
|
+
<button onClick={onClose} className="hover:bg-muted rounded-md p-1">
|
|
204
|
+
<X className="h-5 w-5" />
|
|
205
|
+
</button>
|
|
206
|
+
</div>
|
|
207
|
+
<textarea
|
|
208
|
+
ref={inputRef}
|
|
209
|
+
value={text}
|
|
210
|
+
onChange={(e) => setText(e.target.value)}
|
|
211
|
+
onPaste={(e) => {
|
|
212
|
+
// Handle paste event directly
|
|
213
|
+
const pasted = e.clipboardData?.getData("text");
|
|
214
|
+
if (pasted) {
|
|
215
|
+
e.preventDefault();
|
|
216
|
+
setText((prev) => prev + pasted);
|
|
217
|
+
}
|
|
218
|
+
}}
|
|
219
|
+
placeholder="Tap here, then long-press to paste..."
|
|
220
|
+
autoFocus
|
|
221
|
+
inputMode="text"
|
|
222
|
+
className="bg-muted focus:ring-primary h-24 w-full resize-none rounded-lg px-3 py-2 text-sm focus:ring-2 focus:outline-none"
|
|
223
|
+
/>
|
|
224
|
+
<button
|
|
225
|
+
onClick={handleSend}
|
|
226
|
+
disabled={!text}
|
|
227
|
+
className="bg-primary text-primary-foreground mt-3 flex w-full items-center justify-center gap-2 rounded-lg py-2.5 font-medium disabled:opacity-50"
|
|
228
|
+
>
|
|
229
|
+
<Send className="h-4 w-4" />
|
|
230
|
+
Send to Terminal
|
|
231
|
+
</button>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Terminal shortcuts bar - common keys for terminal interaction
|
|
238
|
+
function TerminalShortcutsBar({
|
|
239
|
+
onKeyPress,
|
|
240
|
+
isListening,
|
|
241
|
+
onMicToggle,
|
|
242
|
+
isMicSupported,
|
|
243
|
+
}: {
|
|
244
|
+
onKeyPress: (key: string) => void;
|
|
245
|
+
isListening?: boolean;
|
|
246
|
+
onMicToggle?: () => void;
|
|
247
|
+
isMicSupported?: boolean;
|
|
248
|
+
}) {
|
|
249
|
+
const [showPasteModal, setShowPasteModal] = useState(false);
|
|
250
|
+
|
|
251
|
+
const shortcuts = [
|
|
252
|
+
{ label: "Esc", key: SPECIAL_KEYS.ESC },
|
|
253
|
+
{ label: "^C", key: SPECIAL_KEYS.CTRL_C, highlight: true },
|
|
254
|
+
{ label: "Tab", key: SPECIAL_KEYS.TAB },
|
|
255
|
+
{ label: "^D", key: SPECIAL_KEYS.CTRL_D },
|
|
256
|
+
{ label: "^Z", key: SPECIAL_KEYS.CTRL_Z },
|
|
257
|
+
{ label: "^L", key: SPECIAL_KEYS.CTRL_L },
|
|
258
|
+
{ label: "↑", key: SPECIAL_KEYS.UP },
|
|
259
|
+
{ label: "↓", key: SPECIAL_KEYS.DOWN },
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
// Handle paste - try clipboard API first, fall back to modal
|
|
263
|
+
const handlePaste = useCallback(async () => {
|
|
264
|
+
try {
|
|
265
|
+
if (navigator.clipboard?.readText) {
|
|
266
|
+
const text = await navigator.clipboard.readText();
|
|
267
|
+
if (text) {
|
|
268
|
+
for (const char of text) {
|
|
269
|
+
onKeyPress(char);
|
|
270
|
+
}
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} catch {
|
|
275
|
+
// Clipboard API failed, show modal
|
|
276
|
+
}
|
|
277
|
+
// Fall back to modal
|
|
278
|
+
setShowPasteModal(true);
|
|
279
|
+
}, [onKeyPress]);
|
|
280
|
+
|
|
281
|
+
// Handle paste from modal
|
|
282
|
+
const handleModalPaste = useCallback(
|
|
283
|
+
(text: string) => {
|
|
284
|
+
for (const char of text) {
|
|
285
|
+
onKeyPress(char);
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
[onKeyPress]
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<>
|
|
293
|
+
<PasteModal
|
|
294
|
+
open={showPasteModal}
|
|
295
|
+
onClose={() => setShowPasteModal(false)}
|
|
296
|
+
onPaste={handleModalPaste}
|
|
297
|
+
/>
|
|
298
|
+
<div className="scrollbar-none flex items-center gap-1.5 overflow-x-auto px-2 py-1.5">
|
|
299
|
+
{/* Paste button */}
|
|
300
|
+
<FastButton
|
|
301
|
+
onPress={handlePaste}
|
|
302
|
+
className="bg-secondary text-secondary-foreground active:bg-primary active:text-primary-foreground flex-shrink-0 touch-manipulation rounded-md px-3 py-1.5 text-xs font-medium select-none"
|
|
303
|
+
>
|
|
304
|
+
<Clipboard className="h-4 w-4" />
|
|
305
|
+
</FastButton>
|
|
306
|
+
{/* Mic button - always visible when supported */}
|
|
307
|
+
{isMicSupported && onMicToggle && (
|
|
308
|
+
<FastButton
|
|
309
|
+
onPress={onMicToggle}
|
|
310
|
+
className={cn(
|
|
311
|
+
"flex-shrink-0 touch-manipulation rounded-md px-3 py-1.5 text-xs font-medium select-none",
|
|
312
|
+
isListening
|
|
313
|
+
? "animate-pulse bg-red-500 text-white"
|
|
314
|
+
: "bg-secondary text-secondary-foreground active:bg-primary active:text-primary-foreground"
|
|
315
|
+
)}
|
|
316
|
+
>
|
|
317
|
+
{isListening ? (
|
|
318
|
+
<MicOff className="h-4 w-4" />
|
|
319
|
+
) : (
|
|
320
|
+
<Mic className="h-4 w-4" />
|
|
321
|
+
)}
|
|
322
|
+
</FastButton>
|
|
323
|
+
)}
|
|
324
|
+
{shortcuts.map((shortcut) => (
|
|
325
|
+
<FastButton
|
|
326
|
+
key={shortcut.label}
|
|
327
|
+
onPress={() => onKeyPress(shortcut.key)}
|
|
328
|
+
className={cn(
|
|
329
|
+
"flex-shrink-0 touch-manipulation rounded-md px-3 py-1.5 text-xs font-medium select-none",
|
|
330
|
+
"active:bg-primary active:text-primary-foreground",
|
|
331
|
+
shortcut.highlight
|
|
332
|
+
? "bg-red-500/20 text-red-500"
|
|
333
|
+
: "bg-secondary text-secondary-foreground"
|
|
334
|
+
)}
|
|
335
|
+
>
|
|
336
|
+
{shortcut.label}
|
|
337
|
+
</FastButton>
|
|
338
|
+
))}
|
|
339
|
+
</div>
|
|
340
|
+
</>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function VirtualKeyboard({
|
|
345
|
+
onKeyPress,
|
|
346
|
+
onImagePick,
|
|
347
|
+
visible = true,
|
|
348
|
+
}: VirtualKeyboardProps) {
|
|
349
|
+
const [mode, setMode] = useState<KeyboardMode>("abc");
|
|
350
|
+
const [shifted, setShifted] = useState(false);
|
|
351
|
+
const keyboardRef = useRef<HTMLDivElement>(null);
|
|
352
|
+
|
|
353
|
+
// Speech recognition - send transcript directly to terminal
|
|
354
|
+
const handleTranscript = useCallback(
|
|
355
|
+
(text: string) => {
|
|
356
|
+
for (const char of text) {
|
|
357
|
+
onKeyPress(char);
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
[onKeyPress]
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
const {
|
|
364
|
+
isListening,
|
|
365
|
+
isSupported: isMicSupported,
|
|
366
|
+
toggle: toggleMic,
|
|
367
|
+
} = useSpeechRecognition(handleTranscript);
|
|
368
|
+
|
|
369
|
+
// Key repeat for backspace
|
|
370
|
+
const handleBackspace = useCallback(() => {
|
|
371
|
+
onKeyPress(SPECIAL_KEYS.BACKSPACE);
|
|
372
|
+
}, [onKeyPress]);
|
|
373
|
+
const { startRepeat: startBackspace, stopRepeat: stopBackspace } =
|
|
374
|
+
useKeyRepeat(handleBackspace);
|
|
375
|
+
|
|
376
|
+
// Event delegation - attach once, handle all keys
|
|
377
|
+
useEffect(() => {
|
|
378
|
+
const el = keyboardRef.current;
|
|
379
|
+
if (!el) return;
|
|
380
|
+
|
|
381
|
+
const handleKey = (key: string) => {
|
|
382
|
+
// Handle special keys
|
|
383
|
+
if (key === "SHIFT") {
|
|
384
|
+
setShifted((s) => !s);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (key === "MODE_ABC") {
|
|
388
|
+
setMode("abc");
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (key === "MODE_NUM") {
|
|
392
|
+
setMode("num");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (key === "MODE_QUICK") {
|
|
396
|
+
setMode("quick");
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (key === "SPACE") {
|
|
400
|
+
onKeyPress(" ");
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (key === "ENTER") {
|
|
404
|
+
onKeyPress(SPECIAL_KEYS.ENTER);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (key === "LEFT") {
|
|
408
|
+
onKeyPress(SPECIAL_KEYS.LEFT);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (key === "RIGHT") {
|
|
412
|
+
onKeyPress(SPECIAL_KEYS.RIGHT);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (key === "UP") {
|
|
416
|
+
onKeyPress(SPECIAL_KEYS.UP);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (key === "DOWN") {
|
|
420
|
+
onKeyPress(SPECIAL_KEYS.DOWN);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (key === "IMAGE" && onImagePick) {
|
|
424
|
+
onImagePick();
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Regular character - apply shift if needed
|
|
429
|
+
const char = shifted ? key.toUpperCase() : key;
|
|
430
|
+
onKeyPress(char);
|
|
431
|
+
if (shifted) setShifted(false);
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const handler = createKeyboardHandler(handleKey);
|
|
435
|
+
|
|
436
|
+
el.addEventListener("touchstart", handler, { passive: false });
|
|
437
|
+
el.addEventListener("mousedown", handler);
|
|
438
|
+
el.addEventListener("contextmenu", (e) => e.preventDefault());
|
|
439
|
+
|
|
440
|
+
return () => {
|
|
441
|
+
el.removeEventListener("touchstart", handler);
|
|
442
|
+
el.removeEventListener("mousedown", handler);
|
|
443
|
+
};
|
|
444
|
+
}, [onKeyPress, shifted, onImagePick]);
|
|
445
|
+
|
|
446
|
+
if (!visible) return null;
|
|
447
|
+
|
|
448
|
+
// Quick mode - just essential terminal keys
|
|
449
|
+
if (mode === "quick") {
|
|
450
|
+
return (
|
|
451
|
+
<div
|
|
452
|
+
ref={keyboardRef}
|
|
453
|
+
className="bg-background flex flex-col select-none"
|
|
454
|
+
>
|
|
455
|
+
{/* Terminal shortcuts */}
|
|
456
|
+
<TerminalShortcutsBar
|
|
457
|
+
onKeyPress={onKeyPress}
|
|
458
|
+
isListening={isListening}
|
|
459
|
+
onMicToggle={toggleMic}
|
|
460
|
+
isMicSupported={isMicSupported}
|
|
461
|
+
/>
|
|
462
|
+
|
|
463
|
+
<div className="flex flex-col gap-1.5 px-2 py-2 pb-[calc(0.5rem+env(safe-area-inset-bottom))]">
|
|
464
|
+
{/* Mode tabs + common keys */}
|
|
465
|
+
<div className="flex gap-1.5">
|
|
466
|
+
<FastKey
|
|
467
|
+
dataKey="MODE_ABC"
|
|
468
|
+
className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] flex-1 touch-manipulation items-center justify-center rounded-md text-xs font-medium select-none"
|
|
469
|
+
>
|
|
470
|
+
ABC
|
|
471
|
+
</FastKey>
|
|
472
|
+
<FastKey
|
|
473
|
+
dataKey="MODE_NUM"
|
|
474
|
+
className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] flex-1 touch-manipulation items-center justify-center rounded-md text-xs font-medium select-none"
|
|
475
|
+
>
|
|
476
|
+
123
|
|
477
|
+
</FastKey>
|
|
478
|
+
{onImagePick && (
|
|
479
|
+
<FastKey
|
|
480
|
+
dataKey="IMAGE"
|
|
481
|
+
className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] w-[44px] touch-manipulation items-center justify-center rounded-md select-none"
|
|
482
|
+
>
|
|
483
|
+
<ImagePlus className="h-5 w-5" />
|
|
484
|
+
</FastKey>
|
|
485
|
+
)}
|
|
486
|
+
<div className="flex-1" />
|
|
487
|
+
<button
|
|
488
|
+
onTouchStart={startBackspace}
|
|
489
|
+
onTouchEnd={stopBackspace}
|
|
490
|
+
onTouchCancel={stopBackspace}
|
|
491
|
+
onMouseDown={startBackspace}
|
|
492
|
+
onMouseUp={stopBackspace}
|
|
493
|
+
onMouseLeave={stopBackspace}
|
|
494
|
+
className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] w-[56px] touch-manipulation items-center justify-center rounded-md text-sm font-medium select-none"
|
|
495
|
+
>
|
|
496
|
+
⌫
|
|
497
|
+
</button>
|
|
498
|
+
</div>
|
|
499
|
+
|
|
500
|
+
{/* Arrow keys + Enter */}
|
|
501
|
+
<div className="flex gap-1.5">
|
|
502
|
+
<FastKey
|
|
503
|
+
dataKey="LEFT"
|
|
504
|
+
className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] w-[44px] touch-manipulation items-center justify-center rounded-md select-none"
|
|
505
|
+
>
|
|
506
|
+
<ChevronLeft className="h-6 w-6" />
|
|
507
|
+
</FastKey>
|
|
508
|
+
<div className="flex flex-col gap-1">
|
|
509
|
+
<FastKey
|
|
510
|
+
dataKey="UP"
|
|
511
|
+
className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[20px] w-[44px] touch-manipulation items-center justify-center rounded-md select-none"
|
|
512
|
+
>
|
|
513
|
+
<ChevronUp className="h-4 w-4" />
|
|
514
|
+
</FastKey>
|
|
515
|
+
<FastKey
|
|
516
|
+
dataKey="DOWN"
|
|
517
|
+
className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[20px] w-[44px] touch-manipulation items-center justify-center rounded-md select-none"
|
|
518
|
+
>
|
|
519
|
+
<ChevronDown className="h-4 w-4" />
|
|
520
|
+
</FastKey>
|
|
521
|
+
</div>
|
|
522
|
+
<FastKey
|
|
523
|
+
dataKey="RIGHT"
|
|
524
|
+
className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] w-[44px] touch-manipulation items-center justify-center rounded-md select-none"
|
|
525
|
+
>
|
|
526
|
+
<ChevronRight className="h-6 w-6" />
|
|
527
|
+
</FastKey>
|
|
528
|
+
<div className="flex-1" />
|
|
529
|
+
<Key
|
|
530
|
+
char="⏎"
|
|
531
|
+
dataKey="ENTER"
|
|
532
|
+
className="bg-primary/30 text-primary w-[68px]"
|
|
533
|
+
/>
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ABC mode - full QWERTY
|
|
541
|
+
if (mode === "abc") {
|
|
542
|
+
return (
|
|
543
|
+
<div
|
|
544
|
+
ref={keyboardRef}
|
|
545
|
+
className="bg-background flex flex-col select-none"
|
|
546
|
+
>
|
|
547
|
+
{/* Terminal shortcuts */}
|
|
548
|
+
<TerminalShortcutsBar
|
|
549
|
+
onKeyPress={onKeyPress}
|
|
550
|
+
isListening={isListening}
|
|
551
|
+
onMicToggle={toggleMic}
|
|
552
|
+
isMicSupported={isMicSupported}
|
|
553
|
+
/>
|
|
554
|
+
|
|
555
|
+
<div className="flex flex-col gap-1.5 px-2 py-2 pb-[calc(0.5rem+env(safe-area-inset-bottom))]">
|
|
556
|
+
{/* QWERTY rows */}
|
|
557
|
+
<div className="flex gap-1">
|
|
558
|
+
{ROWS.row1.map((char) => (
|
|
559
|
+
<Key
|
|
560
|
+
key={char}
|
|
561
|
+
char={shifted ? char.toUpperCase() : char}
|
|
562
|
+
dataKey={char}
|
|
563
|
+
/>
|
|
564
|
+
))}
|
|
565
|
+
</div>
|
|
566
|
+
<div className="flex gap-1 px-4">
|
|
567
|
+
{ROWS.row2.map((char) => (
|
|
568
|
+
<Key
|
|
569
|
+
key={char}
|
|
570
|
+
char={shifted ? char.toUpperCase() : char}
|
|
571
|
+
dataKey={char}
|
|
572
|
+
/>
|
|
573
|
+
))}
|
|
574
|
+
</div>
|
|
575
|
+
<div className="flex gap-1">
|
|
576
|
+
<FastKey
|
|
577
|
+
dataKey="SHIFT"
|
|
578
|
+
className={cn(
|
|
579
|
+
"flex h-[44px] w-[48px] touch-manipulation items-center justify-center rounded-md text-sm font-medium select-none",
|
|
580
|
+
shifted
|
|
581
|
+
? "bg-primary/30 text-primary active:bg-primary active:text-primary-foreground"
|
|
582
|
+
: "bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground"
|
|
583
|
+
)}
|
|
584
|
+
>
|
|
585
|
+
⇧
|
|
586
|
+
</FastKey>
|
|
587
|
+
{ROWS.row3.map((char) => (
|
|
588
|
+
<Key
|
|
589
|
+
key={char}
|
|
590
|
+
char={shifted ? char.toUpperCase() : char}
|
|
591
|
+
dataKey={char}
|
|
592
|
+
/>
|
|
593
|
+
))}
|
|
594
|
+
<button
|
|
595
|
+
onTouchStart={startBackspace}
|
|
596
|
+
onTouchEnd={stopBackspace}
|
|
597
|
+
onTouchCancel={stopBackspace}
|
|
598
|
+
onMouseDown={startBackspace}
|
|
599
|
+
onMouseUp={stopBackspace}
|
|
600
|
+
onMouseLeave={stopBackspace}
|
|
601
|
+
className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] w-[48px] touch-manipulation items-center justify-center rounded-md text-sm font-medium select-none"
|
|
602
|
+
>
|
|
603
|
+
⌫
|
|
604
|
+
</button>
|
|
605
|
+
</div>
|
|
606
|
+
|
|
607
|
+
{/* Bottom row */}
|
|
608
|
+
<div className="flex gap-1">
|
|
609
|
+
<FastKey
|
|
610
|
+
dataKey="MODE_QUICK"
|
|
611
|
+
className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] w-[56px] touch-manipulation items-center justify-center rounded-md text-xs font-medium select-none"
|
|
612
|
+
>
|
|
613
|
+
^C
|
|
614
|
+
</FastKey>
|
|
615
|
+
<FastKey
|
|
616
|
+
dataKey="MODE_NUM"
|
|
617
|
+
className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] w-[48px] touch-manipulation items-center justify-center rounded-md text-xs font-medium select-none"
|
|
618
|
+
>
|
|
619
|
+
123
|
|
620
|
+
</FastKey>
|
|
621
|
+
<FastKey
|
|
622
|
+
dataKey="SPACE"
|
|
623
|
+
className="bg-secondary text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] flex-1 touch-manipulation items-center justify-center rounded-md text-sm select-none"
|
|
624
|
+
>
|
|
625
|
+
space
|
|
626
|
+
</FastKey>
|
|
627
|
+
<FastKey
|
|
628
|
+
dataKey="ENTER"
|
|
629
|
+
className="bg-primary/30 text-primary active:bg-primary active:text-primary-foreground flex h-[44px] w-[68px] touch-manipulation items-center justify-center rounded-md text-sm font-medium select-none"
|
|
630
|
+
>
|
|
631
|
+
⏎
|
|
632
|
+
</FastKey>
|
|
633
|
+
</div>
|
|
634
|
+
</div>
|
|
635
|
+
</div>
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Num mode - numbers and symbols
|
|
640
|
+
return (
|
|
641
|
+
<div ref={keyboardRef} className="bg-background flex flex-col select-none">
|
|
642
|
+
{/* Terminal shortcuts */}
|
|
643
|
+
<TerminalShortcutsBar
|
|
644
|
+
onKeyPress={onKeyPress}
|
|
645
|
+
isListening={isListening}
|
|
646
|
+
onMicToggle={toggleMic}
|
|
647
|
+
isMicSupported={isMicSupported}
|
|
648
|
+
/>
|
|
649
|
+
|
|
650
|
+
<div className="flex flex-col gap-1.5 px-2 py-2 pb-[calc(0.5rem+env(safe-area-inset-bottom))]">
|
|
651
|
+
{/* Number row */}
|
|
652
|
+
<div className="flex gap-1">
|
|
653
|
+
{ROWS.numbers.map((char) => (
|
|
654
|
+
<Key key={char} char={char} />
|
|
655
|
+
))}
|
|
656
|
+
</div>
|
|
657
|
+
|
|
658
|
+
{/* Symbols rows */}
|
|
659
|
+
<div className="flex gap-1">
|
|
660
|
+
{ROWS.symbols.map((char) => (
|
|
661
|
+
<Key key={char} char={char} />
|
|
662
|
+
))}
|
|
663
|
+
</div>
|
|
664
|
+
<div className="flex gap-1">
|
|
665
|
+
{ROWS.symbolsMore.map((char) => (
|
|
666
|
+
<Key key={char} char={char} />
|
|
667
|
+
))}
|
|
668
|
+
</div>
|
|
669
|
+
|
|
670
|
+
{/* Bottom row */}
|
|
671
|
+
<div className="flex gap-1">
|
|
672
|
+
<FastKey
|
|
673
|
+
dataKey="MODE_QUICK"
|
|
674
|
+
className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] w-[56px] touch-manipulation items-center justify-center rounded-md text-xs font-medium select-none"
|
|
675
|
+
>
|
|
676
|
+
^C
|
|
677
|
+
</FastKey>
|
|
678
|
+
<FastKey
|
|
679
|
+
dataKey="MODE_ABC"
|
|
680
|
+
className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] w-[48px] touch-manipulation items-center justify-center rounded-md text-xs font-medium select-none"
|
|
681
|
+
>
|
|
682
|
+
ABC
|
|
683
|
+
</FastKey>
|
|
684
|
+
<FastKey
|
|
685
|
+
dataKey="SPACE"
|
|
686
|
+
className="bg-secondary text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] flex-1 touch-manipulation items-center justify-center rounded-md text-sm select-none"
|
|
687
|
+
>
|
|
688
|
+
space
|
|
689
|
+
</FastKey>
|
|
690
|
+
<button
|
|
691
|
+
onTouchStart={startBackspace}
|
|
692
|
+
onTouchEnd={stopBackspace}
|
|
693
|
+
onTouchCancel={stopBackspace}
|
|
694
|
+
onMouseDown={startBackspace}
|
|
695
|
+
onMouseUp={stopBackspace}
|
|
696
|
+
onMouseLeave={stopBackspace}
|
|
697
|
+
className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] w-[48px] touch-manipulation items-center justify-center rounded-md text-sm font-medium select-none"
|
|
698
|
+
>
|
|
699
|
+
⌫
|
|
700
|
+
</button>
|
|
701
|
+
<FastKey
|
|
702
|
+
dataKey="ENTER"
|
|
703
|
+
className="bg-primary/30 text-primary active:bg-primary active:text-primary-foreground flex h-[44px] w-[68px] touch-manipulation items-center justify-center rounded-md text-sm font-medium select-none"
|
|
704
|
+
>
|
|
705
|
+
⏎
|
|
706
|
+
</FastKey>
|
|
707
|
+
</div>
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
710
|
+
);
|
|
711
|
+
}
|