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
5 changes: 5 additions & 0 deletions .changeset/dashboard-everforest-reskin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@gemstack/the-framework": minor
---

Re-skin the dashboard onto the landing page's Everforest identity (IBM Plex Sans, warm dark palette, squarer corners) and rename the two queues to "Human Queue" and "AI Queue" so the dashboard and the-framework.ai read as one product.
4 changes: 2 additions & 2 deletions packages/framework-dashboard/components/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export function DashboardPage({
</Card>
<Card>
<CardHeader>
<CardTitle>Backlog</CardTitle>
<CardTitle>AI Queue</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<Backlog queue={data.queue} onSelectProject={onSelectProject} />
Expand Down Expand Up @@ -133,7 +133,7 @@ function NeedsYou({ items, onSelectProject }: { items: Intervention[]; onSelectP
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Inbox className="h-4 w-4 text-muted-foreground" />
Needs you
Human Queue
{items.length > 0 && (
<span className="rounded-full bg-primary px-1.5 text-xs font-semibold text-primary-foreground tabular-nums">
{items.length}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,27 @@ afterEach(() => {
const open = () => fireEvent.click(screen.getByRole('button', { name: /notifications/i }))

describe('NotificationsMenu (#676)', () => {
test('the popover groups methods and categories, both "Needs you" and "New activity" toggleable', () => {
test('the popover groups methods and categories, both "Human Queue" and "New activity" toggleable', () => {
render(<NotificationsMenu />)
open()
expect(screen.getByText('Deliver to')).toBeTruthy()
expect(screen.getByText('Browser')).toBeTruthy()
expect(screen.getByText('Discord')).toBeTruthy()
expect(screen.getByText('Notify me about')).toBeTruthy()
expect(screen.getByText('Needs you')).toBeTruthy()
expect(screen.getByText('Human Queue')).toBeTruthy()
expect(screen.queryByText('Always on')).toBeNull() // #627: now a real toggle, no static row
expect(screen.getByText('New activity')).toBeTruthy()
})

test('toggling Discord, New activity, and Needs you writes each preference through', () => {
test('toggling Discord, New activity, and Human Queue writes each preference through', () => {
render(<NotificationsMenu />)
open()
fireEvent.click(screen.getByText('Discord'))
expect(updatePreferences).toHaveBeenCalledWith({ notifyDiscord: true })
fireEvent.click(screen.getByText('New activity'))
expect(updatePreferences).toHaveBeenCalledWith({ notifyNewActivity: true })
// Defaults on, so the click turns it OFF (#627).
fireEvent.click(screen.getByText('Needs you'))
fireEvent.click(screen.getByText('Human Queue'))
expect(updatePreferences).toHaveBeenCalledWith({ notifyHumanIntervention: false })
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export function NotificationsMenu() {
title="A session awaiting your answer, or a PR ready to review"
className="items-start"
>
<OptionLabel label="Needs you" description="A session awaiting you, or a PR to review" />
<OptionLabel label="Human Queue" description="A session awaiting you, or a PR to review" />
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={activity}
Expand Down
2 changes: 1 addition & 1 deletion packages/framework-dashboard/components/ProjectPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export function ProjectPicker({
{interventionCount > 0 && (
<span
className="min-w-5 rounded-full bg-primary px-1.5 text-center text-xs font-semibold text-primary-foreground tabular-nums"
title={`${interventionCount} item${interventionCount === 1 ? '' : 's'} need${interventionCount === 1 ? 's' : ''} you`}
title={`${interventionCount} item${interventionCount === 1 ? '' : 's'} in your Human Queue`}
>
{interventionCount}
</span>
Expand Down
77 changes: 41 additions & 36 deletions packages/framework-dashboard/components/RunHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
SidebarMenu,
SidebarMenuItem,
} from './ui/sidebar.js'
import { ScrollArea } from './ui/scroll-area.js'

// One rendered row of the rail, from either source (a project's own run, or a pooled cross-project
// recent): the RunMeta to show, an optional project label (only on the Overview, where the rail
Expand Down Expand Up @@ -142,43 +143,47 @@ export function RunHistory({
onProjectAdded={onProjectAdded}
/>
</SidebarHeader>
<SidebarContent>
<SidebarGroup className="pt-1">
{/* Recents label sits under the launcher, over the run list (not a page-wide header). */}
{hasRecents && (
<SidebarGroupLabel className="uppercase tracking-wide text-muted-foreground">Recents</SidebarGroupLabel>
)}
<SidebarGroupContent>
<SidebarMenu className="gap-0.5">
{/* A just-started run, before its run.json exists — highlighted while following it. */}
{showOptimistic && (
<SidebarMenuItem>
<RunRow status="running" intent={optimistic ?? undefined} subtitle="starting…" active={starting} dim onClick={() => onSelect(null)} />
</SidebarMenuItem>
)}
{rows.map(row => (
<SidebarMenuItem key={row.key}>
<RunRow
status={row.run.status}
intent={runLabel(row.run)}
driver={row.run.driver}
// On the Overview the project is what tells the rows apart, so it leads the meta
// line; a project's own rail already knows its project, so it shows just the time.
subtitle={row.project ? `${row.project} · ${formatRelative(row.run.startedAt)}` : formatRelative(row.run.startedAt)}
active={row.active}
waiting={row.run.settledAt !== undefined}
remote={row.run.target === 'remote'}
{...(row.run.remoteLabel ? { remoteLabel: row.run.remoteLabel } : {})}
onClick={row.onClick}
/>
</SidebarMenuItem>
))}
</SidebarMenu>
{!hasRecents && (
<p className="whitespace-nowrap px-2 py-1 text-sm text-muted-foreground">No sessions yet.</p>
{/* The themed ScrollArea (#913) instead of the sidebar's native overflow bar, matching the
Overview: suppress SidebarContent's own `overflow-auto` and let the ScrollArea own it. */}
<SidebarContent className="overflow-hidden">
<ScrollArea className="min-h-0 flex-1">
<SidebarGroup className="pt-1">
{/* Recents label sits under the launcher, over the run list (not a page-wide header). */}
{hasRecents && (
<SidebarGroupLabel className="uppercase tracking-wide text-muted-foreground">Recents</SidebarGroupLabel>
)}
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroupContent>
<SidebarMenu className="gap-0.5">
{/* A just-started run, before its run.json exists — highlighted while following it. */}
{showOptimistic && (
<SidebarMenuItem>
<RunRow status="running" intent={optimistic ?? undefined} subtitle="starting…" active={starting} dim onClick={() => onSelect(null)} />
</SidebarMenuItem>
)}
{rows.map(row => (
<SidebarMenuItem key={row.key}>
<RunRow
status={row.run.status}
intent={runLabel(row.run)}
driver={row.run.driver}
// On the Overview the project is what tells the rows apart, so it leads the meta
// line; a project's own rail already knows its project, so it shows just the time.
subtitle={row.project ? `${row.project} · ${formatRelative(row.run.startedAt)}` : formatRelative(row.run.startedAt)}
active={row.active}
waiting={row.run.settledAt !== undefined}
remote={row.run.target === 'remote'}
{...(row.run.remoteLabel ? { remoteLabel: row.run.remoteLabel } : {})}
onClick={row.onClick}
/>
</SidebarMenuItem>
))}
</SidebarMenu>
{!hasRecents && (
<p className="whitespace-nowrap px-2 py-1 text-sm text-muted-foreground">No sessions yet.</p>
)}
</SidebarGroupContent>
</SidebarGroup>
</ScrollArea>
</SidebarContent>
</Sidebar>
)
Expand Down
2 changes: 1 addition & 1 deletion packages/framework-dashboard/components/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export function SettingsPage({ onSelectProject }: { onSelectProject?: ((id: stri
}
/>
<ToggleRow
label="Needs you"
label="Human Queue"
description="A session awaiting your answer, or a PR ready to review."
checked={preferences.notifyHumanIntervention ?? true}
onChange={next => updatePreferences({ notifyHumanIntervention: next })}
Expand Down
89 changes: 50 additions & 39 deletions packages/framework-dashboard/layouts/tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,45 @@
`dark:text-*-400` unapplied, and picking Light on a dark OS applied them when it should not. */
@custom-variant dark (&:where(.dark, .dark *));

/* shadcn-style design tokens (neutral slate), light + dark. The Framework's MVP page
is dark-first; mirror that so the spike reads as the same product. */
/* Design tokens, light + dark, on the landing page's Everforest identity (#1118) so the
dashboard and the-framework.ai read as one product. The dark theme mirrors the page's exact
palette (packages/the-framework.ai/pages/index/styles.css); the light theme is Everforest
Light, so the theme toggle still works rather than dropping to a single dark mode. Components
read the `--color-*` tokens surfaced in `@theme inline`, so this is a palette swap only.
The page is squarer than the old UI, so the radius tightens to ~2px to match. */
@theme {
--radius: 0.5rem;
--radius: 0.125rem;
/* The landing page's typeface (#1118), loaded in pages/+Head.tsx. Overriding these two theme
keys retargets Tailwind's default body font and every `font-mono` to IBM Plex. */
--font-sans: 'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif;
--font-mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}

:root {
/* Let the browser paint native theme-matched scrollbars + form controls (the shadcn way),
instead of a light OS scrollbar on the dark UI. `.dark` flips it below. */
/* Let the browser paint native theme-matched scrollbars + form controls (the shadcn way).
`.dark` flips it below. */
color-scheme: light;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--border: oklch(0.922 0 0);
--primary: oklch(0.58 0.16 264);
--primary-foreground: oklch(0.985 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
/* Everforest Light (medium). Cards recede a shade below the canvas, mirroring the dark theme
where panels are darker than the background. */
--background: #fdf6e3;
--foreground: #5c6a72;
--card: #f4f0d9;
--card-foreground: #5c6a72;
--muted: #efebd4;
--muted-foreground: #829181;
--border: #ddd8be;
--primary: #8da101;
--primary-foreground: #fdf6e3;
--accent: #efebd4;
--accent-foreground: #5c6a72;

/* Status. Every status colour used to be a raw palette value, so "good" was six different
greens and `amber-500` meant both "stopped" and "building, fine". These four are the whole
vocabulary. They are tuned per theme against that theme's canvas rather than picked once
from the middle of a ramp: the old flat `-500` tones sat at ~2:1 on white. */
--success: oklch(0.52 0.13 155);
--warning: oklch(0.52 0.12 70);
--danger: oklch(0.52 0.2 27);
--info: oklch(0.53 0.15 245);
vocabulary, now on the Everforest Light accents. */
--success: #8da101;
--warning: #dfa000;
--danger: #f85552;
--info: #3a94c5;

/* The brand mark's six strands (#757), darkest first, exactly as the brand ships them.
Kept as variables so the mark can be re-ramped for a dark canvas without touching the SVG. */
Expand All @@ -49,23 +58,25 @@

.dark {
color-scheme: dark;
--background: oklch(0.16 0.01 264);
--foreground: oklch(0.95 0 0);
--card: oklch(0.2 0.01 264);
--card-foreground: oklch(0.95 0 0);
--muted: oklch(0.27 0.01 264);
--muted-foreground: oklch(0.7 0 0);
--border: oklch(0.3 0.01 264);
--primary: oklch(0.62 0.17 264);
--primary-foreground: oklch(0.985 0 0);
--accent: oklch(0.27 0.01 264);
--accent-foreground: oklch(0.95 0 0);
/* Everforest Dark (medium) — the landing page's exact values: bg #2d353b, panels #232a2e,
text #d3c6aa, green accent #a7c080, borders #475258. */
--background: #2d353b;
--foreground: #d3c6aa;
--card: #232a2e;
--card-foreground: #d3c6aa;
--muted: #343f44;
--muted-foreground: #9da9a0;
--border: #475258;
--primary: #a7c080;
--primary-foreground: #2d353b;
--accent: #343f44;
--accent-foreground: #d3c6aa;

/* Same four meanings, lifted for the dark canvas. */
--success: oklch(0.78 0.16 155);
--warning: oklch(0.8 0.14 75);
--danger: oklch(0.71 0.18 25);
--info: oklch(0.75 0.13 240);
/* Same four meanings on the Everforest Dark accents. */
--success: #a7c080;
--warning: #dbbc7f;
--danger: #e67e80;
--info: #7fbbb3;

/* The ramp inverted for a dark canvas: the shipped one starts at near-black, which would sink
the leading strands into the background. Same order (leading strand lightest against dark),
Expand Down Expand Up @@ -172,7 +183,7 @@ body {
align-items: center;
border-radius: 0.3rem;
padding: 0 0.3rem;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-family: var(--font-mono);
font-size: 0.8em;
line-height: 1.4;
white-space: nowrap;
Expand Down
2 changes: 1 addition & 1 deletion packages/framework-dashboard/lib/use-notifications.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('useInterventionNotifications (#627)', () => {
expect(ctor).not.toHaveBeenCalled()
rerender({ items: [item(1, 'u1'), item(2, 'u2')] }) // a genuinely new PR -> notify once
expect(ctor).toHaveBeenCalledTimes(1)
expect(ctor.mock.calls[0]![0]).toContain('Needs you')
expect(ctor.mock.calls[0]![0]).toContain('Human Queue')
})

test('fires for a paused run that appears later, showing its question (#636)', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/framework-dashboard/lib/use-notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ function useNewItemNotifications<T>(items: T[], enabled: boolean, spec: Notifica
const INTERVENTIONS: NotificationSpec<Intervention> = {
pickNew: pickNewInterventions,
keyOf: interventionKey,
title: (first, count) => (count === 1 ? `Needs you · ${first.projectName}` : `${count} items need you`),
title: (first, count) => (count === 1 ? `Human Queue · ${first.projectName}` : `${count} items in your Human Queue`),
// A PR by number, a paused run by its question (#636), a finished run by what sits unpushed (#860).
label: item => {
if (item.kind === 'awaiting') return item.title
Expand Down
15 changes: 15 additions & 0 deletions packages/framework-dashboard/pages/+Head.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Load the landing page's typeface (#1118) so the dashboard reads as the same product as
// the-framework.ai: IBM Plex Sans for UI, IBM Plex Mono for code. Same Google Fonts links as
// packages/the-framework.ai/pages/+Head.tsx. Emitted into the prerendered shell's <head>.
export default function Head() {
return (
<>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
<link
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap"
rel="stylesheet"
/>
</>
)
}
40 changes: 11 additions & 29 deletions packages/framework-dashboard/pages/index/+Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { useDocumentTitle } from '../../lib/document-title.js'
import { useWorking } from '../../lib/use-working.js'
import { useFavicon } from '../../lib/favicon.js'
import { useDaemonHealth } from '../../lib/use-daemon-health.js'
import { TriangleAlert, Settings, Plus } from 'lucide-react'
import { TriangleAlert, Settings } from 'lucide-react'

/** Stable, so `files` keeps one identity while no project is selected. */
const EMPTY_FILES: string[] = []
Expand Down Expand Up @@ -187,14 +187,6 @@ export default function Page() {
go({ projectId: id, runId: null })
}

// "New session" (#772): back to the selected project's launcher — the Live home row — with a
// fresh Context, so the navbar button starts a run the same way the rail's Live row does. The
// de-facto standard button rather than a second textarea in the nav.
const newSession = () => {
selectRun(null)
resetContext()
}

// The Overview dashboard (#471): no project selected.
const showDashboard = () => {
setAdopting(false)
Expand Down Expand Up @@ -318,30 +310,20 @@ export default function Page() {
<header className="flex items-center gap-3 border-b border-border px-4 py-3">
<BrandLink working={working} onNavigate={showDashboard} />
{/* The project selection lives in the nav now (#772), not in a rail of its own: always
shown, on every page including the Overview. */}
<ProjectPicker
selectedId={projectId}
onSelect={selectProject}
onDashboard={showDashboard}
interventionCount={interventions.length}
/>
shown, on every page including the Overview. Nudged off the brand mark so the two do
not read as one unit. */}
<div className="ms-10">
<ProjectPicker
selectedId={projectId}
onSelect={selectProject}
onDashboard={showDashboard}
interventionCount={interventions.length}
/>
</div>
<div className="min-w-0 flex-1" />
<div className="flex shrink-0 items-center gap-1">
{/* Which daemon this dashboard is talking to (#1052) — obvious once you can hop devices. */}
<ConnectionIndicator />
{/* Below sm the label folds to the icon alone so the nav fits a narrow viewport (#980);
the aria-label keeps it named for a screen reader either way. */}
<Button
variant="outline"
size="sm"
onClick={newSession}
disabled={!projectId}
title={projectId ? 'New session' : 'Select a project first'}
aria-label="New session"
>
<Plus className="h-4 w-4 shrink-0 sm:hidden" aria-hidden />
<span className="hidden sm:inline">New session</span>
</Button>
<ThemeToggle />
<NotificationsMenu />
<Button variant="ghost" size="sm" onClick={showSettings} title="Settings" aria-label="Settings">
Expand Down
Loading