Skip to content
Merged
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
18 changes: 14 additions & 4 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ measured elements or replace the application tokens with framework defaults.
| Control boundary | `--border-control` | Inputs and controls requiring stronger boundaries |
| Emphasized boundary | `--border-strong` | Rare boundaries requiring emphasis beyond focus or error styling |
| Neutral hover | `--hover` | Available controls under the pointer, compact count surfaces |
| Shell hover | `--shell-hover` | Hover/expanded header controls on `--shell-canvas` |
| Accent | `--accent` | Selection, focus, navigation links, primary commit action |
| Accent interaction | `--accent-hover` | Hover/active on filled primary actions |
| Selected surface | `--accent-bg` | Selected file or pressed toggle |
Expand All @@ -120,7 +121,7 @@ measured elements or replace the application tokens with framework defaults.
- Status always includes text, a sign, an icon, or a shape. Preserve staging's
empty/half/full dot distinction; its half-fill is information, not decoration.
- CPU and RAM in the status bar describe the Diffx server process, not
the browser or whole machine. Keep that scope explicit in the tooltip.
the browser or whole machine. Keep that scope explicit in the status popup.
- Pierre owns syntax colors, changed-line fills, word highlights, line selection,
and code-theme surfaces. App status tokens govern the surrounding UI only.
- Quiet frames use the light canvas/dot tokens or their dark equivalents from
Expand Down Expand Up @@ -204,7 +205,9 @@ such detail into a token. New layout spacing must use the scale.
Keep these frames outside Pierre's measured file geometry. At compact widths,
remove the empty navigation island and retain the inset review surface;
modal drawers use the same inset and corners without clipping focus or menus.
- Header: `--topbar-height` (36 desktop, 44 narrow or coarse pointer). Keep
- Header: `--topbar-height` (36 desktop, 44 narrow or coarse pointer). The
header replaces the shell's top gutter, sitting flush with the window edge
directly above the islands, so its controls center between both edges. Keep
desktop controls at 32px, header icons at 16px, the divider at 16px, and group
spacing at 6px so the header recedes into the editor chrome. Toolbar and sidebar
tabs share the compact 36px desktop rhythm (`--control-small` plus two
Expand All @@ -220,7 +223,8 @@ such detail into a token. New layout spacing must use the scale.
- Header uses `--shell-canvas`, a quiet brand, and the review identity as its primary
context. Keep the secondary scope heading and muted monospace path inline;
hide the path at 768–1011px and both below 768px. Group ghost Refresh and
sync actions at the trailing edge; share `--hover` feedback with the trigger.
sync actions at the trailing edge; share `--shell-hover` feedback with the
trigger, since `--hover` is indistinguishable from the shell canvas.
Theme is the rightmost header action in every layout, with the same ghost
styling, 32px desktop hit area and 16px icon. Keep its outer edge flush with
the shell; never apply the diff scrollbar inset to header actions. Snapshot deletion is absent
Expand Down Expand Up @@ -272,7 +276,13 @@ such detail into a token. New layout spacing must use the scale.
Keep the same-stream notice available. Unchanged submissions must not create
unread prompts. Use existing ghost-button typography, icons and accessible
labels; preserve the header's review identity and compact-layout controls.
- Status bar: `--statusbar-height` (28), with shortcuts shed before server metrics.
- Status bar: `--statusbar-height` (28), with shortcuts shed before server status.
Server status is an icon-only compact ghost trigger at the trailing edge:
success check when ready, reduced-motion-safe spinner while connecting, and
error alert when metrics are unreachable; its accessible name carries the
state. Hover, focus plus Enter, or tap opens a `--surface-raised` popover
titled by the state, with a “Diffx server process” section listing CPU and RAM.
Add future system details as further sections in the same popover.
- The diff display toolbar uses `--panel`, matching the header and status bar.
- Desktop navigation panel: `--sidebar-width` defaults to 330px. `Navigation.tsx` owns
the shared frame and resize handle for Diff and Repos; `use-sidebar.ts` owns
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/DiffWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import {
placementFor,
type CommentAnnotation,
} from "./review-model.ts";
import { ServerMetrics } from "./ServerMetrics.tsx";
import { ServerStatus } from "./ServerStatus.tsx";
import { WorkspaceFeedback } from "./WorkspaceFeedback.tsx";
import { capabilityEnabled, useSession } from "./session-context.tsx";

Expand Down Expand Up @@ -840,7 +840,7 @@ export function DiffWorkspace() {
</>
)}
</span>
<ServerMetrics />
<ServerStatus />
</span>
</footer>
</main>
Expand Down
51 changes: 0 additions & 51 deletions apps/web/src/ServerMetrics.tsx

This file was deleted.

123 changes: 123 additions & 0 deletions apps/web/src/ServerStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Popover } from "@base-ui/react/popover";
import { api, type ApiServerMetrics } from "@diffx/api";
import {
CircleAlertIcon,
CircleCheckIcon,
LoaderCircleIcon,
} from "lucide-react";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { formatBytes } from "./server-metrics.ts";

const refreshMilliseconds = 2_500;

type ServerHealth = "checking" | "ready" | "unavailable";

const statusLabels: Record<ServerHealth, string> = {
checking: "Connecting",
ready: "Ready",
unavailable: "Server unavailable",
};

export function ServerStatus() {
const [health, setHealth] = useState<ServerHealth>("checking");
const [metrics, setMetrics] = useState<ApiServerMetrics | null>(null);
useEffect(() => {
let disposed = false;
let controller = new AbortController();
async function refresh() {
controller.abort();
controller = new AbortController();
const { signal } = controller;
try {
const { data } = await api.GET("/api/v2/metrics", { signal });
if (disposed) return;
if (data) {
setMetrics(data);
setHealth("ready");
} else setHealth("unavailable");
} catch {
if (!disposed && !signal.aborted) setHealth("unavailable");
}
}
void refresh();
const timer = window.setInterval(() => void refresh(), refreshMilliseconds);
return () => {
disposed = true;
controller.abort();
window.clearInterval(timer);
};
}, []);

const label = statusLabels[health];
const current = health === "ready" ? metrics : null;
const cpu = current ? `${current.cpuUsage.toFixed(1)}%` : "--";
const ram = current ? formatBytes(current.rssBytes) : "--";
return (
<Popover.Root>
<Popover.Trigger
id="server-status"
data-status={health}
aria-label={`Server status: ${label}`}
openOnHover
delay={150}
render={
<Button
variant="ghost"
size="icon-xs"
className="server-status-trigger"
/>
}
>
<StatusIcon health={health} />
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner
side="top"
align="end"
sideOffset={8}
collisionPadding={16}
className="z-50"
>
<Popover.Popup className="server-status-popup" data-status={health}>
<Popover.Title className="server-status-title">
<StatusIcon health={health} />
{label}
</Popover.Title>
{health === "unavailable" && (
<Popover.Description className="server-status-description">
Can’t reach the diffx server. Retrying automatically.
</Popover.Description>
)}
<section
className="server-status-section"
aria-labelledby="server-process-heading"
>
<h2 id="server-process-heading">Diffx server process</h2>
<dl>
<div>
<dt>CPU</dt>
<dd id="server-cpu">{cpu}</dd>
</div>
<div>
<dt>RAM</dt>
<dd id="server-ram">{ram}</dd>
</div>
</dl>
</section>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
);
}

function StatusIcon({ health }: { health: ServerHealth }) {
const Icon =
health === "ready"
? CircleCheckIcon
: health === "checking"
? LoaderCircleIcon
: CircleAlertIcon;
return <Icon className="size-(--icon-sm)" aria-hidden="true" />;
}
73 changes: 56 additions & 17 deletions apps/web/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ button[aria-busy="true"] {
}
@media (prefers-reduced-motion: no-preference) {
#refresh[aria-busy="true"] svg,
#sync[aria-busy="true"] svg {
#sync[aria-busy="true"] svg,
:is(#server-status, .server-status-popup)[data-status="checking"] svg {
animation: refresh-spin 800ms linear infinite;
}
}
Expand Down Expand Up @@ -99,6 +100,8 @@ kbd {
}
.topbar {
height: var(--topbar-height);
/* Replace the shell's top gutter so controls center between window and islands. */
margin-top: calc(-1 * var(--shell-gutter));
display: flex;
gap: var(--space-1-5);
align-items: center;
Expand Down Expand Up @@ -197,7 +200,7 @@ kbd {
}
.topbar [data-variant="ghost"]:hover,
.topbar [data-variant="ghost"][aria-expanded="true"] {
background: var(--hover);
background: var(--shell-hover);
}
.header-actions {
display: flex;
Expand Down Expand Up @@ -1041,29 +1044,68 @@ h2 {
.mobile-review-progress {
display: none;
}
.footer-details,
.server-metrics {
.footer-details {
display: inline-flex;
align-items: center;
}
.footer-details {
gap: var(--space-3);
flex-shrink: 0;
}
.server-metrics {
gap: var(--space-1-5);
padding: 1px var(--space-1-5);
.server-status-popup {
width: min(16rem, calc(100vw - 2 * var(--space-4)));
padding: var(--space-3);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
border-radius: var(--radius-control);
background: var(--surface-raised);
color: var(--fg);
font-variant-numeric: tabular-nums;
font-size: var(--text-sm);
}
.server-status-title {
display: flex;
align-items: center;
gap: var(--space-2);
margin: 0;
font-size: var(--text-sm);
font-weight: var(--weight-semibold);
}
.server-metrics:hover {
border-color: var(--accent);
.server-status-description {
margin: var(--space-1) 0 0;
color: var(--text-secondary);
line-height: var(--leading-copy);
}
:is(#server-status, .server-status-popup)[data-status="ready"] svg {
color: var(--success);
}
:is(#server-status, .server-status-popup)[data-status="unavailable"] svg {
color: var(--error);
}
.server-metrics-divider {
.server-status-section {
margin-top: var(--space-3);
padding-top: var(--space-3);
border-top: 1px solid var(--border-muted);
}
.server-status-section h2 {
margin: 0 0 var(--space-2);
color: var(--muted);
font-size: var(--text-xs);
font-weight: var(--weight-normal);
}
.server-status-section dl {
display: grid;
gap: var(--space-1);
margin: 0;
}
.server-status-section dl > div {
display: flex;
justify-content: space-between;
gap: var(--space-4);
}
.server-status-section dt {
color: var(--text-secondary);
}
.server-status-section dd {
margin: 0;
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
}
.mobile-only {
display: none;
Expand Down Expand Up @@ -1186,9 +1228,6 @@ h2 {
.diff-metadata {
gap: var(--space-1);
}
.server-metrics-label {
display: none;
}
}

@theme inline {
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
--border-control: #7f8491;
--border-strong: var(--border-control);
--hover: #eeeef4;
--shell-hover: #e1e2e8;
--diff-file-header: var(--panel);
--diff-canvas: var(--bg);
--diff-canvas-dot: var(--border);
Expand Down Expand Up @@ -95,6 +96,7 @@
--border-control: #747985;
--border-strong: var(--border-control);
--hover: #282930;
--shell-hover: #36373e;
--diff-canvas: #111216;
--diff-canvas-dot: #282a31;
--accent: #aba7ff;
Expand Down
Loading
Loading