gitspace 0.2.0-rc.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.
Files changed (318) hide show
  1. package/.claude/settings.local.json +21 -0
  2. package/.gitspace/bundle.json +50 -0
  3. package/.gitspace/select/01-status.sh +40 -0
  4. package/.gitspace/setup/01-install-deps.sh +12 -0
  5. package/.gitspace/setup/02-typecheck.sh +16 -0
  6. package/AGENTS.md +439 -0
  7. package/CLAUDE.md +1 -0
  8. package/LICENSE +25 -0
  9. package/README.md +607 -0
  10. package/bin/gssh +62 -0
  11. package/bun.lock +647 -0
  12. package/docs/CONNECTION.md +623 -0
  13. package/docs/GATEWAY-WORKER.md +319 -0
  14. package/docs/GETTING-STARTED.md +448 -0
  15. package/docs/GITSPACE-PLATFORM.md +1819 -0
  16. package/docs/INFRASTRUCTURE.md +1347 -0
  17. package/docs/PROTOCOL.md +619 -0
  18. package/docs/QUICKSTART.md +174 -0
  19. package/docs/RELAY.md +327 -0
  20. package/docs/REMOTE-DESIGN.md +549 -0
  21. package/docs/ROADMAP.md +564 -0
  22. package/docs/SITE_DOCS_FIGMA_MAKE.md +1167 -0
  23. package/docs/STACK-DESIGN.md +588 -0
  24. package/docs/UNIFIED_ARCHITECTURE.md +292 -0
  25. package/experiments/pty-benchmark.ts +148 -0
  26. package/experiments/pty-latency.ts +100 -0
  27. package/experiments/router/client.ts +199 -0
  28. package/experiments/router/protocol.ts +74 -0
  29. package/experiments/router/router.ts +217 -0
  30. package/experiments/router/session.ts +180 -0
  31. package/experiments/router/test.ts +133 -0
  32. package/experiments/socket-bandwidth.ts +77 -0
  33. package/homebrew/gitspace.rb +45 -0
  34. package/landing-page/ATTRIBUTIONS.md +3 -0
  35. package/landing-page/README.md +11 -0
  36. package/landing-page/bun.lock +801 -0
  37. package/landing-page/guidelines/Guidelines.md +61 -0
  38. package/landing-page/index.html +37 -0
  39. package/landing-page/package.json +90 -0
  40. package/landing-page/postcss.config.mjs +15 -0
  41. package/landing-page/public/_redirects +1 -0
  42. package/landing-page/public/favicon.png +0 -0
  43. package/landing-page/src/app/App.tsx +53 -0
  44. package/landing-page/src/app/components/figma/ImageWithFallback.tsx +27 -0
  45. package/landing-page/src/app/components/ui/accordion.tsx +66 -0
  46. package/landing-page/src/app/components/ui/alert-dialog.tsx +157 -0
  47. package/landing-page/src/app/components/ui/alert.tsx +66 -0
  48. package/landing-page/src/app/components/ui/aspect-ratio.tsx +11 -0
  49. package/landing-page/src/app/components/ui/avatar.tsx +53 -0
  50. package/landing-page/src/app/components/ui/badge.tsx +46 -0
  51. package/landing-page/src/app/components/ui/breadcrumb.tsx +109 -0
  52. package/landing-page/src/app/components/ui/button.tsx +57 -0
  53. package/landing-page/src/app/components/ui/calendar.tsx +75 -0
  54. package/landing-page/src/app/components/ui/card.tsx +92 -0
  55. package/landing-page/src/app/components/ui/carousel.tsx +241 -0
  56. package/landing-page/src/app/components/ui/chart.tsx +353 -0
  57. package/landing-page/src/app/components/ui/checkbox.tsx +32 -0
  58. package/landing-page/src/app/components/ui/collapsible.tsx +33 -0
  59. package/landing-page/src/app/components/ui/command.tsx +177 -0
  60. package/landing-page/src/app/components/ui/context-menu.tsx +252 -0
  61. package/landing-page/src/app/components/ui/dialog.tsx +135 -0
  62. package/landing-page/src/app/components/ui/drawer.tsx +132 -0
  63. package/landing-page/src/app/components/ui/dropdown-menu.tsx +257 -0
  64. package/landing-page/src/app/components/ui/form.tsx +168 -0
  65. package/landing-page/src/app/components/ui/hover-card.tsx +44 -0
  66. package/landing-page/src/app/components/ui/input-otp.tsx +77 -0
  67. package/landing-page/src/app/components/ui/input.tsx +21 -0
  68. package/landing-page/src/app/components/ui/label.tsx +24 -0
  69. package/landing-page/src/app/components/ui/menubar.tsx +276 -0
  70. package/landing-page/src/app/components/ui/navigation-menu.tsx +168 -0
  71. package/landing-page/src/app/components/ui/pagination.tsx +127 -0
  72. package/landing-page/src/app/components/ui/popover.tsx +48 -0
  73. package/landing-page/src/app/components/ui/progress.tsx +31 -0
  74. package/landing-page/src/app/components/ui/radio-group.tsx +45 -0
  75. package/landing-page/src/app/components/ui/resizable.tsx +56 -0
  76. package/landing-page/src/app/components/ui/scroll-area.tsx +58 -0
  77. package/landing-page/src/app/components/ui/select.tsx +189 -0
  78. package/landing-page/src/app/components/ui/separator.tsx +28 -0
  79. package/landing-page/src/app/components/ui/sheet.tsx +139 -0
  80. package/landing-page/src/app/components/ui/sidebar.tsx +726 -0
  81. package/landing-page/src/app/components/ui/skeleton.tsx +13 -0
  82. package/landing-page/src/app/components/ui/slider.tsx +63 -0
  83. package/landing-page/src/app/components/ui/sonner.tsx +25 -0
  84. package/landing-page/src/app/components/ui/switch.tsx +31 -0
  85. package/landing-page/src/app/components/ui/table.tsx +116 -0
  86. package/landing-page/src/app/components/ui/tabs.tsx +66 -0
  87. package/landing-page/src/app/components/ui/textarea.tsx +18 -0
  88. package/landing-page/src/app/components/ui/toggle-group.tsx +73 -0
  89. package/landing-page/src/app/components/ui/toggle.tsx +47 -0
  90. package/landing-page/src/app/components/ui/tooltip.tsx +61 -0
  91. package/landing-page/src/app/components/ui/use-mobile.ts +21 -0
  92. package/landing-page/src/app/components/ui/utils.ts +6 -0
  93. package/landing-page/src/components/docs/DocsContent.tsx +718 -0
  94. package/landing-page/src/components/docs/DocsSidebar.tsx +84 -0
  95. package/landing-page/src/components/landing/CTA.tsx +59 -0
  96. package/landing-page/src/components/landing/Comparison.tsx +84 -0
  97. package/landing-page/src/components/landing/FaultyTerminal.tsx +424 -0
  98. package/landing-page/src/components/landing/Features.tsx +201 -0
  99. package/landing-page/src/components/landing/Hero.tsx +142 -0
  100. package/landing-page/src/components/landing/Pricing.tsx +140 -0
  101. package/landing-page/src/components/landing/Roadmap.tsx +86 -0
  102. package/landing-page/src/components/landing/Security.tsx +81 -0
  103. package/landing-page/src/components/landing/TerminalWindow.tsx +27 -0
  104. package/landing-page/src/components/landing/UseCases.tsx +55 -0
  105. package/landing-page/src/components/landing/Workflow.tsx +101 -0
  106. package/landing-page/src/components/layout/DashboardNavbar.tsx +37 -0
  107. package/landing-page/src/components/layout/Footer.tsx +55 -0
  108. package/landing-page/src/components/layout/LandingNavbar.tsx +82 -0
  109. package/landing-page/src/components/ui/badge.tsx +39 -0
  110. package/landing-page/src/components/ui/breadcrumb.tsx +115 -0
  111. package/landing-page/src/components/ui/button.tsx +57 -0
  112. package/landing-page/src/components/ui/card.tsx +79 -0
  113. package/landing-page/src/components/ui/mock-terminal.tsx +68 -0
  114. package/landing-page/src/components/ui/separator.tsx +28 -0
  115. package/landing-page/src/lib/utils.ts +6 -0
  116. package/landing-page/src/main.tsx +10 -0
  117. package/landing-page/src/pages/Dashboard.tsx +133 -0
  118. package/landing-page/src/pages/DocsPage.tsx +79 -0
  119. package/landing-page/src/pages/LandingPage.tsx +31 -0
  120. package/landing-page/src/pages/TerminalView.tsx +106 -0
  121. package/landing-page/src/styles/fonts.css +0 -0
  122. package/landing-page/src/styles/index.css +3 -0
  123. package/landing-page/src/styles/tailwind.css +4 -0
  124. package/landing-page/src/styles/theme.css +181 -0
  125. package/landing-page/vite.config.ts +19 -0
  126. package/npm/darwin-arm64/bin/gssh +0 -0
  127. package/npm/darwin-arm64/package.json +20 -0
  128. package/package.json +74 -0
  129. package/scripts/build.ts +284 -0
  130. package/scripts/release.ts +140 -0
  131. package/src/__tests__/test-utils.ts +298 -0
  132. package/src/commands/__tests__/serve-messages.test.ts +190 -0
  133. package/src/commands/access.ts +298 -0
  134. package/src/commands/add.ts +452 -0
  135. package/src/commands/auth.ts +364 -0
  136. package/src/commands/connect.ts +287 -0
  137. package/src/commands/directory.ts +16 -0
  138. package/src/commands/host.ts +396 -0
  139. package/src/commands/identity.ts +184 -0
  140. package/src/commands/list.ts +200 -0
  141. package/src/commands/relay.ts +315 -0
  142. package/src/commands/remove.ts +241 -0
  143. package/src/commands/serve.ts +1493 -0
  144. package/src/commands/share.ts +456 -0
  145. package/src/commands/status.ts +125 -0
  146. package/src/commands/switch.ts +353 -0
  147. package/src/commands/tmux.ts +317 -0
  148. package/src/core/__tests__/access.test.ts +240 -0
  149. package/src/core/access.ts +277 -0
  150. package/src/core/bundle.ts +342 -0
  151. package/src/core/config.ts +510 -0
  152. package/src/core/git.ts +317 -0
  153. package/src/core/github.ts +151 -0
  154. package/src/core/identity.ts +631 -0
  155. package/src/core/linear.ts +225 -0
  156. package/src/core/shell.ts +161 -0
  157. package/src/core/trusted-relays.ts +315 -0
  158. package/src/index.ts +821 -0
  159. package/src/lib/remote-session/index.ts +7 -0
  160. package/src/lib/remote-session/protocol.ts +267 -0
  161. package/src/lib/remote-session/session-handler.ts +581 -0
  162. package/src/lib/remote-session/workspace-scanner.ts +167 -0
  163. package/src/lib/tmux-lite/README.md +81 -0
  164. package/src/lib/tmux-lite/cli.ts +796 -0
  165. package/src/lib/tmux-lite/crypto/__tests__/helpers/handshake-runner.ts +349 -0
  166. package/src/lib/tmux-lite/crypto/__tests__/helpers/mock-relay.ts +291 -0
  167. package/src/lib/tmux-lite/crypto/__tests__/helpers/test-identities.ts +142 -0
  168. package/src/lib/tmux-lite/crypto/__tests__/integration/authorization.integration.test.ts +339 -0
  169. package/src/lib/tmux-lite/crypto/__tests__/integration/e2e-communication.integration.test.ts +477 -0
  170. package/src/lib/tmux-lite/crypto/__tests__/integration/error-handling.integration.test.ts +499 -0
  171. package/src/lib/tmux-lite/crypto/__tests__/integration/handshake.integration.test.ts +371 -0
  172. package/src/lib/tmux-lite/crypto/__tests__/integration/security.integration.test.ts +573 -0
  173. package/src/lib/tmux-lite/crypto/access-control.test.ts +512 -0
  174. package/src/lib/tmux-lite/crypto/access-control.ts +320 -0
  175. package/src/lib/tmux-lite/crypto/frames.test.ts +262 -0
  176. package/src/lib/tmux-lite/crypto/frames.ts +141 -0
  177. package/src/lib/tmux-lite/crypto/handshake.ts +894 -0
  178. package/src/lib/tmux-lite/crypto/identity.test.ts +220 -0
  179. package/src/lib/tmux-lite/crypto/identity.ts +286 -0
  180. package/src/lib/tmux-lite/crypto/index.ts +51 -0
  181. package/src/lib/tmux-lite/crypto/invites.test.ts +381 -0
  182. package/src/lib/tmux-lite/crypto/invites.ts +215 -0
  183. package/src/lib/tmux-lite/crypto/keyexchange.ts +435 -0
  184. package/src/lib/tmux-lite/crypto/keys.test.ts +58 -0
  185. package/src/lib/tmux-lite/crypto/keys.ts +47 -0
  186. package/src/lib/tmux-lite/crypto/secretbox.test.ts +169 -0
  187. package/src/lib/tmux-lite/crypto/secretbox.ts +124 -0
  188. package/src/lib/tmux-lite/handshake-handler.ts +451 -0
  189. package/src/lib/tmux-lite/protocol.test.ts +307 -0
  190. package/src/lib/tmux-lite/protocol.ts +266 -0
  191. package/src/lib/tmux-lite/relay-client.ts +506 -0
  192. package/src/lib/tmux-lite/server.ts +1250 -0
  193. package/src/lib/tmux-lite/shell-integration.sh +37 -0
  194. package/src/lib/tmux-lite/terminal-queries.test.ts +54 -0
  195. package/src/lib/tmux-lite/terminal-queries.ts +49 -0
  196. package/src/relay/__tests__/e2e-flow.test.ts +1284 -0
  197. package/src/relay/__tests__/helpers/auth.ts +354 -0
  198. package/src/relay/__tests__/helpers/ports.ts +51 -0
  199. package/src/relay/__tests__/protocol-validation.test.ts +265 -0
  200. package/src/relay/authorization.ts +303 -0
  201. package/src/relay/embedded-assets.generated.d.ts +15 -0
  202. package/src/relay/identity.ts +352 -0
  203. package/src/relay/index.ts +57 -0
  204. package/src/relay/pipes.test.ts +427 -0
  205. package/src/relay/pipes.ts +195 -0
  206. package/src/relay/protocol.ts +804 -0
  207. package/src/relay/registries.test.ts +437 -0
  208. package/src/relay/registries.ts +593 -0
  209. package/src/relay/server.test.ts +1323 -0
  210. package/src/relay/server.ts +1092 -0
  211. package/src/relay/signing.ts +238 -0
  212. package/src/relay/types.ts +69 -0
  213. package/src/serve/client-session-manager.ts +622 -0
  214. package/src/serve/daemon.ts +497 -0
  215. package/src/serve/pty-session.ts +236 -0
  216. package/src/serve/types.ts +169 -0
  217. package/src/shared/components/Flow.tsx +453 -0
  218. package/src/shared/components/Flow.tui.tsx +343 -0
  219. package/src/shared/components/Flow.web.tsx +442 -0
  220. package/src/shared/components/Inbox.tsx +446 -0
  221. package/src/shared/components/Inbox.tui.tsx +262 -0
  222. package/src/shared/components/Inbox.web.tsx +329 -0
  223. package/src/shared/components/MachineList.tsx +187 -0
  224. package/src/shared/components/MachineList.tui.tsx +161 -0
  225. package/src/shared/components/MachineList.web.tsx +210 -0
  226. package/src/shared/components/ProjectList.tsx +176 -0
  227. package/src/shared/components/ProjectList.tui.tsx +109 -0
  228. package/src/shared/components/ProjectList.web.tsx +143 -0
  229. package/src/shared/components/SpacesBrowser.tsx +332 -0
  230. package/src/shared/components/SpacesBrowser.tui.tsx +163 -0
  231. package/src/shared/components/SpacesBrowser.web.tsx +221 -0
  232. package/src/shared/components/index.ts +103 -0
  233. package/src/shared/hooks/index.ts +16 -0
  234. package/src/shared/hooks/useNavigation.ts +226 -0
  235. package/src/shared/index.ts +122 -0
  236. package/src/shared/providers/LocalMachineProvider.ts +425 -0
  237. package/src/shared/providers/MachineProvider.ts +165 -0
  238. package/src/shared/providers/RemoteMachineProvider.ts +444 -0
  239. package/src/shared/providers/index.ts +26 -0
  240. package/src/shared/types.ts +145 -0
  241. package/src/tui/adapters.ts +120 -0
  242. package/src/tui/app.tsx +1816 -0
  243. package/src/tui/components/Terminal.tsx +580 -0
  244. package/src/tui/hooks/index.ts +35 -0
  245. package/src/tui/hooks/useAppState.ts +314 -0
  246. package/src/tui/hooks/useDaemonStatus.ts +174 -0
  247. package/src/tui/hooks/useInboxTUI.ts +113 -0
  248. package/src/tui/hooks/useRemoteMachines.ts +209 -0
  249. package/src/tui/index.ts +24 -0
  250. package/src/tui/state.ts +299 -0
  251. package/src/tui/terminal-bracketed-paste.test.ts +45 -0
  252. package/src/tui/terminal-bracketed-paste.ts +47 -0
  253. package/src/types/bundle.ts +112 -0
  254. package/src/types/config.ts +89 -0
  255. package/src/types/errors.ts +206 -0
  256. package/src/types/identity.ts +284 -0
  257. package/src/types/workspace-fuzzy.ts +49 -0
  258. package/src/types/workspace.ts +151 -0
  259. package/src/utils/bun-socket-writer.ts +80 -0
  260. package/src/utils/deps.ts +127 -0
  261. package/src/utils/fuzzy-match.ts +125 -0
  262. package/src/utils/logger.ts +127 -0
  263. package/src/utils/markdown.ts +254 -0
  264. package/src/utils/onboarding.ts +229 -0
  265. package/src/utils/prompts.ts +114 -0
  266. package/src/utils/run-commands.ts +112 -0
  267. package/src/utils/run-scripts.ts +142 -0
  268. package/src/utils/sanitize.ts +98 -0
  269. package/src/utils/secrets.ts +122 -0
  270. package/src/utils/shell-escape.ts +40 -0
  271. package/src/utils/utf8.ts +79 -0
  272. package/src/utils/workspace-state.ts +47 -0
  273. package/src/web/README.md +73 -0
  274. package/src/web/bun.lock +575 -0
  275. package/src/web/eslint.config.js +23 -0
  276. package/src/web/index.html +16 -0
  277. package/src/web/package.json +37 -0
  278. package/src/web/public/vite.svg +1 -0
  279. package/src/web/src/App.tsx +604 -0
  280. package/src/web/src/assets/react.svg +1 -0
  281. package/src/web/src/components/Terminal.tsx +207 -0
  282. package/src/web/src/hooks/useRelayConnection.ts +224 -0
  283. package/src/web/src/hooks/useTerminal.ts +699 -0
  284. package/src/web/src/index.css +55 -0
  285. package/src/web/src/lib/crypto/__tests__/web-terminal.test.ts +1158 -0
  286. package/src/web/src/lib/crypto/frames.ts +205 -0
  287. package/src/web/src/lib/crypto/handshake.ts +396 -0
  288. package/src/web/src/lib/crypto/identity.ts +128 -0
  289. package/src/web/src/lib/crypto/keyexchange.ts +246 -0
  290. package/src/web/src/lib/crypto/relay-signing.ts +53 -0
  291. package/src/web/src/lib/invite.ts +58 -0
  292. package/src/web/src/lib/storage/identity-store.ts +94 -0
  293. package/src/web/src/main.tsx +10 -0
  294. package/src/web/src/types/identity.ts +45 -0
  295. package/src/web/tsconfig.app.json +28 -0
  296. package/src/web/tsconfig.json +7 -0
  297. package/src/web/tsconfig.node.json +26 -0
  298. package/src/web/vite.config.ts +31 -0
  299. package/todo-security.md +92 -0
  300. package/tsconfig.json +23 -0
  301. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite +0 -0
  302. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-shm +0 -0
  303. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-wal +0 -0
  304. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite +0 -0
  305. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-shm +0 -0
  306. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-wal +0 -0
  307. package/worker/bun.lock +237 -0
  308. package/worker/package.json +22 -0
  309. package/worker/schema.sql +96 -0
  310. package/worker/src/handlers/auth.ts +451 -0
  311. package/worker/src/handlers/subdomains.ts +376 -0
  312. package/worker/src/handlers/user.ts +98 -0
  313. package/worker/src/index.ts +70 -0
  314. package/worker/src/middleware/auth.ts +152 -0
  315. package/worker/src/services/cloudflare.ts +609 -0
  316. package/worker/src/types.ts +96 -0
  317. package/worker/tsconfig.json +15 -0
  318. package/worker/wrangler.toml +26 -0
@@ -0,0 +1,442 @@
1
+ /** @jsxImportSource react */
2
+ /**
3
+ * Flow - Web Modal Renderers
4
+ *
5
+ * React components for rendering modals/dialogs on web.
6
+ */
7
+
8
+ import { useEffect } from 'react';
9
+ import { createPortal } from 'react-dom';
10
+ import type { UseFlowReturn, FlowState } from './Flow.js';
11
+
12
+ // ============================================================================
13
+ // Props
14
+ // ============================================================================
15
+
16
+ interface FlowWebProps {
17
+ flow: UseFlowReturn;
18
+ }
19
+
20
+ // ============================================================================
21
+ // Main Component
22
+ // ============================================================================
23
+
24
+ export function FlowWeb({ flow }: FlowWebProps) {
25
+ const { flow: state, isOpen, handleConfirm, handleCancel, moveUp, moveDown } = flow;
26
+
27
+ // Debug: log when modal state changes
28
+ useEffect(() => {
29
+ if (isOpen) {
30
+ console.log('Modal OPEN - type:', state.type);
31
+ }
32
+ }, [isOpen, state.type]);
33
+
34
+ // Keyboard handling
35
+ useEffect(() => {
36
+ if (!isOpen) return;
37
+
38
+ const handler = (e: KeyboardEvent) => {
39
+ if (e.key === 'Escape') {
40
+ e.preventDefault();
41
+ handleCancel();
42
+ } else if (e.key === 'Enter') {
43
+ e.preventDefault();
44
+ handleConfirm();
45
+ } else if (e.key === 'ArrowUp' || e.key === 'k') {
46
+ e.preventDefault();
47
+ moveUp();
48
+ } else if (e.key === 'ArrowDown' || e.key === 'j') {
49
+ e.preventDefault();
50
+ moveDown();
51
+ } else if (state.type === 'confirm' && (e.key === 'y' || e.key === 'Y')) {
52
+ e.preventDefault();
53
+ handleConfirm();
54
+ } else if (state.type === 'confirm' && (e.key === 'n' || e.key === 'N')) {
55
+ e.preventDefault();
56
+ handleCancel();
57
+ }
58
+ };
59
+
60
+ window.addEventListener('keydown', handler);
61
+ return () => window.removeEventListener('keydown', handler);
62
+ }, [isOpen, state.type, handleConfirm, handleCancel, moveUp, moveDown]);
63
+
64
+ if (!isOpen) return null;
65
+
66
+ const modalContent = (
67
+ <div
68
+ className="fixed inset-0 flex items-center justify-center"
69
+ style={{ zIndex: 9999, position: 'fixed', top: 0, left: 0, right: 0, bottom: 0 }}
70
+ >
71
+ {/* Backdrop */}
72
+ <div
73
+ className="absolute inset-0 bg-black/80"
74
+ style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}
75
+ onClick={handleCancel}
76
+ />
77
+ {/* Modal */}
78
+ <div className="relative" style={{ zIndex: 10000, position: 'relative' }}>
79
+ {renderModal(state, flow)}
80
+ </div>
81
+ </div>
82
+ );
83
+
84
+ // Use portal to render at document body level
85
+ return createPortal(modalContent, document.body);
86
+ }
87
+
88
+ // ============================================================================
89
+ // Modal Renderers
90
+ // ============================================================================
91
+
92
+ function renderModal(state: FlowState, flow: UseFlowReturn) {
93
+ switch (state.type) {
94
+ case 'none':
95
+ return null;
96
+
97
+ case 'message':
98
+ return (
99
+ <Modal title={state.title}>
100
+ <p className={`mb-4 ${getVariantClass(state.variant)}`}>
101
+ {state.message}
102
+ </p>
103
+ <div className="flex justify-end">
104
+ <button
105
+ onClick={flow.handleConfirm}
106
+ className="px-5 py-3 bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white rounded-lg min-h-[48px]"
107
+ >
108
+ OK
109
+ </button>
110
+ </div>
111
+ </Modal>
112
+ );
113
+
114
+ case 'loading':
115
+ return (
116
+ <Modal title={state.title}>
117
+ <div className="flex items-center gap-3">
118
+ <div className="animate-spin w-5 h-5 border-2 border-blue-500 border-t-transparent rounded-full" />
119
+ <span className="text-gray-300">{state.message}</span>
120
+ </div>
121
+ </Modal>
122
+ );
123
+
124
+ case 'help':
125
+ return (
126
+ <Modal title="Keyboard Shortcuts" width="lg">
127
+ <div className="space-y-3">
128
+ {state.shortcuts.map((shortcut, idx) => (
129
+ <div key={idx} className="flex py-1">
130
+ <span className="w-20 sm:w-24 text-blue-400 font-mono text-sm">{shortcut.key}</span>
131
+ <span className="text-gray-300 text-sm">{shortcut.description}</span>
132
+ </div>
133
+ ))}
134
+ </div>
135
+ <div className="mt-6 text-right">
136
+ <button
137
+ onClick={flow.handleCancel}
138
+ className="px-5 py-3 bg-gray-700 hover:bg-gray-600 active:bg-gray-500 text-white rounded-lg min-h-[48px]"
139
+ >
140
+ Close
141
+ </button>
142
+ </div>
143
+ </Modal>
144
+ );
145
+
146
+ case 'confirm':
147
+ return (
148
+ <Modal title={state.title}>
149
+ <p className={`mb-4 ${getVariantClass(state.variant)}`}>
150
+ {state.message}
151
+ </p>
152
+ <div className="flex flex-col-reverse sm:flex-row justify-end gap-3 mt-6">
153
+ <button
154
+ onClick={flow.handleCancel}
155
+ className="px-5 py-3 bg-gray-700 hover:bg-gray-600 active:bg-gray-500 text-white rounded-lg min-h-[48px]"
156
+ >
157
+ {state.cancelLabel || 'Cancel'}
158
+ </button>
159
+ <button
160
+ onClick={flow.handleConfirm}
161
+ className={`px-5 py-3 rounded-lg text-white min-h-[48px] ${
162
+ state.variant === 'danger'
163
+ ? 'bg-red-600 hover:bg-red-700 active:bg-red-800'
164
+ : 'bg-blue-600 hover:bg-blue-700 active:bg-blue-800'
165
+ }`}
166
+ >
167
+ {state.confirmLabel || 'Confirm'}
168
+ </button>
169
+ </div>
170
+ </Modal>
171
+ );
172
+
173
+ case 'confirm-typed':
174
+ return (
175
+ <Modal title={state.title}>
176
+ <p className="mb-2 text-yellow-400">{state.message}</p>
177
+ {state.warning && (
178
+ <p className="mb-4 text-red-400">⚠️ {state.warning}</p>
179
+ )}
180
+ <p className="mb-2 text-gray-400">
181
+ Type "<span className="text-white font-mono">{state.confirmText}</span>" to confirm:
182
+ </p>
183
+ <input
184
+ type="text"
185
+ value={state.inputValue}
186
+ onChange={(e) => flow.handleInput(e.target.value)}
187
+ className="w-full p-3 text-base bg-gray-700 border border-gray-600 rounded-lg text-white focus:border-blue-500 focus:outline-none"
188
+ autoFocus
189
+ />
190
+ <div className="flex flex-col-reverse sm:flex-row justify-end gap-3 mt-6">
191
+ <button
192
+ onClick={flow.handleCancel}
193
+ className="px-5 py-3 bg-gray-700 hover:bg-gray-600 active:bg-gray-500 text-white rounded-lg min-h-[48px]"
194
+ >
195
+ Cancel
196
+ </button>
197
+ <button
198
+ onClick={flow.handleConfirm}
199
+ disabled={state.inputValue !== state.confirmText}
200
+ className="px-5 py-3 bg-red-600 hover:bg-red-700 active:bg-red-800 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded-lg min-h-[48px]"
201
+ >
202
+ Confirm
203
+ </button>
204
+ </div>
205
+ </Modal>
206
+ );
207
+
208
+ case 'input':
209
+ const validationError = state.validation?.(state.inputValue);
210
+ return (
211
+ <Modal title={state.title}>
212
+ <label className="block mb-2 text-gray-300">{state.label}</label>
213
+ <input
214
+ type="text"
215
+ value={state.inputValue}
216
+ onChange={(e) => flow.handleInput(e.target.value)}
217
+ placeholder={state.placeholder}
218
+ className="w-full p-3 text-base bg-gray-700 border border-gray-600 rounded-lg text-white focus:border-blue-500 focus:outline-none"
219
+ autoFocus
220
+ />
221
+ {validationError && (
222
+ <p className="mt-2 text-red-400 text-sm">{validationError}</p>
223
+ )}
224
+ <div className="flex flex-col-reverse sm:flex-row justify-end gap-3 mt-6">
225
+ <button
226
+ onClick={flow.handleCancel}
227
+ className="px-5 py-3 bg-gray-700 hover:bg-gray-600 active:bg-gray-500 text-white rounded-lg min-h-[48px]"
228
+ >
229
+ Cancel
230
+ </button>
231
+ <button
232
+ onClick={flow.handleConfirm}
233
+ disabled={!!validationError && state.inputValue !== ''}
234
+ className="px-5 py-3 bg-blue-600 hover:bg-blue-700 active:bg-blue-800 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded-lg min-h-[48px]"
235
+ >
236
+ Submit
237
+ </button>
238
+ </div>
239
+ </Modal>
240
+ );
241
+
242
+ case 'select':
243
+ return (
244
+ <Modal title={state.title} width="lg">
245
+ <div className="space-y-2 max-h-64 sm:max-h-80 overflow-y-auto -mx-2 px-2">
246
+ {state.options.map((option, idx) => {
247
+ const isSelected = idx === state.selectedIndex;
248
+ return (
249
+ <div
250
+ key={idx}
251
+ onClick={() => {
252
+ flow.handleSelect(idx);
253
+ flow.handleConfirm();
254
+ }}
255
+ className={`p-4 rounded-lg cursor-pointer min-h-[52px] ${
256
+ isSelected
257
+ ? 'bg-gray-700 border-l-4 border-l-blue-500'
258
+ : 'hover:bg-gray-800 active:bg-gray-700'
259
+ }`}
260
+ >
261
+ <div className="text-white">{option.label}</div>
262
+ {option.description && (
263
+ <div className="text-sm text-gray-400 mt-1">{option.description}</div>
264
+ )}
265
+ </div>
266
+ );
267
+ })}
268
+ </div>
269
+ <div className="flex justify-end gap-3 mt-6">
270
+ <button
271
+ onClick={flow.handleCancel}
272
+ className="px-5 py-3 bg-gray-700 hover:bg-gray-600 active:bg-gray-500 text-white rounded-lg min-h-[48px]"
273
+ >
274
+ Cancel
275
+ </button>
276
+ </div>
277
+ </Modal>
278
+ );
279
+
280
+ case 'wizard':
281
+ const step = state.steps[state.currentStep];
282
+ if (!step) return null;
283
+
284
+ return (
285
+ <Modal title={state.title} width="xl">
286
+ {/* Progress indicator */}
287
+ <div className="flex gap-1 mb-4">
288
+ {state.steps.map((_, idx) => (
289
+ <div
290
+ key={idx}
291
+ className={`h-1.5 flex-1 rounded ${
292
+ idx < state.currentStep
293
+ ? 'bg-green-500'
294
+ : idx === state.currentStep
295
+ ? 'bg-blue-500'
296
+ : 'bg-gray-700'
297
+ }`}
298
+ />
299
+ ))}
300
+ </div>
301
+
302
+ {/* Step content */}
303
+ <div className="mb-6">
304
+ <h3 className="text-lg font-medium text-green-400 mb-2">{step.title}</h3>
305
+ {step.description && (
306
+ <p className="text-gray-400 mb-4">{step.description}</p>
307
+ )}
308
+
309
+ {step.type === 'info' && (
310
+ <p className="text-gray-300">Tap Continue to proceed.</p>
311
+ )}
312
+
313
+ {(step.type === 'input' || step.type === 'secret') && (
314
+ <input
315
+ type={step.type === 'secret' ? 'password' : 'text'}
316
+ value={state.inputValue}
317
+ onChange={(e) => flow.handleInput(e.target.value)}
318
+ placeholder={step.placeholder}
319
+ className="w-full p-3 text-base bg-gray-700 border border-gray-600 rounded-lg text-white focus:border-blue-500 focus:outline-none"
320
+ autoFocus
321
+ />
322
+ )}
323
+
324
+ {step.type === 'confirm' && (
325
+ <div className="p-4 bg-gray-800 rounded-lg">
326
+ {step.checkStatus === 'checking' && (
327
+ <div className="flex items-center gap-2 text-yellow-400">
328
+ <div className="animate-spin w-4 h-4 border-2 border-yellow-400 border-t-transparent rounded-full" />
329
+ Checking...
330
+ </div>
331
+ )}
332
+ {step.checkStatus === 'found' && (
333
+ <div className="text-green-400">✅ Found and ready</div>
334
+ )}
335
+ {step.checkStatus === 'missing' && (
336
+ <div>
337
+ <div className="text-red-400 mb-2">❌ Not found</div>
338
+ {step.installUrl && (
339
+ <a
340
+ href={step.installUrl}
341
+ target="_blank"
342
+ rel="noopener noreferrer"
343
+ className="text-blue-400 hover:underline active:text-blue-300"
344
+ >
345
+ Install from {step.installUrl}
346
+ </a>
347
+ )}
348
+ </div>
349
+ )}
350
+ </div>
351
+ )}
352
+
353
+ {step.type === 'select' && step.options && (
354
+ <div className="space-y-2">
355
+ {step.options.map((option, idx) => (
356
+ <div
357
+ key={idx}
358
+ className="p-4 bg-gray-800 rounded-lg hover:bg-gray-700 active:bg-gray-600 cursor-pointer min-h-[48px]"
359
+ >
360
+ {option.label}
361
+ </div>
362
+ ))}
363
+ </div>
364
+ )}
365
+ </div>
366
+
367
+ {/* Navigation */}
368
+ <div className="flex flex-col-reverse sm:flex-row justify-between gap-3">
369
+ <button
370
+ onClick={flow.prevStep}
371
+ disabled={state.currentStep === 0}
372
+ className="px-5 py-3 bg-gray-700 hover:bg-gray-600 active:bg-gray-500 disabled:bg-gray-800 disabled:text-gray-500 text-white rounded-lg min-h-[48px]"
373
+ >
374
+ ← Back
375
+ </button>
376
+ <div className="flex flex-col-reverse sm:flex-row gap-3">
377
+ <button
378
+ onClick={flow.handleCancel}
379
+ className="px-5 py-3 bg-gray-700 hover:bg-gray-600 active:bg-gray-500 text-white rounded-lg min-h-[48px]"
380
+ >
381
+ Cancel
382
+ </button>
383
+ <button
384
+ onClick={flow.handleConfirm}
385
+ className="px-5 py-3 bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white rounded-lg min-h-[48px]"
386
+ >
387
+ {state.currentStep === state.steps.length - 1 ? 'Finish' : 'Continue →'}
388
+ </button>
389
+ </div>
390
+ </div>
391
+ </Modal>
392
+ );
393
+
394
+ default:
395
+ return null;
396
+ }
397
+ }
398
+
399
+ // ============================================================================
400
+ // Helper Components
401
+ // ============================================================================
402
+
403
+ interface ModalProps {
404
+ title: string;
405
+ children: React.ReactNode;
406
+ width?: 'sm' | 'md' | 'lg' | 'xl';
407
+ }
408
+
409
+ function Modal({ title, children, width = 'md' }: ModalProps) {
410
+ const widthClass = {
411
+ sm: 'sm:max-w-sm',
412
+ md: 'sm:max-w-md',
413
+ lg: 'sm:max-w-lg',
414
+ xl: 'sm:max-w-xl',
415
+ }[width];
416
+
417
+ return (
418
+ <div className={`bg-gray-900 shadow-xl w-full mx-0 sm:mx-4 p-5 sm:p-6 border-0 sm:border border-gray-700
419
+ fixed sm:relative inset-0 sm:inset-auto sm:rounded-lg
420
+ flex flex-col sm:block max-h-screen sm:max-h-[90vh] overflow-y-auto
421
+ ${widthClass}`}
422
+ >
423
+ <h2 className="text-xl font-semibold text-green-400 mb-4 flex-shrink-0">{title}</h2>
424
+ <div className="flex-1 min-h-0">{children}</div>
425
+ </div>
426
+ );
427
+ }
428
+
429
+ // ============================================================================
430
+ // Utilities
431
+ // ============================================================================
432
+
433
+ function getVariantClass(variant?: 'info' | 'success' | 'warning' | 'error' | 'danger'): string {
434
+ switch (variant) {
435
+ case 'success': return 'text-green-400';
436
+ case 'warning': return 'text-yellow-400';
437
+ case 'error':
438
+ case 'danger': return 'text-red-400';
439
+ case 'info':
440
+ default: return 'text-gray-300';
441
+ }
442
+ }