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
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
|
|
2
|
+
import { useState, useCallback } from 'react';
|
|
3
|
+
import { m, AnimatePresence, useReducedMotion } from 'framer-motion';
|
|
4
|
+
import type { WorkItem } from '@/lib/db';
|
|
5
|
+
import { shadow } from '@/lib/shadows';
|
|
6
|
+
import { Button } from '@/components/ui/Button';
|
|
7
|
+
import { TypeIcon } from '@/components/TypeIcon';
|
|
8
|
+
|
|
9
|
+
interface OnboardingWelcomeProps {
|
|
10
|
+
onboardingItems: WorkItem[];
|
|
11
|
+
onStartChore: (id: number, title: string) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const REVEAL_EASE = [0.22, 1, 0.36, 1] as const;
|
|
15
|
+
|
|
16
|
+
export function OnboardingWelcome({ onboardingItems, onStartChore }: OnboardingWelcomeProps) {
|
|
17
|
+
const prefersReducedMotion = useReducedMotion();
|
|
18
|
+
const [phase, setPhase] = useState<'welcome' | 'transitioning' | 'revealed'>('welcome');
|
|
19
|
+
|
|
20
|
+
const handleLetsGo = useCallback(() => {
|
|
21
|
+
setPhase('transitioning');
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
const handleExitComplete = useCallback(() => {
|
|
25
|
+
setPhase('revealed');
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
const handleStartChore = useCallback((item: WorkItem) => {
|
|
29
|
+
onStartChore(item.id, item.title);
|
|
30
|
+
}, [onStartChore]);
|
|
31
|
+
|
|
32
|
+
const hasItems = onboardingItems.length > 0;
|
|
33
|
+
const isRevealed = phase === 'revealed';
|
|
34
|
+
const dur = prefersReducedMotion ? 0.05 : undefined;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
className={`h-full flex justify-center overflow-hidden relative ${isRevealed ? 'items-start pt-16' : 'items-center pt-0'}`}
|
|
39
|
+
style={{ transition: prefersReducedMotion ? 'none' : 'padding-top 0.8s cubic-bezier(0.22, 1, 0.36, 1)' }}
|
|
40
|
+
>
|
|
41
|
+
<div className="flex items-start gap-12">
|
|
42
|
+
{/* Primary column — starts as single centered column, narrows to left column on reveal */}
|
|
43
|
+
<m.div
|
|
44
|
+
className="shrink-0 flex flex-col"
|
|
45
|
+
animate={{ width: isRevealed ? 340 : 480 }}
|
|
46
|
+
initial={false}
|
|
47
|
+
transition={{ duration: dur ?? 0.8, ease: REVEAL_EASE }}
|
|
48
|
+
>
|
|
49
|
+
{/* Greeting + intro — fade out on "Let's go" */}
|
|
50
|
+
<AnimatePresence onExitComplete={handleExitComplete}>
|
|
51
|
+
{phase === 'welcome' && (
|
|
52
|
+
<>
|
|
53
|
+
<m.div
|
|
54
|
+
key="greeting"
|
|
55
|
+
className="text-base leading-relaxed mb-4"
|
|
56
|
+
initial={{ opacity: 0, y: 10 }}
|
|
57
|
+
animate={{ opacity: 1, y: 0 }}
|
|
58
|
+
exit={{ opacity: 0, y: -5, transition: { duration: dur ?? 0.25 } }}
|
|
59
|
+
transition={{ duration: dur ?? 0.4, delay: prefersReducedMotion ? 0 : 0.2 }}
|
|
60
|
+
>
|
|
61
|
+
Ahoy. Welcome to your new project.
|
|
62
|
+
</m.div>
|
|
63
|
+
|
|
64
|
+
<m.div
|
|
65
|
+
key="intro"
|
|
66
|
+
className="text-base leading-relaxed mb-4"
|
|
67
|
+
initial={{ opacity: 0, y: 10 }}
|
|
68
|
+
animate={{ opacity: 1, y: 0 }}
|
|
69
|
+
exit={{ opacity: 0, y: -5, transition: { duration: dur ?? 0.25 } }}
|
|
70
|
+
transition={{ duration: dur ?? 0.4, delay: prefersReducedMotion ? 0 : 0.6 }}
|
|
71
|
+
>
|
|
72
|
+
Here’s what we’ll work through:
|
|
73
|
+
</m.div>
|
|
74
|
+
</>
|
|
75
|
+
)}
|
|
76
|
+
</AnimatePresence>
|
|
77
|
+
|
|
78
|
+
{/* Epic label — appears above cards on reveal */}
|
|
79
|
+
<AnimatePresence>
|
|
80
|
+
{isRevealed && (
|
|
81
|
+
<m.div
|
|
82
|
+
key="epic-label"
|
|
83
|
+
className="flex items-center gap-1.5 text-xs font-semibold text-muted-foreground mb-2.5 uppercase tracking-wide"
|
|
84
|
+
initial={{ opacity: 0 }}
|
|
85
|
+
animate={{ opacity: 1 }}
|
|
86
|
+
transition={{ duration: dur ?? 0.3, delay: prefersReducedMotion ? 0 : 0.3 }}
|
|
87
|
+
>
|
|
88
|
+
<TypeIcon type="epic" className="w-4 h-4 inline" />
|
|
89
|
+
<span>Project Planning</span>
|
|
90
|
+
</m.div>
|
|
91
|
+
)}
|
|
92
|
+
</AnimatePresence>
|
|
93
|
+
|
|
94
|
+
{/* Cards — SAME DOM elements throughout, never unmount */}
|
|
95
|
+
{onboardingItems.map((item, i) => {
|
|
96
|
+
const isFirst = i === 0;
|
|
97
|
+
return (
|
|
98
|
+
<m.div
|
|
99
|
+
key={item.id}
|
|
100
|
+
className="bg-white dark:bg-zinc-900 border-2 border-zinc-200 dark:border-zinc-700 rounded-xl px-3.5 py-3 mb-2 flex items-center justify-between"
|
|
101
|
+
style={{
|
|
102
|
+
boxShadow: isRevealed && isFirst
|
|
103
|
+
? `${shadow.sm}, 0 0 0 3px rgba(129,157,159,0.35)`
|
|
104
|
+
: shadow.sm,
|
|
105
|
+
animation: isRevealed && isFirst ? 'onboarding-pulse-glow 2s ease-in-out infinite' : undefined,
|
|
106
|
+
}}
|
|
107
|
+
initial={{ opacity: 0, y: 10 }}
|
|
108
|
+
animate={{ opacity: 1, y: 0 }}
|
|
109
|
+
transition={{
|
|
110
|
+
duration: dur ?? 0.4,
|
|
111
|
+
delay: prefersReducedMotion ? 0 : 0.8 + i * 0.1,
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<div className="flex items-center gap-2.5">
|
|
115
|
+
<TypeIcon type="chore" className="w-5 h-5 inline" />
|
|
116
|
+
<span className="text-sm font-medium leading-snug">{item.title}</span>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{/* Start button — grows in after cards settle */}
|
|
120
|
+
{isRevealed && (
|
|
121
|
+
<m.div
|
|
122
|
+
initial={{ opacity: 0, scale: 0.9 }}
|
|
123
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
124
|
+
transition={{ duration: dur ?? 0.3, delay: prefersReducedMotion ? 0 : 0.5 }}
|
|
125
|
+
>
|
|
126
|
+
{isFirst ? (
|
|
127
|
+
<button
|
|
128
|
+
onClick={() => handleStartChore(item)}
|
|
129
|
+
className="px-3 py-1 text-[11px] font-semibold rounded-sm border-2 border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-muted-foreground cursor-pointer whitespace-nowrap"
|
|
130
|
+
style={{ animation: 'onboarding-pulse-bg 2s ease-in-out infinite' }}
|
|
131
|
+
>
|
|
132
|
+
start
|
|
133
|
+
</button>
|
|
134
|
+
) : (
|
|
135
|
+
<button
|
|
136
|
+
disabled
|
|
137
|
+
className="px-3 py-1 text-[11px] font-semibold rounded-sm border-2 border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-muted-foreground opacity-40 cursor-default whitespace-nowrap"
|
|
138
|
+
>
|
|
139
|
+
start
|
|
140
|
+
</button>
|
|
141
|
+
)}
|
|
142
|
+
</m.div>
|
|
143
|
+
)}
|
|
144
|
+
</m.div>
|
|
145
|
+
);
|
|
146
|
+
})}
|
|
147
|
+
|
|
148
|
+
{/* Let's go button — fades out with greeting */}
|
|
149
|
+
<AnimatePresence>
|
|
150
|
+
{phase === 'welcome' && (
|
|
151
|
+
<m.div
|
|
152
|
+
key="button"
|
|
153
|
+
className="flex gap-2 mt-2"
|
|
154
|
+
initial={{ opacity: 0 }}
|
|
155
|
+
animate={{ opacity: 1 }}
|
|
156
|
+
exit={{ opacity: 0, transition: { duration: dur ?? 0.15 } }}
|
|
157
|
+
transition={{ duration: dur ?? 0.4, delay: prefersReducedMotion ? 0 : 1.0 + onboardingItems.length * 0.1 }}
|
|
158
|
+
>
|
|
159
|
+
<Button onClick={handleLetsGo} size="sm">
|
|
160
|
+
Let’s go
|
|
161
|
+
</Button>
|
|
162
|
+
</m.div>
|
|
163
|
+
)}
|
|
164
|
+
</AnimatePresence>
|
|
165
|
+
</m.div>
|
|
166
|
+
|
|
167
|
+
{/* Conversation column — expands in from right on reveal */}
|
|
168
|
+
{isRevealed && (
|
|
169
|
+
<m.div
|
|
170
|
+
className="shrink-0 overflow-hidden"
|
|
171
|
+
initial={{ width: 0 }}
|
|
172
|
+
animate={{ width: 480 }}
|
|
173
|
+
transition={{ duration: dur ?? 0.8, ease: REVEAL_EASE }}
|
|
174
|
+
>
|
|
175
|
+
<div className="w-[480px] flex flex-col gap-4">
|
|
176
|
+
<m.div
|
|
177
|
+
className="text-base leading-relaxed"
|
|
178
|
+
initial={{ opacity: 0, x: 20 }}
|
|
179
|
+
animate={{ opacity: 1, x: 0 }}
|
|
180
|
+
transition={{ duration: dur ?? 0.5, delay: prefersReducedMotion ? 0 : 0.3 }}
|
|
181
|
+
>
|
|
182
|
+
Each card is a short conversation — we’ll knock them out one at a time.
|
|
183
|
+
</m.div>
|
|
184
|
+
|
|
185
|
+
{hasItems && (
|
|
186
|
+
<m.div
|
|
187
|
+
className="text-sm leading-normal px-3.5 py-2.5 bg-yellow-50 border-2 border-yellow-200 rounded-lg"
|
|
188
|
+
initial={{ opacity: 0, y: 8 }}
|
|
189
|
+
animate={{ opacity: 1, y: 0 }}
|
|
190
|
+
transition={{ duration: dur ?? 0.4, delay: prefersReducedMotion ? 0 : 0.6 }}
|
|
191
|
+
>
|
|
192
|
+
👈 Click <strong>start</strong> on the first card to begin.
|
|
193
|
+
</m.div>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
</m.div>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{/* Keyframe animations for the highlighted card */}
|
|
201
|
+
<style>{`
|
|
202
|
+
@keyframes onboarding-pulse-glow {
|
|
203
|
+
0%, 100% { box-shadow: ${shadow.sm}, 0 0 0 0px rgba(129,157,159,0); }
|
|
204
|
+
50% { box-shadow: ${shadow.sm}, 0 0 0 3px rgba(129,157,159,0.35); }
|
|
205
|
+
}
|
|
206
|
+
@keyframes onboarding-pulse-bg {
|
|
207
|
+
0%, 100% { background: #ffffff; }
|
|
208
|
+
50% { background: rgba(129,157,159,0.12); border-color: rgba(129,157,159,0.4); color: #3d4d4e; }
|
|
209
|
+
}
|
|
210
|
+
`}</style>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
'use client';
|
|
2
1
|
|
|
3
|
-
import {
|
|
2
|
+
import { m } from 'framer-motion';
|
|
4
3
|
|
|
5
4
|
export function PlaceholderCard() {
|
|
6
5
|
return (
|
|
7
|
-
<
|
|
6
|
+
<m.div
|
|
8
7
|
data-testid="drag-placeholder"
|
|
9
8
|
initial={{ opacity: 0, scaleX: 0.3 }}
|
|
10
9
|
animate={{ opacity: 1, scaleX: 1 }}
|
|
@@ -13,7 +12,7 @@ export function PlaceholderCard() {
|
|
|
13
12
|
style={{
|
|
14
13
|
height: 3,
|
|
15
14
|
borderRadius: 2,
|
|
16
|
-
background: 'linear-gradient(90deg, transparent,
|
|
15
|
+
background: 'linear-gradient(90deg, transparent, #819D9F, transparent)',
|
|
17
16
|
margin: '2px 8px',
|
|
18
17
|
}}
|
|
19
18
|
/>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
'use client';
|
|
2
1
|
|
|
3
2
|
import { useState, useRef, useEffect } from 'react';
|
|
4
3
|
import { createPortal } from 'react-dom';
|
|
5
|
-
import
|
|
4
|
+
import { isTauri, project } from '@/lib/tauri-bridge';
|
|
5
|
+
import type { RecentProject } from '@/lib/tauri-bridge';
|
|
6
6
|
|
|
7
7
|
interface ProjectSwitcherProps {
|
|
8
8
|
projectName: string;
|
|
@@ -13,21 +13,21 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
|
|
|
13
13
|
const [recentProjects, setRecentProjects] = useState<RecentProject[]>([]);
|
|
14
14
|
const [dropdownPosition, setDropdownPosition] = useState<{ top: number; left: number } | null>(null);
|
|
15
15
|
const [error, setError] = useState<string | null>(null);
|
|
16
|
-
const [
|
|
16
|
+
const [isTauriApp, setIsTauriApp] = useState(false);
|
|
17
17
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
18
18
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
19
19
|
|
|
20
|
-
// Detect
|
|
20
|
+
// Detect Tauri after mount to avoid hydration mismatch
|
|
21
21
|
useEffect(() => {
|
|
22
|
-
|
|
22
|
+
setIsTauriApp(isTauri());
|
|
23
23
|
}, []);
|
|
24
24
|
|
|
25
25
|
// Load recent projects when dropdown opens
|
|
26
26
|
useEffect(() => {
|
|
27
|
-
if (isOpen &&
|
|
28
|
-
|
|
27
|
+
if (isOpen && isTauriApp) {
|
|
28
|
+
project.getRecent().then(setRecentProjects);
|
|
29
29
|
}
|
|
30
|
-
}, [isOpen,
|
|
30
|
+
}, [isOpen, isTauriApp]);
|
|
31
31
|
|
|
32
32
|
// Calculate dropdown position when opening
|
|
33
33
|
useEffect(() => {
|
|
@@ -57,18 +57,18 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
|
|
|
57
57
|
}
|
|
58
58
|
}, [isOpen]);
|
|
59
59
|
|
|
60
|
-
const handleProjectClick = async (
|
|
60
|
+
const handleProjectClick = async (p: RecentProject) => {
|
|
61
61
|
setError(null);
|
|
62
62
|
setIsOpen(false);
|
|
63
63
|
try {
|
|
64
|
-
const result = await
|
|
64
|
+
const result = await project.openRecent(p.path);
|
|
65
65
|
if (result.success) {
|
|
66
66
|
window.location.reload();
|
|
67
67
|
} else {
|
|
68
|
-
setError(`Failed to switch to "${
|
|
68
|
+
setError(`Failed to switch to "${p.name}". The project may no longer exist.`);
|
|
69
69
|
}
|
|
70
70
|
} catch {
|
|
71
|
-
setError(`Failed to switch to "${
|
|
71
|
+
setError(`Failed to switch to "${p.name}". The project may no longer exist.`);
|
|
72
72
|
}
|
|
73
73
|
};
|
|
74
74
|
|
|
@@ -76,7 +76,7 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
|
|
|
76
76
|
setError(null);
|
|
77
77
|
setIsOpen(false);
|
|
78
78
|
try {
|
|
79
|
-
const result = await
|
|
79
|
+
const result = await project.newProject();
|
|
80
80
|
if (result.success) {
|
|
81
81
|
window.location.reload();
|
|
82
82
|
}
|
|
@@ -89,7 +89,7 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
|
|
|
89
89
|
setError(null);
|
|
90
90
|
setIsOpen(false);
|
|
91
91
|
try {
|
|
92
|
-
const result = await
|
|
92
|
+
const result = await project.openDialog();
|
|
93
93
|
if (result.success) {
|
|
94
94
|
window.location.reload();
|
|
95
95
|
}
|
|
@@ -99,10 +99,10 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
|
|
|
99
99
|
}
|
|
100
100
|
};
|
|
101
101
|
|
|
102
|
-
// Non-
|
|
103
|
-
if (!
|
|
102
|
+
// Non-Tauri: render static pill (no switching possible)
|
|
103
|
+
if (!isTauriApp) {
|
|
104
104
|
return (
|
|
105
|
-
<span className="px-
|
|
105
|
+
<span className="px-5 py-1.5 text-base bg-zinc-100 text-zinc-600 rounded-full border-2 border-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:border-zinc-700">
|
|
106
106
|
{projectName}
|
|
107
107
|
</span>
|
|
108
108
|
);
|
|
@@ -111,43 +111,43 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
|
|
|
111
111
|
const dropdownContent = (
|
|
112
112
|
<div
|
|
113
113
|
ref={dropdownRef}
|
|
114
|
-
className="fixed z-50 bg-white dark:bg-zinc-800 rounded-lg shadow-lg
|
|
114
|
+
className="fixed z-50 bg-white dark:bg-zinc-800 rounded-lg shadow-lg py-1.5 min-w-[200px] max-w-[320px]"
|
|
115
115
|
style={{
|
|
116
116
|
top: dropdownPosition?.top ?? 0,
|
|
117
117
|
left: dropdownPosition?.left ?? 0,
|
|
118
118
|
}}
|
|
119
119
|
>
|
|
120
120
|
{/* Current project */}
|
|
121
|
-
<div className="px-
|
|
121
|
+
<div className="px-4 py-3 text-base text-zinc-400 dark:text-zinc-500 flex items-center gap-3">
|
|
122
122
|
<span className="w-1.5 h-1.5 rounded-full bg-green-500 flex-shrink-0" />
|
|
123
123
|
<span className="truncate">{projectName}</span>
|
|
124
124
|
</div>
|
|
125
125
|
|
|
126
126
|
{/* Other recent projects */}
|
|
127
127
|
{recentProjects.filter(p => p.name !== projectName).length > 0 && (
|
|
128
|
-
<div className="px-
|
|
128
|
+
<div className="px-4 pt-3 pb-1.5 text-base font-medium text-zinc-400 dark:text-zinc-500 uppercase tracking-wider">Recent</div>
|
|
129
129
|
)}
|
|
130
130
|
{recentProjects
|
|
131
131
|
.filter(p => p.name !== projectName)
|
|
132
132
|
.slice(0, 4)
|
|
133
|
-
.map((
|
|
133
|
+
.map((p) => (
|
|
134
134
|
<button
|
|
135
|
-
key={
|
|
136
|
-
onClick={() => handleProjectClick(
|
|
137
|
-
className="w-full px-
|
|
135
|
+
key={p.path}
|
|
136
|
+
onClick={() => handleProjectClick(p)}
|
|
137
|
+
className="w-full px-4 py-3 text-left text-base text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-3"
|
|
138
138
|
>
|
|
139
139
|
<span className="w-1.5 h-1.5 flex-shrink-0" />
|
|
140
|
-
<span className="truncate">{
|
|
140
|
+
<span className="truncate">{p.name}</span>
|
|
141
141
|
</button>
|
|
142
142
|
))}
|
|
143
143
|
|
|
144
144
|
{/* Divider */}
|
|
145
|
-
<div className="border-t border-zinc-200 dark:border-zinc-700 my-1" />
|
|
145
|
+
<div className="border-t border-zinc-200 dark:border-zinc-700 my-1.5" />
|
|
146
146
|
|
|
147
147
|
{/* New Project action */}
|
|
148
148
|
<button
|
|
149
149
|
onClick={handleNewProject}
|
|
150
|
-
className="w-full px-
|
|
150
|
+
className="w-full px-4 py-3 text-left text-base text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-3"
|
|
151
151
|
>
|
|
152
152
|
<svg className="w-3.5 h-3.5 text-zinc-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
153
153
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
|
|
@@ -158,7 +158,7 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
|
|
|
158
158
|
{/* Open Project action */}
|
|
159
159
|
<button
|
|
160
160
|
onClick={handleOpenProject}
|
|
161
|
-
className="w-full px-
|
|
161
|
+
className="w-full px-4 py-3 text-left text-base text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-3"
|
|
162
162
|
>
|
|
163
163
|
<svg className="w-3.5 h-3.5 text-zinc-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
164
164
|
<path strokeLinecap="round" strokeLinejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
|
@@ -173,7 +173,7 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
|
|
|
173
173
|
<button
|
|
174
174
|
ref={buttonRef}
|
|
175
175
|
onClick={() => { setError(null); setIsOpen(!isOpen); }}
|
|
176
|
-
className="px-
|
|
176
|
+
className="px-5 py-1.5 text-base bg-zinc-100 text-zinc-600 rounded-full border-2 border-zinc-200 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:border-zinc-700 dark:hover:bg-zinc-700 transition-colors duration-200 ease-out cursor-pointer flex items-center gap-1.5"
|
|
177
177
|
>
|
|
178
178
|
{projectName}
|
|
179
179
|
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
@@ -182,7 +182,7 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
|
|
|
182
182
|
</button>
|
|
183
183
|
|
|
184
184
|
{error && (
|
|
185
|
-
<div className="absolute top-full left-0 mt-1 px-
|
|
185
|
+
<div className="absolute top-full left-0 mt-1 px-3 py-2 text-xs text-red-700 bg-red-50 border-2 border-red-200 rounded-lg dark:text-red-400 dark:bg-red-900/20 dark:border-red-800 whitespace-nowrap">
|
|
186
186
|
{error}
|
|
187
187
|
</div>
|
|
188
188
|
)}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
'use client';
|
|
2
1
|
|
|
3
2
|
import { useState, useEffect } from 'react';
|
|
4
|
-
import type { PrototypeDashboardData, Prototype } from '@/lib/
|
|
3
|
+
import type { PrototypeDashboardData, Prototype } from '@/lib/db';
|
|
4
|
+
import { shell } from '@/lib/tauri-bridge';
|
|
5
|
+
import { invoke } from '@/lib/tauri';
|
|
5
6
|
|
|
6
7
|
interface PrototypeCardProps {
|
|
7
8
|
prototype: Prototype;
|
|
@@ -19,30 +20,30 @@ function PrototypeCard({ prototype, isSelected, onSelect, compact }: PrototypeCa
|
|
|
19
20
|
return (
|
|
20
21
|
<div
|
|
21
22
|
onClick={onSelect}
|
|
22
|
-
className={`flex bg-white
|
|
23
|
-
isSelected ? 'border-
|
|
23
|
+
className={`flex bg-white rounded-lg overflow-hidden cursor-pointer transition-colors duration-200 ease-out ${
|
|
24
|
+
isSelected ? 'border-2 border-[#819D9F] bg-[#e8f0f0]' : ''
|
|
24
25
|
}`}
|
|
25
26
|
>
|
|
26
27
|
{/* Date column */}
|
|
27
|
-
<div className="w-12 shrink-0 bg-zinc-100 flex flex-col items-center justify-center py-
|
|
28
|
+
<div className="w-12 shrink-0 bg-zinc-100 flex flex-col items-center justify-center py-3 px-1.5 border-r border-zinc-200">
|
|
28
29
|
<span className="text-base font-semibold text-zinc-900 leading-none">{day}</span>
|
|
29
30
|
<span className="text-[10px] text-zinc-500 uppercase leading-none mt-0.5">{weekday}</span>
|
|
30
31
|
</div>
|
|
31
32
|
|
|
32
33
|
{/* Content */}
|
|
33
|
-
<div className="flex-1 py-
|
|
34
|
-
<div className="flex items-center gap-
|
|
35
|
-
<span className="font-medium text-
|
|
34
|
+
<div className="flex-1 py-3 px-4">
|
|
35
|
+
<div className="flex items-center gap-3">
|
|
36
|
+
<span className="font-medium text-base text-zinc-900 truncate">{prototype.title}</span>
|
|
36
37
|
{prototype.files.length > 1 && (
|
|
37
|
-
<span className="text-[10px] px-
|
|
38
|
+
<span className="text-[10px] px-2 py-1 bg-green-100 text-green-700 rounded-full whitespace-nowrap">
|
|
38
39
|
{prototype.files.length} opts
|
|
39
40
|
</span>
|
|
40
41
|
)}
|
|
41
42
|
</div>
|
|
42
43
|
{!compact && (
|
|
43
|
-
<div className="flex items-center gap-
|
|
44
|
+
<div className="flex items-center gap-3 text-base">
|
|
44
45
|
{prototype.feature ? (
|
|
45
|
-
<span className="text-
|
|
46
|
+
<span className="text-[#5a7d7f]">#{prototype.feature.id}</span>
|
|
46
47
|
) : (
|
|
47
48
|
<span className="text-zinc-500">Research</span>
|
|
48
49
|
)}
|
|
@@ -66,7 +67,7 @@ function MonthGroup({ month, prototypes, selectedId, onSelect, compact }: MonthG
|
|
|
66
67
|
return (
|
|
67
68
|
<div className="mb-4">
|
|
68
69
|
{/* Month header */}
|
|
69
|
-
<div className="flex items-center gap-
|
|
70
|
+
<div className="flex items-center gap-3 mb-3">
|
|
70
71
|
<span className="text-[10px] font-semibold text-zinc-500 uppercase tracking-wider">
|
|
71
72
|
{month}
|
|
72
73
|
</span>
|
|
@@ -74,7 +75,7 @@ function MonthGroup({ month, prototypes, selectedId, onSelect, compact }: MonthG
|
|
|
74
75
|
</div>
|
|
75
76
|
|
|
76
77
|
{/* Prototype cards */}
|
|
77
|
-
<div className="space-y-
|
|
78
|
+
<div className="space-y-2">
|
|
78
79
|
{prototypes.map(prototype => (
|
|
79
80
|
<PrototypeCard
|
|
80
81
|
key={prototype.id}
|
|
@@ -89,15 +90,6 @@ function MonthGroup({ month, prototypes, selectedId, onSelect, compact }: MonthG
|
|
|
89
90
|
);
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
// Convert absolute file path to API URL
|
|
93
|
-
// e.g., /Users/.../prototypes/feature-123/file.html -> /api/prototypes/feature-123/file.html
|
|
94
|
-
function filePathToApiUrl(filePath: string): string {
|
|
95
|
-
const prototypesIndex = filePath.indexOf('/prototypes/');
|
|
96
|
-
if (prototypesIndex === -1) return filePath;
|
|
97
|
-
const relativePath = filePath.slice(prototypesIndex + '/prototypes/'.length);
|
|
98
|
-
return `/api/prototypes/${relativePath}`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
93
|
interface PreviewPanelProps {
|
|
102
94
|
prototype: Prototype;
|
|
103
95
|
onClose: () => void;
|
|
@@ -107,41 +99,52 @@ function PreviewPanel({ prototype, onClose }: PreviewPanelProps) {
|
|
|
107
99
|
// Find first previewable file (HTML preferred)
|
|
108
100
|
const getDefaultFile = () => prototype.files.find(f => f.type === 'html') || prototype.files[0];
|
|
109
101
|
const [selectedFile, setSelectedFile] = useState(getDefaultFile);
|
|
102
|
+
const [fileContent, setFileContent] = useState<string | null>(null);
|
|
103
|
+
const [loadError, setLoadError] = useState<string | null>(null);
|
|
110
104
|
|
|
111
105
|
// Reset selected file when prototype changes
|
|
112
106
|
useEffect(() => {
|
|
113
107
|
setSelectedFile(getDefaultFile());
|
|
114
108
|
}, [prototype.id]);
|
|
115
109
|
|
|
110
|
+
// Load file content via Tauri IPC
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (!selectedFile) return;
|
|
113
|
+
setFileContent(null);
|
|
114
|
+
setLoadError(null);
|
|
115
|
+
invoke<string>('read_prototype_file', { path: selectedFile.path })
|
|
116
|
+
.then(setFileContent)
|
|
117
|
+
.catch((err) => setLoadError(String(err)));
|
|
118
|
+
}, [selectedFile?.path]);
|
|
119
|
+
|
|
116
120
|
// Check if file is previewable in iframe
|
|
117
121
|
const previewableTypes = ['html', 'htm', 'txt', 'md', 'json', 'js', 'ts', 'css'];
|
|
118
122
|
const isPreviewable = selectedFile && previewableTypes.includes(selectedFile.type);
|
|
119
|
-
const previewUrl = selectedFile ? filePathToApiUrl(selectedFile.path) : null;
|
|
120
123
|
|
|
121
124
|
const handleOpen = () => {
|
|
122
|
-
if (
|
|
123
|
-
|
|
125
|
+
if (selectedFile) {
|
|
126
|
+
shell.openPath(selectedFile.path);
|
|
124
127
|
}
|
|
125
128
|
};
|
|
126
129
|
|
|
127
130
|
return (
|
|
128
|
-
<div className="flex flex-col h-full bg-white
|
|
131
|
+
<div className="flex flex-col h-full bg-white rounded-lg overflow-hidden">
|
|
129
132
|
{/* Header */}
|
|
130
|
-
<div className="flex items-center justify-between p-
|
|
133
|
+
<div className="flex items-center justify-between p-6 border-b border-zinc-200">
|
|
131
134
|
<div>
|
|
132
135
|
<h3 className="font-semibold text-zinc-900">{prototype.title}</h3>
|
|
133
|
-
<p className="text-
|
|
136
|
+
<p className="text-base text-zinc-500">{selectedFile?.name || prototype.description}</p>
|
|
134
137
|
</div>
|
|
135
138
|
<div className="flex items-center gap-2">
|
|
136
139
|
<button
|
|
137
140
|
onClick={handleOpen}
|
|
138
|
-
className="px-
|
|
141
|
+
className="px-4 py-2 text-base bg-zinc-100 border border-zinc-300 rounded text-zinc-700 hover:border-[#819D9F] hover:text-[#5a7d7f] transition-colors duration-200 ease-out"
|
|
139
142
|
>
|
|
140
143
|
Open
|
|
141
144
|
</button>
|
|
142
145
|
<button
|
|
143
146
|
onClick={onClose}
|
|
144
|
-
className="p-2 text-zinc-400 hover:text-zinc-700 transition-colors"
|
|
147
|
+
className="p-2 text-zinc-400 hover:text-zinc-700 transition-colors duration-200 ease-out"
|
|
145
148
|
>
|
|
146
149
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
147
150
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
@@ -152,16 +155,16 @@ function PreviewPanel({ prototype, onClose }: PreviewPanelProps) {
|
|
|
152
155
|
|
|
153
156
|
{/* File selector */}
|
|
154
157
|
{prototype.files.length > 1 && (
|
|
155
|
-
<div className="px-
|
|
156
|
-
<div className="flex flex-wrap gap-
|
|
158
|
+
<div className="px-5 py-3 border-b border-zinc-200">
|
|
159
|
+
<div className="flex flex-wrap gap-3">
|
|
157
160
|
{prototype.files.map(file => (
|
|
158
161
|
<button
|
|
159
162
|
key={file.path}
|
|
160
163
|
onClick={() => setSelectedFile(file)}
|
|
161
|
-
className={`px-
|
|
164
|
+
className={`px-3 py-1.5 text-base rounded transition-colors duration-200 ease-out ${
|
|
162
165
|
selectedFile?.path === file.path
|
|
163
|
-
? 'bg-
|
|
164
|
-
: 'bg-zinc-100 border border-zinc-300 text-zinc-700 hover:border-
|
|
166
|
+
? 'bg-[#819D9F] text-white'
|
|
167
|
+
: 'bg-zinc-100 border border-zinc-300 text-zinc-700 hover:border-[#819D9F]'
|
|
165
168
|
}`}
|
|
166
169
|
>
|
|
167
170
|
{file.name}
|
|
@@ -173,25 +176,43 @@ function PreviewPanel({ prototype, onClose }: PreviewPanelProps) {
|
|
|
173
176
|
|
|
174
177
|
{/* Preview content - 75% zoom using transform scale */}
|
|
175
178
|
<div className="flex-1 overflow-hidden">
|
|
176
|
-
{
|
|
179
|
+
{loadError ? (
|
|
180
|
+
<div className="flex items-center justify-center h-full text-red-500">
|
|
181
|
+
<div className="text-center">
|
|
182
|
+
<p className="mb-3">Failed to load preview</p>
|
|
183
|
+
<p className="text-base">{loadError}</p>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
) : fileContent && isPreviewable ? (
|
|
177
187
|
<div className="w-full h-full overflow-hidden">
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
{selectedFile && ['html', 'htm'].includes(selectedFile.type) ? (
|
|
189
|
+
<iframe
|
|
190
|
+
key={selectedFile.path}
|
|
191
|
+
srcDoc={fileContent}
|
|
192
|
+
className="border-0 bg-white origin-top-left"
|
|
193
|
+
style={{
|
|
194
|
+
width: '133.33%',
|
|
195
|
+
height: '133.33%',
|
|
196
|
+
transform: 'scale(0.75)',
|
|
197
|
+
}}
|
|
198
|
+
sandbox="allow-scripts allow-same-origin"
|
|
199
|
+
title={`Preview: ${selectedFile.name}`}
|
|
200
|
+
/>
|
|
201
|
+
) : (
|
|
202
|
+
<pre className="p-6 text-sm font-mono text-zinc-700 overflow-auto h-full whitespace-pre-wrap">
|
|
203
|
+
{fileContent}
|
|
204
|
+
</pre>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
) : fileContent === null && !loadError ? (
|
|
208
|
+
<div className="flex items-center justify-center h-full text-zinc-400">
|
|
209
|
+
Loading...
|
|
189
210
|
</div>
|
|
190
211
|
) : (
|
|
191
212
|
<div className="flex items-center justify-center h-full text-zinc-500">
|
|
192
213
|
<div className="text-center">
|
|
193
|
-
<p className="mb-
|
|
194
|
-
<p className="text-
|
|
214
|
+
<p className="mb-3">Cannot preview this file type</p>
|
|
215
|
+
<p className="text-base">{selectedFile?.name}</p>
|
|
195
216
|
</div>
|
|
196
217
|
</div>
|
|
197
218
|
)}
|
|
@@ -233,9 +254,9 @@ export function PrototypeTimeline({ data }: PrototypeTimelineProps) {
|
|
|
233
254
|
: null;
|
|
234
255
|
|
|
235
256
|
return (
|
|
236
|
-
<div className="flex gap-
|
|
257
|
+
<div className="flex gap-6">
|
|
237
258
|
{/* Timeline list */}
|
|
238
|
-
<div className={`${selectedPrototype ? 'w-[320px] shrink-0' : 'w-full'} transition-
|
|
259
|
+
<div className={`${selectedPrototype ? 'w-[320px] shrink-0' : 'w-full'} transition-[width] duration-200 ease-out`}>
|
|
239
260
|
{months.map(month => (
|
|
240
261
|
<MonthGroup
|
|
241
262
|
key={month}
|