kcg-code 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/README.md +230 -2
  2. package/bin/kcgcode.ts +13 -0
  3. package/bun-env.d.ts +19 -0
  4. package/kcg-code.config.example.json +3 -0
  5. package/package.json +84 -4
  6. package/public/logo.svg +1 -0
  7. package/public/manifest.json +18 -0
  8. package/public/sw.js +49 -0
  9. package/src/App.tsx +39 -0
  10. package/src/__tests__/manifest.test.ts +61 -0
  11. package/src/build-config.ts +9 -0
  12. package/src/cli/__tests__/daemon.test.ts +84 -0
  13. package/src/cli/__tests__/dashboard.test.ts +224 -0
  14. package/src/cli/__tests__/keep-awake.test.ts +44 -0
  15. package/src/cli/__tests__/port.test.ts +86 -0
  16. package/src/cli/args.ts +186 -0
  17. package/src/cli/attach.ts +154 -0
  18. package/src/cli/commands.ts +327 -0
  19. package/src/cli/daemon.ts +275 -0
  20. package/src/cli/dashboard.tsx +1060 -0
  21. package/src/cli/index.ts +47 -0
  22. package/src/cli/keep-awake.ts +148 -0
  23. package/src/cli/port.ts +92 -0
  24. package/src/cli/theme.ts +100 -0
  25. package/src/components/auth/AuthGate.tsx +182 -0
  26. package/src/components/auth/AvatarEditor.tsx +236 -0
  27. package/src/components/auth/LockScreen.tsx +714 -0
  28. package/src/components/auth/ProfileAvatar.tsx +60 -0
  29. package/src/components/auth/WelcomeScreen.tsx +672 -0
  30. package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
  31. package/src/components/auth/avatar-icons.tsx +114 -0
  32. package/src/components/layout/AppShell.tsx +147 -0
  33. package/src/components/layout/AppSidebar.tsx +419 -0
  34. package/src/components/layout/SearchDialog.tsx +189 -0
  35. package/src/components/layout/SidebarAccount.tsx +111 -0
  36. package/src/components/layout/SidebarSessionMenu.tsx +163 -0
  37. package/src/components/layout/ThemeToggle.tsx +67 -0
  38. package/src/components/projects/FolderBrowser.tsx +142 -0
  39. package/src/components/projects/HomeChat.tsx +287 -0
  40. package/src/components/projects/HomeRecentSessions.tsx +90 -0
  41. package/src/components/projects/NewProjectDialog.tsx +249 -0
  42. package/src/components/projects/ProjectInstructions.tsx +218 -0
  43. package/src/components/projects/ProjectRow.tsx +58 -0
  44. package/src/components/projects/ProjectSidePanel.tsx +514 -0
  45. package/src/components/sessions/AgentLogo.tsx +42 -0
  46. package/src/components/sessions/AgentModeList.tsx +265 -0
  47. package/src/components/sessions/AgentPicker.tsx +132 -0
  48. package/src/components/sessions/AssistantTurn.tsx +145 -0
  49. package/src/components/sessions/AttachmentTile.tsx +161 -0
  50. package/src/components/sessions/CodeBlock.tsx +107 -0
  51. package/src/components/sessions/ComposerActionSheet.tsx +96 -0
  52. package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
  53. package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +92 -0
  54. package/src/components/sessions/ContextPanel.tsx +661 -0
  55. package/src/components/sessions/DropPlate.tsx +165 -0
  56. package/src/components/sessions/FileDropOverlay.tsx +232 -0
  57. package/src/components/sessions/FileMentionDropdown.tsx +47 -0
  58. package/src/components/sessions/ImageLightbox.tsx +173 -0
  59. package/src/components/sessions/MarkdownContent.tsx +187 -0
  60. package/src/components/sessions/Mascot.tsx +47 -0
  61. package/src/components/sessions/MessageActions.tsx +60 -0
  62. package/src/components/sessions/MessageRail.tsx +121 -0
  63. package/src/components/sessions/ModelPicker.tsx +247 -0
  64. package/src/components/sessions/ModelSearchList.tsx +231 -0
  65. package/src/components/sessions/NewSessionDialog.tsx +172 -0
  66. package/src/components/sessions/PendingAttachments.tsx +127 -0
  67. package/src/components/sessions/PromptCard.tsx +1085 -0
  68. package/src/components/sessions/PromptPanel.tsx +112 -0
  69. package/src/components/sessions/SessionComposer.tsx +641 -0
  70. package/src/components/sessions/SessionHeader.tsx +176 -0
  71. package/src/components/sessions/SessionList.tsx +503 -0
  72. package/src/components/sessions/SessionRow.tsx +237 -0
  73. package/src/components/sessions/SessionTimeline.tsx +275 -0
  74. package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
  75. package/src/components/sessions/SessionView.tsx +240 -0
  76. package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
  77. package/src/components/sessions/UserMessage.tsx +177 -0
  78. package/src/components/sessions/__tests__/ContextPanel.test.tsx +256 -0
  79. package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
  80. package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
  81. package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
  82. package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
  83. package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
  84. package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
  85. package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
  86. package/src/components/settings/AboutGroup.tsx +170 -0
  87. package/src/components/settings/AccountGroup.tsx +122 -0
  88. package/src/components/settings/DevicesGroup.tsx +151 -0
  89. package/src/components/settings/LhrRemoteAccess.tsx +257 -0
  90. package/src/components/settings/PasskeysGroup.tsx +551 -0
  91. package/src/components/settings/RemoteAccessGroup.tsx +1201 -0
  92. package/src/components/settings/SecurityGroup.tsx +566 -0
  93. package/src/components/settings/WorkspaceGroup.tsx +370 -0
  94. package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
  95. package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
  96. package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
  97. package/src/components/settings/prefs.tsx +333 -0
  98. package/src/components/skills/InstallNotifications.tsx +326 -0
  99. package/src/components/skills/InstallSkillDialog.tsx +390 -0
  100. package/src/components/skills/SkillCards.tsx +254 -0
  101. package/src/components/skills/SkillsProjectPicker.tsx +178 -0
  102. package/src/components/ui/adw-dialog.tsx +114 -0
  103. package/src/components/ui/adw-toggle-group.tsx +75 -0
  104. package/src/components/ui/alert-dialog.tsx +179 -0
  105. package/src/components/ui/alert.tsx +60 -0
  106. package/src/components/ui/badge.tsx +46 -0
  107. package/src/components/ui/bubble.tsx +125 -0
  108. package/src/components/ui/button.tsx +62 -0
  109. package/src/components/ui/card.tsx +75 -0
  110. package/src/components/ui/collapsible.tsx +19 -0
  111. package/src/components/ui/dialog.tsx +142 -0
  112. package/src/components/ui/dropdown-menu.tsx +225 -0
  113. package/src/components/ui/empty.tsx +94 -0
  114. package/src/components/ui/field.tsx +234 -0
  115. package/src/components/ui/input-group.tsx +161 -0
  116. package/src/components/ui/input.tsx +21 -0
  117. package/src/components/ui/label.tsx +19 -0
  118. package/src/components/ui/message-scroller.tsx +131 -0
  119. package/src/components/ui/message.tsx +85 -0
  120. package/src/components/ui/popover.tsx +46 -0
  121. package/src/components/ui/search-entry.tsx +70 -0
  122. package/src/components/ui/select.tsx +172 -0
  123. package/src/components/ui/separator.tsx +28 -0
  124. package/src/components/ui/sheet.tsx +138 -0
  125. package/src/components/ui/sidebar.tsx +699 -0
  126. package/src/components/ui/skeleton.tsx +15 -0
  127. package/src/components/ui/sonner.tsx +45 -0
  128. package/src/components/ui/spinner.tsx +16 -0
  129. package/src/components/ui/switch.tsx +32 -0
  130. package/src/components/ui/textarea.tsx +18 -0
  131. package/src/components/ui/tooltip.tsx +51 -0
  132. package/src/config.ts +114 -0
  133. package/src/db.ts +146 -0
  134. package/src/frontend.tsx +51 -0
  135. package/src/hooks/__tests__/useMention.test.ts +79 -0
  136. package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
  137. package/src/hooks/__tests__/useRouter.test.ts +17 -0
  138. package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
  139. package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
  140. package/src/hooks/__tests__/useTheme.test.ts +118 -0
  141. package/src/hooks/use-mobile.ts +16 -0
  142. package/src/hooks/useAgentPicker.ts +101 -0
  143. package/src/hooks/useMediaQuery.ts +49 -0
  144. package/src/hooks/useMention.ts +236 -0
  145. package/src/hooks/useProjects.ts +88 -0
  146. package/src/hooks/useResyncOnReconnect.ts +28 -0
  147. package/src/hooks/useRouter.ts +92 -0
  148. package/src/hooks/useSessionChat.ts +427 -0
  149. package/src/hooks/useSessionContext.ts +106 -0
  150. package/src/hooks/useSessionList.ts +327 -0
  151. package/src/hooks/useSidebarData.ts +108 -0
  152. package/src/hooks/useSmoothText.ts +122 -0
  153. package/src/hooks/useTheme.ts +164 -0
  154. package/src/hooks/useWebSocket.ts +196 -0
  155. package/src/index.css +27 -0
  156. package/src/index.html +14 -0
  157. package/src/index.ts +49 -0
  158. package/src/lib/__tests__/attachments.test.ts +58 -0
  159. package/src/lib/__tests__/avatar-presets.test.ts +25 -0
  160. package/src/lib/__tests__/collapsible.test.ts +101 -0
  161. package/src/lib/__tests__/composer.test.ts +118 -0
  162. package/src/lib/__tests__/highlight.test.ts +42 -0
  163. package/src/lib/__tests__/home-chat.test.ts +64 -0
  164. package/src/lib/__tests__/lock-screen.test.ts +64 -0
  165. package/src/lib/__tests__/model-picker.test.ts +205 -0
  166. package/src/lib/__tests__/onboarding.test.ts +41 -0
  167. package/src/lib/__tests__/permission-info.test.ts +61 -0
  168. package/src/lib/__tests__/project-extensions.test.ts +83 -0
  169. package/src/lib/__tests__/project-overview.test.ts +188 -0
  170. package/src/lib/__tests__/references.test.ts +121 -0
  171. package/src/lib/__tests__/routes.test.ts +134 -0
  172. package/src/lib/__tests__/session-selection.test.ts +41 -0
  173. package/src/lib/__tests__/session-summary.test.ts +115 -0
  174. package/src/lib/__tests__/session-title.test.ts +58 -0
  175. package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
  176. package/src/lib/__tests__/skill-installs.test.ts +135 -0
  177. package/src/lib/__tests__/skills-catalog.test.ts +124 -0
  178. package/src/lib/__tests__/tone.test.ts +37 -0
  179. package/src/lib/__tests__/turns-delta.test.ts +55 -0
  180. package/src/lib/agent-types.ts +49 -0
  181. package/src/lib/api.ts +306 -0
  182. package/src/lib/attachments.ts +203 -0
  183. package/src/lib/auth.ts +92 -0
  184. package/src/lib/avatar-presets.ts +31 -0
  185. package/src/lib/collapsible.ts +75 -0
  186. package/src/lib/composer.ts +89 -0
  187. package/src/lib/data-events.ts +17 -0
  188. package/src/lib/headlines.ts +30 -0
  189. package/src/lib/highlight.ts +186 -0
  190. package/src/lib/layout.ts +20 -0
  191. package/src/lib/lock-screen.ts +54 -0
  192. package/src/lib/model-picker.ts +133 -0
  193. package/src/lib/onboarding.ts +38 -0
  194. package/src/lib/passkeys.ts +113 -0
  195. package/src/lib/pending-prompt.ts +53 -0
  196. package/src/lib/permission-info.ts +101 -0
  197. package/src/lib/project-extensions.ts +136 -0
  198. package/src/lib/project-overview.ts +123 -0
  199. package/src/lib/prompts.ts +87 -0
  200. package/src/lib/references.ts +106 -0
  201. package/src/lib/routes.ts +124 -0
  202. package/src/lib/session-handoff.ts +25 -0
  203. package/src/lib/session-selection.ts +40 -0
  204. package/src/lib/session-status.ts +45 -0
  205. package/src/lib/session-summary.ts +68 -0
  206. package/src/lib/session-title.ts +63 -0
  207. package/src/lib/sidebar-groups.ts +78 -0
  208. package/src/lib/skill-installs.ts +362 -0
  209. package/src/lib/skills-catalog.ts +138 -0
  210. package/src/lib/time.ts +22 -0
  211. package/src/lib/tone.ts +80 -0
  212. package/src/lib/turns.ts +320 -0
  213. package/src/lib/utils.ts +6 -0
  214. package/src/logo.svg +31 -0
  215. package/src/pages/projects/ProjectDetailPage.tsx +131 -0
  216. package/src/pages/projects/ProjectsPage.tsx +97 -0
  217. package/src/pages/sessions/SessionPage.tsx +122 -0
  218. package/src/pages/settings/SettingsPage.tsx +366 -0
  219. package/src/pages/settings/__tests__/SettingsPage.test.ts +16 -0
  220. package/src/pages/skills/SkillsPage.tsx +745 -0
  221. package/src/paths.ts +121 -0
  222. package/src/runtime.ts +24 -0
  223. package/src/server/__tests__/app.test.ts +1044 -0
  224. package/src/server/__tests__/auth.test.ts +370 -0
  225. package/src/server/__tests__/config.test.ts +101 -0
  226. package/src/server/__tests__/db.test.ts +485 -0
  227. package/src/server/__tests__/host.test.ts +14 -0
  228. package/src/server/__tests__/tunnel-config.test.ts +38 -0
  229. package/src/server/app.ts +427 -0
  230. package/src/server/db/auth.ts +311 -0
  231. package/src/server/db/messages.ts +50 -0
  232. package/src/server/db/projects.ts +106 -0
  233. package/src/server/db/prompts.ts +78 -0
  234. package/src/server/db/rows.ts +150 -0
  235. package/src/server/db/schema.ts +221 -0
  236. package/src/server/db/sessions.ts +239 -0
  237. package/src/server/db/tunnel.ts +124 -0
  238. package/src/server/host.ts +12 -0
  239. package/src/server/result.ts +15 -0
  240. package/src/server/routes/auth-guard.ts +156 -0
  241. package/src/server/routes/auth.routes.ts +287 -0
  242. package/src/server/routes/helpers.ts +165 -0
  243. package/src/server/routes/meta.routes.ts +24 -0
  244. package/src/server/routes/passkeys.routes.ts +161 -0
  245. package/src/server/routes/projects.routes.ts +167 -0
  246. package/src/server/routes/sessions.routes.ts +197 -0
  247. package/src/server/routes/skills.routes.ts +136 -0
  248. package/src/server/routes/tunnel.routes.ts +132 -0
  249. package/src/server/routes/types.ts +35 -0
  250. package/src/server/routes/uploads.routes.ts +54 -0
  251. package/src/server/routes/workspace.routes.ts +127 -0
  252. package/src/server/services/__tests__/app-info.test.ts +18 -0
  253. package/src/server/services/__tests__/attachments.test.ts +159 -0
  254. package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
  255. package/src/server/services/__tests__/auth.test.ts +341 -0
  256. package/src/server/services/__tests__/claude-code.test.ts +609 -0
  257. package/src/server/services/__tests__/claude-mcp.test.ts +90 -0
  258. package/src/server/services/__tests__/claude-skills.test.ts +63 -0
  259. package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
  260. package/src/server/services/__tests__/git-info.test.ts +74 -0
  261. package/src/server/services/__tests__/opencode.test.ts +320 -0
  262. package/src/server/services/__tests__/project-manager.test.ts +190 -0
  263. package/src/server/services/__tests__/sandbox.test.ts +141 -0
  264. package/src/server/services/__tests__/session-manager.test.ts +3509 -0
  265. package/src/server/services/__tests__/skill-install-jobs.test.ts +245 -0
  266. package/src/server/services/__tests__/skills-registry.test.ts +365 -0
  267. package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
  268. package/src/server/services/__tests__/tunnel.test.ts +367 -0
  269. package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
  270. package/src/server/services/__tests__/workspace.test.ts +249 -0
  271. package/src/server/services/app-info.ts +29 -0
  272. package/src/server/services/attachments.ts +193 -0
  273. package/src/server/services/auth-passkeys.ts +459 -0
  274. package/src/server/services/auth.ts +633 -0
  275. package/src/server/services/claude-code-client.ts +1093 -0
  276. package/src/server/services/claude-code-translate.ts +370 -0
  277. package/src/server/services/claude-mcp.ts +122 -0
  278. package/src/server/services/claude-skills.ts +102 -0
  279. package/src/server/services/frpc-installer.ts +183 -0
  280. package/src/server/services/git-info.ts +87 -0
  281. package/src/server/services/opencode-client.ts +714 -0
  282. package/src/server/services/opencode-server.ts +319 -0
  283. package/src/server/services/opencode-sse.ts +82 -0
  284. package/src/server/services/project-manager.ts +147 -0
  285. package/src/server/services/sandbox.ts +83 -0
  286. package/src/server/services/session-events.ts +256 -0
  287. package/src/server/services/session-manager.ts +1439 -0
  288. package/src/server/services/skill-install-jobs.ts +275 -0
  289. package/src/server/services/skills-registry.ts +480 -0
  290. package/src/server/services/tunnel-api.ts +207 -0
  291. package/src/server/services/tunnel-config.ts +45 -0
  292. package/src/server/services/tunnel-lhr.ts +387 -0
  293. package/src/server/services/tunnel.ts +633 -0
  294. package/src/server/services/turn-stream.ts +339 -0
  295. package/src/server/services/websocket-gateway.ts +374 -0
  296. package/src/server/services/workspace.ts +223 -0
  297. package/src/server/services/ws-transport.ts +121 -0
  298. package/src/types/context.ts +69 -0
  299. package/src/types/index.ts +13 -0
  300. package/src/types/message.ts +58 -0
  301. package/src/types/project.ts +16 -0
  302. package/src/types/prompt.ts +118 -0
  303. package/src/types/session.ts +52 -0
  304. package/src/types/turns.ts +47 -0
  305. package/src/ws-protocol.ts +93 -0
  306. package/styles/globals.css +311 -0
  307. package/tsconfig.json +37 -0
@@ -0,0 +1,672 @@
1
+ /**
2
+ * Welcome screen untuk instalasi baru (`AuthStatus.needsOnboarding`).
3
+ *
4
+ * Alur (lihat `lib/onboarding.ts`):
5
+ * 1. Welcome — logo + nama app + tagline, tombol "Get started".
6
+ * 2. Profile — nickname (opsional).
7
+ * 3. Lock — pasang PIN/password (direkomendasikan, bisa dilewati).
8
+ * 4. Done — ringkasan singkat, lalu masuk ke app.
9
+ *
10
+ * Animasi (Motion, otomatis lebih sederhana bila prefers-reduced-motion —
11
+ * `MotionConfig reducedMotion="user"` di App):
12
+ * - Masuk: logo "mendarat" dengan pegas lembut, teks muncul bertahap.
13
+ * - Antar langkah: geser + pudar searah navigasi (maju ke kiri, mundur ke
14
+ * kanan), tinggi kartu tidak melompat karena konten ditumpuk satu per satu
15
+ * dengan `mode="wait"` dan durasi pendek.
16
+ * - Keluar: seluruh layar memudar & sedikit membesar, app di bawahnya
17
+ * (dirender `AuthGate`) muncul dari baliknya.
18
+ *
19
+ * Gaya mengikuti halaman lain (libadwaita): kartu boxed list, tombol pil.
20
+ */
21
+ import {
22
+ ArrowLeftIcon,
23
+ ArrowRightIcon,
24
+ CheckIcon,
25
+ LockIcon,
26
+ ShieldCheckIcon,
27
+ SparklesIcon,
28
+ UserRoundIcon,
29
+ } from "lucide-react";
30
+ import { AnimatePresence } from "motion/react";
31
+ import * as m from "motion/react-m";
32
+ import { useEffect, useId, useRef, useState } from "react";
33
+ import { Button } from "@/components/ui/button";
34
+ import { Spinner } from "@/components/ui/spinner";
35
+ import { ApiError, apiFetch } from "@/lib/api";
36
+ import { refreshAuth } from "@/lib/auth";
37
+ import {
38
+ greeting,
39
+ nextStep,
40
+ type OnboardingStep,
41
+ PROGRESS_STEPS,
42
+ prevStep,
43
+ stepDirection,
44
+ } from "@/lib/onboarding";
45
+ import { cn } from "@/lib/utils";
46
+ import type { AuthStatus } from "@/server/services/auth";
47
+ import logo from "../../logo.svg";
48
+
49
+ type LockKind = "pin" | "password";
50
+
51
+ /** Kurva "mendarat halus" yang sama dengan animasi lain di app. */
52
+ const EASE: [number, number, number, number] = [0.32, 0.72, 0, 1];
53
+
54
+ export function WelcomeScreen({
55
+ status,
56
+ onFinished,
57
+ }: {
58
+ status: AuthStatus;
59
+ /** Dipanggil setelah onboarding ditandai selesai — AuthGate membuka app. */
60
+ onFinished: () => void;
61
+ }) {
62
+ const [step, setStep] = useState<OnboardingStep>("welcome");
63
+ const [dir, setDir] = useState<1 | -1>(1);
64
+ const [nickname, setNickname] = useState(status.profile.nickname ?? "");
65
+ /** Hasil langkah kunci: jenis kunci yang dipasang, atau null = dilewati. */
66
+ const [lockSet, setLockSet] = useState<LockKind | null>(null);
67
+ const [finishing, setFinishing] = useState(false);
68
+
69
+ const go = (to: OnboardingStep) => {
70
+ setDir(stepDirection(step, to));
71
+ setStep(to);
72
+ };
73
+
74
+ /** Selesai: tandai di server, muat ulang status, lalu AuthGate membuka app. */
75
+ const finish = async () => {
76
+ setFinishing(true);
77
+ try {
78
+ await apiFetch("/api/auth/onboarding/complete", { method: "POST" });
79
+ } catch {
80
+ // Gagal menandai tidak boleh mengurung pengguna di welcome screen.
81
+ }
82
+ await refreshAuth();
83
+ onFinished();
84
+ };
85
+
86
+ return (
87
+ <div
88
+ role="dialog"
89
+ aria-modal="true"
90
+ aria-labelledby="welcome-title"
91
+ className="fixed inset-0 z-[90] flex flex-col overflow-y-auto bg-background"
92
+ >
93
+ {/* Cahaya lembut di atas (satu aksen, sama dengan halaman About). */}
94
+ <div
95
+ aria-hidden
96
+ className="pointer-events-none fixed inset-x-0 top-0 h-[60dvh] bg-[radial-gradient(60%_50%_at_50%_0%,rgba(109,92,251,0.10),transparent_70%)] dark:bg-[radial-gradient(60%_50%_at_50%_0%,rgba(109,92,251,0.18),transparent_70%)]"
97
+ />
98
+
99
+ <div className="relative mx-auto flex w-full max-w-md flex-1 flex-col px-6 pt-[max(2rem,env(safe-area-inset-top))] pb-[max(1.5rem,env(safe-area-inset-bottom))]">
100
+ {/* Progres: hanya untuk langkah pengaturan. */}
101
+ <div className="flex h-8 items-center justify-between">
102
+ {step !== "welcome" && step !== "done" ? (
103
+ <Button
104
+ type="button"
105
+ variant="ghost"
106
+ size="icon"
107
+ onClick={() => go(prevStep(step))}
108
+ aria-label="Back"
109
+ className="-ml-2 size-9 rounded-full"
110
+ >
111
+ <ArrowLeftIcon className="size-4" />
112
+ </Button>
113
+ ) : (
114
+ <span />
115
+ )}
116
+ <StepDots step={step} />
117
+ <span className="size-9" />
118
+ </div>
119
+
120
+ <div className="flex flex-1 flex-col justify-center py-6">
121
+ <AnimatePresence mode="wait" initial={false} custom={dir}>
122
+ <m.div
123
+ key={step}
124
+ custom={dir}
125
+ initial={{ opacity: 0, x: dir * 28 }}
126
+ animate={{ opacity: 1, x: 0, transition: { duration: 0.32, ease: EASE } }}
127
+ exit={{ opacity: 0, x: dir * -28, transition: { duration: 0.18, ease: "easeIn" } }}
128
+ >
129
+ {step === "welcome" && (
130
+ <WelcomeStep
131
+ onStart={() => go("profile")}
132
+ onSkip={() => void finish()}
133
+ finishing={finishing}
134
+ />
135
+ )}
136
+ {step === "profile" && (
137
+ <ProfileStep
138
+ value={nickname}
139
+ onChange={setNickname}
140
+ onNext={() => go(nextStep("profile"))}
141
+ />
142
+ )}
143
+ {step === "lock" && (
144
+ <LockStep
145
+ onDone={(kind) => {
146
+ setLockSet(kind);
147
+ go("done");
148
+ }}
149
+ />
150
+ )}
151
+ {step === "done" && (
152
+ <DoneStep
153
+ nickname={nickname.trim() || null}
154
+ lockSet={lockSet}
155
+ finishing={finishing}
156
+ onFinish={() => void finish()}
157
+ />
158
+ )}
159
+ </m.div>
160
+ </AnimatePresence>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ );
165
+ }
166
+
167
+ function StepDots({ step }: { step: OnboardingStep }) {
168
+ if (step === "welcome" || step === "done") return <span />;
169
+ const current = PROGRESS_STEPS.indexOf(step);
170
+ return (
171
+ <ol className="flex items-center gap-1.5" aria-label="Setup progress">
172
+ {PROGRESS_STEPS.map((s, i) => (
173
+ <li
174
+ key={s}
175
+ aria-current={i === current ? "step" : undefined}
176
+ className={cn(
177
+ "h-1.5 rounded-full transition-all duration-300",
178
+ i === current
179
+ ? "w-6 bg-primary"
180
+ : i < current
181
+ ? "w-1.5 bg-primary/50"
182
+ : "w-1.5 bg-muted",
183
+ )}
184
+ >
185
+ <span className="sr-only">
186
+ Step {i + 1} of {PROGRESS_STEPS.length}
187
+ </span>
188
+ </li>
189
+ ))}
190
+ </ol>
191
+ );
192
+ }
193
+
194
+ // ---------------------------------------------------------------- langkah ----
195
+
196
+ /** Teks bertahap: tiap anak muncul sedikit setelah yang sebelumnya. */
197
+ function Stagger({ i, children }: { i: number; children: React.ReactNode }) {
198
+ return (
199
+ <m.div
200
+ initial={{ opacity: 0, y: 10 }}
201
+ animate={{
202
+ opacity: 1,
203
+ y: 0,
204
+ transition: { delay: 0.15 + i * 0.08, duration: 0.4, ease: EASE },
205
+ }}
206
+ >
207
+ {children}
208
+ </m.div>
209
+ );
210
+ }
211
+
212
+ function WelcomeStep({
213
+ onStart,
214
+ onSkip,
215
+ finishing,
216
+ }: {
217
+ onStart: () => void;
218
+ onSkip: () => void;
219
+ finishing: boolean;
220
+ }) {
221
+ const startRef = useRef<HTMLButtonElement>(null);
222
+ useEffect(() => {
223
+ const t = setTimeout(() => startRef.current?.focus({ preventScroll: true }), 400);
224
+ return () => clearTimeout(t);
225
+ }, []);
226
+
227
+ return (
228
+ <div className="flex flex-col items-center text-center">
229
+ <m.span
230
+ initial={{ opacity: 0, scale: 0.8, y: 12 }}
231
+ animate={{
232
+ opacity: 1,
233
+ scale: 1,
234
+ y: 0,
235
+ transition: { type: "spring", stiffness: 260, damping: 22 },
236
+ }}
237
+ className="flex size-24 items-center justify-center rounded-[1.75rem] border bg-card p-5 shadow-sm sm:size-28"
238
+ >
239
+ <img src={logo} alt="" className="size-full" />
240
+ </m.span>
241
+
242
+ <Stagger i={0}>
243
+ <p className="mt-8 text-sm font-medium text-muted-foreground">
244
+ {greeting(new Date().getHours())}
245
+ </p>
246
+ </Stagger>
247
+ <Stagger i={1}>
248
+ <h1 id="welcome-title" className="mt-1 text-3xl font-semibold tracking-tight sm:text-4xl">
249
+ Welcome to KCG Code
250
+ </h1>
251
+ </Stagger>
252
+ <Stagger i={2}>
253
+ <p className="mt-3 max-w-xs text-[15px] leading-relaxed text-pretty text-muted-foreground">
254
+ Run your AI coding agents from any device. Let's get you set up. It takes less than a
255
+ minute.
256
+ </p>
257
+ </Stagger>
258
+
259
+ <Stagger i={3}>
260
+ <ul className="mt-8 flex flex-col gap-3 text-left">
261
+ {[
262
+ { icon: UserRoundIcon, text: "Pick a name for this workspace" },
263
+ { icon: LockIcon, text: "Protect it with a PIN or password" },
264
+ { icon: SparklesIcon, text: "Start your first session" },
265
+ ].map(({ icon: Icon, text }) => (
266
+ <li key={text} className="flex items-center gap-3 text-[14px]">
267
+ <span className="flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
268
+ <Icon className="size-4" aria-hidden />
269
+ </span>
270
+ {text}
271
+ </li>
272
+ ))}
273
+ </ul>
274
+ </Stagger>
275
+
276
+ <Stagger i={4}>
277
+ <div className="mt-10 flex w-full flex-col items-center gap-2">
278
+ <Button
279
+ ref={startRef}
280
+ type="button"
281
+ onClick={onStart}
282
+ className="h-12 w-64 max-w-full rounded-full text-[15px]"
283
+ >
284
+ Get started
285
+ <ArrowRightIcon data-icon="inline-end" />
286
+ </Button>
287
+ <Button
288
+ type="button"
289
+ variant="ghost"
290
+ onClick={onSkip}
291
+ disabled={finishing}
292
+ className="h-10 rounded-full text-muted-foreground"
293
+ >
294
+ {finishing && <Spinner data-icon="inline-start" />}
295
+ Skip for now
296
+ </Button>
297
+ </div>
298
+ </Stagger>
299
+ </div>
300
+ );
301
+ }
302
+
303
+ function StepHeader({
304
+ icon: Icon,
305
+ title,
306
+ description,
307
+ }: {
308
+ icon: typeof UserRoundIcon;
309
+ title: string;
310
+ description: string;
311
+ }) {
312
+ return (
313
+ <div className="flex flex-col items-center gap-3 text-center">
314
+ <span className="flex size-14 items-center justify-center rounded-2xl bg-primary/10 text-primary">
315
+ <Icon className="size-7" aria-hidden />
316
+ </span>
317
+ <div className="flex flex-col gap-1.5">
318
+ <h1 id="welcome-title" className="text-2xl font-semibold tracking-tight">
319
+ {title}
320
+ </h1>
321
+ <p className="max-w-xs text-[14px] leading-snug text-pretty text-muted-foreground">
322
+ {description}
323
+ </p>
324
+ </div>
325
+ </div>
326
+ );
327
+ }
328
+
329
+ function ProfileStep({
330
+ value,
331
+ onChange,
332
+ onNext,
333
+ }: {
334
+ value: string;
335
+ onChange: (v: string) => void;
336
+ onNext: () => void;
337
+ }) {
338
+ const id = useId();
339
+ const inputRef = useRef<HTMLInputElement>(null);
340
+ const [saving, setSaving] = useState(false);
341
+ const [error, setError] = useState<string | null>(null);
342
+
343
+ useEffect(() => {
344
+ const t = setTimeout(() => inputRef.current?.focus({ preventScroll: true }), 350);
345
+ return () => clearTimeout(t);
346
+ }, []);
347
+
348
+ const submit = async (e: React.FormEvent) => {
349
+ e.preventDefault();
350
+ setSaving(true);
351
+ setError(null);
352
+ try {
353
+ await apiFetch("/api/auth/profile", {
354
+ method: "PATCH",
355
+ body: JSON.stringify({ nickname: value.trim() || null }),
356
+ });
357
+ onNext();
358
+ } catch (err) {
359
+ setError(err instanceof ApiError ? err.message : "Couldn't save your name.");
360
+ } finally {
361
+ setSaving(false);
362
+ }
363
+ };
364
+
365
+ return (
366
+ <form onSubmit={submit} className="flex flex-col gap-8">
367
+ <StepHeader
368
+ icon={UserRoundIcon}
369
+ title="What should we call you?"
370
+ description="Your name appears on the lock screen and in the sidebar. You can change it later."
371
+ />
372
+
373
+ <div className="flex flex-col gap-2">
374
+ <div className="overflow-hidden rounded-xl border bg-card shadow-xs focus-within:ring-[3px] focus-within:ring-ring/30">
375
+ <div className="flex flex-col px-4 py-2.5">
376
+ <label htmlFor={id} className="text-[12px] leading-4 text-muted-foreground">
377
+ Nickname
378
+ </label>
379
+ <input
380
+ ref={inputRef}
381
+ id={id}
382
+ value={value}
383
+ maxLength={40}
384
+ autoComplete="nickname"
385
+ placeholder="e.g. Irsyad"
386
+ onChange={(e) => {
387
+ onChange(e.target.value);
388
+ setError(null);
389
+ }}
390
+ aria-invalid={error ? true : undefined}
391
+ // 16px di HP: iOS tidak men-zoom saat fokus.
392
+ className="h-8 w-full bg-transparent text-base outline-none placeholder:text-muted-foreground/60 sm:text-[15px]"
393
+ />
394
+ </div>
395
+ </div>
396
+ {error && (
397
+ <p role="alert" className="px-1 text-sm text-destructive">
398
+ {error}
399
+ </p>
400
+ )}
401
+ </div>
402
+
403
+ <Button type="submit" disabled={saving} className="h-12 rounded-full text-[15px]">
404
+ {saving && <Spinner data-icon="inline-start" />}
405
+ {value.trim() ? "Continue" : "Skip"}
406
+ {!saving && <ArrowRightIcon data-icon="inline-end" />}
407
+ </Button>
408
+ </form>
409
+ );
410
+ }
411
+
412
+ function LockStep({ onDone }: { onDone: (kind: LockKind | null) => void }) {
413
+ const [kind, setKind] = useState<LockKind>("pin");
414
+ const [secret, setSecret] = useState("");
415
+ const [confirm, setConfirm] = useState("");
416
+ const [busy, setBusy] = useState(false);
417
+ const [error, setError] = useState<string | null>(null);
418
+ const secretId = useId();
419
+ const confirmId = useId();
420
+ const secretRef = useRef<HTMLInputElement>(null);
421
+
422
+ useEffect(() => {
423
+ const t = setTimeout(() => secretRef.current?.focus({ preventScroll: true }), 350);
424
+ return () => clearTimeout(t);
425
+ }, []);
426
+
427
+ const label = kind === "pin" ? "PIN" : "password";
428
+ const hint =
429
+ kind === "pin"
430
+ ? "6–12 digits. Avoid repeated or sequential digits."
431
+ : "At least 8 characters. A short phrase works well.";
432
+
433
+ const clean = (v: string) => (kind === "pin" ? v.replace(/\D/g, "") : v);
434
+
435
+ const submit = async (e: React.FormEvent) => {
436
+ e.preventDefault();
437
+ if (kind === "pin" && (secret.length < 6 || secret.length > 12)) {
438
+ setError("PIN must be 6–12 digits.");
439
+ return;
440
+ }
441
+ if (kind === "password" && secret.length < 8) {
442
+ setError("Password must be at least 8 characters.");
443
+ return;
444
+ }
445
+ if (secret !== confirm) {
446
+ setError(`The ${label}s don't match.`);
447
+ return;
448
+ }
449
+ setBusy(true);
450
+ setError(null);
451
+ try {
452
+ await apiFetch("/api/auth/lock", { method: "PUT", body: JSON.stringify({ kind, secret }) });
453
+ onDone(kind);
454
+ } catch (err) {
455
+ setError(err instanceof ApiError ? err.message : "Couldn't save the lock.");
456
+ } finally {
457
+ setBusy(false);
458
+ }
459
+ };
460
+
461
+ const field = (
462
+ id: string,
463
+ title: string,
464
+ value: string,
465
+ set: (v: string) => void,
466
+ ref?: React.Ref<HTMLInputElement>,
467
+ ) => (
468
+ <div className="flex flex-col px-4 py-2.5 focus-within:bg-muted/30">
469
+ <label htmlFor={id} className="text-[12px] leading-4 text-muted-foreground">
470
+ {title}
471
+ </label>
472
+ <input
473
+ ref={ref}
474
+ id={id}
475
+ type="password"
476
+ inputMode={kind === "pin" ? "numeric" : undefined}
477
+ autoComplete="new-password"
478
+ maxLength={kind === "pin" ? 12 : 128}
479
+ value={value}
480
+ disabled={busy}
481
+ onChange={(e) => {
482
+ set(clean(e.target.value));
483
+ setError(null);
484
+ }}
485
+ className={cn(
486
+ "h-8 w-full bg-transparent text-base outline-none sm:text-[15px]",
487
+ kind === "pin" && "tracking-[0.3em]",
488
+ )}
489
+ />
490
+ </div>
491
+ );
492
+
493
+ return (
494
+ <form onSubmit={submit} className="flex flex-col gap-7">
495
+ <StepHeader
496
+ icon={ShieldCheckIcon}
497
+ title="Lock your workspace"
498
+ description="Anyone who can open this page can control your agents. A lock keeps it yours, especially when you use Remote access."
499
+ />
500
+
501
+ {/* Segmented control ala GNOME (sama dengan dialog di Settings). */}
502
+ <fieldset
503
+ aria-label="Lock type"
504
+ className="grid grid-cols-2 gap-1 rounded-lg border-0 bg-muted p-1"
505
+ >
506
+ {(["pin", "password"] as const).map((k) => (
507
+ <label
508
+ key={k}
509
+ className={cn(
510
+ "cursor-pointer rounded-md px-3 py-1.5 text-center text-sm font-medium transition-colors has-[:focus-visible]:ring-[3px] has-[:focus-visible]:ring-ring/50",
511
+ kind === k
512
+ ? "bg-background text-foreground shadow-sm"
513
+ : "text-muted-foreground hover:text-foreground",
514
+ )}
515
+ >
516
+ <input
517
+ type="radio"
518
+ name="welcome-lock-kind"
519
+ value={k}
520
+ checked={kind === k}
521
+ onChange={() => {
522
+ setKind(k);
523
+ setSecret("");
524
+ setConfirm("");
525
+ setError(null);
526
+ }}
527
+ className="sr-only"
528
+ />
529
+ {k === "pin" ? "PIN" : "Password"}
530
+ </label>
531
+ ))}
532
+ </fieldset>
533
+
534
+ <div className="flex flex-col gap-2">
535
+ <div className="overflow-hidden rounded-xl border bg-card shadow-xs">
536
+ <div className="flex flex-col divide-y">
537
+ {field(secretId, `New ${label}`, secret, setSecret, secretRef)}
538
+ {field(confirmId, `Confirm ${label}`, confirm, setConfirm)}
539
+ </div>
540
+ </div>
541
+ <p className="px-1 text-[12px] text-muted-foreground">{hint}</p>
542
+ {error && (
543
+ <p role="alert" className="px-1 text-sm text-destructive">
544
+ {error}
545
+ </p>
546
+ )}
547
+ </div>
548
+
549
+ <div className="flex flex-col gap-2">
550
+ <Button
551
+ type="submit"
552
+ disabled={busy || secret === "" || confirm === ""}
553
+ className="h-12 rounded-full text-[15px]"
554
+ >
555
+ {busy ? <Spinner data-icon="inline-start" /> : <LockIcon data-icon="inline-start" />}
556
+ Turn on lock
557
+ </Button>
558
+ <Button
559
+ type="button"
560
+ variant="ghost"
561
+ disabled={busy}
562
+ onClick={() => onDone(null)}
563
+ className="h-10 rounded-full text-muted-foreground"
564
+ >
565
+ Skip, I'll do it later
566
+ </Button>
567
+ </div>
568
+ </form>
569
+ );
570
+ }
571
+
572
+ function DoneStep({
573
+ nickname,
574
+ lockSet,
575
+ finishing,
576
+ onFinish,
577
+ }: {
578
+ nickname: string | null;
579
+ lockSet: LockKind | null;
580
+ finishing: boolean;
581
+ onFinish: () => void;
582
+ }) {
583
+ const btnRef = useRef<HTMLButtonElement>(null);
584
+ useEffect(() => {
585
+ const t = setTimeout(() => btnRef.current?.focus({ preventScroll: true }), 450);
586
+ return () => clearTimeout(t);
587
+ }, []);
588
+
589
+ return (
590
+ <div className="flex flex-col items-center text-center">
591
+ {/* Centang: lingkaran membesar lalu garis centang tergambar. */}
592
+ <m.span
593
+ initial={{ scale: 0.6, opacity: 0 }}
594
+ animate={{
595
+ scale: 1,
596
+ opacity: 1,
597
+ transition: { type: "spring", stiffness: 280, damping: 18 },
598
+ }}
599
+ className="flex size-20 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg"
600
+ >
601
+ <svg viewBox="0 0 24 24" className="size-10" fill="none" aria-hidden="true">
602
+ <m.path
603
+ d="M5 12.5l4.5 4.5L19 7.5"
604
+ stroke="currentColor"
605
+ strokeWidth={2.4}
606
+ strokeLinecap="round"
607
+ strokeLinejoin="round"
608
+ initial={{ pathLength: 0 }}
609
+ animate={{ pathLength: 1, transition: { delay: 0.2, duration: 0.45, ease: EASE } }}
610
+ />
611
+ </svg>
612
+ </m.span>
613
+
614
+ <Stagger i={1}>
615
+ <h1 id="welcome-title" className="mt-7 text-2xl font-semibold tracking-tight">
616
+ {nickname ? `You're all set, ${nickname}` : "You're all set"}
617
+ </h1>
618
+ </Stagger>
619
+ <Stagger i={2}>
620
+ <ul className="mt-6 flex flex-col gap-2.5 text-left text-[14px]">
621
+ <SummaryRow
622
+ ok
623
+ text={nickname ? `Name set to ${nickname}` : "Name skipped"}
624
+ muted={!nickname}
625
+ />
626
+ <SummaryRow
627
+ ok={lockSet !== null}
628
+ text={
629
+ lockSet
630
+ ? `Protected with a ${lockSet === "pin" ? "PIN" : "password"}`
631
+ : "No lock yet. You can add one in Settings → Security."
632
+ }
633
+ muted={lockSet === null}
634
+ />
635
+ </ul>
636
+ </Stagger>
637
+ <Stagger i={3}>
638
+ <Button
639
+ ref={btnRef}
640
+ type="button"
641
+ onClick={onFinish}
642
+ disabled={finishing}
643
+ className="mt-10 h-12 w-64 max-w-full rounded-full text-[15px]"
644
+ >
645
+ {finishing ? <Spinner data-icon="inline-start" /> : null}
646
+ Open KCG Code
647
+ {!finishing && <ArrowRightIcon data-icon="inline-end" />}
648
+ </Button>
649
+ </Stagger>
650
+ </div>
651
+ );
652
+ }
653
+
654
+ function SummaryRow({ ok, text, muted }: { ok: boolean; text: string; muted?: boolean }) {
655
+ return (
656
+ <li className={cn("flex items-start gap-2.5", muted && "text-muted-foreground")}>
657
+ <span
658
+ className={cn(
659
+ "mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full",
660
+ ok && !muted ? "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400" : "bg-muted",
661
+ )}
662
+ >
663
+ {ok && !muted ? (
664
+ <CheckIcon className="size-3" strokeWidth={3} aria-hidden />
665
+ ) : (
666
+ <span className="size-1.5 rounded-full bg-muted-foreground/50" aria-hidden />
667
+ )}
668
+ </span>
669
+ {text}
670
+ </li>
671
+ );
672
+ }