From 688dfc246e920b691178d2fa6f9bea3b1e40db0a Mon Sep 17 00:00:00 2001 From: Suleiman Shahbari Date: Fri, 24 Jul 2026 20:16:45 +0300 Subject: [PATCH 1/3] feat(dashboard): re-skin onto the landing page theme + Human/AI Queue labels Adopt the-framework.ai's visual identity so the dashboard and landing page read as one product (#1118), and align the queue vocabulary (#1116). Theme (#1118): - Remap the :root/.dark tokens onto Everforest. Dark uses the landing page's exact values (bg #2d353b, panels #232a2e, text #d3c6aa, green accent #a7c080, borders #475258); light uses Everforest Light so the theme toggle still works rather than dropping to dark-only. Components already read --color-*, so this is a token swap plus the status colours retuned per canvas. - Load IBM Plex Sans + Mono via a +Head.tsx mirroring the landing page, and point --font-sans / --font-mono at them. - Tighten --radius to ~2px to match the squarer page. Labels (#1116): - Rename the two named-queue surfaces to the landing page's vocabulary: the "Needs you" review inbox becomes "Human Queue" (card, notification and settings toggles, picker badge tooltip, browser-notification title), and the cross-project "Backlog" card becomes "AI Queue". Ticket surfaces are left alone: onboarding copy establishes tickets/ as a distinct concept that feeds the queue, not the queue itself. Closes #1118 Closes #1116 --- .changeset/dashboard-everforest-reskin.md | 5 ++ .../components/DashboardPage.tsx | 4 +- .../components/NotificationsMenu.test.tsx | 8 +- .../components/NotificationsMenu.tsx | 2 +- .../components/ProjectPicker.tsx | 2 +- .../components/SettingsPage.tsx | 2 +- .../framework-dashboard/layouts/tailwind.css | 89 +++++++++++-------- .../lib/use-notifications.test.ts | 2 +- .../lib/use-notifications.ts | 2 +- packages/framework-dashboard/pages/+Head.tsx | 15 ++++ 10 files changed, 81 insertions(+), 50 deletions(-) create mode 100644 .changeset/dashboard-everforest-reskin.md create mode 100644 packages/framework-dashboard/pages/+Head.tsx diff --git a/.changeset/dashboard-everforest-reskin.md b/.changeset/dashboard-everforest-reskin.md new file mode 100644 index 000000000..ded83b98e --- /dev/null +++ b/.changeset/dashboard-everforest-reskin.md @@ -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. diff --git a/packages/framework-dashboard/components/DashboardPage.tsx b/packages/framework-dashboard/components/DashboardPage.tsx index 0d3dcdb99..e5a5cb43d 100644 --- a/packages/framework-dashboard/components/DashboardPage.tsx +++ b/packages/framework-dashboard/components/DashboardPage.tsx @@ -96,7 +96,7 @@ export function DashboardPage({ - Backlog + AI Queue @@ -133,7 +133,7 @@ function NeedsYou({ items, onSelectProject }: { items: Intervention[]; onSelectP - Needs you + Human Queue {items.length > 0 && ( {items.length} diff --git a/packages/framework-dashboard/components/NotificationsMenu.test.tsx b/packages/framework-dashboard/components/NotificationsMenu.test.tsx index 3f5f18264..86de85a3d 100644 --- a/packages/framework-dashboard/components/NotificationsMenu.test.tsx +++ b/packages/framework-dashboard/components/NotificationsMenu.test.tsx @@ -37,19 +37,19 @@ 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() 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() open() fireEvent.click(screen.getByText('Discord')) @@ -57,7 +57,7 @@ describe('NotificationsMenu (#676)', () => { 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 }) }) diff --git a/packages/framework-dashboard/components/NotificationsMenu.tsx b/packages/framework-dashboard/components/NotificationsMenu.tsx index 1a3d97fb1..6e922d054 100644 --- a/packages/framework-dashboard/components/NotificationsMenu.tsx +++ b/packages/framework-dashboard/components/NotificationsMenu.tsx @@ -110,7 +110,7 @@ export function NotificationsMenu() { title="A session awaiting your answer, or a PR ready to review" className="items-start" > - + 0 && ( {interventionCount} diff --git a/packages/framework-dashboard/components/SettingsPage.tsx b/packages/framework-dashboard/components/SettingsPage.tsx index 4cfe27959..2e622590f 100644 --- a/packages/framework-dashboard/components/SettingsPage.tsx +++ b/packages/framework-dashboard/components/SettingsPage.tsx @@ -158,7 +158,7 @@ export function SettingsPage({ onSelectProject }: { onSelectProject?: ((id: stri } /> updatePreferences({ notifyHumanIntervention: next })} diff --git a/packages/framework-dashboard/layouts/tailwind.css b/packages/framework-dashboard/layouts/tailwind.css index c7fe43612..510c3363e 100644 --- a/packages/framework-dashboard/layouts/tailwind.css +++ b/packages/framework-dashboard/layouts/tailwind.css @@ -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. */ @@ -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), @@ -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; diff --git a/packages/framework-dashboard/lib/use-notifications.test.ts b/packages/framework-dashboard/lib/use-notifications.test.ts index c38f3e77a..42b3a0312 100644 --- a/packages/framework-dashboard/lib/use-notifications.test.ts +++ b/packages/framework-dashboard/lib/use-notifications.test.ts @@ -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)', () => { diff --git a/packages/framework-dashboard/lib/use-notifications.ts b/packages/framework-dashboard/lib/use-notifications.ts index ace146b37..e4d925a16 100644 --- a/packages/framework-dashboard/lib/use-notifications.ts +++ b/packages/framework-dashboard/lib/use-notifications.ts @@ -65,7 +65,7 @@ function useNewItemNotifications(items: T[], enabled: boolean, spec: Notifica const INTERVENTIONS: NotificationSpec = { 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 diff --git a/packages/framework-dashboard/pages/+Head.tsx b/packages/framework-dashboard/pages/+Head.tsx new file mode 100644 index 000000000..f0906b883 --- /dev/null +++ b/packages/framework-dashboard/pages/+Head.tsx @@ -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 . +export default function Head() { + return ( + <> + + + + + ) +} From 7586cdebabba54948411cc2f3c01bb6c190a1ebd Mon Sep 17 00:00:00 2001 From: Suleiman Shahbari Date: Fri, 24 Jul 2026 20:28:04 +0300 Subject: [PATCH 2/3] fix(dashboard): use the themed ScrollArea for the sidebar Recents list The sidebar rail scrolled on SidebarContent's native `overflow-auto`, so it showed the raw OS scrollbar instead of the themed bar the Overview uses. Suppress that instance's overflow and wrap the run list in the shadcn Base UI ScrollArea (#913), matching the Overview: a thin muted-foreground thumb that only appears while the list overflows. --- .../components/RunHistory.tsx | 77 ++++++++++--------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/packages/framework-dashboard/components/RunHistory.tsx b/packages/framework-dashboard/components/RunHistory.tsx index c038074b4..df7e37d41 100644 --- a/packages/framework-dashboard/components/RunHistory.tsx +++ b/packages/framework-dashboard/components/RunHistory.tsx @@ -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 @@ -142,43 +143,47 @@ export function RunHistory({ onProjectAdded={onProjectAdded} /> - - - {/* Recents label sits under the launcher, over the run list (not a page-wide header). */} - {hasRecents && ( - Recents - )} - - - {/* A just-started run, before its run.json exists — highlighted while following it. */} - {showOptimistic && ( - - onSelect(null)} /> - - )} - {rows.map(row => ( - - - - ))} - - {!hasRecents && ( -

No sessions yet.

+ {/* 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. */} + + + + {/* Recents label sits under the launcher, over the run list (not a page-wide header). */} + {hasRecents && ( + Recents )} -
-
+ + + {/* A just-started run, before its run.json exists — highlighted while following it. */} + {showOptimistic && ( + + onSelect(null)} /> + + )} + {rows.map(row => ( + + + + ))} + + {!hasRecents && ( +

No sessions yet.

+ )} +
+ +
) From 71979ac0b1d27e97f031058436739fa0c3d9ba9a Mon Sep 17 00:00:00 2001 From: Suleiman Shahbari Date: Fri, 24 Jul 2026 20:39:09 +0300 Subject: [PATCH 3/3] =?UTF-8?q?feat(dashboard):=20navbar=20polish=20?= =?UTF-8?q?=E2=80=94=20space=20the=20project=20picker,=20drop=20the=20New?= =?UTF-8?q?=20session=20button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nudge the project picker off the brand mark (ms-10) so the two do not read as one unit. - Remove the navbar "New session" button (added in #772). The sidebar's "New" launcher already starts a session, so the navbar copy was redundant; drop the now-dead newSession handler and Plus import with it. --- .../framework-dashboard/pages/index/+Page.tsx | 40 +++++-------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/packages/framework-dashboard/pages/index/+Page.tsx b/packages/framework-dashboard/pages/index/+Page.tsx index f12ba491d..8ec4d12bc 100644 --- a/packages/framework-dashboard/pages/index/+Page.tsx +++ b/packages/framework-dashboard/pages/index/+Page.tsx @@ -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[] = [] @@ -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) @@ -318,30 +310,20 @@ export default function Page() {
{/* The project selection lives in the nav now (#772), not in a rail of its own: always - shown, on every page including the Overview. */} - + shown, on every page including the Overview. Nudged off the brand mark so the two do + not read as one unit. */} +
+ +
{/* Which daemon this dashboard is talking to (#1052) — obvious once you can hop devices. */} - {/* 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. */} -