diff --git a/CHANGELOG.md b/CHANGELOG.md index e30ee73..919de9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,28 @@ All notable changes to Abduction are documented here. This project follows ## [Unreleased] +### Changed + +- Redesigned the desktop chrome: the workspace header row is gone, repository actions (pull, editor, GitHub, clone) live in the titlebar, and the navigation rail is a fixed-width strip with always-visible labels instead of expanding over content on hover. +- Introduced a real type scale (nothing smaller than 10px), a single interactive highlight colour, consistent corner radii, and a restrained glow budget so active states and primary actions stand out. +- Consolidated three accreted stylesheets into `styles.css`, `themes.css`, and `splash.css`, removing dead rules and every `!important` override pass. +- Moved linting behind a Lint button in the reader header so the code canvas is not pushed down by a permanent toolbar. +- Replaced the branch-count "signal" bar on Stats with a contributor commit-share bar. +- Shortened the launch splash to five seconds and made it skippable with a click anywhere. +- Toggling the file tree now uses `T` instead of `Tab`, so keyboard focus navigation works as expected. + +### Added + +- Failures now surface as a dismissible toast with a "View logs" shortcut instead of only a badge on the rail. +- The command palette lists local repositories ("Switch to …") plus Pull latest and Refresh repository. +- Merged pull requests are labelled distinctly from open ones in Reviews. + +### Fixed + +- The branch picker no longer displays the first branch when the current branch is unknown; it shows a placeholder instead. +- The whole-app scale transform now sizes the shell from the real viewport and the root element is clipped, so 120% scale fills the window instead of leaving a gap, and keyboard navigation no longer scrolls the whole page out of view. +- The Selenium smoke harness mocks the Git identity call, so the suite runs again instead of crashing the titlebar on startup. + ## [0.1.17] - 2026-09-03 ### Fixed diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2b7e303..90d4be9 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -47,8 +47,9 @@ repository · code · GitHub · tools · analysis · security - `frontend/src/features/settings/` — themes, diagnostics, configuration, and empty-workspace guidance. - `frontend/src/features/shell/` — titlebar, repository picker, rail, and workspace chrome. - `frontend/src/components/` — reusable interface components shared by features. -- `frontend/src/styles.css` — design tokens, themes, layout, and components. -- `frontend/src/security.css` — focused code-search, review, and scanner surfaces. +- `frontend/src/styles.css` — design tokens, layout, and every component, in one cascade with no override passes. +- `frontend/src/themes.css` — one block per theme: palette, atmosphere, code canvas, glass cast, and selection colour. +- `frontend/src/splash.css` — the animated brand pieces: alien glyph, UFO loader, and ASCII launch splash. ## Security boundaries diff --git a/frontend/e2e/mock-backend.ts b/frontend/e2e/mock-backend.ts index ed00613..ba1d3d6 100644 --- a/frontend/e2e/mock-backend.ts +++ b/frontend/e2e/mock-backend.ts @@ -52,6 +52,7 @@ const config: Config = { glow: 1.2, radius: 18, glass: 0.8, + scale: 1, }; let currentBootstrap: Bootstrap = { @@ -159,6 +160,7 @@ export const mockBackend = { PullRequestDetail: async (): Promise => ({ number: 42, title: "Add Selenium smoke harness", author: "bill", state: "OPEN", draft: false, updated: "2026-09-01T00:00:00Z", url: "https://github.com/b404dev/Abduction/pull/42", headBranch: "feat/selenium-e2e-suite", baseBranch: "main", body: "Adds browser-level smoke coverage.", additions: 42, deletions: 3, changedFiles: 2, commits: 1, reviewDecision: "APPROVED", mergeable: "MERGEABLE", files: [{ path: "frontend/e2e/main.tsx", additions: 42, deletions: 3 }], diff: "diff --git a/frontend/e2e/main.tsx b/frontend/e2e/main.tsx\n+smoke test" }), SubmitPullRequestReview: async () => undefined, OpenURL: async () => undefined, + GitIdentity: async () => ({ name: "Bill", email: "bill@example.com" }), Branches: async () => ["main", "feat/selenium-e2e-suite"], SwitchBranch: async (_repositoryPath: string, branch: string) => branch, PullLatest: async () => "Already up to date.", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8a058c7..038f861 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Braces, Command, GitPullRequestArrow, Keyboard, type LucideIcon } from "lucide-react"; +import { AlertTriangle, ArrowDownToLine, Braces, Command, FolderGit2, GitPullRequestArrow, Keyboard, RefreshCw, X, type LucideIcon } from "lucide-react"; import { api } from "./api"; import type { Bootstrap, Repo, ThemeName, ViewName } from "./types"; import { FirstRunGuide } from "./components/FirstRunGuide"; @@ -8,10 +8,11 @@ import { CodeView } from "./features/code/CodeView"; import { HistoryView, ReviewsView, StatsView } from "./features/repository/RepositoryViews"; import { AnalysisView, SecurityView, ToolsView } from "./features/operations/OperationViews"; import { EmptyWorkspace, LogsView, SettingsView, ThemeSwitcher, themes, type LogEntry } from "./features/settings/SettingsViews"; -import { Loading, Rail, RemoteRepositoryNotice, Titlebar, WorkspaceHeader, destinations, isEditingTarget } from "./features/shell/Shell"; +import { Loading, Rail, RemoteRepositoryNotice, Titlebar, destinations, isEditingTarget } from "./features/shell/Shell"; import { updateViewportUnits } from "./viewport"; type AppCommand = { id: string; label: string; detail: string; keys: string[]; icon: LucideIcon; run: () => void }; +type Toast = { id: number; message: string }; // App coordinates the small amount of shared desktop navigation state. export default function App() { @@ -24,6 +25,7 @@ export default function App() { }); const [error, setError] = useState(""); const [logs, setLogs] = useState([]); + const [toasts, setToasts] = useState([]); const [splashReady, setSplashReady] = useState(() => Date.now() - Number(localStorage.getItem("abduction-splash-seen") ?? 0) < 6 * 60 * 60 * 1000); const [guideOpen, setGuideOpen] = useState(() => localStorage.getItem("abduction-guide-seen") !== "1"); const [emptySetupDismissed, setEmptySetupDismissed] = useState(false); @@ -32,12 +34,18 @@ export default function App() { const [repositoryEpoch, setRepositoryEpoch] = useState(0); const repositoryFingerprint = useRef(""); - // recordError retains actionable failures in the diagnostic workspace. + // dismissToast removes one transient failure notice. + const dismissToast = useCallback((toastID: number) => setToasts((currentToasts) => currentToasts.filter((toast) => toast.id !== toastID)), []); + + // recordError retains actionable failures in the diagnostic workspace and surfaces them briefly as a toast. const recordError = useCallback((message: string) => { const normalizedMessage = message.trim() || "Unknown application error"; + const toastID = Date.now() + Math.random(); setError(normalizedMessage); setLogs((currentLogs) => [...currentLogs.slice(-249), { id: Date.now() + currentLogs.length, timestamp: new Date().toISOString(), level: "error", message: normalizedMessage }]); - }, []); + setToasts((currentToasts) => [...currentToasts.slice(-2), { id: toastID, message: normalizedMessage }]); + window.setTimeout(() => dismissToast(toastID), 7000); + }, [dismissToast]); const refreshActiveRepository = useCallback(async () => { if (!selectedRepo?.path) return; @@ -116,10 +124,15 @@ export default function App() { const commands = useMemo(() => [ ...destinations.map((destination) => ({ id: `view.${destination.name}`, label: `Open ${destination.label}`, detail: "Navigate", keys: [destination.key], icon: destination.icon, run: () => setView(destination.name) })), - ...(selectedRepo?.path ? [{ id: "repository.editor", label: "Open repository in editor", detail: selectedRepo.name, keys: ["E"], icon: Braces, run: () => { api.openInEditor(selectedRepo.path, ""); } }] : []), + ...(selectedRepo?.path ? [ + { id: "repository.pull", label: "Pull latest", detail: selectedRepo.name, keys: [], icon: ArrowDownToLine, run: () => { api.pullLatest(selectedRepo.path).then(refreshActiveRepository).catch((reason: unknown) => recordError(String(reason))); } }, + { id: "repository.refresh", label: "Refresh repository", detail: "Re-read files, branches, and history", keys: [], icon: RefreshCw, run: () => { void refreshActiveRepository().catch((reason: unknown) => recordError(String(reason))); } }, + { id: "repository.editor", label: "Open repository in editor", detail: selectedRepo.name, keys: ["E"], icon: Braces, run: () => { api.openInEditor(selectedRepo.path, ""); } }, + ] : []), { id: "repository.github", label: "Open repository on GitHub", detail: selectedRepo?.fullName ?? "No GitHub remote", keys: ["G"], icon: GitPullRequestArrow, run: () => { if (selectedRepo?.githubUrl) api.openOnGitHub(selectedRepo); } }, { id: "help.shortcuts", label: "Show keyboard shortcuts", detail: "Help", keys: ["?"], icon: Keyboard, run: () => setShortcutsOpen(true) }, - ], [selectedRepo]); + ...(bootstrap?.repos ?? []).filter((repository) => repository.path && repository.path !== selectedRepo?.path).map((repository) => ({ id: `repository.switch.${repository.fullName}`, label: `Switch to ${repository.name}`, detail: repository.fullName, keys: [], icon: FolderGit2, run: () => setSelectedRepo(repository) })), + ], [selectedRepo, bootstrap?.repos, refreshActiveRepository, recordError]); useEffect(() => { function handleGlobalShortcut(event: KeyboardEvent) { @@ -143,10 +156,9 @@ export default function App() { if (!bootstrap) return ; return (
- { setSelectedRepo(repository); api.refreshRepos().then((repositories) => setBootstrap({ ...bootstrap, repos: repositories })).catch((reason: unknown) => recordError(String(reason))); }} onCommand={() => setCommandOpen(true)} onShortcuts={() => setShortcutsOpen(true)} onError={recordError} /> - + { setSelectedRepo(repository); api.refreshRepos().then((repositories) => setBootstrap({ ...bootstrap, repos: repositories })).catch((reason: unknown) => recordError(String(reason))); }} onClone={cloneRemoteRepository} onRefresh={refreshActiveRepository} onCommand={() => setCommandOpen(true)} onShortcuts={() => setShortcutsOpen(true)} onError={recordError} /> +
- {view === "themes" ? : view === "logs" ? setLogs([])} /> : view === "settings" ? { setBootstrap(nextBootstrap); setTheme(nextBootstrap.config.theme); setSelectedRepo(nextBootstrap.repos.find((repository) => repository.path === selectedRepo?.path) ?? nextBootstrap.repos[0] ?? null); }} onError={recordError} /> : !selectedRepo ? { setEmptySetupDismissed(false); setGuideOpen(true); }} onSettings={() => setView("settings")} /> : !selectedRepo.path && view !== "code" ? : view === "code" ? : view === "history" ? : view === "stats" ? @@ -156,6 +168,7 @@ export default function App() { view === "analysis" ? : }
+ {toasts.length ?
{toasts.map((toast) =>

{toast.message}

)}
: null} {commandOpen ? setCommandOpen(false)}/> : null} {shortcutsOpen ? setShortcutsOpen(false)}/> : null} {guideOpen || (!emptySetupDismissed && bootstrap.repos.length === 0) ? api.selectWorkspace()} onConnect={async (workspace) => { const nextBootstrap = await api.updateConfig({ ...bootstrap.config, workspace }); if (nextBootstrap.error) throw new Error(nextBootstrap.error); setBootstrap(nextBootstrap); setSelectedRepo(nextBootstrap.repos[0] ?? null); return nextBootstrap.repos.length; }} onClose={() => { localStorage.setItem("abduction-guide-seen", "1"); setGuideOpen(false); setEmptySetupDismissed(true); }}/> : null} @@ -177,14 +190,14 @@ function CommandPalette({ commands, onClose }: { commands: AppCommand[]; onClose if (event.key === "ArrowDown" || event.key === "ArrowUp") { event.preventDefault(); setActiveIndex((currentIndex) => (currentIndex + (event.key === "ArrowDown" ? 1 : -1) + filteredCommands.length) % Math.max(filteredCommands.length, 1)); } if (event.key === "Enter" && filteredCommands[activeIndex]) { event.preventDefault(); runCommand(filteredCommands[activeIndex]); } } - return
event.stopPropagation()}>
setQuery(event.target.value)} onKeyDown={handleKey} placeholder="Type a command…" aria-label="Search commands"/>esc
{filteredCommands.map((command, commandIndex) => )}{!filteredCommands.length ?
No matching command
: null}
↑↓ select↵ run{filteredCommands.length} commands
; + return
event.stopPropagation()}>
setQuery(event.target.value)} onKeyDown={handleKey} placeholder="Type a command or repository…" aria-label="Search commands"/>esc
{filteredCommands.map((command, commandIndex) => )}{!filteredCommands.length ?
No matching command
: null}
↑↓ select↵ run{filteredCommands.length} commands
; } // ShortcutHelp keeps the complete keyboard model discoverable inside the application. function ShortcutHelp({ onClose }: { onClose: () => void }) { const groups = [ - { label: "Navigate", shortcuts: [["1–9", "Open primary view"], ["0", "Open settings"], ["⌘/Ctrl K", "Command palette"], ["⌘/Ctrl P", "Repository switcher"], ["[ ]", "Previous / next repository"]] }, - { label: "Work", shortcuts: [["/", "Focus repository search"], ["J K / ↑ ↓", "Move through items"], ["Enter", "Open or run selected item"], ["← →", "Collapse or expand folder"], ["Esc", "Close the active layer"]] }, + { label: "Navigate", shortcuts: [["1–9", "Open primary view"], ["0", "Open settings"], ["⌘/Ctrl K", "Command palette"], ["⌘/Ctrl P", "Repository switcher"], ["[ ]", "Previous / next repository"], ["Esc", "Close the active layer"]] }, + { label: "Work", shortcuts: [["/", "Focus repository search"], ["T", "Show or hide the file tree"], ["E", "Open repository in editor"], ["G", "Open repository on GitHub"], ["J K / ↑ ↓", "Move through items"], ["Enter", "Open or run selected item"], ["← →", "Collapse or expand folder"]] }, ]; return
event.stopPropagation()}>
Input system

Move at thought speed

{groups.map((group) =>

{group.label}

{group.shortcuts.map(([keys, label]) =>
{keys}{label}
)}
)}
Shortcuts pause automatically while you type in a field.
; } diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 678ace1..a7c6017 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -54,7 +54,8 @@ function cached(key: string, load: () => Promise, ttl = Number.POSITIVE_IN const existing = queryCache.get(key); if (existing?.promise) return existing.promise as Promise; if (existing && existing.expiresAt > now && "value" in existing) return Promise.resolve(existing.value as T); - const promise = load().then((value) => { + // A missing or broken bridge method rejects instead of throwing mid-render. + const promise = new Promise((resolve) => resolve(load())).then((value) => { queryCache.set(key, { value, expiresAt: Date.now() + ttl }); return value; }).catch((reason) => { diff --git a/frontend/src/cockpit.css b/frontend/src/cockpit.css deleted file mode 100644 index 7c0f130..0000000 --- a/frontend/src/cockpit.css +++ /dev/null @@ -1,240 +0,0 @@ -/* Cockpit is the final composition layer: depth, energy, and keyboard feedback. */ -:root { - --depth-canvas: color-mix(in srgb, var(--page) 96%, black); - --depth-recessed: color-mix(in srgb, var(--surface) 82%, var(--page)); - --depth-surface: color-mix(in srgb, var(--surface-2) 78%, transparent); - --depth-raised: color-mix(in srgb, var(--surface-3) 82%, var(--surface)); - --energy-line: linear-gradient(180deg, var(--cyan), var(--accent) 50%, var(--accent-2)); - --shadow-raised: 0 18px 42px rgba(0,0,0,.34), inset 0 1px rgba(255,255,255,.06); - --shadow-float: 0 34px 100px rgba(0,0,0,.68), 0 0 64px color-mix(in srgb,var(--accent) 18%,transparent); -} - -/* Reaper's theme family is intentionally narrow: four identities with focused variants. */ -:root[data-theme="reaper-blood"] { --page:#080308;--surface:#110711;--surface-2:#1b0c18;--surface-3:#2b1123;--border:rgba(255,49,95,.28);--text:#fff1f4;--muted:#a97886;--accent:#ff315f;--accent-2:#a84dff;--cyan:#ff784f;--green:#55e6a5;--yellow:#ffc857;--red:#ff315f;--ambient-primary:#ff315f;--ambient-secondary:#a84dff;--ambient-cyan:#ff784f;--code-canvas:#060205;--glass-cast:#1b0c18;--shadow-cast:rgba(255,49,95,.22);--selection:rgba(255,49,95,.3); } -:root[data-theme="reaper-void"] { --page:#020106;--surface:#070510;--surface-2:#100a20;--surface-3:#1c1235;--border:rgba(122,60,255,.3);--text:#f7f2ff;--muted:#7d729a;--accent:#7a3cff;--accent-2:#e349ff;--cyan:#00e5ff;--green:#45f2b1;--yellow:#ffd166;--red:#ff4d8d;--ambient-primary:#7a3cff;--ambient-secondary:#e349ff;--ambient-cyan:#00e5ff;--code-canvas:#010104;--glass-cast:#100a20;--shadow-cast:rgba(122,60,255,.24);--selection:rgba(0,229,255,.25); } -:root[data-theme="tokyo-neon"] { --page:#080b18;--surface:#0e1224;--surface-2:#151a31;--surface-3:#202844;--border:rgba(42,195,255,.26);--text:#e9f4ff;--muted:#6f7ca8;--accent:#2ac3ff;--accent-2:#ff3dbb;--cyan:#43f7ff;--green:#adff2f;--yellow:#ffe45e;--red:#ff477e;--ambient-primary:#2ac3ff;--ambient-secondary:#ff3dbb;--ambient-cyan:#43f7ff;--code-canvas:#060914;--glass-cast:#151a31;--shadow-cast:rgba(42,195,255,.22);--selection:rgba(255,61,187,.26); } -:root[data-theme="tokyo-dusk"] { --page:#171522;--surface:#1e1b2c;--surface-2:#28233a;--surface-3:#37304d;--border:rgba(139,124,255,.25);--text:#f1ecff;--muted:#968da9;--accent:#8b7cff;--accent-2:#ff7eb6;--cyan:#78dce8;--green:#a9dc76;--yellow:#ffc66d;--red:#ff6188;--ambient-primary:#8b7cff;--ambient-secondary:#ff7eb6;--ambient-cyan:#78dce8;--code-canvas:#12101b;--glass-cast:#28233a;--shadow-cast:rgba(139,124,255,.2);--selection:rgba(255,126,182,.24); } -:root[data-theme="matte-ember"] { --page:#070605;--surface:#0e0b08;--surface-2:#17110c;--surface-3:#251910;--border:rgba(255,122,24,.25);--text:#fff4e8;--muted:#8f7560;--accent:#ff7a18;--accent-2:#db2b39;--cyan:#ffd166;--green:#8fd694;--yellow:#ffbf46;--red:#db2b39;--ambient-primary:#ff7a18;--ambient-secondary:#db2b39;--ambient-cyan:#ffd166;--code-canvas:#050403;--glass-cast:#17110c;--shadow-cast:rgba(255,122,24,.2);--selection:rgba(255,122,24,.27); } -:root[data-theme="matte-ice"] { --page:#050708;--surface:#0a0e10;--surface-2:#10171b;--surface-3:#1a272e;--border:rgba(131,233,255,.23);--text:#eefcff;--muted:#71878d;--accent:#83e9ff;--accent-2:#5773ff;--cyan:#d7f6ff;--green:#69e3b1;--yellow:#ffe69a;--red:#ff6b83;--ambient-primary:#83e9ff;--ambient-secondary:#5773ff;--ambient-cyan:#d7f6ff;--code-canvas:#030506;--glass-cast:#10171b;--shadow-cast:rgba(131,233,255,.18);--selection:rgba(131,233,255,.24); } -:root[data-theme="hackerman-amber"] { --page:#070603;--surface:#0d0b05;--surface-2:#171308;--surface-3:#27200d;--border:rgba(255,191,54,.25);--text:#fff8dc;--muted:#8f8057;--accent:#ffbf36;--accent-2:#ff6b2c;--cyan:#ffef9a;--green:#c7ef4b;--yellow:#ffbf36;--red:#ff5d3d;--ambient-primary:#ffbf36;--ambient-secondary:#ff6b2c;--ambient-cyan:#ffef9a;--code-canvas:#040301;--glass-cast:#171308;--shadow-cast:rgba(255,191,54,.2);--selection:rgba(255,191,54,.26); } -:root[data-theme="hackerman-ghost"] { --page:#020807;--surface:#06100e;--surface-2:#091b17;--surface-3:#0e2b25;--border:rgba(47,255,193,.24);--text:#e5fff8;--muted:#5d8d80;--accent:#2fffc1;--accent-2:#00a8ff;--cyan:#b8fff1;--green:#2fffc1;--yellow:#adff7d;--red:#64ffda;--ambient-primary:#2fffc1;--ambient-secondary:#00a8ff;--ambient-cyan:#b8fff1;--code-canvas:#010504;--glass-cast:#091b17;--shadow-cast:rgba(47,255,193,.19);--selection:rgba(47,255,193,.24); } - -body { background: var(--depth-canvas); } -.shell { position:relative; isolation:isolate; grid-template:68px minmax(0,1fr) / 70px minmax(0,1fr); background:radial-gradient(900px 520px at 62% -30%,color-mix(in srgb,var(--ambient-primary) 28%,transparent),transparent 66%),radial-gradient(650px 520px at 105% 100%,color-mix(in srgb,var(--ambient-secondary) 18%,transparent),transparent 70%),var(--depth-canvas); width:calc(100% / var(--scale,1)); height:calc(calc(100 * var(--vh)) / var(--scale,1)); transform:scale(var(--scale,1)); transform-origin:top left; } -.shell::before { content:"";position:fixed;inset:0;z-index:-1;pointer-events:none;opacity:.22;background-image:linear-gradient(color-mix(in srgb,var(--accent) 10%,transparent) 1px,transparent 1px),linear-gradient(90deg,color-mix(in srgb,var(--accent) 10%,transparent) 1px,transparent 1px);background-size:54px 54px;mask-image:linear-gradient(to bottom,black,transparent 72%); } - -.titlebar { margin:0;padding:0 18px;border:0;border-bottom:1px solid color-mix(in srgb,var(--cyan) 24%,var(--border));border-radius:0;background:linear-gradient(100deg,color-mix(in srgb,var(--surface-2) 94%,var(--page)),color-mix(in srgb,var(--page) 92%,transparent))!important;box-shadow:0 14px 48px rgba(0,0,0,.38)!important; } -.shell--mac .titlebar { --wails-draggable:no-drag; } -.top-context,.repo-picker,.repo-picker *,.branch-picker,.branch-picker *,.titlebar__actions,.titlebar__actions * { --wails-draggable:no-drag!important;-webkit-app-region:no-drag; } -.top-context,.repo-picker,.branch-picker,.titlebar__actions { position:relative;z-index:12;pointer-events:auto; } -.titlebar::after { content:"";position:absolute;left:70px;right:0;bottom:-1px;height:1px;background:linear-gradient(90deg,var(--cyan),color-mix(in srgb,var(--accent) 32%,transparent) 38%,transparent 76%);box-shadow:0 0 16px color-mix(in srgb,var(--cyan) 52%,transparent); } -.titlebar .brand__icon { border-radius:5px 14px 5px 14px;transform:rotate(-3deg); } -.top-context { width:min(690px,100%); } -.repo-picker,.branch-picker { border-radius:8px 14px 8px 14px; } -.titlebar__actions { justify-self:end;display:flex;align-items:center;gap:7px;--wails-draggable:no-drag; } -.titlebar__actions>button { display:flex;align-items:center;gap:7px;height:34px;padding:0 9px;border:1px solid color-mix(in srgb,var(--accent) 24%,var(--border));border-radius:8px;color:var(--muted);background:var(--depth-recessed); } -.titlebar__actions>button:hover { color:var(--cyan);border-color:color-mix(in srgb,var(--cyan) 48%,var(--border));transform:translateY(-1px); } -.titlebar__actions small { margin-left:5px;color:var(--muted);font:9px var(--mono); } -.git-identity { display:flex;align-items:center;gap:5px;margin-left:2px;color:var(--muted);font:9px var(--mono);white-space:nowrap; } -.git-identity svg { color:var(--cyan); } - -.rail { position:relative;width:70px;margin:0;padding:14px 10px;border:0;border-right:1px solid color-mix(in srgb,var(--accent) 24%,var(--border));border-radius:0;overflow:hidden;background:linear-gradient(180deg,color-mix(in srgb,var(--surface-2) 96%,var(--page)),color-mix(in srgb,var(--page) 97%,black))!important;box-shadow:12px 0 46px rgba(0,0,0,.32)!important;transition:width 220ms cubic-bezier(.2,.8,.2,1),box-shadow 220ms ease; } -.rail:hover,.rail:has(.rail__item:focus-visible) { width:204px;align-items:stretch;box-shadow:28px 0 70px rgba(0,0,0,.58),12px 0 50px color-mix(in srgb,var(--accent) 12%,transparent)!important; } -.rail::before { content:"NAVIGATION";height:14px;margin:0 8px 5px;color:var(--cyan);font:700 7px/14px var(--mono);letter-spacing:.2em;white-space:nowrap;opacity:0;transform:translateX(-8px);transition:opacity 140ms ease,transform 200ms ease; } -.rail:hover::before,.rail:has(.rail__item:focus-visible)::before { opacity:.62;transform:none; } -.rail::after { content:"";position:absolute;right:-1px;top:0;width:1px;height:38%;background:var(--energy-line);box-shadow:0 0 15px var(--accent);opacity:.75;animation:rail-energy 5s ease-in-out infinite alternate; } -.rail__item { flex:0 0 44px;display:grid;grid-template-columns:48px minmax(0,1fr) 22px;align-items:center;justify-items:center;width:50px;height:44px;border-radius:7px 15px 7px 15px;transition:width 220ms cubic-bezier(.2,.8,.2,1),transform 150ms ease,background 150ms ease,border-color 150ms ease!important; } -.rail:hover .rail__item,.rail:has(.rail__item:focus-visible) .rail__item { width:184px;justify-items:start; } -.rail__icon { grid-column:1;justify-self:center; } -.rail .rail__label { position:static;grid-column:2;min-width:0;width:100%;height:auto;overflow:hidden;clip-path:none;color:var(--muted);font-size:11px;font-weight:650;letter-spacing:.01em;white-space:nowrap;text-overflow:ellipsis;opacity:0;transform:translateX(-8px);transition:opacity 120ms ease,transform 180ms ease; } -.rail:hover .rail__label,.rail:has(.rail__item:focus-visible) .rail__label { opacity:1;transform:none; } -.rail__item::after { left:auto!important;right:10px!important;top:50%!important;padding:0!important;border:0!important;color:var(--muted)!important;background:transparent!important;box-shadow:none!important;font:8px var(--mono)!important;opacity:0!important;transform:translateY(-50%)!important; } -.rail:hover .rail__item::after,.rail:has(.rail__item:focus-visible) .rail__item::after { opacity:.58!important; } -.rail__item:hover { transform:translateX(4px);background:linear-gradient(90deg,color-mix(in srgb,var(--cyan) 14%,var(--surface-2)),var(--surface-2))!important; } -.rail__item--active { transform:translateX(5px);border-color:color-mix(in srgb,var(--cyan) 42%,var(--border));background:linear-gradient(105deg,color-mix(in srgb,var(--cyan) 24%,var(--surface-2)),color-mix(in srgb,var(--accent) 14%,var(--surface-2)))!important;box-shadow:inset 3px 0 var(--cyan),0 12px 30px rgba(0,0,0,.34),0 0 24px color-mix(in srgb,var(--cyan) 17%,transparent)!important; } -.rail__item--active .rail__label { color:var(--text); } -.rail__item--active::before { display:none; } - -.workspace { grid-template-rows:64px minmax(0,1fr);gap:8px;padding:8px 10px 10px; } -.workspace__header { margin:0;padding:10px 22px;border:1px solid color-mix(in srgb,var(--accent) 16%,var(--border));border-radius:var(--radius);background:linear-gradient(90deg,color-mix(in srgb,var(--surface) 86%,var(--page)),color-mix(in srgb,var(--page) 72%,transparent))!important;box-shadow:none!important; } -.workspace__header h1 { font-size:20px;letter-spacing:-.04em; } -.workspace__header h1::after { content:"";display:inline-block;width:34px;height:3px;margin-left:12px;border-radius:99px;background:linear-gradient(90deg,var(--cyan),var(--accent-2));box-shadow:0 0 12px var(--accent);vertical-align:middle; } -.pull-status { color:var(--green);font:8px var(--mono);white-space:nowrap; } -.code-layout { margin:0;border:1px solid color-mix(in srgb,var(--accent) 17%,var(--border));border-radius:calc(var(--radius) + 2px);box-shadow:none!important; } -.code-layout--tree-hidden { grid-template-columns:minmax(0,1fr); } -.code-layout--tree-hidden.code-layout--chat { grid-template-columns:minmax(0,1fr) minmax(300px,380px); } -.tree { position:relative;border-right-color:color-mix(in srgb,var(--accent) 25%,var(--border));background:linear-gradient(180deg,var(--depth-recessed),color-mix(in srgb,var(--page) 96%,black))!important; } -.reader { background:var(--code-canvas)!important; } -.reader__head { border-bottom-color:color-mix(in srgb,var(--cyan) 20%,var(--border));background:linear-gradient(90deg,color-mix(in srgb,var(--surface) 98%,var(--page)),color-mix(in srgb,var(--page) 94%,transparent))!important; } -.tree__entry:focus-visible,.search-results>button:focus-visible { outline:0!important;border-color:color-mix(in srgb,var(--cyan) 55%,transparent)!important;background:linear-gradient(90deg,color-mix(in srgb,var(--cyan) 18%,transparent),color-mix(in srgb,var(--accent-2) 7%,transparent))!important;box-shadow:inset 3px 0 var(--cyan),0 0 22px color-mix(in srgb,var(--cyan) 14%,transparent)!important;transform:translateX(3px); } -.tree__root { grid-template-columns:auto 1fr auto; } -.tree__root kbd { margin-left:auto; } -.tree { display:grid;grid-template-rows:auto minmax(0,1fr);min-width:0;overflow:hidden!important;padding:0 10px 0;background:linear-gradient(180deg,color-mix(in srgb,var(--surface-2) 96%,var(--page)),color-mix(in srgb,var(--page) 98%,black))!important; } -.tree-search-panel { position:relative;z-index:3;padding-bottom:2px;background:color-mix(in srgb,var(--surface-2) 96%,var(--page)); } -.tree-scroll-region { min-width:0;min-height:0;overflow:auto;padding:0 0 16px;scrollbar-gutter:stable; } -.tree__list { min-width:0; } -.tree-children { min-width:0; } -.tree__title>div { display:flex;align-items:center;gap:7px; } -.tree__title button { display:grid;place-items:center;width:22px;height:22px;padding:0;border:1px solid color-mix(in srgb,var(--accent) 22%,var(--border));border-radius:6px;color:var(--cyan);background:var(--surface-2);font:15px/1 var(--mono); } -.tree__title button:disabled { opacity:.28; } -.tree__root { margin:5px 1px 8px;padding:10px;border:1px solid color-mix(in srgb,var(--cyan) 20%,var(--border));border-radius:9px;color:var(--text);background:linear-gradient(110deg,color-mix(in srgb,var(--cyan) 9%,var(--surface-2)),var(--surface-2));box-shadow:0 8px 22px rgba(0,0,0,.2); } -.tree__root>svg { color:var(--cyan);filter:drop-shadow(0 0 5px color-mix(in srgb,var(--cyan) 45%,transparent)); } -.tree__list { padding:2px 0 14px; } -.tree-node { position:relative; } -.tree-children { position:relative;margin-left:13px;padding-left:10px;border-left:1px solid color-mix(in srgb,var(--cyan) 30%,var(--border)); } -.tree-children::before { display:none; } -.tree-children>.tree-node::before { content:"";position:absolute;left:-10px;top:16px;width:9px;height:1px;background:color-mix(in srgb,var(--cyan) 34%,var(--border)); } -.tree-children>.tree-node:last-child::after { content:"";position:absolute;left:-11px;top:17px;bottom:0;width:2px;background:var(--depth-recessed); } -.tree__entry { grid-template-columns:14px 18px minmax(0,1fr) auto;gap:5px;min-height:34px;padding:4px 7px;border:1px solid transparent;border-radius:7px;color:var(--muted); } -.tree__entry:hover { color:var(--text);border-color:color-mix(in srgb,var(--cyan) 20%,var(--border));background:linear-gradient(90deg,color-mix(in srgb,var(--cyan) 11%,var(--surface-2)),var(--surface-2))!important;transform:translateX(2px); } -.tree__entry--directory { color:color-mix(in srgb,var(--text) 88%,var(--muted));font-weight:650; } -.tree__entry--expanded { color:var(--text);background:linear-gradient(90deg,color-mix(in srgb,var(--accent) 12%,var(--surface-2)),var(--surface-2)); } -.tree__entry--active { color:var(--text);border-color:color-mix(in srgb,var(--cyan) 42%,var(--border));background:linear-gradient(90deg,color-mix(in srgb,var(--cyan) 18%,var(--surface-2)),color-mix(in srgb,var(--accent) 8%,var(--surface-2)))!important;box-shadow:inset 3px 0 var(--cyan),0 5px 15px rgba(0,0,0,.2); } -.tree-caret { grid-column:1;color:var(--muted);transform:rotate(0);transition:transform 120ms ease; } -.tree__entry--file .tree-caret { visibility:hidden; } -.tree__entry--expanded .tree-caret { transform:rotate(90deg);color:var(--cyan); } -.tree-kind-icon { grid-column:2;display:grid;place-items:center;color:color-mix(in srgb,var(--muted) 85%,var(--text)); } -.tree__entry--directory .tree-kind-icon { color:var(--yellow);filter:drop-shadow(0 0 4px color-mix(in srgb,var(--yellow) 22%,transparent)); } -.tree__entry--active .tree-kind-icon { color:var(--cyan); } -.tree-language-icon { display:grid;place-items:center;min-width:17px;height:15px;border:1px solid currentColor;border-radius:3px;font:800 6px/1 var(--mono);letter-spacing:-.04em;background:color-mix(in srgb,currentColor 10%,var(--surface-2));box-shadow:0 0 6px color-mix(in srgb,currentColor 18%,transparent); } -.tree-language-icon--blue { color:#61a9ff; }.tree-language-icon--cyan { color:#45dff2; }.tree-language-icon--green { color:#63d98b; } -.tree-language-icon--yellow { color:#f5d547; }.tree-language-icon--orange { color:#f08d49; }.tree-language-icon--red { color:#ef6262; } -.tree-language-icon--violet { color:#b590ff; }.tree-language-icon--pink { color:#ef7fba; } -.tree__entry--active .tree-language-icon { filter:brightness(1.2); } -.tree-entry-name { min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; } -.tree__entry small { color:var(--muted);font-size:7px;text-transform:uppercase;letter-spacing:.04em;opacity:.65; } -.primary { border:0;border-radius:7px 12px 7px 12px;background:linear-gradient(115deg,var(--cyan),var(--accent) 55%,var(--accent-2));box-shadow:0 10px 28px color-mix(in srgb,var(--accent) 36%,transparent),inset 0 1px rgba(255,255,255,.28); } -.primary:hover { transform:translateY(-2px);filter:saturate(1.2) brightness(1.08); } - -.picker__option--active { border-color:color-mix(in srgb,var(--cyan) 48%,transparent)!important;background:linear-gradient(90deg,color-mix(in srgb,var(--cyan) 18%,transparent),color-mix(in srgb,var(--accent-2) 9%,transparent))!important;box-shadow:inset 3px 0 var(--cyan); } -.repo-action { padding:5px 8px;border:1px solid color-mix(in srgb,var(--cyan) 35%,var(--border));border-radius:7px;color:var(--cyan);background:color-mix(in srgb,var(--cyan) 8%,transparent);font:8px var(--mono);white-space:nowrap; } -.repo-action--local { color:var(--muted);border-color:var(--border);background:transparent; } -.command-backdrop { position:fixed;inset:0;z-index:1000;display:grid;place-items:start center;padding-top:clamp(70px,calc(12 * var(--vh)),130px);background:rgba(0,1,8,.68);backdrop-filter:blur(8px);animation:command-fade 130ms ease-out; } -.command-palette,.shortcut-help { width:min(680px,calc(calc(100 * var(--vw)) - 32px));overflow:hidden;border:1px solid color-mix(in srgb,var(--cyan) 38%,var(--border));border-radius:10px 24px 10px 24px;background:linear-gradient(145deg,color-mix(in srgb,var(--surface-2) 98%,var(--page)),color-mix(in srgb,var(--page) 97%,black));box-shadow:var(--shadow-float);animation:command-rise 160ms cubic-bezier(.2,.8,.2,1); } -.command-palette>header { display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:12px;padding:14px 16px;border-bottom:1px solid var(--border);color:var(--cyan); } -.command-palette>header input { width:100%;padding:5px;border:0;outline:0;color:var(--text);background:transparent;font-size:16px; } -.command-list { max-height:min(520px,calc(58 * var(--vh)));overflow:auto;padding:7px; } -.command-item { display:grid;grid-template-columns:36px minmax(0,1fr) auto;align-items:center;gap:11px;width:100%;padding:10px;border:1px solid transparent;border-radius:7px 13px 7px 13px;color:var(--text);background:transparent;text-align:left; } -.command-item__icon { display:grid;place-items:center;width:34px;height:34px;border-radius:7px;color:var(--muted);background:var(--depth-recessed); } -.command-item strong,.command-item small { display:block; } -.command-item strong { font-size:12px; }.command-item small { margin-top:3px;color:var(--muted);font-size:9px; } -.command-item--active { border-color:color-mix(in srgb,var(--cyan) 34%,transparent);background:linear-gradient(90deg,color-mix(in srgb,var(--cyan) 15%,transparent),color-mix(in srgb,var(--accent-2) 8%,transparent));box-shadow:inset 3px 0 var(--cyan);transform:translateX(2px); } -.command-item--active .command-item__icon { color:var(--cyan);box-shadow:0 0 18px color-mix(in srgb,var(--cyan) 22%,transparent); } -.command-keys { display:flex;gap:4px; }.command-empty { padding:35px;color:var(--muted);text-align:center; } -.command-palette>footer { display:flex;gap:18px;padding:10px 15px;border-top:1px solid var(--border);color:var(--muted);font-size:9px; }.command-palette>footer span:last-child { margin-left:auto; } - -.shortcut-help>header { display:flex;align-items:center;justify-content:space-between;padding:22px 24px;border-bottom:1px solid var(--border); }.shortcut-help h2 { margin:0;font-size:24px; }.shortcut-help>header button { width:32px;height:32px;border:1px solid var(--border);border-radius:8px;color:var(--muted);background:var(--depth-recessed); } -.shortcut-help>div { display:grid;grid-template-columns:1fr 1fr;gap:12px;padding:18px; }.shortcut-help article { padding:15px;border-left:2px solid color-mix(in srgb,var(--accent) 48%,transparent);background:color-mix(in srgb,var(--surface) 58%,transparent); }.shortcut-help h3 { margin:0 0 12px;color:var(--cyan);font-size:11px;text-transform:uppercase;letter-spacing:.12em; }.shortcut-help article>div { display:grid;grid-template-columns:100px 1fr;align-items:center;gap:10px;min-height:32px;color:var(--muted);font-size:10px; }.shortcut-help article kbd { justify-self:start;color:var(--text); }.shortcut-help>footer { padding:11px 24px;border-top:1px solid var(--border);color:var(--muted);font-size:9px; } - -@keyframes command-fade { from { opacity:0 } } -@keyframes command-rise { from { opacity:0;transform:translateY(-12px) scale(.985) } } -@keyframes rail-energy { from { top:-18%;height:32% } to { top:68%;height:46% } } -@media(max-width:900px){.titlebar__actions small,.git-identity,.titlebar__actions>button:first-child kbd{display:none}.shortcut-help>div{grid-template-columns:1fr}} -@media(max-width:800px){ - .shell{grid-template:56px minmax(0,1fr) / 54px minmax(0,1fr)} - .titlebar{grid-template-columns:auto minmax(0,1fr) auto;gap:8px;padding:0 10px} - .titlebar::after{left:54px} - .brand__word,.titlebar__actions small{display:none} - .top-context{grid-template-columns:minmax(0,1fr);width:100%} - .branch-picker{display:none} - .rail{width:54px;padding:9px 5px} - .rail:hover,.rail:has(.rail__item:focus-visible){width:190px} - .rail__item{width:42px;grid-template-columns:42px minmax(0,1fr) 20px} - .rail:hover .rail__item,.rail:has(.rail__item:focus-visible) .rail__item{width:178px} - .workspace{grid-template-rows:56px minmax(0,1fr);gap:6px;padding:6px} - .workspace__header{padding:8px 12px} - .workspace__header h1{font-size:16px} - .workspace__header h1::after{display:none} - .header-actions{gap:5px} - .header-actions select{min-width:88px;max-width:130px} - .header-actions .editor-action,.header-actions .github-action,.pull-status{display:none} - .code-layout{grid-template-columns:minmax(180px,min(var(--tree-pane-width, 300px), calc(32 * var(--vw)))) minmax(0,1fr)} - .code-layout--chat{grid-template-columns:minmax(180px,calc(28 * var(--vw))) minmax(0,1fr) minmax(260px,calc(36 * var(--vw)))} - .document.markdown{padding:28px 30px 70px} -} -@media(max-height:650px){ - .shell{grid-template-rows:52px minmax(0,1fr)} - .workspace{grid-template-rows:50px minmax(0,1fr)} - .workspace__header{padding-block:5px} - .rail{padding-top:6px;padding-bottom:6px} - .rail__item{height:38px;flex-basis:38px} -} -@media(prefers-reduced-motion:reduce){.command-backdrop,.command-palette,.shortcut-help,.rail::after{animation:none}.rail,.rail__item,.rail__label{transition:none!important}.rail__item--active,.tree__entry:focus-visible{transform:none}} - -/* The launch sequence stages the harvest: acquire, tension, cut, split, reveal. */ -.splash { background:radial-gradient(780px 480px at 50% 48%,rgba(49,92,255,.2),transparent 66%),radial-gradient(520px 320px at 62% 42%,rgba(164,73,255,.1),transparent 70%),#010208; } -.splash__vignette { position:absolute;inset:0;z-index:8;pointer-events:none;background:radial-gradient(circle at center,transparent 34%,rgba(0,0,0,.38) 78%,rgba(0,0,0,.88));animation:splash-vignette 3.4s ease both; } -.splash__aura { width:680px;height:430px;animation:splash-aura-dramatic 3.4s cubic-bezier(.2,.7,.2,1) both; } -.splash__binary { gap:27px;color:rgba(122,162,255,.38);font-size:23px;letter-spacing:.48em;text-shadow:0 0 12px rgba(56,221,255,.28); } -.splash__binary>div { animation:binary-scan 1.15s ease-in-out infinite alternate; } -.splash__binary>div:nth-child(2){animation-delay:-.55s}.splash__binary>div:nth-child(3){animation-delay:-.3s}.splash__binary>div:nth-child(4){animation-delay:-.8s} -.splash__binary--upper { animation:binary-harvest-upper 3.2s cubic-bezier(.18,.76,.2,1) both; } -.splash__binary--lower { animation:binary-harvest-lower 3.2s cubic-bezier(.18,.76,.2,1) both; } -.splash__scythe { z-index:5;width:min(760px,calc(78 * var(--vw)));transform-origin:76% 23%;animation:scythe-harvest 2.25s .55s cubic-bezier(.12,.74,.18,1) both; } -.splash__handle { stroke-width:8;stroke:rgba(225,233,255,.62); } -.splash__blade { stroke-width:12;filter:drop-shadow(0 0 5px #38ddff); } -.splash__cut { z-index:4;height:4px;animation:harvest-cut 1.05s 1.52s cubic-bezier(.15,.7,.2,1) both; } -.splash__impact { position:absolute;z-index:6;width:18px;height:18px;border-radius:50%;background:white;box-shadow:0 0 12px 5px #fff,0 0 35px 14px var(--cyan),0 0 90px 34px var(--accent),0 0 150px 55px var(--accent-2);opacity:0;transform:translate(52px,-20px);animation:harvest-impact .75s 1.72s ease-out both; } -.splash__shards { position:absolute;z-index:6;width:10px;height:10px;transform:translate(52px,-20px) rotate(-19deg); } -.splash__shards i { --shard-angle:0deg;position:absolute;width:clamp(28px,calc(5 * var(--vw)),72px);height:2px;background:linear-gradient(90deg,#fff,var(--cyan),transparent);box-shadow:0 0 8px var(--cyan);opacity:0;transform-origin:left center;animation:harvest-shard .82s 1.8s cubic-bezier(.1,.7,.2,1) both; } -.splash__shards i:nth-child(1){--shard-angle:-78deg}.splash__shards i:nth-child(2){--shard-angle:-58deg;animation-delay:1.84s}.splash__shards i:nth-child(3){--shard-angle:-38deg}.splash__shards i:nth-child(4){--shard-angle:-18deg;animation-delay:1.88s}.splash__shards i:nth-child(5){--shard-angle:2deg}.splash__shards i:nth-child(6){--shard-angle:22deg;animation-delay:1.82s}.splash__shards i:nth-child(7){--shard-angle:42deg}.splash__shards i:nth-child(8){--shard-angle:62deg;animation-delay:1.9s}.splash__shards i:nth-child(9){--shard-angle:82deg}.splash__shards i:nth-child(10){--shard-angle:112deg;animation-delay:1.86s}.splash__shards i:nth-child(11){--shard-angle:148deg}.splash__shards i:nth-child(12){--shard-angle:188deg;animation-delay:1.92s} -.splash__identity { z-index:7;animation:harvest-identity .92s 2.16s cubic-bezier(.16,.8,.2,1) both; } -.splash__identity h1 { margin-top:20px;font-size:40px;letter-spacing:.08em; } -.splash__identity p { margin-top:3px; } -.splash__identity>span { margin-top:15px;color:rgba(174,179,213,.62);font:7px var(--mono);letter-spacing:.16em;text-transform:uppercase;animation:startup-status 1.1s 2.35s ease both; } - -@keyframes binary-scan { to { color:rgba(56,221,255,.58);text-shadow:0 0 16px rgba(56,221,255,.55) } } -@keyframes binary-harvest-upper { 0%{opacity:0;transform:rotate(-8deg) scale(1.34)} 24%{opacity:.92;transform:rotate(-8deg) scale(1.2)} 51%{opacity:.92;transform:rotate(-8deg) scale(1.2)} 57%{opacity:1;filter:brightness(2.2)} 77%,100%{opacity:0;transform:translate(-74px,-62px) rotate(-11deg) scale(1.22);filter:blur(3px)} } -@keyframes binary-harvest-lower { 0%{opacity:0;transform:rotate(-8deg) scale(1.34)} 24%{opacity:.92;transform:rotate(-8deg) scale(1.2)} 51%{opacity:.92;transform:rotate(-8deg) scale(1.2)} 57%{opacity:1;filter:brightness(2.2)} 77%,100%{opacity:0;transform:translate(74px,62px) rotate(-5deg) scale(1.22);filter:blur(3px)} } -@keyframes scythe-harvest { 0%{opacity:0;transform:translate(72%,-65%) rotate(-58deg) scale(.82)} 22%{opacity:.45;transform:translate(48%,-40%) rotate(-32deg) scale(.9)} 45%{opacity:1;transform:translate(33%,-28%) rotate(-24deg) scale(1)} 55%{opacity:1;transform:translate(30%,-26%) rotate(-28deg) scale(1.03)} 72%{opacity:1;transform:translate(-18%,9%) rotate(4deg) scale(1.08)} 100%{opacity:0;transform:translate(-76%,58%) rotate(31deg) scale(1.12)} } -@keyframes harvest-cut { 0%{opacity:0;transform:rotate(-19deg) scaleX(0)} 22%{opacity:1;transform:rotate(-19deg) scaleX(.08)} 58%{opacity:1;transform:rotate(-19deg) scaleX(1);box-shadow:0 0 10px var(--cyan),0 0 42px var(--accent),0 0 85px var(--accent-2)} 100%{opacity:0;transform:rotate(-19deg) scaleX(1.08)} } -@keyframes harvest-impact { 0%{opacity:0;transform:translate(52px,-20px) scale(.1)} 18%{opacity:1;transform:translate(52px,-20px) scale(1.7)} 100%{opacity:0;transform:translate(52px,-20px) scale(4.5)} } -@keyframes harvest-shard { 0%{opacity:0;transform:rotate(var(--shard-angle)) scaleX(.05)} 22%{opacity:1} 100%{opacity:0;transform:rotate(var(--shard-angle)) translateX(150px) scaleX(1.35)} } -@keyframes harvest-identity { 0%{opacity:0;clip-path:inset(48% 0 48%);transform:scale(.92);filter:blur(7px)} 55%{opacity:1;clip-path:inset(0);filter:blur(0)} 100%{opacity:1;clip-path:inset(0);transform:scale(1)} } -@keyframes splash-aura-dramatic { 0%{opacity:0;transform:scale(.52)} 42%{opacity:.9;transform:scale(.92)} 58%{opacity:1;transform:scale(1.18)} 100%{opacity:.64;transform:scale(1.05)} } -@keyframes splash-vignette { 0%,48%{opacity:1} 62%{opacity:.52} 100%{opacity:.72} } -@keyframes startup-status { from{opacity:0;transform:translateY(5px)} to{opacity:1;transform:none} } - -@media(prefers-reduced-motion:reduce){.splash__vignette,.splash__shards,.splash__impact{display:none}.splash__identity>span{animation:none;opacity:1}.splash__identity{animation:none;opacity:1;clip-path:none;filter:none}} - -.splash-encounter { position:absolute;inset:0;z-index:10;display:grid;place-content:center;justify-items:center;padding:24px;background:radial-gradient(ellipse at center,rgba(5,18,37,.88),rgba(1,3,9,.94) 68%);animation:encounter-reveal .5s ease-out both; } -.encounter-quote { width:min(680px,calc(86 * var(--vw)));margin:0 auto 18px;padding:18px 22px 16px;border:1px solid rgba(111,199,211,.28);border-radius:10px 22px 10px 22px;background:rgba(1,5,13,.72);box-shadow:0 18px 55px rgba(0,0,0,.45),0 0 30px rgba(70,232,255,.1);backdrop-filter:blur(8px); } -.encounter-quote blockquote { margin:0;color:#d9ffff;font:clamp(11px,calc(1.25 * var(--vw)),15px)/1.45 var(--mono);letter-spacing:.015em;text-wrap:balance; } -.encounter-quote figcaption { display:flex;flex-wrap:wrap;justify-content:center;gap:5px 12px;margin-top:8px;font:9px/1.35 var(--mono);letter-spacing:.08em;text-transform:uppercase; } -.encounter-quote figcaption strong { color:#9ef8ff; } -.encounter-quote figcaption span { color:#607d88; } -.splash-encounter>button { display:flex;align-items:center;gap:12px;padding:9px 15px;border:1px solid rgba(158,248,255,.34);border-radius:7px 14px 7px 14px;color:#d9ffff;background:rgba(15,44,58,.7);font:10px var(--mono);letter-spacing:.08em;text-transform:uppercase;box-shadow:0 0 18px rgba(70,232,255,.12); } -.splash-encounter>button:hover,.splash-encounter>button:focus-visible { border-color:#9ef8ff;background:rgba(25,69,82,.8);box-shadow:0 0 24px rgba(70,232,255,.25); } -.splash-encounter>button span { color:#607d88;font-size:8px; } -@keyframes encounter-reveal { from{opacity:0;filter:blur(7px)} to{opacity:1;filter:none} } -@media(max-height:720px){.splash--ascii .splash__identity h1{font-size:clamp(20px,calc(2.5 * var(--vw)),32px)}.encounter-quote{padding:12px 16px}.encounter-quote blockquote{font-size:10px}.splash--ascii .splash__identity>span{display:none}} - -.picker { display:flex;flex-direction:column; } -.picker__list { flex:1;min-height:120px; } -.repo-source-tabs { display:grid;grid-template-columns:repeat(3,1fr);gap:5px;margin:0 14px 7px;padding:4px;border:1px solid var(--border);border-radius:11px;background:var(--page); } -.repo-source-tab { display:flex;align-items:center;justify-content:center;gap:7px;padding:8px 6px;border:0;border-radius:8px;color:var(--muted);background:transparent;font-size:9px; } -.repo-source-tab b { min-width:18px;padding:2px 4px;border-radius:8px;color:var(--muted);background:var(--surface-3);font:8px var(--mono); } -.repo-source-tab--active { color:var(--cyan);background:color-mix(in srgb,var(--cyan) 12%,var(--surface-2));box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--cyan) 25%,transparent); } -.repo-source-tab--active b { color:var(--cyan); } -.picker-source-error { margin:0 14px 9px;padding:9px 11px;border:1px solid color-mix(in srgb,var(--yellow) 38%,var(--border));border-radius:8px;color:var(--yellow);background:color-mix(in srgb,var(--yellow) 8%,transparent);font:9px/1.5 var(--mono); } -.picker__list>.ufo-loader { min-height:150px; } -.picker-empty { padding:25px;color:var(--muted);text-align:center;font-size:10px; } - -.guide-backdrop { position:fixed;inset:0;z-index:100;display:grid;place-items:center;padding:24px;background:rgba(1,3,10,.8);backdrop-filter:blur(12px); } -.first-run-guide { width:min(720px,calc(92 * var(--vw)));padding:28px;border:1px solid color-mix(in srgb,var(--cyan) 35%,var(--border));border-radius:14px 30px 14px 30px;background:linear-gradient(145deg,color-mix(in srgb,var(--surface-2) 96%,#07152b),var(--surface));box-shadow:0 34px 100px rgba(0,0,0,.68),0 0 60px color-mix(in srgb,var(--cyan) 10%,transparent); } -.first-run-guide h2 { margin:5px 0 6px;font-size:25px; } -.first-run-guide>p { margin:0;color:var(--muted); } -.first-run-guide>div { display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin:23px 0; } -.first-run-guide article { display:grid;align-content:start;gap:7px;padding:15px;border:1px solid var(--border);border-radius:10px 18px 10px 18px;background:color-mix(in srgb,var(--page) 60%,transparent); } -.first-run-guide article b { display:grid;place-items:center;width:24px;height:24px;border-radius:50%;color:var(--page);background:var(--cyan);font:10px var(--mono); } -.first-run-guide article strong { font-size:12px; } -.first-run-guide article span { color:var(--muted);font-size:10px;line-height:1.55; } -.first-run-guide footer { display:flex;align-items:center;justify-content:space-between;gap:20px; } -.first-run-guide footer small { color:var(--muted); } -.first-run-setup { width:min(780px,calc(92 * var(--vw))); } -.first-run-setup .workspace-setup-field { display:grid;grid-template-columns:1fr;gap:8px;margin:22px 0 0;color:var(--muted);font:10px var(--mono);letter-spacing:.08em;text-transform:uppercase; } -.first-run-setup .workspace-setup-field>div { display:flex;gap:8px; } -.first-run-setup .workspace-setup-field input { min-width:0;flex:1;padding:12px 14px;border:1px solid var(--border);border-radius:9px;background:var(--page);color:var(--text);font:12px var(--mono);text-transform:none;letter-spacing:0; } -.first-run-setup .workspace-setup-message { margin:12px 0 0;padding:10px 12px;border:1px solid color-mix(in srgb,var(--cyan) 30%,var(--border));border-radius:8px;color:var(--text);background:color-mix(in srgb,var(--cyan) 7%,transparent);font-size:11px; } -.first-run-setup footer>div,.empty-actions { display:flex;gap:8px; } -@media(max-width:700px){.first-run-guide>div{grid-template-columns:1fr}.first-run-guide{max-height:calc(90 * var(--vh));overflow:auto}} diff --git a/frontend/src/components/Splash.test.tsx b/frontend/src/components/Splash.test.tsx index 37718e8..1b201b5 100644 --- a/frontend/src/components/Splash.test.tsx +++ b/frontend/src/components/Splash.test.tsx @@ -1,5 +1,5 @@ // @vitest-environment jsdom -import { act, fireEvent, render, screen } from "@testing-library/react"; +import { act, cleanup, fireEvent, render, screen } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { Splash } from "./Splash"; @@ -10,6 +10,7 @@ describe("Splash", () => { }); afterEach(() => { + cleanup(); vi.useRealTimers(); vi.unstubAllGlobals(); }); @@ -19,18 +20,26 @@ describe("Splash", () => { render(); expect(screen.queryByRole("button", { name: /Press Enter/ })).toBeNull(); - act(() => { vi.advanceTimersByTime(3000); }); + act(() => { vi.advanceTimersByTime(1600); }); expect(screen.getByRole("button", { name: /Press Enter/ })).toBeTruthy(); fireEvent.keyDown(window, { key: "Enter" }); expect(onComplete).toHaveBeenCalledOnce(); }); - it("auto-starts five seconds after revealing the encounter", () => { + it("starts when the splash is clicked anywhere", () => { const onComplete = vi.fn(); render(); - act(() => { vi.advanceTimersByTime(7999); }); + fireEvent.click(screen.getByRole("main")); + expect(onComplete).toHaveBeenCalledOnce(); + }); + + it("auto-starts five seconds after launch", () => { + const onComplete = vi.fn(); + render(); + + act(() => { vi.advanceTimersByTime(4999); }); expect(onComplete).not.toHaveBeenCalled(); act(() => { vi.advanceTimersByTime(1); }); expect(onComplete).toHaveBeenCalledOnce(); diff --git a/frontend/src/components/Splash.tsx b/frontend/src/components/Splash.tsx index 43b16f5..d0e1db0 100644 --- a/frontend/src/components/Splash.tsx +++ b/frontend/src/components/Splash.tsx @@ -19,13 +19,13 @@ export function Splash({ onComplete }: { onComplete: () => void }) { const [quoteVisible, setQuoteVisible] = useState(false); useEffect(() => { const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; - const revealTimer = window.setTimeout(() => setQuoteVisible(true), reducedMotion ? 300 : 3000); - const completeTimer = window.setTimeout(onComplete, reducedMotion ? 5300 : 8000); + const revealTimer = window.setTimeout(() => setQuoteVisible(true), reducedMotion ? 300 : 1600); + const completeTimer = window.setTimeout(onComplete, reducedMotion ? 3000 : 5000); function handleStart(event: KeyboardEvent) { if (event.key === "Enter") onComplete(); } window.addEventListener("keydown", handleStart); return () => { window.clearTimeout(revealTimer); window.clearTimeout(completeTimer); window.removeEventListener("keydown", handleStart); }; }, [onComplete]); - return
+ return
{"· . ✦ . *\n * · . +\n . + · .\n · * . ✦\n * . · + ."}
{"    .-'''-.\n  .'  ·  . '.\n / .   ◦    ·\\\n|   ·    .   |\n \\  .  ·   /\n  '._   _.'\n     '''"}
@@ -35,6 +35,6 @@ export function Splash({ onComplete }: { onComplete: () => void }) {
{"             __..---~~~~---..__                    __..--~~--..__\n      _..--''                    ''--..__    __..--'              '--.._"}
{"      /\\          /\\    /\\            /\\       /\\       /\\\n  /\\/▓▓\\ /\\    /▓▓\\  /▓▓\\   /\\    /▓▓\\ /\\  /▓▓\\     /▓▓\\\n /▓▓▓▓▓▓▓\\▓▓\\  /▓▓▓▓\\/▓▓▓▓\\ /▓▓\\  /▓▓▓▓▓▓▓\\/▓▓▓▓\\ /\\/▓▓▓▓\\\n   ||  ||   ||      ||    ||     ||      ||  ||   ||     ||  ||\n...||..||...||......||....||.....||......||..||...||.....||..||...\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^"}

[ ABDUCTION ]

{target === "code" ? "YOUR CODE HAS BEEN SELECTED" : `${target.toUpperCase()} ENCOUNTER IN PROGRESS`}

> establishing repository contact_
- {quoteVisible ?
“{encounter.quote}”
{encounter.witness}{encounter.detail}
: null} + {quoteVisible ?
“{encounter.quote}”
{encounter.witness}{encounter.detail}
: null}
; } diff --git a/frontend/src/features/code/CodeView.tsx b/frontend/src/features/code/CodeView.tsx index 0b666ce..b762b36 100644 --- a/frontend/src/features/code/CodeView.tsx +++ b/frontend/src/features/code/CodeView.tsx @@ -62,7 +62,7 @@ export function CodeView({ repo, theme, onError }: { repo: Repo; theme: ThemeNam function handleCodeShortcut(event: KeyboardEvent) { if (isEditingTarget(event.target) || event.ctrlKey || event.metaKey || event.altKey) return; if (event.key === "/") { event.preventDefault(); searchRef.current?.focus(); } - if (event.key === "Tab") { event.preventDefault(); setTreeVisible((visible) => !visible); } + if (event.key.toLowerCase() === "t") { event.preventDefault(); setTreeVisible((visible) => !visible); } } window.addEventListener("keydown", handleCodeShortcut); return () => window.removeEventListener("keydown", handleCodeShortcut); @@ -149,7 +149,7 @@ function FileLanguageIcon({ name }: { name: string }) { // TreeNodes renders a lazy recursive hierarchy while preserving folder context. function TreeNodes({ entries, depth, activePath, expandedPaths, childrenByPath, onOpen }: { entries: TreeEntry[]; depth: number; activePath: string; expandedPaths: Set; childrenByPath: Record; onOpen: (entry: TreeEntry) => void }) { - return <>{entries.map((entry) => { const expanded = expandedPaths.has(entry.path); const active = entry.kind === "file" && entry.path === activePath; const entryClass = ["tree__entry", entry.kind === "directory" ? "tree__entry--directory" : "tree__entry--file", expanded ? "tree__entry--expanded" : "", active ? "tree__entry--active" : ""].filter(Boolean).join(" "); return
{entry.kind === "directory" && expanded ?
: null}
; })}; + return <>{entries.map((entry) => { const expanded = expandedPaths.has(entry.path); const active = entry.kind === "file" && entry.path === activePath; const entryClass = ["tree__entry", entry.kind === "directory" ? "tree__entry--directory" : "tree__entry--file", expanded ? "tree__entry--expanded" : "", active ? "tree__entry--active" : ""].filter(Boolean).join(" "); return
{entry.kind === "directory" && expanded ?
: null}
; })}; } // DocumentView displays trusted HTML and navigates highlighted search matches. @@ -157,6 +157,9 @@ function DocumentView({ document, repo, findQuery, findRegex, findLine, onChat, const documentRoot = useRef(null); const [matches, setMatches] = useState([]); const [activeMatch, setActiveMatch] = useState(0); + const [lintOpen, setLintOpen] = useState(false); + const [lintStatus, setLintStatus] = useState(""); + const lintAvailable = Boolean(repo.path && !document.binary && document.path); useEffect(() => { const rootElement = documentRoot.current; @@ -199,11 +202,11 @@ function DocumentView({ document, repo, findQuery, findRegex, findLine, onChat, window.setTimeout(() => { copyButton.textContent = "Copy"; copyButton.classList.remove("markdown-copy--done"); }, 1400); } - return <>
{document.language || "preview"}

{document.name}

{findQuery ?
{matches.length ? `${activeMatch + 1} / ${matches.length}` : "No matches"}
: null}{document.lines ? `${document.lines} lines` : "preview unavailable"}{formatBytes(document.size)}{focusMode ? : null}{repo.path ? : null}
{repo.path && !document.binary && document.path ? documentRoot.current?.querySelector(`#line-${line}`)?.scrollIntoView({ behavior: "smooth", block: "center" })}/> : null}{document.binary ?

Preview unavailable

This file is binary or larger than the safe 16 MB in-app preview limit. Open it in your configured editor to inspect it.

:
}; + return <>
{document.language || "preview"}

{document.name}

{findQuery ?
{matches.length ? `${activeMatch + 1} / ${matches.length}` : "No matches"}
: null}{document.lines ? `${document.lines} lines` : "preview unavailable"}{formatBytes(document.size)}{focusMode ? : null}{lintAvailable ? : null}{repo.path ? : null}
{lintAvailable ? documentRoot.current?.querySelector(`#line-${line}`)?.scrollIntoView({ behavior: "smooth", block: "center" })}/> : null}{document.binary ?

Preview unavailable

This file is binary or larger than the safe 16 MB in-app preview limit. Open it in your configured editor to inspect it.

:
}; } -function LintPanel({ repo, document, onJump }: { repo: Repo; document: Document; onJump: (line: number) => void }) { - const [open, setOpen] = useState(false); +// LintPanel selects installed linters for the active file and lists their normalised diagnostics. +function LintPanel({ repo, document, open, onStatus, onJump }: { repo: Repo; document: Document; open: boolean; onStatus: (status: string) => void; onJump: (line: number) => void }) { const [linters, setLinters] = useState([]); const [selected, setSelected] = useState([]); const [reports, setReports] = useState([]); @@ -232,7 +235,8 @@ function LintPanel({ repo, document, onJump }: { repo: Repo; document: Document; } const diagnostics = reports.flatMap((report) => report.diagnostics); - return
{open ?
Tool selection{document.language} linters
{linters.length ? linters.map((linter) =>
{linter.available ? null : }
) :

No registered linters for {document.language} yet.

}
{lintError ?
{lintError}
: null}{reports.map((report) =>
{report.linter}{report.error || `${report.diagnostics.length} findings`}
{report.diagnostics.map((diagnostic, index) => )}{!report.diagnostics.length && report.output ?
{report.output}
: null}{!report.diagnostics.length && !report.output && !report.error ?

Clean. No findings.

: null}
)}{!reports.length && !running ?
Choose one or more installed tools, then run them against this file.
: null}
: null}
; + useEffect(() => { onStatus(running ? "checking…" : reports.length ? `${diagnostics.length} findings` : ""); }, [running, reports.length, diagnostics.length, onStatus]); + return ; } // activateSearchMatch marks one occurrence active and scrolls it into view. diff --git a/frontend/src/features/operations/OperationViews.tsx b/frontend/src/features/operations/OperationViews.tsx index a1fd481..9ac6b1e 100644 --- a/frontend/src/features/operations/OperationViews.tsx +++ b/frontend/src/features/operations/OperationViews.tsx @@ -87,7 +87,7 @@ export function AnalysisView({ repo, tools, onError }: { repo: Repo; tools: Boot if (jobID) api.cancelAnalysis(jobID).catch((reason: unknown) => onError(String(reason))); } - return