diff --git a/apps/cockpit/src/app/cockpit.css b/apps/cockpit/src/app/cockpit.css index 88e5c95b..5d7d9802 100644 --- a/apps/cockpit/src/app/cockpit.css +++ b/apps/cockpit/src/app/cockpit.css @@ -208,7 +208,6 @@ pre.shiki { /* Shared prose layer — docs + api + code mode content */ .cockpit-prose { max-width: 42rem; - margin-inline: auto; font-size: 0.9rem; line-height: 1.7; color: var(--ds-text-secondary); @@ -235,6 +234,34 @@ pre.shiki { .cockpit-prose table.params th { font-family: var(--font-mono), monospace; font-size: 0.6rem; letter-spacing: 0.06em; text-transform: uppercase; padding-bottom: 0.5rem; border-bottom: 1px solid var(--ds-border); } .cockpit-prose table.params td { padding: 0.5rem 0.75rem 0.5rem 0; border-bottom: 1px solid var(--ds-border); } +/* Sidebar navigation items — bg-only active/hover, no left border */ +.cockpit-nav-item { + display: block; + padding: 5px 14px; + margin: 0 8px; + border-radius: 6px; + font-size: 0.825rem; + color: var(--ds-text-secondary); + text-decoration: none; + transition: background 0.15s ease, color 0.15s ease; +} +.cockpit-nav-item:hover { background: var(--ds-surface-dim); color: var(--ds-text-primary); } +.cockpit-nav-item[aria-current="page"] { background: var(--ds-accent-surface); color: var(--ds-accent); } + +/* Sidebar group caret — matches the file-tree chevron */ +.cockpit-nav-caret { + display: inline-flex; + align-items: center; + justify-content: center; + width: 0.85rem; + height: 0.85rem; + color: var(--ds-text-muted); + flex: none; + transition: transform 150ms ease; +} +.cockpit-nav-caret svg { display: block; } +.cockpit-nav-caret--open { transform: rotate(90deg); } + /* Code-mode file tree */ .cockpit-file-tree { list-style: none; padding: 0; margin: 0; font-size: 12px; line-height: 1.7; } .cockpit-file-tree ul { list-style: none; padding: 0; margin: 0; } @@ -246,7 +273,18 @@ pre.shiki { border-left: 2px solid transparent; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .cockpit-file-tree__folder { color: var(--ds-text-muted); display: flex; align-items: center; } -.cockpit-file-tree__caret { font-size: 9px; color: var(--ds-text-muted); width: 0.65rem; flex-shrink: 0; } +.cockpit-file-tree__caret { + display: inline-flex; + align-items: center; + justify-content: center; + width: 0.85rem; + height: 0.85rem; + color: var(--ds-text-muted); + flex: none; + transition: transform 150ms ease; +} +.cockpit-file-tree__caret svg { display: block; } +.cockpit-file-tree__caret--open { transform: rotate(90deg); } .cockpit-file-tree__label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .cockpit-file-tree__chip { font-family: var(--font-mono), monospace; font-size: 9px; @@ -272,3 +310,39 @@ pre.shiki { .cockpit-tab-trigger:hover .cockpit-tab-trigger__close, .cockpit-tab-trigger[data-state="active"] .cockpit-tab-trigger__close { opacity: 1; } .cockpit-tab-trigger__close:hover { background: var(--ds-accent-surface); color: var(--ds-text-primary); } + +/* Code-mode editor pane — no chrome, no separate background, full-bleed under the tab strip. */ +.cockpit-code-pane { + min-width: 0; +} +.cockpit-code-pane pre.shiki { + background: transparent !important; + margin: 0; + border-radius: 0; + padding: 1rem 1.25rem; + font-size: 0.8125rem; + white-space: pre; + overflow-x: auto; + max-width: 100%; +} + +/* Shiki dual-theme: light mode uses inline `color` (github-light), dark mode swaps to the --shiki-dark CSS variable (tokyo-night). */ +[data-theme="dark"] .shiki, +[data-theme="dark"] .shiki span { color: var(--shiki-dark) !important; } +[data-theme="dark"] .shiki { background-color: var(--shiki-dark-bg) !important; } +[data-theme="dark"] .cockpit-code-pane .shiki { background-color: transparent !important; } +.cockpit-code-pane--plain { + margin: 0; + padding: 1rem 1.25rem; + color: var(--ds-text-secondary); + font-family: var(--font-mono), "JetBrains Mono", monospace; + font-size: 0.8125rem; + line-height: 1.6; + white-space: pre-wrap; + max-width: 100%; +} +.cockpit-code-pane__empty { + padding: 1rem 1.25rem; + color: var(--ds-text-muted); + font-size: 0.875rem; +} diff --git a/apps/cockpit/src/components/branding/logo.spec.tsx b/apps/cockpit/src/components/branding/logo.spec.tsx index 535b87c8..77154c3e 100644 --- a/apps/cockpit/src/components/branding/logo.spec.tsx +++ b/apps/cockpit/src/components/branding/logo.spec.tsx @@ -9,11 +9,6 @@ describe('Logo', () => { expect(html).toContain('Threadplane'); }); - it('renders the cockpit qualifier', () => { - const html = renderToStaticMarkup(); - expect(html).toContain('cockpit'); - }); - it('exposes a stable data-ui selector', () => { const html = renderToStaticMarkup(); expect(html).toContain('data-ui="cockpit-logo"'); diff --git a/apps/cockpit/src/components/branding/logo.tsx b/apps/cockpit/src/components/branding/logo.tsx index 64ff6dd2..fe39f2d0 100644 --- a/apps/cockpit/src/components/branding/logo.tsx +++ b/apps/cockpit/src/components/branding/logo.tsx @@ -13,9 +13,6 @@ export function Logo({ className, style, ...rest }: HTMLAttributes Threadplane - - cockpit - ); } diff --git a/apps/cockpit/src/components/cockpit-shell.tsx b/apps/cockpit/src/components/cockpit-shell.tsx index 2c6956dc..85add0bc 100644 --- a/apps/cockpit/src/components/cockpit-shell.tsx +++ b/apps/cockpit/src/components/cockpit-shell.tsx @@ -79,11 +79,11 @@ export function CockpitShell({ onClose={() => setIsSidebarOpen(false)} /> -
+
-
+
-

{contextLabel}

+

{contextLabel}

-
+
({ track: vi.fn() })); - -import { track } from '../../lib/analytics/client'; import { CodeMode } from './code-mode'; describe('CodeMode', () => { @@ -18,7 +15,6 @@ describe('CodeMode', () => { root?.unmount(); }); container?.remove(); - vi.clearAllMocks(); }); it('renders Shiki-highlighted HTML for the active file', () => { @@ -47,8 +43,9 @@ describe('CodeMode', () => { }); expect(container.querySelector('.shiki')).not.toBeNull(); - const fileLabel = container.querySelector('.doc-codeblock__file'); - expect(fileLabel?.textContent).toBe('page.tsx'); + // The filename now lives in the tab strip (no chrome around the code body). + const activeTab = container.querySelector('[role="tab"][data-state="active"]'); + expect((activeTab?.textContent ?? '').replace(/×/g, '').trim()).toBe('page.tsx'); expect(container.textContent).toContain('export default function Page() {}'); const tabs = Array.from(container.querySelectorAll('[role="tab"]')); @@ -284,41 +281,4 @@ describe('CodeMode', () => { expect(tabs).toEqual(['b.ts']); }); - it('fires cockpit:code_copied when the Copy button is clicked', () => { - container = document.createElement('div'); - document.body.appendChild(container); - root = createRoot(container); - - Object.assign(navigator, { - clipboard: { writeText: vi.fn(() => Promise.resolve()) }, - }); - - act(() => { - root!.render( - const x = 1;' }} - promptFiles={{}} - capability="streaming" - />, - ); - }); - - const copyBtn = container.querySelector( - 'button[aria-label^="Copy"]', - ) as HTMLButtonElement | null; - expect(copyBtn).not.toBeNull(); - - act(() => { - copyBtn!.click(); - }); - - expect(track).toHaveBeenCalledWith('cockpit:code_copied', { - capability: 'streaming', - surface: 'code_mode', - file_path: 'src/app.tsx', - }); - }); }); diff --git a/apps/cockpit/src/components/code-mode/code-mode.tsx b/apps/cockpit/src/components/code-mode/code-mode.tsx index 5e013fc6..9f532edc 100644 --- a/apps/cockpit/src/components/code-mode/code-mode.tsx +++ b/apps/cockpit/src/components/code-mode/code-mode.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { track } from '../../lib/analytics/client'; import { FileTree } from './file-tree'; interface CodeModeProps { @@ -19,38 +18,14 @@ const getTabLabel = (path: string): string => path.split('/').pop() ?? path; function CodeFileContent({ path, content, - capability, }: { path: string; content: string | undefined; - capability?: string; }) { if (!content) { - return

No source available for {getTabLabel(path)}

; + return

No source available for {getTabLabel(path)}

; } - - const label = getTabLabel(path); - const dotIdx = label.lastIndexOf('.'); - const ext = dotIdx > 0 ? label.slice(dotIdx + 1).toUpperCase() : ''; - - return ( -
-
- {label} - {ext ? {ext} : null} - -
-
-
- ); + return
; } export function CodeMode({ entryTitle, codeAssetPaths, backendAssetPaths, codeFiles, promptFiles, capability }: CodeModeProps) { @@ -206,24 +181,20 @@ export function CodeMode({ entryTitle, codeAssetPaths, backendAssetPaths, codeFi {openPaths.filter((p) => !isPromptPath(p)).map((path) => ( - -
- -
+ + ))} {openPaths.filter(isPromptPath).map((path) => { const content = promptFiles[path]; return ( - -
- {content ? ( -
{content}
- ) : ( -

No content for {getTabLabel(path)}

- )} -
+ + {content ? ( +
{content}
+ ) : ( +

No content for {getTabLabel(path)}

+ )}
); })} diff --git a/apps/cockpit/src/components/code-mode/file-tree.tsx b/apps/cockpit/src/components/code-mode/file-tree.tsx index 1b17fe2c..17f15293 100644 --- a/apps/cockpit/src/components/code-mode/file-tree.tsx +++ b/apps/cockpit/src/components/code-mode/file-tree.tsx @@ -89,7 +89,14 @@ function Node({ node, depth, folderId, activePath, collapsedFolders, onToggleFol className="cockpit-file-tree__folder" style={{ paddingLeft: `${0.5 + depth * 0.75}rem` }} > - + {folder.label} {!isCollapsed ? ( diff --git a/apps/cockpit/src/components/sidebar/navigation-groups.tsx b/apps/cockpit/src/components/sidebar/navigation-groups.tsx index a682d0ff..dd132c3e 100644 --- a/apps/cockpit/src/components/sidebar/navigation-groups.tsx +++ b/apps/cockpit/src/components/sidebar/navigation-groups.tsx @@ -50,13 +50,13 @@ function ProductGroup({ }}> {label} - - ▾ + @@ -85,18 +85,7 @@ function ProductGroup({ }); }} aria-current={isActive ? 'page' : undefined} - className={isActive ? 'border-l-2 border-[var(--ds-accent)]' : 'border-l-2 border-transparent'} - style={{ - display: 'block', - padding: '5px 16px 5px 14px', - margin: '0 8px', - borderRadius: 6, - fontSize: '0.825rem', - color: isActive ? 'var(--ds-accent)' : 'var(--ds-text-secondary)', - background: isActive ? 'var(--ds-accent-surface)' : 'transparent', - textDecoration: 'none', - transition: 'all 0.15s', - }} + className="cockpit-nav-item" > {stripProductPrefix(entry.title)} diff --git a/apps/cockpit/src/lib/content-bundle.ts b/apps/cockpit/src/lib/content-bundle.ts index d9fbee10..a9ecc89f 100644 --- a/apps/cockpit/src/lib/content-bundle.ts +++ b/apps/cockpit/src/lib/content-bundle.ts @@ -101,7 +101,7 @@ async function highlightCode( try { return await codeToHtml(source, { lang: detectLang(filePath), - theme: 'tokyo-night', + themes: { light: 'github-light', dark: 'tokyo-night' }, }); } catch { return `
${escapeHtml(source)}
`; diff --git a/apps/cockpit/src/lib/render-markdown.ts b/apps/cockpit/src/lib/render-markdown.ts index 0ac11cfe..e9718d2f 100644 --- a/apps/cockpit/src/lib/render-markdown.ts +++ b/apps/cockpit/src/lib/render-markdown.ts @@ -112,7 +112,7 @@ async function parseStepContent( const codeToHighlight = filename ? cleanedCode : block.code; let highlighted: string; try { - highlighted = await codeToHtml(codeToHighlight, { lang: block.lang, theme: 'tokyo-night' }); + highlighted = await codeToHtml(codeToHighlight, { lang: block.lang, themes: { light: 'github-light', dark: 'tokyo-night' } }); } catch { const escaped = codeToHighlight.replace(//g, '>'); highlighted = `
${escaped}
`; @@ -169,7 +169,7 @@ export async function renderMarkdown(source: string): Promise const codeToHighlight = filename ? cleanedCode : block.code; let highlighted: string; try { - highlighted = await codeToHtml(codeToHighlight, { lang: block.lang, theme: 'tokyo-night' }); + highlighted = await codeToHtml(codeToHighlight, { lang: block.lang, themes: { light: 'github-light', dark: 'tokyo-night' } }); } catch { const escaped = codeToHighlight.replace(//g, '>'); highlighted = `
${escaped}
`;