Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"ci": "biome ci src/ && pnpm run typecheck && vitest run && vite build"
},
"dependencies": {
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/ibm-plex-sans": "^5.2.8",
"@fontsource/ibm-plex-serif": "^5.2.7",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-scroll-area": "^1.2.10",
Expand Down
24 changes: 24 additions & 0 deletions dashboard/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dashboard/src/components/layout/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function AppShell({ children }: { children: ReactNode }) {
<Header />
<TopProgressBar />
<main className="flex-1 px-4 py-5 md:px-6 md:py-6">
<div key={pathname} className="mx-auto max-w-[1400px] animate-fade-in">
<div key={pathname} className="mx-auto max-w-[1240px] animate-page-rise">
<RouteErrorBoundary>{children}</RouteErrorBoundary>
</div>
</main>
Expand Down
37 changes: 37 additions & 0 deletions dashboard/src/components/layout/brand-mark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* The taskito mark — a rounded "task token" with two dot eyes and a
* check-smile, filled with the live accent. Colors come from CSS vars so it
* tracks the active theme/accent automatically.
*/
export function BrandMark({ size = 38, className }: { size?: number; className?: string }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 40 40"
fill="none"
className={className}
aria-hidden="true"
>
<title>taskito</title>
<rect x="3" y="3" width="34" height="34" rx="11" fill="var(--accent)" />
<rect x="3" y="3" width="34" height="34" rx="11" fill="url(#tk-g)" fillOpacity="0.18" />
<defs>
<linearGradient id="tk-g" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor="#fff" />
<stop offset="1" stopColor="#fff" stopOpacity="0" />
</linearGradient>
</defs>
<circle cx="15" cy="16" r="2.1" fill="var(--accent-fg)" />
<circle cx="25" cy="16" r="2.1" fill="var(--accent-fg)" />
<path
d="M14 24.5l3.4 3.4L27 19"
stroke="var(--accent-fg)"
strokeWidth="2.6"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
</svg>
);
}
3 changes: 1 addition & 2 deletions dashboard/src/components/layout/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ export function Header() {
<MobileMenu />
<Button
variant="outline"
size="sm"
onClick={() => setOpen(true)}
className="hidden md:inline-flex w-[260px] justify-between text-[var(--fg-subtle)] font-normal"
className="hidden w-[300px] max-w-[38vw] justify-between font-normal text-[var(--fg-subtle)] md:inline-flex"
>
<span className="inline-flex items-center gap-2">
<Search className="size-3.5" aria-hidden />
Expand Down
1 change: 1 addition & 0 deletions dashboard/src/components/layout/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export { LastRefreshed } from "./last-refreshed";
export { MobileMenu } from "./mobile-menu";
export { PageHeader } from "./page-header";
export { RouteErrorBoundary } from "./route-error-boundary";
export { SectionHeading } from "./section-heading";
export { Sidebar } from "./sidebar";
export { ThemeToggle } from "./theme-toggle";
export { TopProgressBar } from "./top-progress-bar";
3 changes: 2 additions & 1 deletion dashboard/src/components/layout/last-refreshed.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { LiveDot } from "@/components/ui";
import { useLastRefreshed } from "@/hooks";
import { cn } from "@/lib/cn";

Expand Down Expand Up @@ -55,7 +56,7 @@ export function LastRefreshed({ className }: { className?: string }) {
{isFetching ? (
<Loader2 className="size-3 animate-spin text-accent" aria-hidden />
) : (
<span aria-hidden className="size-1.5 rounded-full bg-success" />
<LiveDot tone="success" size={6} />
)}
{label}
</span>
Expand Down
14 changes: 8 additions & 6 deletions dashboard/src/components/layout/page-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,26 @@ export function PageHeader({
return (
<div
className={cn(
"mb-6 flex flex-col gap-3 md:flex-row md:items-start md:justify-between",
"mb-[22px] flex flex-col gap-3 md:flex-row md:items-end md:justify-between",
className,
)}
>
<div>
<div className="min-w-0">
{breadcrumbs && breadcrumbs.length > 0 ? (
<Breadcrumbs items={breadcrumbs} className="mb-2" />
) : eyebrow ? (
<div className="mb-1 text-[11px] font-semibold uppercase tracking-wider text-[var(--fg-subtle)]">
<div className="mb-1.5 font-mono text-[0.68rem] font-semibold uppercase tracking-[0.13em] text-[var(--accent-ink)]">
{eyebrow}
</div>
) : null}
<h1 className="text-2xl font-semibold tracking-tight text-[var(--fg)]">{title}</h1>
<h1 className="font-serif text-[2rem] font-semibold leading-[1.05] tracking-[-0.02em] text-[var(--fg)]">
{title}
</h1>
{description ? (
<p className="mt-1.5 text-sm text-[var(--fg-muted)]">{description}</p>
<p className="mt-2 max-w-[60ch] text-[0.92rem] text-[var(--fg-muted)]">{description}</p>
) : null}
</div>
{actions ? <div className="flex flex-wrap items-center gap-2">{actions}</div> : null}
{actions ? <div className="flex flex-wrap items-center gap-2.5">{actions}</div> : null}
</div>
);
}
20 changes: 20 additions & 0 deletions dashboard/src/components/layout/section-heading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { ReactNode } from "react";
import { cn } from "@/lib/cn";

interface SectionHeadingProps {
title: ReactNode;
action?: ReactNode;
className?: string;
}

/** A serif section title with an optional right-aligned action. */
export function SectionHeading({ title, action, className }: SectionHeadingProps) {
return (
<div className={cn("mb-3 flex min-h-[30px] items-center justify-between gap-3", className)}>
<h2 className="font-serif text-[1.18rem] font-semibold tracking-[-0.01em] whitespace-nowrap text-[var(--fg)]">
{title}
</h2>
{action ?? null}
</div>
);
}
78 changes: 53 additions & 25 deletions dashboard/src/components/layout/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,82 @@
import { useQuery } from "@tanstack/react-query";
import { Link, useLocation } from "@tanstack/react-router";
import { AlertOctagon, ExternalLink as ExternalLinkIcon } from "lucide-react";
import { ExternalLink as ExternalLinkIcon } from "lucide-react";
import { LiveDot } from "@/components/ui";
import { statsQuery } from "@/features/overview/hooks";
import { useBranding, useExternalLinks } from "@/features/settings";
import { cn } from "@/lib/cn";
import { formatCount } from "@/lib/number";
import { site } from "@/lib/site";
import { BrandMark } from "./brand-mark";
import { NAV } from "./nav-config";

export function Sidebar() {
const { pathname } = useLocation();
const { title } = useBranding();
const externalLinks = useExternalLinks();
// Shares the ["stats"] cache with the Overview — no extra polling here, just
// a one-time fetch that stays in sync as other views refetch.
const { data: stats } = useQuery(statsQuery());
const deadCount = stats?.dead ?? 0;
const isDefaultBrand = title === site.name;

return (
<aside className="hidden lg:flex w-60 shrink-0 flex-col border-r border-[var(--border)] bg-[var(--bg-subtle)]">
<div className="flex items-center gap-2 px-5 py-4">
<div className="grid place-items-center size-7 rounded-md bg-accent text-accent-fg">
<AlertOctagon className="size-4" aria-hidden />
</div>
<div className="flex flex-col leading-tight">
<span className="text-sm font-semibold tracking-tight">{title}</span>
<span className="text-[10px] uppercase tracking-wider text-[var(--fg-subtle)]">
Dashboard
</span>
<aside className="hidden w-[248px] shrink-0 flex-col border-r border-[var(--border)] bg-[var(--bg-subtle)] lg:flex">
<div className="flex items-center gap-2.5 px-[18px] pt-[18px] pb-4">
<BrandMark size={38} />
<div className="leading-none">
<div className="text-[1.18rem] font-semibold tracking-[-0.02em]">
{isDefaultBrand ? (
<>
task<span className="text-[var(--accent-ink)]">ito</span>
</>
) : (
title
)}
</div>
<div className="mt-[3px] font-mono text-[0.66rem] uppercase tracking-[0.16em] text-[var(--fg-subtle)]">
Queue console
</div>
</div>
</div>
<nav className="flex-1 overflow-y-auto px-3 pb-4">
{NAV.map((group) => (
<div key={group.title} className="mt-5 first:mt-2">
<div className="px-2 pb-1.5 text-[10px] font-semibold uppercase tracking-wider text-[var(--fg-subtle)]">
<div key={group.title} className="mt-[18px] first:mt-1.5">
<div className="px-2.5 pb-[7px] font-mono text-[0.62rem] font-semibold uppercase tracking-[0.13em] text-[var(--fg-subtle)]">
{group.title}
</div>
<ul className="flex flex-col gap-0.5">
{group.items.map(({ to, label, icon: Icon }) => {
const active = pathname === to || (to !== "/" && pathname.startsWith(`${to}/`));
const count = to === "/dead-letters" && deadCount > 0 ? deadCount : null;
return (
<li key={to}>
<Link
to={to}
aria-current={active ? "page" : undefined}
className={cn(
"relative flex items-center gap-2.5 rounded-md px-2 py-1.5 text-sm transition-colors",
"relative flex items-center gap-2.5 rounded-[9px] px-2.5 py-2 text-sm transition-colors",
active
? "bg-[var(--surface-2)] text-[var(--fg)] shadow-xs"
: "text-[var(--fg-muted)] hover:bg-[var(--surface-2)]/60 hover:text-[var(--fg)]",
? "bg-[var(--surface)] font-semibold text-[var(--fg)] shadow-[var(--card-shadow)]"
: "font-normal text-[var(--fg-muted)] hover:bg-[var(--surface-2)]/70 hover:text-[var(--fg)]",
)}
>
{active ? (
<span
aria-hidden
className="absolute -left-3 inset-y-1.5 w-0.5 rounded-r-full bg-accent"
className="absolute -left-3 inset-y-2 w-[3px] rounded-r-full bg-accent"
/>
) : null}
<Icon className={cn("size-4", active && "text-accent")} aria-hidden />
<span>{label}</span>
<Icon
className={cn("size-[17px] shrink-0", active && "text-accent")}
aria-hidden
/>
<span className="truncate">{label}</span>
{count != null ? (
<span className="ml-auto font-mono text-[0.7rem] font-semibold tabular-nums text-danger">
{formatCount(count)}
</span>
) : null}
</Link>
</li>
);
Expand All @@ -58,8 +85,8 @@ export function Sidebar() {
</div>
))}
{externalLinks.length > 0 ? (
<div className="mt-5">
<div className="px-2 pb-1.5 text-[10px] font-semibold uppercase tracking-wider text-[var(--fg-subtle)]">
<div className="mt-[18px]">
<div className="px-2.5 pb-[7px] font-mono text-[0.62rem] font-semibold uppercase tracking-[0.13em] text-[var(--fg-subtle)]">
Links
</div>
<ul className="flex flex-col gap-0.5">
Expand All @@ -69,9 +96,9 @@ export function Sidebar() {
href={link.url}
target="_blank"
rel="noreferrer noopener"
className="flex items-center gap-2.5 rounded-md px-2 py-1.5 text-sm text-[var(--fg-muted)] transition-colors hover:bg-[var(--surface-2)] hover:text-[var(--fg)]"
className="flex items-center gap-2.5 rounded-[9px] px-2.5 py-2 text-sm text-[var(--fg-muted)] transition-colors hover:bg-[var(--surface-2)] hover:text-[var(--fg)]"
>
<ExternalLinkIcon className="size-4" aria-hidden />
<ExternalLinkIcon className="size-[17px] shrink-0" aria-hidden />
<span className="truncate">{link.label}</span>
</a>
</li>
Expand All @@ -80,8 +107,9 @@ export function Sidebar() {
</div>
) : null}
</nav>
<div className="border-t border-[var(--border)] px-5 py-3 text-[11px] text-[var(--fg-subtle)]">
{import.meta.env.DEV ? "dev build" : "production"}
<div className="flex items-center gap-2 border-t border-[var(--border)] px-[18px] py-3 text-[0.72rem] text-[var(--fg-subtle)]">
<LiveDot tone="success" />
Core healthy · {import.meta.env.DEV ? "dev build" : "production"}
</div>
</aside>
);
Expand Down
29 changes: 18 additions & 11 deletions dashboard/src/components/ui/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@ import type { HTMLAttributes } from "react";
import { cn } from "@/lib/cn";

const badgeVariants = cva(
"inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium tracking-tight",
"inline-flex items-center gap-1.5 rounded-[var(--chip-radius)] border px-2.5 py-0.5 text-[0.74rem] font-medium whitespace-nowrap",
{
variants: {
tone: {
neutral:
"bg-[var(--surface-3)] text-[var(--fg-muted)] ring-1 ring-inset ring-[var(--border-strong)]",
accent: "bg-accent-dim text-accent ring-1 ring-inset ring-accent/30",
info: "bg-info-dim text-info ring-1 ring-inset ring-info/30",
success: "bg-success-dim text-success ring-1 ring-inset ring-success/30",
warning: "bg-warning-dim text-warning ring-1 ring-inset ring-warning/30",
danger: "bg-danger-dim text-danger ring-1 ring-inset ring-danger/30",
neutral: "bg-[var(--surface-3)] text-[var(--fg-muted)] border-[var(--border-strong)]",
accent: "bg-accent-dim text-accent-ink border-accent/30",
info: "bg-info-dim text-info border-info/30",
success: "bg-success-dim text-success border-success/30",
warning: "bg-warning-dim text-warning border-warning/30",
danger: "bg-danger-dim text-danger border-danger/30",
},
},
defaultVariants: {
Expand All @@ -24,10 +23,18 @@ const badgeVariants = cva(

export interface BadgeProps
extends HTMLAttributes<HTMLSpanElement>,
VariantProps<typeof badgeVariants> {}
VariantProps<typeof badgeVariants> {
/** Render a small leading dot in the badge's own color (status pills). */
dot?: boolean;
}

export function Badge({ className, tone, ...props }: BadgeProps) {
return <span className={cn(badgeVariants({ tone, className }))} {...props} />;
export function Badge({ className, tone, dot, children, ...props }: BadgeProps) {
return (
<span className={cn(badgeVariants({ tone }), className)} {...props}>
{dot ? <span className="size-1.5 shrink-0 rounded-full bg-current" aria-hidden /> : null}
{children}
</span>
);
}

export { badgeVariants };
Loading