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
32 changes: 23 additions & 9 deletions apps/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,21 @@ serves from its own origin (`vite build`, then point `HUB_STATIC_DIR` at
## Layout

Every signed-in screen renders inside the same four-column shell
(`src/shell/`), assembled from `@corbits/react-ui`'s sidebar rail and
sidebar panel pieces:
(`src/shell/`), built from `@corbits/react-ui`'s sidebar panel pieces plus a
workbench-composed rail:

1. **Rail** — the global page icons, one per screen, each carrying its page
name as its accessible name and hover/focus tooltip.
2. **Contextual column** — the active page's own options: channels,
routines, the page list in full labels, with the bench switcher and the
signed-in account pinned to the bottom.
1. **Rail** — global and stable: it never changes with navigation or with
the selected bench. One icon per screen with its name captioned
underneath (not tooltip-only), plus the bench switcher and the
signed-in account's settings/sign-out at the bottom. Answers "where am I
in the product, and which bench am I in". Fixed width at every
breakpoint — it never joins the columns that withdraw as the viewport
narrows.
2. **Contextual column** — bench-scoped and live: channels, chats, running
routines, and notifications for the _currently selected_ bench. It
refetches when the bench changes, not when the route does, so its
contents can persist or travel across page navigation rather than being
a per-page list. Answers "what is happening in this bench right now".
3. **Main pane** — whatever the route renders, taking all remaining width.
4. **Canvas** — an optional fourth column for running agents, live
workflow walkthroughs and analytics. Collapsed by default and collapsed
Expand All @@ -29,8 +36,15 @@ chat dock squeezing the content area resizes the columns instead of
clipping them.

A new page needs one entry in `NAV_ROUTES` (`src/routes.tsx`) — the rail's
icon, the contextual column's labeled row, and the route switch all read
from that single table, so a page cannot appear in one without the other.
icon and the route switch both read from that single table, so a page
cannot appear in one without the other. The contextual column no longer
reads `NAV_ROUTES` at all: it has nothing to do with which pages exist.

Running routines in the contextual column are sourced today from
`@corbits/chat-ui`'s workflow-run listing (`src/shell/routine-activity.ts`)
rather than a dedicated routines package, which isn't published yet — the
column depends only on that file's `RoutineActivityItem` shape, so swapping
in a real `@corbits/routines` listing later touches nothing else.

## Screens

Expand Down
41 changes: 18 additions & 23 deletions apps/web/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -110,25 +110,24 @@
`prefers-reduced-motion` in its theme.css), and these transitions inherit
that like any other. */

/* Contextual panel footer: bench switcher above the identity row. */
.shell-contextual-footer {
display: flex;
flex-direction: column;
}

/* Column 1's footer docks: the bench switcher and identity row. The rail
itself is `@corbits/react-ui`'s `SidebarRail` (showLabels), styled in its
own stylesheet; only the footer parts composed into its `footer` slot
carry local CSS here. */
.shell-bench-dock {
padding: 0.25rem 0.25rem 0.5rem;
width: 100%;
padding: 0 0.375rem;
}

.shell-identity-dock {
display: grid;
grid-template-columns: auto auto 1fr;
.shell-rail-identity {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.25rem 0.25rem;
gap: 0.25rem;
padding-bottom: 0.25rem;
}

.shell-identity-avatar {
.shell-rail-identity-avatar {
display: grid;
height: 28px;
width: 28px;
Expand All @@ -141,7 +140,7 @@
color: var(--primary-foreground);
}

.shell-identity-settings {
.shell-rail-identity-settings {
display: grid;
height: 28px;
width: 28px;
Expand All @@ -151,18 +150,14 @@
transition: color 150ms;
}

.shell-identity-settings:hover,
.shell-identity-settings[aria-current="page"] {
.shell-rail-identity-settings:hover,
.shell-rail-identity-settings[aria-current="page"] {
color: var(--foreground);
}

.shell-identity-email {
grid-column: 1 / -1;
overflow: hidden;
font-size: 0.75rem;
color: var(--muted-foreground);
text-overflow: ellipsis;
white-space: nowrap;
.shell-activity-skeleton {
height: 6rem;
width: 100%;
}

/* Auth screen — the two-column corbits.dev sign-in shell. The right panel is
Expand Down
14 changes: 7 additions & 7 deletions apps/web/src/shell/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ export function AppShell({

return (
<div className="shell-frame" ref={frameRef}>
<Rail path={path} onNavigate={navigate} />
<Rail
path={path}
onNavigate={navigate}
user={user}
onSignOut={onSignOut}
/>
{contextualPanelVisible(layoutMode) && (
<ContextualPanel
path={path}
onNavigate={navigate}
user={user}
onSignOut={onSignOut}
/>
<ContextualPanel path={path} onNavigate={navigate} />
)}
<div className="shell-main">
{canvasAllowed && (
Expand Down
62 changes: 62 additions & 0 deletions apps/web/src/shell/bench-activity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// The second column's one data source: everything happening in the
// currently selected bench right now. Channels and chats come from
// `@corbits/chat-ui`'s own validated fetches; running routines come through
// the seam in `./routine-activity.ts`. Notifications have no backing feature
// in the hub yet, so they are not fetched here at all — the column renders
// an honest empty state for that section instead of a query with nowhere
// to point.

import { useEffect, useState } from "react";
import { listChannels } from "@corbits/chat-ui";
import type { Channel } from "@corbits/chat-ui";

import { listRoutineActivity } from "./routine-activity";
import type { RoutineActivityItem } from "./routine-activity";

export type BenchActivityQuery =
| { readonly kind: "loading" }
| { readonly kind: "empty" }
| { readonly kind: "error"; readonly message: string }
| {
readonly kind: "ready";
readonly channels: readonly Channel[];
readonly chats: readonly Channel[];
readonly routines: readonly RoutineActivityItem[];
};

/** Bench-scoped live activity for the second column, refetched whenever the
* selected bench changes — nothing here is page-scoped, so a route change
* alone never triggers a refetch. */
export function useBenchActivity(tenantId: string | null): BenchActivityQuery {
const [state, setState] = useState<BenchActivityQuery>({ kind: "loading" });

useEffect(() => {
if (tenantId === null) {
setState({ kind: "empty" });
return;
}
let cancelled = false;
setState({ kind: "loading" });
Promise.all([
listChannels(tenantId, "channel"),
listChannels(tenantId, "chat"),
listRoutineActivity(tenantId),
])
.then(([channels, chats, routines]) => {
if (cancelled) return;
setState({ kind: "ready", channels, chats, routines });
})
.catch((cause: unknown) => {
if (cancelled) return;
setState({
kind: "error",
message: cause instanceof Error ? cause.message : String(cause),
});
});
return () => {
cancelled = true;
};
}, [tenantId]);

return state;
}
Loading
Loading