jettypod 4.4.118 → 4.4.121
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/.env +4 -3
- package/Cargo.lock +6450 -0
- package/Cargo.toml +35 -0
- package/README.md +5 -1
- package/TAURI-MIGRATION-PLAN.md +840 -0
- package/apps/dashboard/app/connect-claude/page.tsx +5 -6
- package/apps/dashboard/app/decision/[id]/page.tsx +63 -58
- package/apps/dashboard/app/demo/gates/page.tsx +43 -45
- package/apps/dashboard/app/design-system/page.tsx +868 -0
- package/apps/dashboard/app/globals.css +80 -4
- package/apps/dashboard/app/install-claude/page.tsx +4 -6
- package/apps/dashboard/app/login/page.tsx +72 -54
- package/apps/dashboard/app/page.tsx +101 -48
- package/apps/dashboard/app/settings/page.tsx +61 -13
- package/apps/dashboard/app/signup/page.tsx +242 -0
- package/apps/dashboard/app/subscribe/page.tsx +0 -2
- package/apps/dashboard/app/tests/page.tsx +37 -4
- package/apps/dashboard/app/welcome/page.tsx +13 -16
- package/apps/dashboard/app/work/[id]/page.tsx +117 -118
- package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
- package/apps/dashboard/components/AppShell.tsx +92 -85
- package/apps/dashboard/components/CardMenu.tsx +45 -12
- package/apps/dashboard/components/ClaudePanel.tsx +771 -850
- package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
- package/apps/dashboard/components/CopyableId.tsx +3 -4
- package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
- package/apps/dashboard/components/DragContext.tsx +134 -63
- package/apps/dashboard/components/DraggableCard.tsx +3 -5
- package/apps/dashboard/components/DropZone.tsx +6 -7
- package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
- package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
- package/apps/dashboard/components/EditableTitle.tsx +26 -7
- package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
- package/apps/dashboard/components/EpicGroup.tsx +359 -0
- package/apps/dashboard/components/GateCard.tsx +79 -17
- package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
- package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
- package/apps/dashboard/components/JettyLoader.tsx +37 -0
- package/apps/dashboard/components/KanbanBoard.tsx +368 -958
- package/apps/dashboard/components/KanbanCard.tsx +740 -0
- package/apps/dashboard/components/LazyCard.tsx +62 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
- package/apps/dashboard/components/MainNav.tsx +38 -73
- package/apps/dashboard/components/MessageBlock.tsx +468 -0
- package/apps/dashboard/components/ModeStartCard.tsx +15 -16
- package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
- package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
- package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
- package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
- package/apps/dashboard/components/ReviewFooter.tsx +139 -0
- package/apps/dashboard/components/SessionList.tsx +19 -19
- package/apps/dashboard/components/SubscribeContent.tsx +91 -47
- package/apps/dashboard/components/TestTree.tsx +16 -16
- package/apps/dashboard/components/TipCard.tsx +16 -17
- package/apps/dashboard/components/Toast.tsx +5 -6
- package/apps/dashboard/components/TypeIcon.tsx +55 -0
- package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
- package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
- package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
- package/apps/dashboard/components/WorkItemTree.tsx +11 -32
- package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
- package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
- package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +74 -152
- package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
- package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
- package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
- package/apps/dashboard/components/ui/Button.tsx +104 -0
- package/apps/dashboard/components/ui/Input.tsx +78 -0
- package/apps/dashboard/components.json +1 -1
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
- package/apps/dashboard/contexts/UsageContext.tsx +87 -32
- package/apps/dashboard/dev.sh +35 -0
- package/apps/dashboard/eslint.config.mjs +9 -9
- package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
- package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
- package/apps/dashboard/hooks/useWebSocket.ts +138 -83
- package/apps/dashboard/index.html +73 -0
- package/apps/dashboard/lib/constants.ts +43 -0
- package/apps/dashboard/lib/data-bridge.ts +722 -0
- package/apps/dashboard/lib/db.ts +69 -1265
- package/apps/dashboard/lib/environment-config.ts +173 -0
- package/apps/dashboard/lib/environment-verification.ts +119 -0
- package/apps/dashboard/lib/kanban-utils.ts +270 -0
- package/apps/dashboard/lib/proof-run.ts +495 -0
- package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
- package/apps/dashboard/lib/run-migrations.js +27 -2
- package/apps/dashboard/lib/service-recovery.ts +326 -0
- package/apps/dashboard/lib/session-state-machine.ts +1 -0
- package/apps/dashboard/lib/session-state-utils.ts +0 -164
- package/apps/dashboard/lib/session-stream-manager.ts +308 -134
- package/apps/dashboard/lib/shadows.ts +7 -0
- package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
- package/apps/dashboard/lib/tauri-bridge.ts +102 -0
- package/apps/dashboard/lib/tauri.ts +106 -0
- package/apps/dashboard/lib/utils.ts +6 -0
- package/apps/dashboard/next-env.d.ts +1 -1
- package/apps/dashboard/package.json +21 -32
- package/apps/dashboard/public/bug-icon.png +0 -0
- package/apps/dashboard/public/buoy-icon.png +0 -0
- package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
- package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
- package/apps/dashboard/public/in-flight-seagull.png +0 -0
- package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
- package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
- package/apps/dashboard/public/jettypod_logo.png +0 -0
- package/apps/dashboard/public/pier-icon.png +0 -0
- package/apps/dashboard/public/star-icon.png +0 -0
- package/apps/dashboard/public/wrench-icon.png +0 -0
- package/apps/dashboard/scripts/tauri-build.js +228 -0
- package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
- package/apps/dashboard/scripts/ws-server.js +191 -0
- package/apps/dashboard/src/main.tsx +12 -0
- package/apps/dashboard/src/router.tsx +107 -0
- package/apps/dashboard/src/vite-env.d.ts +1 -0
- package/apps/dashboard/tsconfig.json +7 -12
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
- package/apps/dashboard/vite.config.ts +33 -0
- package/apps/update-server/src/index.ts +228 -80
- package/claude-hooks/global-guardrails.js +14 -13
- package/crates/jettypod-cli/Cargo.toml +19 -0
- package/crates/jettypod-cli/src/commands.rs +1249 -0
- package/crates/jettypod-cli/src/main.rs +595 -0
- package/crates/jettypod-core/Cargo.toml +26 -0
- package/crates/jettypod-core/build.rs +98 -0
- package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
- package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
- package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
- package/crates/jettypod-core/src/auth.rs +294 -0
- package/crates/jettypod-core/src/config.rs +397 -0
- package/crates/jettypod-core/src/db/mod.rs +507 -0
- package/crates/jettypod-core/src/db/recovery.rs +114 -0
- package/crates/jettypod-core/src/db/startup.rs +101 -0
- package/crates/jettypod-core/src/db/validate.rs +149 -0
- package/crates/jettypod-core/src/error.rs +76 -0
- package/crates/jettypod-core/src/git.rs +458 -0
- package/crates/jettypod-core/src/lib.rs +20 -0
- package/crates/jettypod-core/src/sessions.rs +625 -0
- package/crates/jettypod-core/src/skills.rs +556 -0
- package/crates/jettypod-core/src/work.rs +1086 -0
- package/crates/jettypod-core/src/worktree.rs +628 -0
- package/crates/jettypod-core/src/ws.rs +767 -0
- package/cucumber-test.cjs +6 -0
- package/cucumber.js +9 -3
- package/docs/COMMAND_REFERENCE.md +34 -0
- package/hooks/post-checkout +32 -75
- package/hooks/post-merge +111 -10
- package/jest.setup.js +1 -0
- package/jettypod.js +145 -116
- package/lib/bdd-preflight.js +96 -0
- package/lib/chore-taxonomy.js +33 -10
- package/lib/database.js +36 -16
- package/lib/db-watcher.js +1 -1
- package/lib/git-hooks/pre-commit +1 -1
- package/lib/jettypod-backup.js +27 -4
- package/lib/merge-lock.js +111 -253
- package/lib/migrations/027-plan-at-creation-column.js +3 -1
- package/lib/migrations/029-remove-autoincrement.js +307 -0
- package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
- package/lib/migrations/030-rejection-round-columns.js +54 -0
- package/lib/migrations/031-session-isolation-index.js +17 -0
- package/lib/migrations/index.js +47 -4
- package/lib/schema.js +10 -5
- package/lib/seed-onboarding.js +1 -1
- package/lib/update-command/index.js +9 -175
- package/lib/work-commands/index.js +144 -19
- package/lib/work-tracking/index.js +148 -27
- package/lib/worktree-diagnostics.js +16 -16
- package/lib/worktree-facade.js +1 -1
- package/lib/worktree-manager.js +8 -8
- package/lib/worktree-reconciler.js +5 -5
- package/package.json +9 -2
- package/scripts/ndjson-to-cucumber-json.js +152 -0
- package/scripts/postinstall.js +25 -0
- package/skills-templates/bug-mode/SKILL.md +79 -20
- package/skills-templates/bug-planning/SKILL.md +25 -29
- package/skills-templates/chore-mode/SKILL.md +171 -69
- package/skills-templates/chore-mode/verification.js +51 -10
- package/skills-templates/chore-planning/SKILL.md +47 -18
- package/skills-templates/design-system-selection/SKILL.md +273 -0
- package/skills-templates/epic-planning/SKILL.md +82 -48
- package/skills-templates/external-transition/SKILL.md +47 -47
- package/skills-templates/feature-planning/SKILL.md +173 -74
- package/skills-templates/production-mode/SKILL.md +69 -49
- package/skills-templates/request-routing/SKILL.md +4 -4
- package/skills-templates/simple-improvement/SKILL.md +74 -29
- package/skills-templates/speed-mode/SKILL.md +217 -141
- package/skills-templates/stable-mode/SKILL.md +148 -89
- package/apps/dashboard/README.md +0 -36
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
- package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -378
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
- package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
- package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
- package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
- package/apps/dashboard/app/api/kanban/route.ts +0 -15
- package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
- package/apps/dashboard/app/api/settings/general/route.ts +0 -21
- package/apps/dashboard/app/api/tests/route.ts +0 -9
- package/apps/dashboard/app/api/tests/run/route.ts +0 -82
- package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
- package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
- package/apps/dashboard/app/api/usage/route.ts +0 -17
- package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
- package/apps/dashboard/app/layout.tsx +0 -43
- package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
- package/apps/dashboard/electron/ipc-handlers.js +0 -1028
- package/apps/dashboard/electron/main.js +0 -2124
- package/apps/dashboard/electron/preload.js +0 -123
- package/apps/dashboard/electron/session-manager.js +0 -141
- package/apps/dashboard/electron-builder.config.js +0 -357
- package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
- package/apps/dashboard/lib/claude-process-manager.ts +0 -492
- package/apps/dashboard/lib/db-bridge.ts +0 -282
- package/apps/dashboard/lib/prototypes.ts +0 -202
- package/apps/dashboard/lib/test-results-db.ts +0 -307
- package/apps/dashboard/lib/tests.ts +0 -282
- package/apps/dashboard/next.config.js +0 -50
- package/apps/dashboard/postcss.config.mjs +0 -7
- package/apps/dashboard/public/file.svg +0 -1
- package/apps/dashboard/public/globe.svg +0 -1
- package/apps/dashboard/public/next.svg +0 -1
- package/apps/dashboard/public/vercel.svg +0 -1
- package/apps/dashboard/public/window.svg +0 -1
- package/apps/dashboard/scripts/download-node.js +0 -104
- package/apps/dashboard/scripts/upload-to-r2.js +0 -89
- package/docs/bdd-guidance.md +0 -390
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
/* Font declarations */
|
|
2
|
+
@font-face {
|
|
3
|
+
font-family: 'Satoshi';
|
|
4
|
+
src: url('/fonts/Satoshi-Variable.woff2') format('woff2');
|
|
5
|
+
font-style: normal;
|
|
6
|
+
font-display: swap;
|
|
7
|
+
font-weight: 100 900;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@font-face {
|
|
11
|
+
font-family: 'Satoshi';
|
|
12
|
+
src: url('/fonts/Satoshi-VariableItalic.woff2') format('woff2');
|
|
13
|
+
font-style: italic;
|
|
14
|
+
font-display: swap;
|
|
15
|
+
font-weight: 100 900;
|
|
16
|
+
}
|
|
17
|
+
|
|
1
18
|
@import "tailwindcss";
|
|
2
19
|
@import "tw-animate-css";
|
|
3
20
|
|
|
@@ -6,7 +23,7 @@
|
|
|
6
23
|
@theme inline {
|
|
7
24
|
--color-background: var(--background);
|
|
8
25
|
--color-foreground: var(--foreground);
|
|
9
|
-
--font-sans: var(--font-
|
|
26
|
+
--font-sans: var(--font-satoshi);
|
|
10
27
|
--font-mono: var(--font-geist-mono);
|
|
11
28
|
--color-sidebar-ring: var(--sidebar-ring);
|
|
12
29
|
--color-sidebar-border: var(--sidebar-border);
|
|
@@ -45,7 +62,9 @@
|
|
|
45
62
|
|
|
46
63
|
:root {
|
|
47
64
|
--radius: 0.625rem;
|
|
48
|
-
--
|
|
65
|
+
--font-satoshi: 'Satoshi', system-ui, sans-serif;
|
|
66
|
+
--font-geist-mono: 'Geist Mono Variable', 'Geist Mono', monospace;
|
|
67
|
+
--background: oklch(0.98 0.002 80);
|
|
49
68
|
--foreground: oklch(0.145 0 0);
|
|
50
69
|
--card: oklch(1 0 0);
|
|
51
70
|
--card-foreground: oklch(0.145 0 0);
|
|
@@ -113,10 +132,67 @@
|
|
|
113
132
|
}
|
|
114
133
|
|
|
115
134
|
@layer base {
|
|
116
|
-
|
|
117
|
-
|
|
135
|
+
*,
|
|
136
|
+
::before,
|
|
137
|
+
::after {
|
|
138
|
+
border-color: var(--color-border);
|
|
118
139
|
}
|
|
119
140
|
body {
|
|
120
141
|
@apply bg-background text-foreground;
|
|
142
|
+
font-family: var(--font-satoshi);
|
|
143
|
+
-webkit-font-smoothing: antialiased;
|
|
144
|
+
-moz-osx-font-smoothing: grayscale;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
html {
|
|
149
|
+
font-size: 16px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* Kanban card hover — instant shadow swap, no transition (WebKit-friendly) */
|
|
153
|
+
.kanban-card {
|
|
154
|
+
contain: content;
|
|
155
|
+
content-visibility: auto;
|
|
156
|
+
}
|
|
157
|
+
.kanban-card:hover {
|
|
158
|
+
box-shadow: var(--hover-shadow) !important;
|
|
159
|
+
transform: translateY(-2px);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* Highlight pulse — CSS-only, no framer-motion runtime cost */
|
|
163
|
+
@keyframes highlight-pulse {
|
|
164
|
+
0%, 100% { outline: 3px solid rgba(129, 157, 159, 0); outline-offset: 0; }
|
|
165
|
+
50% { outline: 3px solid rgba(129, 157, 159, 0.4); outline-offset: 0; }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* View Transitions API — fast cross-fade for route changes */
|
|
169
|
+
::view-transition-old(root) {
|
|
170
|
+
animation: vt-fade-out 120ms ease-out;
|
|
171
|
+
}
|
|
172
|
+
::view-transition-new(root) {
|
|
173
|
+
animation: vt-fade-in 200ms ease-out;
|
|
174
|
+
}
|
|
175
|
+
@keyframes vt-fade-out {
|
|
176
|
+
to { opacity: 0; }
|
|
177
|
+
}
|
|
178
|
+
@keyframes vt-fade-in {
|
|
179
|
+
from { opacity: 0; }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* Skeleton loading pulse */
|
|
183
|
+
@keyframes skeleton-pulse {
|
|
184
|
+
0%, 100% { opacity: 0.5; }
|
|
185
|
+
50% { opacity: 0.25; }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* Respect reduced motion globally — disables all animations/transitions */
|
|
189
|
+
@media (prefers-reduced-motion: reduce) {
|
|
190
|
+
*,
|
|
191
|
+
::before,
|
|
192
|
+
::after {
|
|
193
|
+
animation-duration: 0.01ms !important;
|
|
194
|
+
animation-iteration-count: 1 !important;
|
|
195
|
+
transition-duration: 0.01ms !important;
|
|
196
|
+
scroll-behavior: auto !important;
|
|
121
197
|
}
|
|
122
198
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
1
|
import { useState } from 'react';
|
|
4
2
|
import { InstallClaudeScreen } from '@/components/InstallClaudeScreen';
|
|
3
|
+
import { isTauri, claudeCode } from '@/lib/tauri-bridge';
|
|
5
4
|
|
|
6
5
|
export default function InstallClaudePage() {
|
|
7
6
|
const [isInstalling, setIsInstalling] = useState(false);
|
|
@@ -12,15 +11,14 @@ export default function InstallClaudePage() {
|
|
|
12
11
|
setError(null);
|
|
13
12
|
setIsInstalling(true);
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
if (!window.electronAPI?.isElectron) {
|
|
14
|
+
if (!isTauri()) {
|
|
17
15
|
setError('Installation is only available in the desktop app.');
|
|
18
16
|
setIsInstalling(false);
|
|
19
17
|
return;
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
try {
|
|
23
|
-
const result = await
|
|
21
|
+
const result = await claudeCode.install();
|
|
24
22
|
|
|
25
23
|
if (!result.success) {
|
|
26
24
|
setError(result.error || 'Installation failed');
|
|
@@ -44,7 +42,7 @@ export default function InstallClaudePage() {
|
|
|
44
42
|
return (
|
|
45
43
|
<>
|
|
46
44
|
{error && (
|
|
47
|
-
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 bg-red-
|
|
45
|
+
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 bg-red-50 dark:bg-red-900/20 border-2 border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-4 py-3 rounded-xl text-base z-50">
|
|
48
46
|
{error}
|
|
49
47
|
</div>
|
|
50
48
|
)}
|
|
@@ -1,25 +1,11 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
1
|
import { useState, useEffect, useRef } from 'react';
|
|
4
|
-
import
|
|
2
|
+
import { Link } from 'react-router-dom';
|
|
3
|
+
import { Button } from '@/components/ui/Button';
|
|
4
|
+
import { Input } from '@/components/ui/Input';
|
|
5
|
+
import { isTauri, auth } from '@/lib/tauri-bridge';
|
|
5
6
|
|
|
6
7
|
const API_BASE = 'https://jettypod-update-server.spangbaryn2.workers.dev';
|
|
7
8
|
|
|
8
|
-
const buttonGradientStyle = {
|
|
9
|
-
background: 'linear-gradient(145deg, #ffffff 0%, #faf9f7 10%, #f0f4f4 35%, #c8d9da 55%, #819D9F 90%)',
|
|
10
|
-
color: '#3d4d4e',
|
|
11
|
-
boxShadow: `
|
|
12
|
-
0 1px 1px rgba(0, 0, 0, 0.02),
|
|
13
|
-
0 2px 4px rgba(0, 0, 0, 0.03),
|
|
14
|
-
0 6px 12px rgba(0, 0, 0, 0.05),
|
|
15
|
-
0 12px 24px rgba(0, 0, 0, 0.06),
|
|
16
|
-
0 20px 40px rgba(129, 157, 159, 0.2),
|
|
17
|
-
0 32px 64px rgba(129, 157, 159, 0.18),
|
|
18
|
-
inset 0 2px 4px rgba(255, 255, 255, 1),
|
|
19
|
-
inset 0 -2px 4px rgba(129, 157, 159, 0.05)
|
|
20
|
-
`,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
9
|
export default function LoginPage() {
|
|
24
10
|
const [email, setEmail] = useState('');
|
|
25
11
|
const [otpCode, setOtpCode] = useState('');
|
|
@@ -29,10 +15,28 @@ export default function LoginPage() {
|
|
|
29
15
|
const [error, setError] = useState<string | null>(null);
|
|
30
16
|
|
|
31
17
|
// Poll for auth completion after Google sign-in.
|
|
32
|
-
// The deep link handler
|
|
33
|
-
// and navigates to the dashboard
|
|
18
|
+
// The deep link handler saves the token — this polling detects it
|
|
19
|
+
// and navigates to the dashboard.
|
|
34
20
|
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
35
21
|
|
|
22
|
+
// Redirect already-authenticated users to dashboard
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
async function checkIfAlreadyAuthenticated() {
|
|
25
|
+
if (isTauri()) {
|
|
26
|
+
try {
|
|
27
|
+
const status = await auth.getStatus();
|
|
28
|
+
if (status.authenticated) {
|
|
29
|
+
const path = await auth.getPostLoginPath() || '/';
|
|
30
|
+
window.location.href = path;
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
// Ignore — stay on login page
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
checkIfAlreadyAuthenticated();
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
36
40
|
useEffect(() => {
|
|
37
41
|
return () => {
|
|
38
42
|
if (pollRef.current) clearInterval(pollRef.current);
|
|
@@ -40,16 +44,17 @@ export default function LoginPage() {
|
|
|
40
44
|
}, []);
|
|
41
45
|
|
|
42
46
|
const handleGoogleSignIn = () => {
|
|
43
|
-
if (!
|
|
44
|
-
|
|
47
|
+
if (!isTauri()) return;
|
|
48
|
+
auth.loginWithGoogle();
|
|
45
49
|
|
|
46
50
|
// Start polling for auth status (token saved by deep link handler)
|
|
47
51
|
pollRef.current = setInterval(async () => {
|
|
48
52
|
try {
|
|
49
|
-
const status = await
|
|
53
|
+
const status = await auth.getStatus();
|
|
50
54
|
if (status.authenticated) {
|
|
51
55
|
if (pollRef.current) clearInterval(pollRef.current);
|
|
52
|
-
|
|
56
|
+
const path = await auth.getPostLoginPath() || '/';
|
|
57
|
+
window.location.href = path;
|
|
53
58
|
}
|
|
54
59
|
} catch {
|
|
55
60
|
// Ignore — keep polling
|
|
@@ -116,12 +121,13 @@ export default function LoginPage() {
|
|
|
116
121
|
|
|
117
122
|
const data = await res.json() as { token: string; user: { id: string; email: string; plan: string } };
|
|
118
123
|
|
|
119
|
-
// Save auth state via
|
|
120
|
-
if (
|
|
121
|
-
await
|
|
124
|
+
// Save auth state via Tauri IPC
|
|
125
|
+
if (isTauri()) {
|
|
126
|
+
await auth.saveToken(data.token, data.user);
|
|
122
127
|
}
|
|
123
128
|
|
|
124
|
-
|
|
129
|
+
const path = await auth.getPostLoginPath() || '/';
|
|
130
|
+
window.location.href = path;
|
|
125
131
|
} catch {
|
|
126
132
|
setError('Failed to verify code. Check your connection.');
|
|
127
133
|
setIsVerifying(false);
|
|
@@ -130,15 +136,14 @@ export default function LoginPage() {
|
|
|
130
136
|
|
|
131
137
|
return (
|
|
132
138
|
<div className="flex flex-col items-center justify-center min-h-screen bg-white dark:bg-zinc-900 p-8">
|
|
133
|
-
<div className="max-w-md w-full space-y-
|
|
139
|
+
<div className="max-w-md w-full space-y-10">
|
|
134
140
|
{/* Logo */}
|
|
135
|
-
<div className="flex flex-col items-center space-y-
|
|
136
|
-
<
|
|
141
|
+
<div className="flex flex-col items-center space-y-6">
|
|
142
|
+
<img
|
|
137
143
|
src="/jettypod_wordmark.png"
|
|
138
144
|
alt="JettyPod"
|
|
139
145
|
width={160}
|
|
140
146
|
height={40}
|
|
141
|
-
priority
|
|
142
147
|
/>
|
|
143
148
|
<h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 text-center">
|
|
144
149
|
Sign in to JettyPod
|
|
@@ -150,54 +155,55 @@ export default function LoginPage() {
|
|
|
150
155
|
|
|
151
156
|
{/* Error */}
|
|
152
157
|
{error && (
|
|
153
|
-
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-
|
|
158
|
+
<div className="bg-red-50 dark:bg-red-900/20 border-2 border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-5 py-4 rounded-xl text-base">
|
|
154
159
|
{error}
|
|
155
160
|
</div>
|
|
156
161
|
)}
|
|
157
162
|
|
|
158
163
|
{/* Google Sign-In */}
|
|
159
164
|
<div className="pt-4">
|
|
160
|
-
<
|
|
165
|
+
<Button
|
|
161
166
|
onClick={handleGoogleSignIn}
|
|
162
|
-
|
|
163
|
-
|
|
167
|
+
size="lg"
|
|
168
|
+
fullWidth
|
|
164
169
|
>
|
|
165
170
|
Sign in with Google
|
|
166
|
-
</
|
|
171
|
+
</Button>
|
|
167
172
|
</div>
|
|
168
173
|
|
|
169
174
|
{/* Divider */}
|
|
170
175
|
<div className="flex items-center gap-4">
|
|
171
176
|
<div className="flex-1 h-px bg-zinc-200 dark:bg-zinc-700" />
|
|
172
|
-
<span className="text-
|
|
177
|
+
<span className="text-base text-zinc-400 dark:text-zinc-500">or</span>
|
|
173
178
|
<div className="flex-1 h-px bg-zinc-200 dark:bg-zinc-700" />
|
|
174
179
|
</div>
|
|
175
180
|
|
|
176
181
|
{/* Email OTP */}
|
|
177
182
|
{!otpSent ? (
|
|
178
|
-
<form onSubmit={handleSendOTP} className="space-y-
|
|
179
|
-
<
|
|
183
|
+
<form onSubmit={handleSendOTP} className="space-y-6">
|
|
184
|
+
<Input
|
|
180
185
|
type="email"
|
|
181
186
|
value={email}
|
|
182
187
|
onChange={(e) => setEmail(e.target.value)}
|
|
183
188
|
placeholder="Enter your email"
|
|
184
189
|
disabled={isSending}
|
|
185
|
-
className="w-full px-4 py-3 rounded-xl border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 dark:placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-400 dark:focus:ring-zinc-500 disabled:opacity-50"
|
|
186
190
|
/>
|
|
187
|
-
<
|
|
191
|
+
<Button
|
|
188
192
|
type="submit"
|
|
193
|
+
variant="secondary"
|
|
194
|
+
size="lg"
|
|
195
|
+
fullWidth
|
|
189
196
|
disabled={isSending || !email.trim()}
|
|
190
|
-
className="w-full py-3 px-6 rounded-xl font-medium border border-zinc-300 dark:border-zinc-600 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors disabled:opacity-50 disabled:pointer-events-none"
|
|
191
197
|
>
|
|
192
198
|
{isSending ? 'Sending code...' : 'Sign in with email'}
|
|
193
|
-
</
|
|
199
|
+
</Button>
|
|
194
200
|
</form>
|
|
195
201
|
) : (
|
|
196
|
-
<form onSubmit={handleVerifyOTP} className="space-y-
|
|
197
|
-
<p className="text-
|
|
202
|
+
<form onSubmit={handleVerifyOTP} className="space-y-6">
|
|
203
|
+
<p className="text-base text-zinc-500 dark:text-zinc-400">
|
|
198
204
|
We sent a 6-digit code to <span className="font-medium text-zinc-700 dark:text-zinc-300">{email}</span>
|
|
199
205
|
</p>
|
|
200
|
-
<
|
|
206
|
+
<Input
|
|
201
207
|
type="text"
|
|
202
208
|
value={otpCode}
|
|
203
209
|
onChange={(e) => setOtpCode(e.target.value)}
|
|
@@ -205,24 +211,36 @@ export default function LoginPage() {
|
|
|
205
211
|
maxLength={6}
|
|
206
212
|
autoFocus
|
|
207
213
|
disabled={isVerifying}
|
|
208
|
-
className="
|
|
214
|
+
className="text-center text-xl tracking-widest font-mono"
|
|
209
215
|
/>
|
|
210
|
-
<
|
|
216
|
+
<Button
|
|
211
217
|
type="submit"
|
|
218
|
+
variant="secondary"
|
|
219
|
+
size="lg"
|
|
220
|
+
fullWidth
|
|
212
221
|
disabled={isVerifying || !otpCode.trim()}
|
|
213
|
-
className="w-full py-3 px-6 rounded-xl font-medium border border-zinc-300 dark:border-zinc-600 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors disabled:opacity-50 disabled:pointer-events-none"
|
|
214
222
|
>
|
|
215
223
|
{isVerifying ? 'Verifying...' : 'Verify code'}
|
|
216
|
-
</
|
|
217
|
-
<
|
|
224
|
+
</Button>
|
|
225
|
+
<Button
|
|
218
226
|
type="button"
|
|
227
|
+
variant="ghost"
|
|
228
|
+
size="sm"
|
|
229
|
+
fullWidth
|
|
219
230
|
onClick={() => { setOtpSent(false); setOtpCode(''); setError(null); }}
|
|
220
|
-
className="w-full text-sm text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-300 transition-colors"
|
|
221
231
|
>
|
|
222
232
|
Use a different email
|
|
223
|
-
</
|
|
233
|
+
</Button>
|
|
224
234
|
</form>
|
|
225
235
|
)}
|
|
236
|
+
|
|
237
|
+
{/* Switch to signup */}
|
|
238
|
+
<p className="text-center text-base text-zinc-500 dark:text-zinc-400">
|
|
239
|
+
Don't have an account?{' '}
|
|
240
|
+
<Link to="/signup" className="font-medium hover:underline" style={{ color: '#819D9F' }}>
|
|
241
|
+
Create one
|
|
242
|
+
</Link>
|
|
243
|
+
</p>
|
|
226
244
|
</div>
|
|
227
245
|
</div>
|
|
228
246
|
);
|
|
@@ -1,61 +1,114 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useNavigate, useLocation, Outlet } from 'react-router-dom';
|
|
3
3
|
import { RealTimeKanbanWrapper } from '@/components/RealTimeKanbanWrapper';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export const dynamic = 'force-dynamic';
|
|
4
|
+
import { dataBridge, prefetch } from '@/lib/data-bridge';
|
|
5
|
+
import type { KanbanData } from '@/lib/data-bridge';
|
|
7
6
|
|
|
8
7
|
export default function Home() {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
const navigate = useNavigate();
|
|
9
|
+
const { pathname } = useLocation();
|
|
10
|
+
const isChildRoute = pathname !== '/';
|
|
11
|
+
const [data, setData] = useState<KanbanData | null>(null);
|
|
12
|
+
const [projectPath, setProjectPath] = useState('');
|
|
13
|
+
const [isBlank, setIsBlank] = useState(false);
|
|
14
|
+
const [error, setError] = useState<string | null>(null);
|
|
15
|
+
const [loading, setLoading] = useState(true);
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
async function loadData() {
|
|
19
|
+
try {
|
|
20
|
+
// getProjectRoot() is cached after first call — no redundant IPC
|
|
21
|
+
const root = await dataBridge.getProjectRoot();
|
|
22
|
+
if (!root) {
|
|
23
|
+
navigate('/welcome', { replace: true });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
18
26
|
|
|
19
|
-
|
|
20
|
-
const serializedData = {
|
|
21
|
-
inFlight: data.inFlight,
|
|
22
|
-
backlog: Array.from(data.backlog.entries()),
|
|
23
|
-
done: Array.from(data.done.entries()),
|
|
24
|
-
};
|
|
27
|
+
const kanbanData = await prefetch.backlog();
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
setData(kanbanData);
|
|
30
|
+
setProjectPath(root || '');
|
|
31
|
+
// isBlank detection: fresh project with only the seeded "Project Planning" epic
|
|
32
|
+
const onlyGroup = kanbanData.backlog.size === 1
|
|
33
|
+
? [...kanbanData.backlog.values()][0]
|
|
34
|
+
: null;
|
|
35
|
+
const hasOnlyOnboarding = kanbanData.inFlight.length === 0
|
|
36
|
+
&& kanbanData.done.size === 0
|
|
37
|
+
&& onlyGroup?.epicTitle === 'Project Planning';
|
|
38
|
+
setIsBlank(hasOnlyOnboarding);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
41
|
+
} finally {
|
|
42
|
+
setLoading(false);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
loadData();
|
|
46
|
+
}, [navigate]);
|
|
34
47
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
48
|
+
// Serialize Map data for RealTimeKanbanWrapper (it expects this format)
|
|
49
|
+
const serializedData = data ? {
|
|
50
|
+
inFlight: data.inFlight,
|
|
51
|
+
backlog: Array.from(data.backlog.entries()),
|
|
52
|
+
done: Array.from(data.done.entries()),
|
|
53
|
+
} : null;
|
|
39
54
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
55
|
+
// Kanban board content based on loading/error/data state
|
|
56
|
+
const kanbanContent = loading ? (
|
|
57
|
+
<div className="max-w-7xl w-full mx-auto px-4 py-4">
|
|
58
|
+
<div className="flex gap-4" style={{ height: 'calc(var(--main-h, 100vh) - 2rem)' }}>
|
|
59
|
+
{/* Backlog column skeleton */}
|
|
60
|
+
<div className="flex-1 max-w-[600px] flex flex-col min-h-0">
|
|
61
|
+
<div className="bg-zinc-100 dark:bg-zinc-900 rounded-xl p-4 flex-1 min-h-0">
|
|
62
|
+
<div className="flex items-center justify-between mb-4">
|
|
63
|
+
<div className="h-6 w-24 bg-zinc-200 dark:bg-zinc-800 rounded" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
|
|
64
|
+
<div className="h-6 w-10 bg-zinc-200 dark:bg-zinc-800 rounded-full" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
|
|
65
|
+
</div>
|
|
66
|
+
<div className="space-y-3">
|
|
67
|
+
{[1, 2, 3, 4, 5].map(i => (
|
|
68
|
+
<div key={i} className="h-20 bg-zinc-200 dark:bg-zinc-800 rounded-xl" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
47
71
|
</div>
|
|
48
|
-
|
|
49
|
-
|
|
72
|
+
</div>
|
|
73
|
+
{/* Done column skeleton */}
|
|
74
|
+
<div className="flex-1 max-w-[600px] flex flex-col min-h-0">
|
|
75
|
+
<div className="bg-zinc-100 dark:bg-zinc-900 rounded-xl p-4 flex-1 min-h-0">
|
|
76
|
+
<div className="flex items-center justify-between mb-4">
|
|
77
|
+
<div className="h-6 w-16 bg-zinc-200 dark:bg-zinc-800 rounded" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
|
|
78
|
+
<div className="h-6 w-10 bg-zinc-200 dark:bg-zinc-800 rounded-full" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
|
|
79
|
+
</div>
|
|
80
|
+
<div className="space-y-3">
|
|
81
|
+
{[1, 2, 3].map(i => (
|
|
82
|
+
<div key={i} className="h-16 bg-zinc-200 dark:bg-zinc-800 rounded-xl" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
50
85
|
</div>
|
|
51
|
-
{errorStack && (
|
|
52
|
-
<details className="mt-4">
|
|
53
|
-
<summary className="text-sm text-gray-500 cursor-pointer">Stack trace</summary>
|
|
54
|
-
<pre className="mt-2 text-xs text-gray-500 overflow-auto p-2 bg-gray-50 rounded">{errorStack}</pre>
|
|
55
|
-
</details>
|
|
56
|
-
)}
|
|
57
86
|
</div>
|
|
58
87
|
</div>
|
|
59
|
-
|
|
60
|
-
|
|
88
|
+
</div>
|
|
89
|
+
) : error ? (
|
|
90
|
+
<div className="flex-1 flex items-center justify-center p-8">
|
|
91
|
+
<div className="max-w-2xl w-full bg-red-50 border-2 border-red-200 rounded-lg p-8">
|
|
92
|
+
<h1 className="text-xl font-bold text-red-800 mb-4">Failed to load project</h1>
|
|
93
|
+
<div className="bg-white border-2 border-red-100 rounded p-6 mb-6">
|
|
94
|
+
<p className="font-mono text-base text-red-700 whitespace-pre-wrap">{error}</p>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
) : serializedData ? (
|
|
99
|
+
<div className="max-w-7xl w-full mx-auto px-4 py-4">
|
|
100
|
+
<RealTimeKanbanWrapper initialData={serializedData} isBlank={isBlank} projectPath={projectPath} />
|
|
101
|
+
</div>
|
|
102
|
+
) : null;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<>
|
|
106
|
+
{/* Kanban board — always mounted, hidden when viewing a child route */}
|
|
107
|
+
<div style={isChildRoute ? { display: 'none' } : undefined}>
|
|
108
|
+
{kanbanContent}
|
|
109
|
+
</div>
|
|
110
|
+
{/* Child route content (work detail, proof dashboard, decision) */}
|
|
111
|
+
<Outlet />
|
|
112
|
+
</>
|
|
113
|
+
);
|
|
61
114
|
}
|
|
@@ -1,27 +1,75 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
2
|
import { AccountSection } from '@/components/settings/AccountSection';
|
|
3
3
|
import { EnvVarsSection } from '@/components/settings/EnvVarsSection';
|
|
4
4
|
import { GeneralSection } from '@/components/settings/GeneralSection';
|
|
5
|
+
import { AiContextSection } from '@/components/settings/AiContextSection';
|
|
6
|
+
import { ProjectStackSection } from '@/components/settings/ProjectStackSection';
|
|
5
7
|
import { SettingsLayout } from '@/components/settings/SettingsLayout';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
import { dataBridge, prefetch } from '@/lib/data-bridge';
|
|
9
|
+
import type { ContextDocument } from '@/lib/data-bridge';
|
|
10
|
+
import type { EnvironmentConfig } from '@/lib/environment-config';
|
|
8
11
|
|
|
9
12
|
export default function SettingsPage() {
|
|
10
|
-
const envFiles =
|
|
11
|
-
const selectedFile =
|
|
12
|
-
const envVars =
|
|
13
|
-
const mainBranch =
|
|
13
|
+
const [envFiles, setEnvFiles] = useState<string[]>([]);
|
|
14
|
+
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
|
15
|
+
const [envVars, setEnvVars] = useState<Array<{ key: string; value: string }>>([]);
|
|
16
|
+
const [mainBranch, setMainBranch] = useState('main');
|
|
17
|
+
const [claudeModel, setClaudeModel] = useState<string | null>(null);
|
|
18
|
+
const [designSystemDir, setDesignSystemDir] = useState<string | null>(null);
|
|
19
|
+
const [contextDocuments, setContextDocuments] = useState<ContextDocument[]>([]);
|
|
20
|
+
const [environmentConfig, setEnvironmentConfig] = useState<EnvironmentConfig | null>(null);
|
|
21
|
+
const [loading, setLoading] = useState(true);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
async function loadSettings() {
|
|
25
|
+
try {
|
|
26
|
+
const { files, selected, branch, claudeModel: model, designSystemDir: dsDir, contextDocuments: ctxDocs, environmentConfig: envConfig } = await prefetch.settings();
|
|
27
|
+
setEnvFiles(files);
|
|
28
|
+
const activeFile = selected || (files.includes('.env') ? '.env' : files[0] || null);
|
|
29
|
+
setSelectedFile(activeFile);
|
|
30
|
+
if (activeFile) {
|
|
31
|
+
const vars = await dataBridge.getEnvVars(activeFile);
|
|
32
|
+
setEnvVars(vars);
|
|
33
|
+
}
|
|
34
|
+
setMainBranch(branch);
|
|
35
|
+
setClaudeModel(model);
|
|
36
|
+
setDesignSystemDir(dsDir);
|
|
37
|
+
setContextDocuments(ctxDocs);
|
|
38
|
+
setEnvironmentConfig(envConfig);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error('Failed to load settings:', err);
|
|
41
|
+
} finally {
|
|
42
|
+
setLoading(false);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
loadSettings();
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
if (loading) return (
|
|
49
|
+
<div className="flex-1 overflow-auto max-w-7xl w-full mx-auto px-4 py-4">
|
|
50
|
+
<div className="h-8 w-40 bg-zinc-200 dark:bg-zinc-800 rounded mb-8" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
|
|
51
|
+
<div className="flex gap-4 mb-6">
|
|
52
|
+
{[1, 2, 3, 4, 5].map(i => (
|
|
53
|
+
<div key={i} className="h-9 w-32 bg-zinc-200 dark:bg-zinc-800 rounded-lg" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
<div className="space-y-4">
|
|
57
|
+
<div className="h-12 bg-zinc-200 dark:bg-zinc-800 rounded-lg" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
|
|
58
|
+
<div className="h-12 bg-zinc-200 dark:bg-zinc-800 rounded-lg" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
14
62
|
|
|
15
63
|
return (
|
|
16
|
-
<div className="flex-1 overflow-auto max-w-
|
|
17
|
-
<h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mb-
|
|
18
|
-
Settings
|
|
19
|
-
</h1>
|
|
64
|
+
<div className="flex-1 overflow-auto max-w-7xl w-full mx-auto px-4 py-4">
|
|
65
|
+
<h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mb-8">Settings</h1>
|
|
20
66
|
<SettingsLayout
|
|
21
67
|
tabs={[
|
|
22
68
|
{ id: 'account', label: 'Account', content: <AccountSection /> },
|
|
23
|
-
{ id: 'general', label: 'General', content: <GeneralSection initialMainBranch={mainBranch} /> },
|
|
24
|
-
{ id: 'env-vars', label: 'Environment Variables', content: <EnvVarsSection initialEnvVars={envVars} envFiles={envFiles} selectedFile={selectedFile} /> },
|
|
69
|
+
{ id: 'general', label: 'General', content: <GeneralSection initialMainBranch={{ branch: mainBranch, source: 'detected' }} initialClaudeModel={claudeModel} /> },
|
|
70
|
+
{ id: 'env-vars', label: 'Environment Variables', content: <EnvVarsSection initialEnvVars={envVars.map(v => ({ name: v.key, value: v.value }))} envFiles={envFiles} selectedFile={selectedFile} /> },
|
|
71
|
+
{ id: 'ai-context', label: 'AI Context', content: <AiContextSection initialDesignSystemDir={designSystemDir} initialContextDocuments={contextDocuments} /> },
|
|
72
|
+
{ id: 'project-stack', label: 'Your Project Stack', content: <ProjectStackSection initialConfig={environmentConfig} /> },
|
|
25
73
|
]}
|
|
26
74
|
/>
|
|
27
75
|
</div>
|