khal-os 1.260324.2

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 (408) hide show
  1. package/.env.example +23 -0
  2. package/.genie/mailbox/cli-sent.jsonl +3 -0
  3. package/.genie/mailbox/ds1-wave2-engineer-1.json +15 -0
  4. package/.genie/mailbox/ds1-wave2-engineer-2.json +15 -0
  5. package/.genie/mailbox/ds1-wave2-engineer-3.json +15 -0
  6. package/.genie/state/os-observability.json +39 -0
  7. package/.genie/state/tmux-control-mode-terminal.json +28 -0
  8. package/.genie/wishes/genieos-one-theme/WISH.md +417 -0
  9. package/.genie/wishes/workos-prod-rbac/WISH.md +345 -0
  10. package/.github/workflows/ci.yml +39 -0
  11. package/.github/workflows/release.yml +78 -0
  12. package/.github/workflows/version.yml +122 -0
  13. package/.husky/pre-commit +1 -0
  14. package/.pnpm-approve-builds.json +1 -0
  15. package/CLAUDE.md +117 -0
  16. package/LICENSE +21 -0
  17. package/README.md +38 -0
  18. package/biome.json +124 -0
  19. package/bun.lock +1249 -0
  20. package/docs/workos-setup.md +116 -0
  21. package/ecosystem.config.cjs +26 -0
  22. package/instrumentation.ts +8 -0
  23. package/knip.json +35 -0
  24. package/nats.conf +7 -0
  25. package/next.config.ts +25 -0
  26. package/package.json +78 -0
  27. package/packages/dev3000-app/components.ts +12 -0
  28. package/packages/dev3000-app/manifest.ts +19 -0
  29. package/packages/dev3000-app/package.json +23 -0
  30. package/packages/dev3000-app/views/dev3000/Dev3000App.tsx +758 -0
  31. package/packages/dev3000-app/views/dev3000/ErrorsPanel.tsx +160 -0
  32. package/packages/dev3000-app/views/dev3000/dev3000-context.tsx +21 -0
  33. package/packages/dev3000-app/views/dev3000/index.ts +4 -0
  34. package/packages/dev3000-app/views/dev3000/schema.ts +55 -0
  35. package/packages/dev3000-app/views/dev3000/service/index.ts +358 -0
  36. package/packages/dev3000-app/views/dev3000/service/runtime +1 -0
  37. package/packages/dev3000-app/views/dev3000/subjects.ts +9 -0
  38. package/packages/dev3000-app/views/dev3000/types.ts +77 -0
  39. package/packages/files-app/components.ts +12 -0
  40. package/packages/files-app/manifest.ts +19 -0
  41. package/packages/files-app/package.json +23 -0
  42. package/packages/files-app/views/files/ContextMenu.tsx +151 -0
  43. package/packages/files-app/views/files/DeleteConfirmDialog.tsx +39 -0
  44. package/packages/files-app/views/files/FileItem.tsx +128 -0
  45. package/packages/files-app/views/files/FilesApp.tsx +509 -0
  46. package/packages/files-app/views/files/FilesListView.tsx +201 -0
  47. package/packages/files-app/views/files/FilesToolbar.tsx +117 -0
  48. package/packages/files-app/views/files/GridView.tsx +90 -0
  49. package/packages/files-app/views/files/InlineInput.tsx +131 -0
  50. package/packages/files-app/views/files/UploadOverlay.tsx +61 -0
  51. package/packages/files-app/views/files/schema.ts +49 -0
  52. package/packages/files-app/views/files/service/index.ts +184 -0
  53. package/packages/files-app/views/files/service/runtime +1 -0
  54. package/packages/files-app/views/files/use-files.ts +201 -0
  55. package/packages/files-app/views/files/use-upload.ts +105 -0
  56. package/packages/genie-app/components.ts +12 -0
  57. package/packages/genie-app/lib/subjects.ts +87 -0
  58. package/packages/genie-app/manifest.ts +19 -0
  59. package/packages/genie-app/package.json +29 -0
  60. package/packages/genie-app/views/genie/service/agent-lifecycle.ts +136 -0
  61. package/packages/genie-app/views/genie/service/cli.ts +114 -0
  62. package/packages/genie-app/views/genie/service/comms.ts +141 -0
  63. package/packages/genie-app/views/genie/service/directory.ts +167 -0
  64. package/packages/genie-app/views/genie/service/index.ts +219 -0
  65. package/packages/genie-app/views/genie/service/system.ts +123 -0
  66. package/packages/genie-app/views/genie/service/teams.ts +191 -0
  67. package/packages/genie-app/views/genie/service/terminal-proxy.ts +184 -0
  68. package/packages/genie-app/views/genie/service/tmux-control.ts +318 -0
  69. package/packages/genie-app/views/genie/service/wishes.ts +270 -0
  70. package/packages/genie-app/views/genie/ui/GenieApp.tsx +5 -0
  71. package/packages/genie-app/views/genie/ui/PaneCard.tsx +307 -0
  72. package/packages/genie-app/views/genie/ui/Sidebar.tsx +212 -0
  73. package/packages/genie-app/views/genie/ui/TabBar.tsx +70 -0
  74. package/packages/genie-app/views/genie/ui/WorkspaceCanvas.tsx +343 -0
  75. package/packages/genie-app/views/genie/ui/XTermPane.tsx +306 -0
  76. package/packages/genie-app/views/genie/ui/hooks/useNatsAction.ts +54 -0
  77. package/packages/genie-app/views/genie/ui/hooks/useNatsRequest.ts +68 -0
  78. package/packages/genie-app/views/genie/ui/panels/AgentsPanel.tsx +399 -0
  79. package/packages/genie-app/views/genie/ui/panels/ChatPanel.tsx +351 -0
  80. package/packages/genie-app/views/genie/ui/panels/SystemPanel.tsx +195 -0
  81. package/packages/genie-app/views/genie/ui/panels/TeamsPanel.tsx +560 -0
  82. package/packages/genie-app/views/genie/ui/panels/WishesPanel.tsx +424 -0
  83. package/packages/nats-viewer-app/components.ts +12 -0
  84. package/packages/nats-viewer-app/manifest.ts +18 -0
  85. package/packages/nats-viewer-app/package.json +14 -0
  86. package/packages/nats-viewer-app/views/nats-viewer/ActiveSubs.tsx +34 -0
  87. package/packages/nats-viewer-app/views/nats-viewer/MessageLog.tsx +247 -0
  88. package/packages/nats-viewer-app/views/nats-viewer/NatsViewer.tsx +209 -0
  89. package/packages/nats-viewer-app/views/nats-viewer/PublishPanel.tsx +111 -0
  90. package/packages/nats-viewer-app/views/nats-viewer/RequestPanel.tsx +165 -0
  91. package/packages/nats-viewer-app/views/nats-viewer/Sidebar.tsx +59 -0
  92. package/packages/nats-viewer-app/views/nats-viewer/SubjectCatalog.tsx +63 -0
  93. package/packages/nats-viewer-app/views/nats-viewer/SubscribeInput.tsx +59 -0
  94. package/packages/nats-viewer-app/views/nats-viewer/index.ts +5 -0
  95. package/packages/nats-viewer-app/views/nats-viewer/nats-viewer-context.tsx +31 -0
  96. package/packages/nats-viewer-app/views/nats-viewer/types.ts +7 -0
  97. package/packages/nats-viewer-app/views/nats-viewer/use-message-buffer.ts +55 -0
  98. package/packages/os-cli/package.json +18 -0
  99. package/packages/os-cli/src/commands/events.ts +176 -0
  100. package/packages/os-cli/src/commands/logs.ts +96 -0
  101. package/packages/os-cli/src/commands/status.ts +53 -0
  102. package/packages/os-cli/src/commands/traces.ts +115 -0
  103. package/packages/os-cli/src/index.ts +15 -0
  104. package/packages/os-cli/src/lib/formatter.ts +123 -0
  105. package/packages/os-cli/src/lib/nats.ts +16 -0
  106. package/packages/os-cli/src/lib/trace-tree.ts +144 -0
  107. package/packages/os-cli/tsconfig.json +12 -0
  108. package/packages/os-sdk/package.json +27 -0
  109. package/packages/os-sdk/src/api/handler.ts +67 -0
  110. package/packages/os-sdk/src/config.ts +68 -0
  111. package/packages/os-sdk/src/db/factory.test.ts +42 -0
  112. package/packages/os-sdk/src/db/factory.ts +72 -0
  113. package/packages/os-sdk/src/db/migrate.ts +140 -0
  114. package/packages/os-sdk/src/db/provision.ts +44 -0
  115. package/packages/os-sdk/src/index.ts +36 -0
  116. package/packages/os-sdk/src/service/console-intercept.ts +60 -0
  117. package/packages/os-sdk/src/service/logger.ts +88 -0
  118. package/packages/os-sdk/src/service/o11y-streams.ts +88 -0
  119. package/packages/os-sdk/src/service/runtime.ts +259 -0
  120. package/packages/os-sdk/src/service/trace.ts +71 -0
  121. package/packages/os-sdk/tsconfig.json +16 -0
  122. package/packages/os-ui/package.json +13 -0
  123. package/packages/os-ui/src/index.ts +29 -0
  124. package/packages/os-ui/src/server.ts +4 -0
  125. package/packages/os-ui/tsconfig.json +19 -0
  126. package/packages/settings-app/components.ts +12 -0
  127. package/packages/settings-app/manifest.ts +18 -0
  128. package/packages/settings-app/package.json +14 -0
  129. package/packages/settings-app/views/settings/Settings.tsx +492 -0
  130. package/packages/terminal-app/components.ts +12 -0
  131. package/packages/terminal-app/manifest.ts +20 -0
  132. package/packages/terminal-app/package.json +23 -0
  133. package/packages/terminal-app/views/terminal/schema.ts +82 -0
  134. package/packages/terminal-app/views/terminal/service/index.ts +133 -0
  135. package/packages/terminal-app/views/terminal/service/runtime +1 -0
  136. package/packages/terminal-app/views/terminal/service/session.ts +290 -0
  137. package/packages/terminal-app/views/terminal/service/shell-hooks/bashrc-hook.sh +21 -0
  138. package/packages/terminal-app/views/terminal/types.ts +26 -0
  139. package/packages/terminal-app/views/terminal/ui/MultiTerminalApp.tsx +615 -0
  140. package/packages/terminal-app/views/terminal/ui/SplitDragHandle.tsx +91 -0
  141. package/packages/terminal-app/views/terminal/ui/SplitPaneRenderer.tsx +112 -0
  142. package/packages/terminal-app/views/terminal/ui/TerminalPane.tsx +478 -0
  143. package/packages/terminal-app/views/terminal/ui/TerminalTabBar.tsx +131 -0
  144. package/pnpm-workspace.yaml +9 -0
  145. package/postcss.config.mjs +7 -0
  146. package/public/file.svg +1 -0
  147. package/public/globe.svg +1 -0
  148. package/public/icons/code-server.svg +6 -0
  149. package/public/icons/default.svg +5 -0
  150. package/public/icons/dusk/1password.svg +1 -0
  151. package/public/icons/dusk/activity_monitor.svg +1 -0
  152. package/public/icons/dusk/app_store.svg +1 -0
  153. package/public/icons/dusk/atom.svg +1 -0
  154. package/public/icons/dusk/brave.svg +1 -0
  155. package/public/icons/dusk/calculator.svg +1 -0
  156. package/public/icons/dusk/calendar.svg +1 -0
  157. package/public/icons/dusk/chrome.svg +1 -0
  158. package/public/icons/dusk/chrome2.svg +1 -0
  159. package/public/icons/dusk/dashboard.svg +13 -0
  160. package/public/icons/dusk/discord.svg +1 -0
  161. package/public/icons/dusk/dropbox.svg +1 -0
  162. package/public/icons/dusk/electron.svg +1 -0
  163. package/public/icons/dusk/figma.svg +1 -0
  164. package/public/icons/dusk/finder.svg +1 -0
  165. package/public/icons/dusk/finder2.svg +1 -0
  166. package/public/icons/dusk/finder3.svg +1 -0
  167. package/public/icons/dusk/firefox.svg +1 -0
  168. package/public/icons/dusk/framer.svg +1 -0
  169. package/public/icons/dusk/gimp.svg +1 -0
  170. package/public/icons/dusk/github_desktop.svg +1 -0
  171. package/public/icons/dusk/hyper.svg +1 -0
  172. package/public/icons/dusk/hyper3.svg +1 -0
  173. package/public/icons/dusk/intellij.svg +1 -0
  174. package/public/icons/dusk/iterm2.svg +1 -0
  175. package/public/icons/dusk/itunes.svg +1 -0
  176. package/public/icons/dusk/mail.svg +1 -0
  177. package/public/icons/dusk/messenger.svg +1 -0
  178. package/public/icons/dusk/mongodb.svg +1 -0
  179. package/public/icons/dusk/notes.svg +1 -0
  180. package/public/icons/dusk/notion.svg +1 -0
  181. package/public/icons/dusk/obs.svg +1 -0
  182. package/public/icons/dusk/pages.svg +1 -0
  183. package/public/icons/dusk/photos.svg +1 -0
  184. package/public/icons/dusk/postman.svg +1 -0
  185. package/public/icons/dusk/preview.svg +1 -0
  186. package/public/icons/dusk/reminders.svg +1 -0
  187. package/public/icons/dusk/safari.svg +1 -0
  188. package/public/icons/dusk/sequel_pro.svg +1 -0
  189. package/public/icons/dusk/sketch.svg +1 -0
  190. package/public/icons/dusk/skype.svg +1 -0
  191. package/public/icons/dusk/slack.svg +1 -0
  192. package/public/icons/dusk/slack2.svg +1 -0
  193. package/public/icons/dusk/spotify.svg +1 -0
  194. package/public/icons/dusk/steam.svg +1 -0
  195. package/public/icons/dusk/system_preferences.svg +1 -0
  196. package/public/icons/dusk/tableplus.svg +1 -0
  197. package/public/icons/dusk/teams.svg +1 -0
  198. package/public/icons/dusk/telegram.svg +1 -0
  199. package/public/icons/dusk/terminal.svg +1 -0
  200. package/public/icons/dusk/todoist.svg +1 -0
  201. package/public/icons/dusk/trash.svg +1 -0
  202. package/public/icons/dusk/trello.svg +1 -0
  203. package/public/icons/dusk/vivaldi.svg +1 -0
  204. package/public/icons/dusk/vlc.svg +1 -0
  205. package/public/icons/dusk/vscode.svg +1 -0
  206. package/public/icons/dusk/whatsapp.svg +1 -0
  207. package/public/icons/dusk/xeyes.svg +1 -0
  208. package/public/icons/dusk/zoom.svg +1 -0
  209. package/public/icons/files.svg +5 -0
  210. package/public/icons/pwa/icon-192.png +0 -0
  211. package/public/icons/pwa/icon-512.png +0 -0
  212. package/public/icons/settings.svg +14 -0
  213. package/public/icons/terminal.svg +5 -0
  214. package/public/icons/text-editor.svg +7 -0
  215. package/public/manifest.json +38 -0
  216. package/public/next.svg +1 -0
  217. package/public/sw.js +41 -0
  218. package/public/vercel.svg +1 -0
  219. package/public/wallpapers/default.svg +10 -0
  220. package/public/window.svg +1 -0
  221. package/scripts/generate-pwa-icons.mjs +33 -0
  222. package/scripts/install-nats.sh +37 -0
  223. package/sentry.client.config.ts +21 -0
  224. package/sentry.edge.config.ts +12 -0
  225. package/sentry.server.config.ts +12 -0
  226. package/src/app/api/files/download/route.ts +81 -0
  227. package/src/app/api/files/download-zip/route.ts +102 -0
  228. package/src/app/api/files/upload/route.ts +58 -0
  229. package/src/app/api/webhooks/workos/route.ts +98 -0
  230. package/src/app/auth/callback/route.ts +16 -0
  231. package/src/app/auth/logout/route.ts +15 -0
  232. package/src/app/desktop/desktop-shell.tsx +110 -0
  233. package/src/app/desktop/layout.tsx +8 -0
  234. package/src/app/desktop/page.tsx +24 -0
  235. package/src/app/favicon.ico +0 -0
  236. package/src/app/globals.css +7 -0
  237. package/src/app/layout.tsx +64 -0
  238. package/src/app/offline/page.tsx +83 -0
  239. package/src/app/page.tsx +5 -0
  240. package/src/app/standalone/[appId]/page.tsx +28 -0
  241. package/src/app/standalone/layout.tsx +10 -0
  242. package/src/components/app-icon.tsx +55 -0
  243. package/src/components/apps/_echo/schema.ts +14 -0
  244. package/src/components/apps/_echo/service/index.ts +42 -0
  245. package/src/components/apps/app-manifest.ts +97 -0
  246. package/src/components/apps/app-registry.ts +55 -0
  247. package/src/components/apps/dev3000/Dev3000App.tsx +224 -0
  248. package/src/components/apps/dev3000/ErrorsPanel.tsx +160 -0
  249. package/src/components/apps/dev3000/Sidebar.tsx +41 -0
  250. package/src/components/apps/dev3000/TimelineLog.tsx +173 -0
  251. package/src/components/apps/dev3000/dev3000-context.tsx +29 -0
  252. package/src/components/apps/dev3000/index.ts +4 -0
  253. package/src/components/apps/dev3000/schema.ts +48 -0
  254. package/src/components/apps/dev3000/service/index.ts +520 -0
  255. package/src/components/apps/dev3000/service/runtime +1 -0
  256. package/src/components/apps/dev3000/types.ts +15 -0
  257. package/src/components/apps/dev3000/use-message-buffer.ts +46 -0
  258. package/src/components/apps/files/ContextMenu.tsx +151 -0
  259. package/src/components/apps/files/DeleteConfirmDialog.tsx +78 -0
  260. package/src/components/apps/files/FileItem.tsx +128 -0
  261. package/src/components/apps/files/FilesApp.tsx +509 -0
  262. package/src/components/apps/files/FilesListView.tsx +201 -0
  263. package/src/components/apps/files/FilesToolbar.tsx +117 -0
  264. package/src/components/apps/files/GridView.tsx +90 -0
  265. package/src/components/apps/files/InlineInput.tsx +131 -0
  266. package/src/components/apps/files/UploadOverlay.tsx +61 -0
  267. package/src/components/apps/files/schema.ts +49 -0
  268. package/src/components/apps/files/service/index.ts +227 -0
  269. package/src/components/apps/files/service/runtime +1 -0
  270. package/src/components/apps/files/use-files.ts +201 -0
  271. package/src/components/apps/files/use-upload.ts +105 -0
  272. package/src/components/apps/nats-viewer/ActiveSubs.tsx +34 -0
  273. package/src/components/apps/nats-viewer/MessageLog.tsx +247 -0
  274. package/src/components/apps/nats-viewer/NatsViewer.tsx +209 -0
  275. package/src/components/apps/nats-viewer/PublishPanel.tsx +113 -0
  276. package/src/components/apps/nats-viewer/RequestPanel.tsx +167 -0
  277. package/src/components/apps/nats-viewer/Sidebar.tsx +62 -0
  278. package/src/components/apps/nats-viewer/SubjectCatalog.tsx +64 -0
  279. package/src/components/apps/nats-viewer/SubscribeInput.tsx +59 -0
  280. package/src/components/apps/nats-viewer/index.ts +5 -0
  281. package/src/components/apps/nats-viewer/nats-viewer-context.tsx +31 -0
  282. package/src/components/apps/nats-viewer/types.ts +7 -0
  283. package/src/components/apps/nats-viewer/use-message-buffer.ts +55 -0
  284. package/src/components/apps/settings/Settings.tsx +492 -0
  285. package/src/components/apps/terminal/schema.ts +82 -0
  286. package/src/components/apps/terminal/service/index.ts +189 -0
  287. package/src/components/apps/terminal/service/runtime +1 -0
  288. package/src/components/apps/terminal/service/session.ts +296 -0
  289. package/src/components/apps/terminal/service/shell-hooks/bashrc-hook.sh +21 -0
  290. package/src/components/apps/terminal/types.ts +26 -0
  291. package/src/components/apps/terminal/ui/MultiTerminalApp.tsx +617 -0
  292. package/src/components/apps/terminal/ui/SplitDragHandle.tsx +91 -0
  293. package/src/components/apps/terminal/ui/SplitPaneRenderer.tsx +112 -0
  294. package/src/components/apps/terminal/ui/TerminalPane.tsx +476 -0
  295. package/src/components/apps/terminal/ui/TerminalTabBar.tsx +131 -0
  296. package/src/components/desktop/AnimatedBackground.tsx +69 -0
  297. package/src/components/desktop/Desktop.tsx +79 -0
  298. package/src/components/desktop/DesktopBackground.tsx +16 -0
  299. package/src/components/desktop/DesktopIcon.tsx +49 -0
  300. package/src/components/desktop/ShortcutViewer.tsx +136 -0
  301. package/src/components/desktop/WindowRenderer.tsx +34 -0
  302. package/src/components/desktop/WindowSwitcher.tsx +42 -0
  303. package/src/components/notifications/NotificationCenter.tsx +153 -0
  304. package/src/components/notifications/NotificationToasts.tsx +66 -0
  305. package/src/components/notifications/OrphanSessionToast.tsx +293 -0
  306. package/src/components/os-primitives/collapsible-sidebar.tsx +226 -0
  307. package/src/components/os-primitives/dialog.tsx +76 -0
  308. package/src/components/os-primitives/empty-state.tsx +43 -0
  309. package/src/components/os-primitives/index.ts +21 -0
  310. package/src/components/os-primitives/list-view.tsx +155 -0
  311. package/src/components/os-primitives/property-panel.tsx +108 -0
  312. package/src/components/os-primitives/section-header.tsx +19 -0
  313. package/src/components/os-primitives/sidebar-nav.tsx +110 -0
  314. package/src/components/os-primitives/split-pane.tsx +146 -0
  315. package/src/components/os-primitives/status-badge.tsx +10 -0
  316. package/src/components/os-primitives/status-bar.tsx +100 -0
  317. package/src/components/os-primitives/toolbar.tsx +152 -0
  318. package/src/components/taskbar/AppLauncher.tsx +114 -0
  319. package/src/components/taskbar/RunningApps.tsx +71 -0
  320. package/src/components/taskbar/SystemTray.tsx +134 -0
  321. package/src/components/taskbar/Taskbar.tsx +45 -0
  322. package/src/components/taskbar/UserMenu.tsx +138 -0
  323. package/src/components/taskbar/WorkspaceSwitcher.tsx +9 -0
  324. package/src/components/ui/ContextMenu.tsx +130 -0
  325. package/src/components/ui/badge.tsx +39 -0
  326. package/src/components/ui/button.tsx +102 -0
  327. package/src/components/ui/command.tsx +165 -0
  328. package/src/components/ui/dropdown-menu.tsx +233 -0
  329. package/src/components/ui/input.tsx +48 -0
  330. package/src/components/ui/note.tsx +55 -0
  331. package/src/components/ui/separator.tsx +25 -0
  332. package/src/components/ui/spinner.tsx +42 -0
  333. package/src/components/ui/switch.tsx +36 -0
  334. package/src/components/ui/theme-provider.tsx +24 -0
  335. package/src/components/ui/theme-switcher.tsx +51 -0
  336. package/src/components/ui/tooltip.tsx +62 -0
  337. package/src/components/window/MobileWindowStack.tsx +218 -0
  338. package/src/components/window/SnapPreview.tsx +37 -0
  339. package/src/components/window/StandaloneFrame.tsx +170 -0
  340. package/src/components/window/Window.tsx +423 -0
  341. package/src/components/window/WindowContent.tsx +14 -0
  342. package/src/components/window/WindowControlsOverlay.tsx +89 -0
  343. package/src/components/window/WindowFrame.tsx +124 -0
  344. package/src/lib/auth/index.ts +27 -0
  345. package/src/lib/auth/roles.ts +50 -0
  346. package/src/lib/auth/types.ts +32 -0
  347. package/src/lib/auth/use-auth.ts +53 -0
  348. package/src/lib/auth/webhook-handler.ts +87 -0
  349. package/src/lib/auth/workos.ts +67 -0
  350. package/src/lib/constants.ts +1 -0
  351. package/src/lib/desktop/dedup.ts +57 -0
  352. package/src/lib/desktop/schema.ts +55 -0
  353. package/src/lib/files/filename-validation.ts +41 -0
  354. package/src/lib/files/safe-path.ts +49 -0
  355. package/src/lib/hooks/use-desktop-nats.ts +438 -0
  356. package/src/lib/hooks/use-is-mobile.ts +23 -0
  357. package/src/lib/hooks/use-launch-app.ts +79 -0
  358. package/src/lib/hooks/use-nats-notifications.ts +84 -0
  359. package/src/lib/hooks/use-nats.ts +60 -0
  360. package/src/lib/hooks/use-visual-viewport.ts +72 -0
  361. package/src/lib/icons/resolve-window-icon.ts +10 -0
  362. package/src/lib/keyboard/defaults.ts +146 -0
  363. package/src/lib/keyboard/types.ts +52 -0
  364. package/src/lib/keyboard/use-global-keybinds.ts +231 -0
  365. package/src/lib/nats-client.ts +255 -0
  366. package/src/lib/nats.ts +35 -0
  367. package/src/lib/notifications/schema.ts +12 -0
  368. package/src/lib/service-loader.ts +171 -0
  369. package/src/lib/subjects.ts +64 -0
  370. package/src/lib/utils.ts +6 -0
  371. package/src/lib/ws-bridge.ts +288 -0
  372. package/src/lib/ws-protocol.ts +53 -0
  373. package/src/lib/ws-server.ts +167 -0
  374. package/src/middleware.ts +57 -0
  375. package/src/stores/desktop-store.ts +112 -0
  376. package/src/stores/keybind-store.ts +66 -0
  377. package/src/stores/notification-store.ts +271 -0
  378. package/src/stores/theme-store.ts +25 -0
  379. package/src/stores/window-store.ts +294 -0
  380. package/src/theme/animations.css +68 -0
  381. package/src/theme/base.css +123 -0
  382. package/src/theme/controls.css +35 -0
  383. package/src/theme/design-tokens.css +276 -0
  384. package/src/theme/index.css +23 -0
  385. package/src/theme/menus.css +45 -0
  386. package/src/theme/status.css +41 -0
  387. package/src/theme/surfaces.css +94 -0
  388. package/src/theme/tailwind-map.css +138 -0
  389. package/src/theme/taskbar.css +25 -0
  390. package/src/theme/terminal.css +55 -0
  391. package/src/theme/typography.css +26 -0
  392. package/src/theme/utilities.css +156 -0
  393. package/src/theme/window.css +103 -0
  394. package/src/types/desktop-entry.ts +12 -0
  395. package/src/types/use-descendants.d.ts +13 -0
  396. package/src/types/window.ts +28 -0
  397. package/src/types.d.ts +9 -0
  398. package/tauri/Cargo.lock +5464 -0
  399. package/tauri/Cargo.toml +19 -0
  400. package/tauri/build.rs +3 -0
  401. package/tauri/capabilities/default.json +36 -0
  402. package/tauri/icons/128x128.png +0 -0
  403. package/tauri/icons/128x128@2x.png +0 -0
  404. package/tauri/icons/32x32.png +0 -0
  405. package/tauri/icons/icon.png +0 -0
  406. package/tauri/src/main.rs +396 -0
  407. package/tauri/tauri.conf.json +23 -0
  408. package/tsconfig.json +43 -0
@@ -0,0 +1,71 @@
1
+ 'use client';
2
+
3
+ import { AppIcon } from '@/components/app-icon';
4
+ import { Tooltip } from '@/components/ui/tooltip';
5
+ import { useWindowStore } from '@/stores/window-store';
6
+
7
+ export function RunningApps() {
8
+ const windowsByWorkspace = useWindowStore((s) => s.windowsByWorkspace);
9
+ const activeWorkspaceId = useWindowStore((s) => s.activeWorkspaceId);
10
+ const focusWindow = useWindowStore((s) => s.focusWindow);
11
+ const minimizeWindow = useWindowStore((s) => s.minimizeWindow);
12
+
13
+ const windows = activeWorkspaceId ? windowsByWorkspace[activeWorkspaceId] || [] : [];
14
+
15
+ if (windows.length === 0) {
16
+ return <div className="flex flex-1 items-center px-1" />;
17
+ }
18
+
19
+ return (
20
+ <div
21
+ className="flex min-w-0 flex-1 items-center gap-0.5 overflow-x-auto px-1 no-scrollbar"
22
+ role="tablist"
23
+ aria-label="Running applications"
24
+ >
25
+ {windows.map((win) => {
26
+ const isActive = win.focused && !win.minimized;
27
+
28
+ return (
29
+ <Tooltip key={win.id} text={win.title} position="top" delay delayTime={400} desktopOnly>
30
+ <button
31
+ role="tab"
32
+ aria-selected={isActive}
33
+ aria-label={`${win.title}${isActive ? ' (active)' : win.minimized ? ' (minimized)' : ''}`}
34
+ className={`group relative flex h-7 shrink-0 items-center gap-1.5 rounded-md px-1.5 text-label-13 transition-all sm:max-w-44 sm:px-2.5 ${
35
+ isActive ? '' : 'hover:text-gray-1000'
36
+ }`}
37
+ style={{
38
+ background: isActive ? 'var(--khal-taskbar-active-bg)' : undefined,
39
+ color: isActive ? 'var(--khal-text-primary)' : 'var(--khal-text-secondary)',
40
+ }}
41
+ onMouseEnter={(e) => {
42
+ if (!isActive) (e.currentTarget as HTMLElement).style.background = 'var(--khal-taskbar-hover-bg)';
43
+ }}
44
+ onMouseLeave={(e) => {
45
+ if (!isActive) (e.currentTarget as HTMLElement).style.background = '';
46
+ }}
47
+ onClick={() => {
48
+ if (isActive) {
49
+ minimizeWindow(win.id);
50
+ } else {
51
+ focusWindow(win.id);
52
+ }
53
+ }}
54
+ >
55
+ <AppIcon appId={win.appId} size={16} className="h-4 w-4 shrink-0" />
56
+ <span className="hidden truncate sm:inline">{win.title}</span>
57
+ <span
58
+ className={`absolute bottom-0 left-1/2 h-[2px] -translate-x-1/2 rounded-full transition-all ${
59
+ isActive ? 'w-4' : 'w-2 group-hover:w-3'
60
+ }`}
61
+ style={{
62
+ background: isActive ? 'var(--khal-taskbar-active-indicator)' : 'var(--khal-text-muted)',
63
+ }}
64
+ />
65
+ </button>
66
+ </Tooltip>
67
+ );
68
+ })}
69
+ </div>
70
+ );
71
+ }
@@ -0,0 +1,134 @@
1
+ 'use client';
2
+
3
+ import { Bell, Monitor, Moon, Sun } from 'lucide-react';
4
+ import { useTheme } from 'next-themes';
5
+ import { useEffect, useRef } from 'react';
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuContent,
9
+ DropdownMenuItem,
10
+ DropdownMenuTrigger,
11
+ } from '@/components/ui/dropdown-menu';
12
+ import { Tooltip } from '@/components/ui/tooltip';
13
+ import { useNotificationStore } from '@/stores/notification-store';
14
+ import { UserMenu } from './UserMenu';
15
+
16
+ const THEME_OPTIONS = [
17
+ { key: 'system', label: 'System', Icon: Monitor },
18
+ { key: 'light', label: 'Light', Icon: Sun },
19
+ { key: 'dark', label: 'Dark', Icon: Moon },
20
+ ] as const;
21
+
22
+ function ThemeDropdown() {
23
+ const { theme, setTheme } = useTheme();
24
+
25
+ const current = THEME_OPTIONS.find((o) => o.key === theme) ?? THEME_OPTIONS[0];
26
+ const CurrentIcon = current.Icon;
27
+
28
+ return (
29
+ <DropdownMenu>
30
+ <Tooltip text="Theme" position="top" delay delayTime={400} desktopOnly>
31
+ <DropdownMenuTrigger
32
+ className="flex h-7 w-7 items-center justify-center rounded-md transition-colors"
33
+ style={{ color: 'var(--khal-text-secondary)' }}
34
+ onMouseEnter={(e) => {
35
+ (e.currentTarget as HTMLElement).style.background = 'var(--khal-taskbar-hover-bg)';
36
+ }}
37
+ onMouseLeave={(e) => {
38
+ (e.currentTarget as HTMLElement).style.background = '';
39
+ }}
40
+ aria-label="Change theme"
41
+ >
42
+ <CurrentIcon size={16} />
43
+ </DropdownMenuTrigger>
44
+ </Tooltip>
45
+ <DropdownMenuContent align="end" side="top" className="w-40">
46
+ {THEME_OPTIONS.map(({ key, label, Icon }) => (
47
+ <DropdownMenuItem key={key} onClick={() => setTheme(key)}>
48
+ <Icon size={16} />
49
+ {label}
50
+ {theme === key && (
51
+ <span className="ml-auto h-1.5 w-1.5 rounded-full" style={{ background: 'var(--khal-accent-primary)' }} />
52
+ )}
53
+ </DropdownMenuItem>
54
+ ))}
55
+ </DropdownMenuContent>
56
+ </DropdownMenu>
57
+ );
58
+ }
59
+
60
+ export function SystemTray() {
61
+ const timeRef = useRef<HTMLSpanElement>(null);
62
+ const dateRef = useRef<HTMLSpanElement>(null);
63
+
64
+ const unreadCount = useNotificationStore((s) => s.unreadCount);
65
+ const toggleCenter = useNotificationStore((s) => s.toggleCenter);
66
+
67
+ useEffect(() => {
68
+ const update = () => {
69
+ const now = new Date();
70
+ if (timeRef.current) {
71
+ timeRef.current.textContent = now.toLocaleTimeString([], {
72
+ hour: '2-digit',
73
+ minute: '2-digit',
74
+ });
75
+ }
76
+ if (dateRef.current) {
77
+ dateRef.current.textContent = now.toLocaleDateString([], {
78
+ month: 'short',
79
+ day: 'numeric',
80
+ });
81
+ }
82
+ };
83
+ update();
84
+ const id = setInterval(update, 10_000);
85
+ return () => clearInterval(id);
86
+ }, []);
87
+
88
+ return (
89
+ <div className="flex items-center gap-1.5 px-3">
90
+ <Tooltip text="Notifications" position="top" delay delayTime={400} desktopOnly>
91
+ <button
92
+ className="relative flex h-7 w-7 items-center justify-center rounded-md transition-colors"
93
+ style={{ color: 'var(--khal-text-secondary)' }}
94
+ onMouseEnter={(e) => {
95
+ (e.currentTarget as HTMLElement).style.background = 'var(--khal-taskbar-hover-bg)';
96
+ }}
97
+ onMouseLeave={(e) => {
98
+ (e.currentTarget as HTMLElement).style.background = '';
99
+ }}
100
+ onClick={toggleCenter}
101
+ aria-label={`Notifications${unreadCount > 0 ? ` (${unreadCount} unread)` : ''}`}
102
+ >
103
+ <Bell size={16} />
104
+ {unreadCount > 0 && (
105
+ <span
106
+ className="absolute -top-0.5 -right-0.5 flex h-3.5 min-w-3.5 items-center justify-center rounded-full px-1 text-[9px] font-bold"
107
+ style={{
108
+ background: 'var(--khal-accent-primary)',
109
+ color: 'var(--khal-text-inverse)',
110
+ }}
111
+ aria-hidden="true"
112
+ >
113
+ {unreadCount > 99 ? '99+' : unreadCount}
114
+ </span>
115
+ )}
116
+ </button>
117
+ </Tooltip>
118
+
119
+ <ThemeDropdown />
120
+
121
+ <div
122
+ className="hidden flex-col items-end leading-tight sm:flex"
123
+ role="status"
124
+ aria-live="polite"
125
+ aria-label="Current time and date"
126
+ >
127
+ <span ref={timeRef} className="text-label-12" style={{ color: 'var(--khal-text-primary)' }} />
128
+ <span ref={dateRef} className="text-label-12" style={{ color: 'var(--khal-text-secondary)' }} />
129
+ </div>
130
+
131
+ <UserMenu />
132
+ </div>
133
+ );
134
+ }
@@ -0,0 +1,45 @@
1
+ 'use client';
2
+
3
+ import { Separator } from '@/components/ui/separator';
4
+ import { AppLauncher } from './AppLauncher';
5
+ import { RunningApps } from './RunningApps';
6
+ import { SystemTray } from './SystemTray';
7
+ import { WorkspaceSwitcher } from './WorkspaceSwitcher';
8
+
9
+ export function Taskbar({ launcherToggle }: { launcherToggle?: number }) {
10
+ return (
11
+ <nav
12
+ className="fixed inset-x-0 bottom-0 z-[9000] flex h-11 items-center pb-[env(safe-area-inset-bottom,0px)]"
13
+ style={{
14
+ background: 'var(--khal-taskbar-bg)',
15
+ backdropFilter: 'blur(var(--khal-glass-blur, 32px)) saturate(1.8)',
16
+ WebkitBackdropFilter: 'blur(var(--khal-glass-blur, 32px)) saturate(1.8)',
17
+ borderTop: '1px solid var(--khal-taskbar-border)',
18
+ boxShadow: 'var(--khal-taskbar-shadow)',
19
+ }}
20
+ aria-label="Taskbar"
21
+ >
22
+ <div className="flex items-center px-1 sm:px-1.5">
23
+ <AppLauncher externalToggle={launcherToggle} />
24
+ </div>
25
+ <Separator
26
+ orientation="vertical"
27
+ className="mx-0.5 h-5 sm:mx-1"
28
+ style={{ background: 'var(--khal-border-default)' }}
29
+ />
30
+ <RunningApps />
31
+ <Separator
32
+ orientation="vertical"
33
+ className="mx-0.5 h-5 sm:mx-1"
34
+ style={{ background: 'var(--khal-border-default)' }}
35
+ />
36
+ <WorkspaceSwitcher />
37
+ <Separator
38
+ orientation="vertical"
39
+ className="mx-0.5 h-5 sm:mx-1"
40
+ style={{ background: 'var(--khal-border-default)' }}
41
+ />
42
+ <SystemTray />
43
+ </nav>
44
+ );
45
+ }
@@ -0,0 +1,138 @@
1
+ 'use client';
2
+
3
+ import { useAuth } from '@workos-inc/authkit-nextjs/components';
4
+ import { Building2, LogOut, Shield, User } from 'lucide-react';
5
+ import { useCallback, useRef } from 'react';
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuContent,
9
+ DropdownMenuItem,
10
+ DropdownMenuLabel,
11
+ DropdownMenuSeparator,
12
+ DropdownMenuTrigger,
13
+ } from '@/components/ui/dropdown-menu';
14
+ import { Tooltip } from '@/components/ui/tooltip';
15
+ import { useKhalAuth } from '@/lib/auth/use-auth';
16
+
17
+ const ROLE_COLORS: Record<string, string> = {
18
+ 'platform-owner': 'var(--khal-accent-warning, #f59e0b)',
19
+ 'platform-admin': 'var(--khal-accent-primary)',
20
+ 'platform-dev': 'var(--khal-accent-secondary, #6366f1)',
21
+ member: 'var(--khal-text-muted)',
22
+ };
23
+
24
+ const ROLE_LABELS: Record<string, string> = {
25
+ 'platform-owner': 'Owner',
26
+ 'platform-admin': 'Admin',
27
+ 'platform-dev': 'Developer',
28
+ member: 'Member',
29
+ };
30
+
31
+ function getInitial(user: { firstName?: string | null; lastName?: string | null; email: string }): string {
32
+ if (user.firstName) return user.firstName[0].toUpperCase();
33
+ if (user.lastName) return user.lastName[0].toUpperCase();
34
+ return user.email[0].toUpperCase();
35
+ }
36
+
37
+ function getDisplayName(user: { firstName?: string | null; lastName?: string | null; email: string }): string {
38
+ const parts = [user.firstName, user.lastName].filter(Boolean);
39
+ return parts.length > 0 ? parts.join(' ') : user.email;
40
+ }
41
+
42
+ export function UserMenu() {
43
+ const { user, organizationId, loading } = useAuth();
44
+ const khalAuth = useKhalAuth();
45
+
46
+ const logoutFormRef = useRef<HTMLFormElement>(null);
47
+ const handleLogout = useCallback(() => {
48
+ logoutFormRef.current?.submit();
49
+ }, []);
50
+
51
+ if (loading || !user) {
52
+ return (
53
+ <div className="flex h-7 w-7 items-center justify-center rounded-md" style={{ color: 'var(--khal-text-muted)' }}>
54
+ <User size={16} />
55
+ </div>
56
+ );
57
+ }
58
+
59
+ const initial = getInitial(user);
60
+ const displayName = getDisplayName(user);
61
+ const role = khalAuth?.role ?? 'member';
62
+ const roleColor = ROLE_COLORS[role] ?? ROLE_COLORS.member;
63
+
64
+ return (
65
+ <>
66
+ <form ref={logoutFormRef} method="POST" action="/auth/logout" className="hidden" />
67
+ <DropdownMenu>
68
+ <Tooltip text={displayName} position="top" delay delayTime={400} desktopOnly>
69
+ <DropdownMenuTrigger
70
+ className="flex h-7 w-7 items-center justify-center rounded-md transition-colors"
71
+ onMouseEnter={(e) => {
72
+ (e.currentTarget as HTMLElement).style.background = 'var(--khal-taskbar-hover-bg)';
73
+ }}
74
+ onMouseLeave={(e) => {
75
+ (e.currentTarget as HTMLElement).style.background = '';
76
+ }}
77
+ aria-label="User menu"
78
+ >
79
+ {user.profilePictureUrl ? (
80
+ <img src={user.profilePictureUrl} alt={displayName} className="h-5 w-5 rounded-full object-cover" />
81
+ ) : (
82
+ <span
83
+ className="flex h-5 w-5 items-center justify-center rounded-full text-[11px] font-semibold"
84
+ style={{
85
+ background: 'var(--khal-accent-primary)',
86
+ color: 'var(--khal-text-inverse)',
87
+ }}
88
+ >
89
+ {initial}
90
+ </span>
91
+ )}
92
+ </DropdownMenuTrigger>
93
+ </Tooltip>
94
+ <DropdownMenuContent align="end" side="top" className="w-56">
95
+ <DropdownMenuLabel className="flex flex-col gap-0.5 font-normal">
96
+ <span className="text-copy-13 font-medium" style={{ color: 'var(--khal-text-primary)' }}>
97
+ {displayName}
98
+ </span>
99
+ <span className="text-label-12" style={{ color: 'var(--khal-text-muted)' }}>
100
+ {user.email}
101
+ </span>
102
+ </DropdownMenuLabel>
103
+
104
+ <DropdownMenuSeparator />
105
+
106
+ {(organizationId || khalAuth?.orgId) && (
107
+ <DropdownMenuItem disabled className="gap-2 opacity-100">
108
+ <Building2 size={14} style={{ color: 'var(--khal-text-muted)' }} />
109
+ <span className="truncate" style={{ color: 'var(--khal-text-secondary)' }}>
110
+ {khalAuth?.orgId ?? organizationId}
111
+ </span>
112
+ </DropdownMenuItem>
113
+ )}
114
+
115
+ <DropdownMenuItem disabled className="gap-2 opacity-100">
116
+ <Shield size={14} style={{ color: roleColor }} />
117
+ <span
118
+ className="rounded px-1.5 py-0.5 text-label-12 font-medium"
119
+ style={{
120
+ background: `color-mix(in srgb, ${roleColor} 15%, transparent)`,
121
+ color: roleColor,
122
+ }}
123
+ >
124
+ {ROLE_LABELS[role] ?? role}
125
+ </span>
126
+ </DropdownMenuItem>
127
+
128
+ <DropdownMenuSeparator />
129
+
130
+ <DropdownMenuItem className="gap-2" onClick={handleLogout}>
131
+ <LogOut size={14} />
132
+ Logout
133
+ </DropdownMenuItem>
134
+ </DropdownMenuContent>
135
+ </DropdownMenu>
136
+ </>
137
+ );
138
+ }
@@ -0,0 +1,9 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Workspace switcher stub — no workspaces in stripped mode.
5
+ * Renders nothing, keeping the taskbar layout intact.
6
+ */
7
+ export function WorkspaceSwitcher() {
8
+ return <div className="flex items-center px-1.5" />;
9
+ }
@@ -0,0 +1,130 @@
1
+ 'use client';
2
+
3
+ import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';
4
+ import * as React from 'react';
5
+ import { cn } from '@/lib/utils';
6
+
7
+ const ContextMenu = ContextMenuPrimitive.Root;
8
+ const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
9
+ const ContextMenuGroup = ContextMenuPrimitive.Group;
10
+ const ContextMenuPortal = ContextMenuPrimitive.Portal;
11
+ const ContextMenuSub = ContextMenuPrimitive.Sub;
12
+ const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
13
+
14
+ const ContextMenuSubTrigger = React.forwardRef<
15
+ React.ComponentRef<typeof ContextMenuPrimitive.SubTrigger>,
16
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
17
+ inset?: boolean;
18
+ }
19
+ >(({ className, inset, children, ...props }, ref) => (
20
+ <ContextMenuPrimitive.SubTrigger
21
+ ref={ref}
22
+ className={cn(
23
+ 'flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-copy-13 outline-none select-none',
24
+ 'focus:bg-[var(--khal-menu-hover)]',
25
+ 'data-[state=open]:bg-[var(--khal-menu-hover)]',
26
+ inset && 'pl-8',
27
+ className
28
+ )}
29
+ style={{ color: 'var(--khal-text-primary)' }}
30
+ {...props}
31
+ >
32
+ {children}
33
+ </ContextMenuPrimitive.SubTrigger>
34
+ ));
35
+ ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
36
+
37
+ const ContextMenuSubContent = React.forwardRef<
38
+ React.ComponentRef<typeof ContextMenuPrimitive.SubContent>,
39
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
40
+ >(({ className, ...props }, ref) => (
41
+ <ContextMenuPrimitive.SubContent
42
+ ref={ref}
43
+ className={cn(
44
+ 'z-[9999] min-w-[8rem] overflow-hidden rounded-xl p-1',
45
+ 'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
46
+ 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
47
+ className
48
+ )}
49
+ style={{
50
+ background: 'var(--khal-menu-bg)',
51
+ border: '1px solid var(--khal-menu-border)',
52
+ boxShadow: 'var(--khal-menu-shadow)',
53
+ color: 'var(--khal-text-primary)',
54
+ }}
55
+ {...props}
56
+ />
57
+ ));
58
+ ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
59
+
60
+ const ContextMenuContent = React.forwardRef<
61
+ React.ComponentRef<typeof ContextMenuPrimitive.Content>,
62
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
63
+ >(({ className, ...props }, ref) => (
64
+ <ContextMenuPrimitive.Portal>
65
+ <ContextMenuPrimitive.Content
66
+ ref={ref}
67
+ className={cn(
68
+ 'z-[9999] min-w-[8rem] overflow-hidden rounded-xl p-1',
69
+ 'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
70
+ className
71
+ )}
72
+ style={{
73
+ background: 'var(--khal-menu-bg)',
74
+ border: '1px solid var(--khal-menu-border)',
75
+ boxShadow: 'var(--khal-menu-shadow)',
76
+ color: 'var(--khal-text-primary)',
77
+ }}
78
+ {...props}
79
+ />
80
+ </ContextMenuPrimitive.Portal>
81
+ ));
82
+ ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
83
+
84
+ const ContextMenuItem = React.forwardRef<
85
+ React.ComponentRef<typeof ContextMenuPrimitive.Item>,
86
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
87
+ inset?: boolean;
88
+ }
89
+ >(({ className, inset, ...props }, ref) => (
90
+ <ContextMenuPrimitive.Item
91
+ ref={ref}
92
+ className={cn(
93
+ 'relative flex cursor-default items-center gap-2 rounded-lg px-2 py-1.5 text-copy-13 outline-none select-none transition-colors',
94
+ 'focus:bg-[var(--khal-menu-hover)]',
95
+ 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
96
+ inset && 'pl-8',
97
+ className
98
+ )}
99
+ style={{ color: 'var(--khal-text-primary)' }}
100
+ {...props}
101
+ />
102
+ ));
103
+ ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
104
+
105
+ const ContextMenuSeparator = React.forwardRef<
106
+ React.ComponentRef<typeof ContextMenuPrimitive.Separator>,
107
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
108
+ >(({ className, ...props }, ref) => (
109
+ <ContextMenuPrimitive.Separator
110
+ ref={ref}
111
+ className={cn('-mx-1 my-1 h-px', className)}
112
+ style={{ background: 'var(--khal-border-default)' }}
113
+ {...props}
114
+ />
115
+ ));
116
+ ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
117
+
118
+ export {
119
+ ContextMenu,
120
+ ContextMenuTrigger,
121
+ ContextMenuContent,
122
+ ContextMenuItem,
123
+ ContextMenuSeparator,
124
+ ContextMenuGroup,
125
+ ContextMenuPortal,
126
+ ContextMenuSub,
127
+ ContextMenuSubContent,
128
+ ContextMenuSubTrigger,
129
+ ContextMenuRadioGroup,
130
+ };
@@ -0,0 +1,39 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority';
2
+ import * as React from 'react';
3
+ import { cn } from '@/lib/utils';
4
+
5
+ const badgeVariants = cva(
6
+ 'inline-flex items-center rounded-full border font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-blue-700 focus:ring-offset-2',
7
+ {
8
+ variants: {
9
+ variant: {
10
+ gray: 'border-gray-alpha-400 bg-gray-100 text-gray-900',
11
+ blue: 'border-blue-300 bg-blue-100 text-blue-900',
12
+ green: 'border-green-300 bg-green-100 text-green-900',
13
+ amber: 'border-amber-300 bg-amber-100 text-amber-900',
14
+ red: 'border-red-300 bg-red-100 text-red-900',
15
+ purple: 'border-purple-300 bg-purple-100 text-purple-900',
16
+ pink: 'border-pink-300 bg-pink-100 text-pink-900',
17
+ teal: 'border-teal-300 bg-teal-100 text-teal-900',
18
+ },
19
+ size: {
20
+ sm: 'px-2 py-0 text-[11px] leading-[18px]',
21
+ md: 'px-2.5 py-0.5 text-[12px] leading-[18px]',
22
+ },
23
+ },
24
+ defaultVariants: {
25
+ variant: 'gray',
26
+ size: 'md',
27
+ },
28
+ }
29
+ );
30
+
31
+ interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof badgeVariants> {
32
+ contrast?: 'low' | 'high';
33
+ }
34
+
35
+ function Badge({ className, variant, size, contrast, ...props }: BadgeProps) {
36
+ return <span className={cn(badgeVariants({ variant, size }), className)} {...props} />;
37
+ }
38
+
39
+ export { Badge, badgeVariants };
@@ -0,0 +1,102 @@
1
+ 'use client';
2
+
3
+ import { Slot } from '@radix-ui/react-slot';
4
+ import { cva, type VariantProps } from 'class-variance-authority';
5
+ import * as React from 'react';
6
+ import { cn } from '@/lib/utils';
7
+ import { Spinner } from './spinner';
8
+
9
+ const buttonVariants = cva(
10
+ 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-copy-13 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--khal-accent-primary)] focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 cursor-pointer',
11
+ {
12
+ variants: {
13
+ variant: {
14
+ default: 'bg-gray-1000 text-white [color:white] dark:text-black dark:[color:black] hover:bg-gray-900',
15
+ secondary: 'bg-background-100 text-gray-1000 border border-gray-alpha-400 hover:bg-gray-alpha-100',
16
+ tertiary: 'bg-transparent text-gray-1000 hover:bg-gray-alpha-200',
17
+ error: 'bg-red-700 text-white [color:white] hover:bg-red-600',
18
+ warning: 'bg-amber-700 text-white [color:white] hover:bg-amber-600',
19
+ ghost: 'hover:bg-gray-alpha-200 text-gray-1000',
20
+ link: 'text-blue-700 underline-offset-4 hover:underline',
21
+ },
22
+ size: {
23
+ small: 'h-8 px-3 text-copy-13',
24
+ medium: 'h-9 px-4 text-copy-13',
25
+ large: 'h-10 px-5 text-copy-14',
26
+ icon: 'h-9 w-9',
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: 'default',
31
+ size: 'medium',
32
+ },
33
+ }
34
+ );
35
+
36
+ type ButtonVariantProps = VariantProps<typeof buttonVariants>;
37
+
38
+ interface ButtonProps
39
+ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'type' | 'prefix'>,
40
+ ButtonVariantProps {
41
+ asChild?: boolean;
42
+ loading?: boolean;
43
+ prefix?: React.ReactNode;
44
+ suffix?: React.ReactNode;
45
+ /** HTML button type attribute (named typeName for compat with geistcn API) */
46
+ typeName?: 'submit' | 'button' | 'reset';
47
+ /** Visual type — maps to variant for compat */
48
+ type?: 'shadow' | 'invert' | 'unstyled';
49
+ }
50
+
51
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
52
+ (
53
+ {
54
+ className,
55
+ variant,
56
+ size,
57
+ asChild = false,
58
+ loading = false,
59
+ prefix,
60
+ suffix,
61
+ typeName,
62
+ type,
63
+ disabled,
64
+ children,
65
+ ...props
66
+ },
67
+ ref
68
+ ) => {
69
+ // Map geistcn "type" prop to variant if variant not explicitly set
70
+ let resolvedVariant = variant;
71
+ if (!resolvedVariant && type) {
72
+ if (type === 'invert') resolvedVariant = 'default';
73
+ else if (type === 'shadow') resolvedVariant = 'secondary';
74
+ else if (type === 'unstyled') resolvedVariant = 'ghost';
75
+ }
76
+
77
+ const Comp = asChild ? Slot : 'button';
78
+ return (
79
+ <Comp
80
+ className={cn(buttonVariants({ variant: resolvedVariant, size, className }))}
81
+ ref={ref}
82
+ type={typeName ?? 'button'}
83
+ disabled={disabled || loading}
84
+ {...props}
85
+ >
86
+ {loading ? (
87
+ <Spinner size="sm" />
88
+ ) : (
89
+ <>
90
+ {prefix && <span className="inline-flex shrink-0">{prefix}</span>}
91
+ {children}
92
+ {suffix && <span className="inline-flex shrink-0">{suffix}</span>}
93
+ </>
94
+ )}
95
+ </Comp>
96
+ );
97
+ }
98
+ );
99
+ Button.displayName = 'Button';
100
+
101
+ export { Button, buttonVariants };
102
+ export type { ButtonProps };