Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16,190 changes: 14,360 additions & 1,830 deletions apps/web/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"canvas-confetti": "^1.9.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"lucide-react": "^1.14.0",
"motion": "^12.38.0",
"next": "16.2.4",
Expand Down
68 changes: 1 addition & 67 deletions apps/web/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,7 @@
}

:root {
--radius: 0.625rem;
--glow-color: oklch(0.6 0.15 250);
--glow-color-current: oklch(0.7 0.18 80);
--gradient-start: oklch(0.7 0.15 250);
--gradient-mid: oklch(0.7 0.15 300);
--gradient-end: oklch(0.7 0.15 200);
--radius: 0.375rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
Expand Down Expand Up @@ -88,11 +83,6 @@
}

.dark {
--glow-color: oklch(0.5 0.2 250);
--glow-color-current: oklch(0.6 0.22 80);
--gradient-start: oklch(0.5 0.2 250);
--gradient-mid: oklch(0.5 0.2 300);
--gradient-end: oklch(0.5 0.2 200);
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
Expand Down Expand Up @@ -137,70 +127,18 @@

/* === Keyframe Animations === */

@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}

@keyframes pulse-dot {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.85); }
}

@keyframes checkmark-draw {
0% { stroke-dashoffset: 24; }
100% { stroke-dashoffset: 0; }
}

@keyframes shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-2px); }
40% { transform: translateX(2px); }
60% { transform: translateX(-1px); }
80% { transform: translateX(1px); }
}

@keyframes edge-flow {
0% { stroke-dashoffset: 0; }
100% { stroke-dashoffset: -20; }
}

@keyframes gradient-shift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}

@keyframes mesh-float {
0%, 100% { transform: translate(0, 0) scale(1); }
33% { transform: translate(30px, -20px) scale(1.05); }
66% { transform: translate(-20px, 15px) scale(0.95); }
}

@keyframes fade-in-up {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}

/* === Utility Classes === */

.animate-shimmer {
animation: shimmer 2s infinite;
}

.animate-pulse-dot {
animation: pulse-dot 1.5s ease-in-out infinite;
}

.animate-shake {
animation: shake 0.4s ease-in-out;
}

.animate-gradient-shift {
background-size: 200% 200%;
animation: gradient-shift 6s ease infinite;
}

.animate-fade-in-up {
animation: fade-in-up 0.3s ease-out both;
}
Expand All @@ -216,10 +154,6 @@
/* === Reduced Motion === */

@media (prefers-reduced-motion: reduce) {
.animate-shimmer,
.animate-pulse-dot,
.animate-shake,
.animate-gradient-shift,
.animate-fade-in-up,
.animate-edge-flow,
.animate-edge-flow-fast {
Expand Down
147 changes: 93 additions & 54 deletions apps/web/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
"use client";

import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import dynamic from "next/dynamic";
import { useRepo } from "@/components/providers/repo-provider";
import { OwnerSwimlane } from "@/components/swimlane/owner-swimlane";
import { getLastActiveDate } from "@/lib/swimlane-grouping";
import { BranchDetail } from "@/components/branch-detail/branch-detail";
import { StackDetailPanel } from "@/components/branch-detail/stack-detail";
import { DetailEmptyState } from "@/components/branch-detail/detail-empty-state";
import {
CommandPalette,
useCommandPaletteShortcut,
} from "@/components/layout/command-palette";
import { EventFeed } from "@/components/layout/event-feed";
import { Header } from "@/components/layout/header";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
import { RecentlyMerged } from "@/components/recently-merged/recently-merged";
import { BackgroundMesh } from "@/components/ui/background-mesh";
import { SkeletonSwimlane } from "@/components/ui/skeleton-shimmer";
import { SkeletonSwimlane } from "@/components/ui/skeleton-swimlane";
import { useUrlSelection } from "@/hooks/use-url-selection";
import { groupStacksByOwner } from "@/lib/swimlane-grouping";

Expand All @@ -32,6 +42,8 @@ const BranchDiffWorkspace = dynamic(
}
);

type RightTab = "detail" | "stack" | "activity";

export default function Home() {
const {
repo,
Expand Down Expand Up @@ -63,6 +75,18 @@ export default function Home() {
const hasSelection = selectedBranch || selectedStack;
const showRecentCommits = selectedBranch ? false : recentCommitsPreference;
const branchOverlayMode = Boolean(selectedBranch && selectedBranchStack);

const [activeTab, setActiveTab] = useState<RightTab>("activity");
useEffect(() => {
if (selectedBranch) setActiveTab("detail");
else if (selectedStack) setActiveTab("stack");
}, [selectedBranch, selectedStack]);

const [paletteOpen, setPaletteOpen] = useState(false);
useCommandPaletteShortcut(setPaletteOpen);

const stackForStackTab = selectedStack ?? selectedBranchStack ?? null;

const stacksAndHistoryContent =
stackDetails.length > 0 ? (
<div className="flex flex-col justify-end min-h-full">
Expand Down Expand Up @@ -101,20 +125,22 @@ export default function Home() {

{/* Trunk line */}
<div className="flex items-center gap-2 px-6 pb-2 shrink-0">
<div className="flex-1 h-[2px] bg-gradient-to-r from-transparent via-muted-foreground/30 to-muted-foreground/30" />
<Separator className="flex-1" />
{selectedBranch ? (
<span className="text-xs font-mono text-muted-foreground/70 px-2">
{repo?.trunk}
</span>
) : (
<button
<Button
variant="ghost"
size="xs"
onClick={() => setRecentCommitsPreference((prev) => !prev)}
className="text-xs font-mono text-muted-foreground/70 px-2 hover:text-muted-foreground transition-colors cursor-pointer"
className="font-mono text-muted-foreground/70 hover:text-muted-foreground"
>
{repo?.trunk}
</button>
</Button>
)}
<div className="flex-1 h-[2px] bg-gradient-to-l from-transparent via-muted-foreground/30 to-muted-foreground/30" />
<Separator className="flex-1" />
</div>

{/* Recent trunk commits */}
Expand All @@ -137,12 +163,7 @@ export default function Home() {
);

if (loading) {
return (
<>
<BackgroundMesh />
<SkeletonSwimlane />
</>
);
return <SkeletonSwimlane />;
}

if (error) {
Expand All @@ -153,20 +174,27 @@ export default function Home() {
Make sure stackit-web is running on{" "}
{process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080"}
</p>
<button
onClick={refresh}
className="text-sm underline text-muted-foreground hover:text-foreground"
>
<Button variant="link" size="sm" onClick={refresh}>
Retry
</button>
</Button>
</div>
);
}

return (
<div className="flex flex-col h-screen">
<BackgroundMesh />
<Header repo={repo ?? null} lastUpdated={lastUpdated ?? null} refresh={refresh} />
<Header
repo={repo ?? null}
lastUpdated={lastUpdated ?? null}
refresh={refresh}
onOpenPalette={() => setPaletteOpen(true)}
/>
<CommandPalette
open={paletteOpen}
onOpenChange={setPaletteOpen}
onSelectBranch={handleSelectBranch}
onSelectStack={handleSelectStack}
/>

{/* Main content: stacks area + detail panel */}
<div className="flex flex-1 overflow-hidden">
Expand All @@ -190,48 +218,59 @@ export default function Home() {
)}
</div>

{/* Right: detail + event feed panel (always visible) */}
{/* Right: tabbed detail panel */}
<div className="flex shrink-0">
<Separator orientation="vertical" />
<div className="w-[480px] shrink-0 flex flex-col overflow-hidden">
{branchOverlayMode && selectedBranchStack ? (
<div className="flex-1 overflow-auto p-4">
<Tabs
value={activeTab}
onValueChange={(v) => setActiveTab(v as RightTab)}
className="w-[480px] shrink-0 flex flex-col overflow-hidden gap-0"
>
<div className="px-3 pt-3 pb-2 shrink-0">
<TabsList className="w-full">
<TabsTrigger value="detail" disabled={!selectedBranch}>
Detail
</TabsTrigger>
<TabsTrigger value="stack" disabled={!stackForStackTab}>
Stack
</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
</TabsList>
</div>

<TabsContent value="detail" className="flex-1 overflow-auto p-4 mt-0">
{selectedBranch ? (
<BranchDetail
branch={selectedBranch}
onNavigateToBranch={handleNavigateToBranch}
/>
) : (
<DetailEmptyState />
)}
</TabsContent>

<TabsContent value="stack" className="flex-1 overflow-auto p-4 mt-0">
{stackForStackTab ? (
<StackDetailPanel
stack={selectedBranchStack}
stack={stackForStackTab}
onSelectBranch={handleStackBranchSelect}
selectedBranchName={selectedBranch?.name ?? null}
/>
</div>
) : hasSelection ? (
<div className="flex-1 overflow-auto p-4">
{selectedBranch && (
<BranchDetail
branch={selectedBranch}
onNavigateToBranch={handleNavigateToBranch}
/>
)}
{selectedStack && (
<StackDetailPanel
stack={selectedStack}
onSelectBranch={handleStackBranchSelect}
selectedBranchName={null}
/>
)}
</div>
) : (
<div className="flex-1">
) : (
<DetailEmptyState />
</div>
)}
{!branchOverlayMode && (
<>
<Separator />
<div className="p-3 overflow-auto">
)}
</TabsContent>

<TabsContent value="activity" className="flex-1 overflow-auto p-4 mt-0">
{hasSelection ? (
<div className="space-y-3">
<EventFeed />
</div>
</>
)}
</div>
) : (
<EventFeed />
)}
</TabsContent>
</Tabs>
</div>
</div>
</div>
Expand Down
Loading
Loading