diff --git a/apps/web/package.json b/apps/web/package.json index 70699f228..244e52396 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -42,13 +42,11 @@ }, "devDependencies": { "@intx/inference": "workspace:*", - "@tailwindcss/vite": "^4.3.3", "@types/bun": "catalog:", "@types/react": "^19.2.2", "@types/react-dom": "catalog:", "@vitejs/plugin-react": "^5.0.0", "sonner": "^2.0.7", - "tailwindcss": "^4.3.3", "typescript": "catalog:", "vite": "^7.1.0" } diff --git a/apps/web/src/bench/approvals-insights.tsx b/apps/web/src/bench/approvals-insights.tsx index 58f7552a0..95a7008af 100644 --- a/apps/web/src/bench/approvals-insights.tsx +++ b/apps/web/src/bench/approvals-insights.tsx @@ -51,7 +51,7 @@ export function ApprovalsCard({ {capped ? ` Based on the ${runs.length} most recent runs.` : ""}

{tally.kind === "loading" ? ( - + ) : tally.kind === "error" ? (

Couldn't load approvals.

) : ( @@ -83,7 +83,7 @@ export function ApprovalsLine({ readonly runs: readonly InsightsRun[]; }) { const tally = useApprovalTally(tenantId, runs); - if (tally.kind === "loading") return ; + if (tally.kind === "loading") return ; if (tally.kind === "error") return

Couldn't load approvals.

; return (
diff --git a/apps/web/src/bench/artifacts-tab.tsx b/apps/web/src/bench/artifacts-tab.tsx index efcbb98ad..91ed01e06 100644 --- a/apps/web/src/bench/artifacts-tab.tsx +++ b/apps/web/src/bench/artifacts-tab.tsx @@ -21,7 +21,7 @@ export function ArtifactsTab({ workbenchTenantId }: { readonly workbenchTenantId

Files saved here

Open all
- {page.kind === "loading" ? : null} + {page.kind === "loading" ? : null} {page.kind === "error" ?

{page.message}

: null} {page.kind === "ready" && rows.length === 0 ? (

Nothing saved here yet.

diff --git a/apps/web/src/bench/bench-pill.tsx b/apps/web/src/bench/bench-pill.tsx index 1983bcc50..da4982391 100644 --- a/apps/web/src/bench/bench-pill.tsx +++ b/apps/web/src/bench/bench-pill.tsx @@ -33,7 +33,7 @@ export function BenchPill({ > {worker === undefined ? ( rosterReady ? null : ( - + ) ) : ( diff --git a/apps/web/src/bench/information-tab.tsx b/apps/web/src/bench/information-tab.tsx index 6b6a0c018..fc3b1b155 100644 --- a/apps/web/src/bench/information-tab.tsx +++ b/apps/web/src/bench/information-tab.tsx @@ -247,7 +247,7 @@ export function InformationTab({ ) : null}
- {approvalsQuery.kind === "loading" ? : null} + {approvalsQuery.kind === "loading" ? : null} {approvalsQuery.kind === "error" ? (

{approvalsQuery.message}

) : null} @@ -283,7 +283,7 @@ export function InformationTab({ title="Active workflows" action={{ to: benchLink(WORKFLOWS_PATH_PREFIX, workbenchTenantId), label: "View all" }} > - {flows.isLoading ? : null} + {flows.isLoading ? : null} {flows.isSuccess && liveFlows.length === 0 ? (

No workflows are running here.

) : null} @@ -308,7 +308,7 @@ export function InformationTab({ title="Latest artifacts" action={{ to: benchLink(ARTIFACTS_PATH_PREFIX, workbenchTenantId), label: "View all" }} > - {artifacts.kind === "loading" ? : null} + {artifacts.kind === "loading" ? : null} {artifacts.kind === "ready" && latest.length === 0 ? (

Nothing saved here yet.

) : null} @@ -334,7 +334,7 @@ export function InformationTab({ title="Skills" action={{ to: benchLink(SKILLS_PATH_PREFIX, workbenchTenantId), label: "View all" }} > - {skills.isLoading ? : null} + {skills.isLoading ? : null} {skills.isSuccess && shownSkills.length === 0 ? (

No skills here yet.

) : null} diff --git a/apps/web/src/bench/insights-tab.tsx b/apps/web/src/bench/insights-tab.tsx index d8ad390e4..14472ca82 100644 --- a/apps/web/src/bench/insights-tab.tsx +++ b/apps/web/src/bench/insights-tab.tsx @@ -38,7 +38,7 @@ export function InsightsTab({ workbenchTenantId }: { readonly workbenchTenantId: ), }); - if (runs.kind === "loading") return ; + if (runs.kind === "loading") return ; if (runs.kind !== "ready") { return ( People Invite - {people.isPending ? : null} + {people.isPending ? : null} {people.isError ? ( Workers Add - {loading ? : null} + {loading ? : null} {!loading && workers.length === 0 ? (

No workers yet.

) : null} diff --git a/apps/web/src/bench/workflows-tab.tsx b/apps/web/src/bench/workflows-tab.tsx index b8ff6dacf..a25a81ba1 100644 --- a/apps/web/src/bench/workflows-tab.tsx +++ b/apps/web/src/bench/workflows-tab.tsx @@ -26,7 +26,7 @@ export function WorkflowsTab({ workbenchTenantId }: { readonly workbenchTenantId

Scheduled here

Open all - {flows.isLoading ? : null} + {flows.isLoading ? : null} {flows.isError ? (

{describeApiError(flows.error, "loading workflows")} diff --git a/apps/web/src/chat/avatar.css b/apps/web/src/chat/avatar.css index fc992bd59..de4e09f41 100644 --- a/apps/web/src/chat/avatar.css +++ b/apps/web/src/chat/avatar.css @@ -17,6 +17,23 @@ color: var(--canvas); } +.avatar-fill-1 { + background: var(--avatar-1); + color: black; +} +.avatar-fill-2 { + background: var(--avatar-2); + color: black; +} +.avatar-fill-3 { + background: var(--avatar-3); + color: black; +} +.avatar-fill-4 { + background: var(--avatar-4); + color: black; +} + /* The status dot is the only round non-initials child. */ .wb-av > span.rounded-full { width: 8px; diff --git a/apps/web/src/chat/avatar.tsx b/apps/web/src/chat/avatar.tsx index 637ef7978..3df3972b4 100644 --- a/apps/web/src/chat/avatar.tsx +++ b/apps/web/src/chat/avatar.tsx @@ -27,10 +27,10 @@ const TONE_BY_COLOR: Record = { }; export const avatarColorClass: Record = { - "--avatar-1": "bg-(--avatar-1) text-black", - "--avatar-2": "bg-(--avatar-2) text-black", - "--avatar-3": "bg-(--avatar-3) text-black", - "--avatar-4": "bg-(--avatar-4) text-black", + "--avatar-1": "avatar-fill-1", + "--avatar-2": "avatar-fill-2", + "--avatar-3": "avatar-fill-3", + "--avatar-4": "avatar-fill-4", }; const WORKER_HUES = ["orange", "blue", "green"] as const; diff --git a/apps/web/src/components/confirm-button.tsx b/apps/web/src/components/confirm-button.tsx index 6dae6086b..7a19d4fb9 100644 --- a/apps/web/src/components/confirm-button.tsx +++ b/apps/web/src/components/confirm-button.tsx @@ -21,7 +21,7 @@ export function ConfirmButton({ onConfirm, children, confirmLabel, ...props }: C return ( { if (e.key === "Escape") setArmed(false); }} diff --git a/apps/web/src/components/page-parts.css b/apps/web/src/components/page-parts.css new file mode 100644 index 000000000..d5376a895 --- /dev/null +++ b/apps/web/src/components/page-parts.css @@ -0,0 +1,170 @@ +/* The frame every routed page shares: a column that fills the stage, with + one scrolling body. */ +.page-frame { + display: flex; + height: 100%; + min-height: 0; + flex-direction: column; +} + +.page-scroll { + min-height: 0; + flex: 1; + overflow-y: auto; +} + +.page-scroll-auto { + min-height: 0; + flex: 1; + overflow: auto; +} + +.page-scroll-column { + display: flex; + min-height: 0; + flex: 1; + flex-direction: column; + overflow-y: auto; +} + +.page-stack { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.page-note { + font-size: 0.875rem; + line-height: 1.25rem; + color: var(--muted-foreground); +} + +.page-error { + font-size: 0.875rem; + line-height: 1.25rem; + color: var(--destructive); +} + +.page-error-inline { + margin-bottom: 0.75rem; + font-size: 0.875rem; + line-height: 1.25rem; + color: var(--destructive); +} + +.page-meta { + font-size: 0.75rem; + line-height: 1rem; + color: var(--muted-foreground); +} + +.inline-actions { + display: inline-flex; + align-items: center; + gap: 0.25rem; +} + +.inline-row { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + border-width: 0; +} + +/* Icons default to the surrounding text size; `:where` keeps zero + specificity so a sized icon class still wins. */ +:where(svg.lucide) { + width: 1em; + height: 1em; +} + +.icon-sm { + width: 1.5rem; + height: 1.5rem; +} + +/* Loading placeholders, sized by the content they stand in for. */ +.skeleton-line { + width: 100%; + height: 1.25rem; +} +.skeleton-row { + width: 100%; + height: 2.5rem; +} +.skeleton-row-lg { + width: 100%; + height: 3rem; +} +.skeleton-card { + width: 100%; + height: 4rem; +} +.skeleton-card-lg { + width: 100%; + height: 6rem; +} +.skeleton-panel-sm { + width: 100%; + height: 8rem; +} +.skeleton-panel { + width: 100%; + height: 10rem; +} +.skeleton-panel-lg { + width: 100%; + height: 12rem; +} +.skeleton-avatar { + width: 1.5rem; + height: 1.5rem; + border-radius: 9999px; +} +.skeleton-avatar-lg { + width: 2rem; + height: 2rem; + border-radius: 9999px; +} +.skeleton-name { + width: 5rem; + height: 1rem; +} +.skeleton-stat { + width: 4rem; + height: 26px; +} + +.page-meta-plain { + font-size: 0.75rem; + line-height: 1rem; +} + +.page-error-list { + list-style: inside disc; +} + +.code-textarea { + font-family: var(--font-mono); + font-size: 0.8125rem; + line-height: 1.5; +} + +.code-textarea-canvas { + background: var(--background); +} + +.code-textarea-tall { + min-height: 320px; +} diff --git a/apps/web/src/lib/icons/index.tsx b/apps/web/src/lib/icons/index.tsx index 18fc374c0..d73cf8e36 100644 --- a/apps/web/src/lib/icons/index.tsx +++ b/apps/web/src/lib/icons/index.tsx @@ -88,7 +88,7 @@ export { } from "lucide-react"; /** Wraps a subtree so every icon under it defaults to a 2px stroke without - * repeating it at each call site (the `1em` default size is `tailwind.css`). + * repeating it at each call site (the `1em` default size is `page-parts.css`). * Mounted once at each app's root (see `apps/web/src/app.tsx`). */ export function BoldIconProvider({ children }: { children: ReactNode }) { return {children}; diff --git a/apps/web/src/library/artifact-renderer.css b/apps/web/src/library/artifact-renderer.css new file mode 100644 index 000000000..d3861baea --- /dev/null +++ b/apps/web/src/library/artifact-renderer.css @@ -0,0 +1,61 @@ +.artifact-doc { + display: flex; + flex-direction: column; + gap: 0.5rem; + font-size: 0.875rem; + line-height: 1.625; + color: var(--foreground); +} + +.artifact-doc-heading { + font-weight: 600; + line-height: 1.375; +} + +.artifact-doc-bullets { + list-style: disc; + padding-left: 1.25rem; +} + +.artifact-pdf { + border-radius: var(--ui-radius-md); + border: 1px solid var(--border); + background: var(--card); + padding: 1.5rem; + box-shadow: + 0 1px 3px 0 rgb(0 0 0 / 0.1), + 0 1px 2px -1px rgb(0 0 0 / 0.1); +} + +.artifact-pdf-title { + margin-bottom: 0.75rem; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 600; +} + +.artifact-pdf-text { + white-space: pre-wrap; + font-size: 0.875rem; + line-height: 1.625; + color: var(--foreground); +} + +.artifact-html-frame { + width: 100%; + height: 100%; + min-height: 24rem; + border: 0; +} + +.artifact-unsupported { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + padding: 2rem 0; + text-align: center; + font-size: 0.875rem; + line-height: 1.25rem; + color: var(--muted-foreground); +} diff --git a/apps/web/src/library/artifact-renderer.tsx b/apps/web/src/library/artifact-renderer.tsx index f0a0e36bb..513332dbd 100644 --- a/apps/web/src/library/artifact-renderer.tsx +++ b/apps/web/src/library/artifact-renderer.tsx @@ -3,6 +3,7 @@ import { CsvTable } from "@corbits/react-ui"; import { FileDashed } from "@/lib/icons"; +import "./artifact-renderer.css"; import type { ArtifactRendererKind } from "./renderer-kind"; export type ArtifactRenderProps = { @@ -76,19 +77,19 @@ function DocRenderer({ const lines = parseDocLines(content); const HeadingTag = ["h1", "h2", "h3"] as const; return ( -

+
{lines.map((line, index) => { if (line.kind === "heading") { const Tag = HeadingTag[line.level - 1] ?? "h3"; return ( - + {line.text} ); } if (line.kind === "bullet") { return ( -
    +
    • {line.text}
    ); @@ -143,9 +144,9 @@ function PdfRenderer({ ); } return ( -
    -

    {title}

    -

    {content}

    +
    +

    {title}

    +

    {content}

    ); } @@ -170,19 +171,19 @@ function HtmlPreviewRenderer({ title={`${title} preview`} src={previewSrc} sandbox="allow-scripts" - className="h-full min-h-[24rem] w-full border-0" + className="artifact-html-frame" /> ); } function EmptyContent({ message }: { readonly message: string }) { - return

    {message}

    ; + return

    {message}

    ; } function UnsupportedRenderer({ unavailableReason }: { readonly unavailableReason?: string }) { return ( -
    -