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.
- package/README.md +230 -2
- package/bin/kcgcode.ts +13 -0
- package/bun-env.d.ts +19 -0
- package/kcg-code.config.example.json +3 -0
- package/package.json +84 -4
- package/public/logo.svg +1 -0
- package/public/manifest.json +18 -0
- package/public/sw.js +49 -0
- package/src/App.tsx +39 -0
- package/src/__tests__/manifest.test.ts +61 -0
- package/src/build-config.ts +9 -0
- package/src/cli/__tests__/daemon.test.ts +84 -0
- package/src/cli/__tests__/dashboard.test.ts +224 -0
- package/src/cli/__tests__/keep-awake.test.ts +44 -0
- package/src/cli/__tests__/port.test.ts +86 -0
- package/src/cli/args.ts +186 -0
- package/src/cli/attach.ts +154 -0
- package/src/cli/commands.ts +327 -0
- package/src/cli/daemon.ts +275 -0
- package/src/cli/dashboard.tsx +1060 -0
- package/src/cli/index.ts +47 -0
- package/src/cli/keep-awake.ts +148 -0
- package/src/cli/port.ts +92 -0
- package/src/cli/theme.ts +100 -0
- package/src/components/auth/AuthGate.tsx +182 -0
- package/src/components/auth/AvatarEditor.tsx +236 -0
- package/src/components/auth/LockScreen.tsx +714 -0
- package/src/components/auth/ProfileAvatar.tsx +60 -0
- package/src/components/auth/WelcomeScreen.tsx +672 -0
- package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
- package/src/components/auth/avatar-icons.tsx +114 -0
- package/src/components/layout/AppShell.tsx +147 -0
- package/src/components/layout/AppSidebar.tsx +419 -0
- package/src/components/layout/SearchDialog.tsx +189 -0
- package/src/components/layout/SidebarAccount.tsx +111 -0
- package/src/components/layout/SidebarSessionMenu.tsx +163 -0
- package/src/components/layout/ThemeToggle.tsx +67 -0
- package/src/components/projects/FolderBrowser.tsx +142 -0
- package/src/components/projects/HomeChat.tsx +287 -0
- package/src/components/projects/HomeRecentSessions.tsx +90 -0
- package/src/components/projects/NewProjectDialog.tsx +249 -0
- package/src/components/projects/ProjectInstructions.tsx +218 -0
- package/src/components/projects/ProjectRow.tsx +58 -0
- package/src/components/projects/ProjectSidePanel.tsx +514 -0
- package/src/components/sessions/AgentLogo.tsx +42 -0
- package/src/components/sessions/AgentModeList.tsx +265 -0
- package/src/components/sessions/AgentPicker.tsx +132 -0
- package/src/components/sessions/AssistantTurn.tsx +145 -0
- package/src/components/sessions/AttachmentTile.tsx +161 -0
- package/src/components/sessions/CodeBlock.tsx +107 -0
- package/src/components/sessions/ComposerActionSheet.tsx +96 -0
- package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
- package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +92 -0
- package/src/components/sessions/ContextPanel.tsx +661 -0
- package/src/components/sessions/DropPlate.tsx +165 -0
- package/src/components/sessions/FileDropOverlay.tsx +232 -0
- package/src/components/sessions/FileMentionDropdown.tsx +47 -0
- package/src/components/sessions/ImageLightbox.tsx +173 -0
- package/src/components/sessions/MarkdownContent.tsx +187 -0
- package/src/components/sessions/Mascot.tsx +47 -0
- package/src/components/sessions/MessageActions.tsx +60 -0
- package/src/components/sessions/MessageRail.tsx +121 -0
- package/src/components/sessions/ModelPicker.tsx +247 -0
- package/src/components/sessions/ModelSearchList.tsx +231 -0
- package/src/components/sessions/NewSessionDialog.tsx +172 -0
- package/src/components/sessions/PendingAttachments.tsx +127 -0
- package/src/components/sessions/PromptCard.tsx +1085 -0
- package/src/components/sessions/PromptPanel.tsx +112 -0
- package/src/components/sessions/SessionComposer.tsx +641 -0
- package/src/components/sessions/SessionHeader.tsx +176 -0
- package/src/components/sessions/SessionList.tsx +503 -0
- package/src/components/sessions/SessionRow.tsx +237 -0
- package/src/components/sessions/SessionTimeline.tsx +275 -0
- package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
- package/src/components/sessions/SessionView.tsx +240 -0
- package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
- package/src/components/sessions/UserMessage.tsx +177 -0
- package/src/components/sessions/__tests__/ContextPanel.test.tsx +256 -0
- package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
- package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
- package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
- package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
- package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
- package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
- package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
- package/src/components/settings/AboutGroup.tsx +170 -0
- package/src/components/settings/AccountGroup.tsx +122 -0
- package/src/components/settings/DevicesGroup.tsx +151 -0
- package/src/components/settings/LhrRemoteAccess.tsx +257 -0
- package/src/components/settings/PasskeysGroup.tsx +551 -0
- package/src/components/settings/RemoteAccessGroup.tsx +1201 -0
- package/src/components/settings/SecurityGroup.tsx +566 -0
- package/src/components/settings/WorkspaceGroup.tsx +370 -0
- package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
- package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
- package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
- package/src/components/settings/prefs.tsx +333 -0
- package/src/components/skills/InstallNotifications.tsx +326 -0
- package/src/components/skills/InstallSkillDialog.tsx +390 -0
- package/src/components/skills/SkillCards.tsx +254 -0
- package/src/components/skills/SkillsProjectPicker.tsx +178 -0
- package/src/components/ui/adw-dialog.tsx +114 -0
- package/src/components/ui/adw-toggle-group.tsx +75 -0
- package/src/components/ui/alert-dialog.tsx +179 -0
- package/src/components/ui/alert.tsx +60 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/bubble.tsx +125 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/card.tsx +75 -0
- package/src/components/ui/collapsible.tsx +19 -0
- package/src/components/ui/dialog.tsx +142 -0
- package/src/components/ui/dropdown-menu.tsx +225 -0
- package/src/components/ui/empty.tsx +94 -0
- package/src/components/ui/field.tsx +234 -0
- package/src/components/ui/input-group.tsx +161 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/message-scroller.tsx +131 -0
- package/src/components/ui/message.tsx +85 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/search-entry.tsx +70 -0
- package/src/components/ui/select.tsx +172 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +138 -0
- package/src/components/ui/sidebar.tsx +699 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/sonner.tsx +45 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +51 -0
- package/src/config.ts +114 -0
- package/src/db.ts +146 -0
- package/src/frontend.tsx +51 -0
- package/src/hooks/__tests__/useMention.test.ts +79 -0
- package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
- package/src/hooks/__tests__/useRouter.test.ts +17 -0
- package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
- package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
- package/src/hooks/__tests__/useTheme.test.ts +118 -0
- package/src/hooks/use-mobile.ts +16 -0
- package/src/hooks/useAgentPicker.ts +101 -0
- package/src/hooks/useMediaQuery.ts +49 -0
- package/src/hooks/useMention.ts +236 -0
- package/src/hooks/useProjects.ts +88 -0
- package/src/hooks/useResyncOnReconnect.ts +28 -0
- package/src/hooks/useRouter.ts +92 -0
- package/src/hooks/useSessionChat.ts +427 -0
- package/src/hooks/useSessionContext.ts +106 -0
- package/src/hooks/useSessionList.ts +327 -0
- package/src/hooks/useSidebarData.ts +108 -0
- package/src/hooks/useSmoothText.ts +122 -0
- package/src/hooks/useTheme.ts +164 -0
- package/src/hooks/useWebSocket.ts +196 -0
- package/src/index.css +27 -0
- package/src/index.html +14 -0
- package/src/index.ts +49 -0
- package/src/lib/__tests__/attachments.test.ts +58 -0
- package/src/lib/__tests__/avatar-presets.test.ts +25 -0
- package/src/lib/__tests__/collapsible.test.ts +101 -0
- package/src/lib/__tests__/composer.test.ts +118 -0
- package/src/lib/__tests__/highlight.test.ts +42 -0
- package/src/lib/__tests__/home-chat.test.ts +64 -0
- package/src/lib/__tests__/lock-screen.test.ts +64 -0
- package/src/lib/__tests__/model-picker.test.ts +205 -0
- package/src/lib/__tests__/onboarding.test.ts +41 -0
- package/src/lib/__tests__/permission-info.test.ts +61 -0
- package/src/lib/__tests__/project-extensions.test.ts +83 -0
- package/src/lib/__tests__/project-overview.test.ts +188 -0
- package/src/lib/__tests__/references.test.ts +121 -0
- package/src/lib/__tests__/routes.test.ts +134 -0
- package/src/lib/__tests__/session-selection.test.ts +41 -0
- package/src/lib/__tests__/session-summary.test.ts +115 -0
- package/src/lib/__tests__/session-title.test.ts +58 -0
- package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
- package/src/lib/__tests__/skill-installs.test.ts +135 -0
- package/src/lib/__tests__/skills-catalog.test.ts +124 -0
- package/src/lib/__tests__/tone.test.ts +37 -0
- package/src/lib/__tests__/turns-delta.test.ts +55 -0
- package/src/lib/agent-types.ts +49 -0
- package/src/lib/api.ts +306 -0
- package/src/lib/attachments.ts +203 -0
- package/src/lib/auth.ts +92 -0
- package/src/lib/avatar-presets.ts +31 -0
- package/src/lib/collapsible.ts +75 -0
- package/src/lib/composer.ts +89 -0
- package/src/lib/data-events.ts +17 -0
- package/src/lib/headlines.ts +30 -0
- package/src/lib/highlight.ts +186 -0
- package/src/lib/layout.ts +20 -0
- package/src/lib/lock-screen.ts +54 -0
- package/src/lib/model-picker.ts +133 -0
- package/src/lib/onboarding.ts +38 -0
- package/src/lib/passkeys.ts +113 -0
- package/src/lib/pending-prompt.ts +53 -0
- package/src/lib/permission-info.ts +101 -0
- package/src/lib/project-extensions.ts +136 -0
- package/src/lib/project-overview.ts +123 -0
- package/src/lib/prompts.ts +87 -0
- package/src/lib/references.ts +106 -0
- package/src/lib/routes.ts +124 -0
- package/src/lib/session-handoff.ts +25 -0
- package/src/lib/session-selection.ts +40 -0
- package/src/lib/session-status.ts +45 -0
- package/src/lib/session-summary.ts +68 -0
- package/src/lib/session-title.ts +63 -0
- package/src/lib/sidebar-groups.ts +78 -0
- package/src/lib/skill-installs.ts +362 -0
- package/src/lib/skills-catalog.ts +138 -0
- package/src/lib/time.ts +22 -0
- package/src/lib/tone.ts +80 -0
- package/src/lib/turns.ts +320 -0
- package/src/lib/utils.ts +6 -0
- package/src/logo.svg +31 -0
- package/src/pages/projects/ProjectDetailPage.tsx +131 -0
- package/src/pages/projects/ProjectsPage.tsx +97 -0
- package/src/pages/sessions/SessionPage.tsx +122 -0
- package/src/pages/settings/SettingsPage.tsx +366 -0
- package/src/pages/settings/__tests__/SettingsPage.test.ts +16 -0
- package/src/pages/skills/SkillsPage.tsx +745 -0
- package/src/paths.ts +121 -0
- package/src/runtime.ts +24 -0
- package/src/server/__tests__/app.test.ts +1044 -0
- package/src/server/__tests__/auth.test.ts +370 -0
- package/src/server/__tests__/config.test.ts +101 -0
- package/src/server/__tests__/db.test.ts +485 -0
- package/src/server/__tests__/host.test.ts +14 -0
- package/src/server/__tests__/tunnel-config.test.ts +38 -0
- package/src/server/app.ts +427 -0
- package/src/server/db/auth.ts +311 -0
- package/src/server/db/messages.ts +50 -0
- package/src/server/db/projects.ts +106 -0
- package/src/server/db/prompts.ts +78 -0
- package/src/server/db/rows.ts +150 -0
- package/src/server/db/schema.ts +221 -0
- package/src/server/db/sessions.ts +239 -0
- package/src/server/db/tunnel.ts +124 -0
- package/src/server/host.ts +12 -0
- package/src/server/result.ts +15 -0
- package/src/server/routes/auth-guard.ts +156 -0
- package/src/server/routes/auth.routes.ts +287 -0
- package/src/server/routes/helpers.ts +165 -0
- package/src/server/routes/meta.routes.ts +24 -0
- package/src/server/routes/passkeys.routes.ts +161 -0
- package/src/server/routes/projects.routes.ts +167 -0
- package/src/server/routes/sessions.routes.ts +197 -0
- package/src/server/routes/skills.routes.ts +136 -0
- package/src/server/routes/tunnel.routes.ts +132 -0
- package/src/server/routes/types.ts +35 -0
- package/src/server/routes/uploads.routes.ts +54 -0
- package/src/server/routes/workspace.routes.ts +127 -0
- package/src/server/services/__tests__/app-info.test.ts +18 -0
- package/src/server/services/__tests__/attachments.test.ts +159 -0
- package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
- package/src/server/services/__tests__/auth.test.ts +341 -0
- package/src/server/services/__tests__/claude-code.test.ts +609 -0
- package/src/server/services/__tests__/claude-mcp.test.ts +90 -0
- package/src/server/services/__tests__/claude-skills.test.ts +63 -0
- package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
- package/src/server/services/__tests__/git-info.test.ts +74 -0
- package/src/server/services/__tests__/opencode.test.ts +320 -0
- package/src/server/services/__tests__/project-manager.test.ts +190 -0
- package/src/server/services/__tests__/sandbox.test.ts +141 -0
- package/src/server/services/__tests__/session-manager.test.ts +3509 -0
- package/src/server/services/__tests__/skill-install-jobs.test.ts +245 -0
- package/src/server/services/__tests__/skills-registry.test.ts +365 -0
- package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
- package/src/server/services/__tests__/tunnel.test.ts +367 -0
- package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
- package/src/server/services/__tests__/workspace.test.ts +249 -0
- package/src/server/services/app-info.ts +29 -0
- package/src/server/services/attachments.ts +193 -0
- package/src/server/services/auth-passkeys.ts +459 -0
- package/src/server/services/auth.ts +633 -0
- package/src/server/services/claude-code-client.ts +1093 -0
- package/src/server/services/claude-code-translate.ts +370 -0
- package/src/server/services/claude-mcp.ts +122 -0
- package/src/server/services/claude-skills.ts +102 -0
- package/src/server/services/frpc-installer.ts +183 -0
- package/src/server/services/git-info.ts +87 -0
- package/src/server/services/opencode-client.ts +714 -0
- package/src/server/services/opencode-server.ts +319 -0
- package/src/server/services/opencode-sse.ts +82 -0
- package/src/server/services/project-manager.ts +147 -0
- package/src/server/services/sandbox.ts +83 -0
- package/src/server/services/session-events.ts +256 -0
- package/src/server/services/session-manager.ts +1439 -0
- package/src/server/services/skill-install-jobs.ts +275 -0
- package/src/server/services/skills-registry.ts +480 -0
- package/src/server/services/tunnel-api.ts +207 -0
- package/src/server/services/tunnel-config.ts +45 -0
- package/src/server/services/tunnel-lhr.ts +387 -0
- package/src/server/services/tunnel.ts +633 -0
- package/src/server/services/turn-stream.ts +339 -0
- package/src/server/services/websocket-gateway.ts +374 -0
- package/src/server/services/workspace.ts +223 -0
- package/src/server/services/ws-transport.ts +121 -0
- package/src/types/context.ts +69 -0
- package/src/types/index.ts +13 -0
- package/src/types/message.ts +58 -0
- package/src/types/project.ts +16 -0
- package/src/types/prompt.ts +118 -0
- package/src/types/session.ts +52 -0
- package/src/types/turns.ts +47 -0
- package/src/ws-protocol.ts +93 -0
- package/styles/globals.css +311 -0
- 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
|
+
}
|