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
78 changes: 76 additions & 2 deletions apps/cockpit/src/app/cockpit.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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; }
Expand All @@ -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;
Expand All @@ -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;
}
5 changes: 0 additions & 5 deletions apps/cockpit/src/components/branding/logo.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,6 @@ describe('Logo', () => {
expect(html).toContain('Threadplane');
});

it('renders the cockpit qualifier', () => {
const html = renderToStaticMarkup(<Logo />);
expect(html).toContain('cockpit');
});

it('exposes a stable data-ui selector', () => {
const html = renderToStaticMarkup(<Logo />);
expect(html).toContain('data-ui="cockpit-logo"');
Expand Down
3 changes: 0 additions & 3 deletions apps/cockpit/src/components/branding/logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ export function Logo({ className, style, ...rest }: HTMLAttributes<HTMLSpanEleme
<span style={{ fontFamily: 'var(--font-garamond), "EB Garamond", Georgia, serif', fontSize: 16, fontWeight: 600, color: 'var(--ds-text-primary)' }}>
Threadplane
</span>
<span style={{ fontFamily: 'var(--font-mono), "JetBrains Mono", monospace', fontSize: 12, color: 'var(--ds-text-muted)' }}>
cockpit
</span>
</span>
);
}
10 changes: 5 additions & 5 deletions apps/cockpit/src/components/cockpit-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ export function CockpitShell({
onClose={() => setIsSidebarOpen(false)}
/>

<section className="grid grid-rows-[auto_1fr] overflow-hidden bg-[var(--ds-surface)]">
<section className="grid grid-rows-[auto_1fr] grid-cols-[minmax(0,1fr)] overflow-hidden bg-[var(--ds-surface)]">
<header
className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between px-4 py-3 border-b border-[var(--ds-border)]"
className="flex items-center justify-between gap-3 px-4 py-3 border-b border-[var(--ds-border)]"
>
<div className="flex items-center gap-3">
<div className="flex items-center gap-3 min-w-0">
<button
className="md:hidden"
onClick={() => setIsSidebarOpen(true)}
Expand All @@ -93,9 +93,9 @@ export function CockpitShell({
>
<MenuIcon />
</button>
<p className="hidden md:block text-[var(--ds-text-muted)] font-mono text-xs">{contextLabel}</p>
<p className="hidden md:block text-[var(--ds-text-muted)] font-mono text-xs truncate">{contextLabel}</p>
</div>
<div className="overflow-x-auto -mx-4 px-4 md:mx-0 md:px-0">
<div className="shrink-0 overflow-x-auto">
<ModeSwitcher
modes={PRIMARY_MODES}
activeMode={activeMode}
Expand Down
48 changes: 4 additions & 44 deletions apps/cockpit/src/components/code-mode/code-mode.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@
import React from 'react';
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { afterEach, describe, expect, it } from 'vitest';

vi.mock('../../lib/analytics/client', () => ({ track: vi.fn() }));

import { track } from '../../lib/analytics/client';
import { CodeMode } from './code-mode';

describe('CodeMode', () => {
Expand All @@ -18,7 +15,6 @@ describe('CodeMode', () => {
root?.unmount();
});
container?.remove();
vi.clearAllMocks();
});

it('renders Shiki-highlighted HTML for the active file', () => {
Expand Down Expand Up @@ -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"]'));
Expand Down Expand Up @@ -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(
<CodeMode
entryTitle="Test"
codeAssetPaths={['src/app.tsx']}
backendAssetPaths={[]}
codeFiles={{ 'src/app.tsx': '<pre class="shiki"><code>const x = 1;</code></pre>' }}
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',
});
});
});
49 changes: 10 additions & 39 deletions apps/cockpit/src/components/code-mode/code-mode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 <p className="text-sm text-[var(--ds-text-muted)]">No source available for {getTabLabel(path)}</p>;
return <p className="cockpit-code-pane__empty">No source available for {getTabLabel(path)}</p>;
}

const label = getTabLabel(path);
const dotIdx = label.lastIndexOf('.');
const ext = dotIdx > 0 ? label.slice(dotIdx + 1).toUpperCase() : '';

return (
<div className="doc-codeblock">
<div className="doc-codeblock__header">
<span className="doc-codeblock__file">{label}</span>
{ext ? <span className="doc-codeblock__lang">{ext}</span> : null}
<button
className="doc-codeblock__copy"
aria-label={`Copy ${label}`}
onClick={() => {
track('cockpit:code_copied', { capability, surface: 'code_mode', file_path: path });
const el = document.querySelector(`[data-code-path="${CSS.escape(path)}"] pre code`);
if (el) navigator.clipboard.writeText(el.textContent ?? '');
}}
>Copy</button>
</div>
<div data-code-path={path} dangerouslySetInnerHTML={{ __html: content }} />
</div>
);
return <div className="cockpit-code-pane" dangerouslySetInnerHTML={{ __html: content }} />;
}

export function CodeMode({ entryTitle, codeAssetPaths, backendAssetPaths, codeFiles, promptFiles, capability }: CodeModeProps) {
Expand Down Expand Up @@ -206,24 +181,20 @@ export function CodeMode({ entryTitle, codeAssetPaths, backendAssetPaths, codeFi
</TabsList>

{openPaths.filter((p) => !isPromptPath(p)).map((path) => (
<TabsContent key={path} value={path} className="flex-1 overflow-auto py-6 px-4 md:px-8">
<div className="cockpit-prose cockpit-prose--code">
<CodeFileContent path={path} content={codeFiles[path]} capability={capability} />
</div>
<TabsContent key={path} value={path} className="flex-1 overflow-auto">
<CodeFileContent path={path} content={codeFiles[path]} />
</TabsContent>
))}

{openPaths.filter(isPromptPath).map((path) => {
const content = promptFiles[path];
return (
<TabsContent key={path} value={path} className="flex-1 overflow-auto py-6 px-4 md:px-8">
<div className="cockpit-prose cockpit-prose--code">
{content ? (
<pre className="font-mono text-sm whitespace-pre-wrap">{content}</pre>
) : (
<p className="text-sm text-[var(--ds-text-muted)]">No content for {getTabLabel(path)}</p>
)}
</div>
<TabsContent key={path} value={path} className="flex-1 overflow-auto">
{content ? (
<pre className="cockpit-code-pane cockpit-code-pane--plain">{content}</pre>
) : (
<p className="cockpit-code-pane__empty">No content for {getTabLabel(path)}</p>
)}
</TabsContent>
);
})}
Expand Down
9 changes: 8 additions & 1 deletion apps/cockpit/src/components/code-mode/file-tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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` }}
>
<span className="cockpit-file-tree__caret" aria-hidden="true">{isCollapsed ? '▸' : '▾'}</span>
<span
className={`cockpit-file-tree__caret${isCollapsed ? '' : ' cockpit-file-tree__caret--open'}`}
aria-hidden="true"
>
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
<path d="M3.5 2.5 6.5 5 3.5 7.5" />
</svg>
</span>
<span className="cockpit-file-tree__label">{folder.label}</span>
</button>
{!isCollapsed ? (
Expand Down
27 changes: 8 additions & 19 deletions apps/cockpit/src/components/sidebar/navigation-groups.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ function ProductGroup({
}}>
{label}
</span>
<span style={{
color: 'var(--ds-text-muted)',
fontSize: 10,
transition: 'transform 0.2s',
transform: open ? 'rotate(0)' : 'rotate(-90deg)',
}}>
<span
className={`cockpit-nav-caret${open ? ' cockpit-nav-caret--open' : ''}`}
aria-hidden="true"
>
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
<path d="M3.5 2.5 6.5 5 3.5 7.5" />
</svg>
</span>
</button>

Expand Down Expand Up @@ -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)}
</a>
Expand Down
2 changes: 1 addition & 1 deletion apps/cockpit/src/lib/content-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<pre><code>${escapeHtml(source)}</code></pre>`;
Expand Down
4 changes: 2 additions & 2 deletions apps/cockpit/src/lib/render-markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, '&lt;').replace(/>/g, '&gt;');
highlighted = `<pre><code>${escaped}</code></pre>`;
Expand Down Expand Up @@ -169,7 +169,7 @@ export async function renderMarkdown(source: string): Promise<RenderedMarkdown>
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, '&lt;').replace(/>/g, '&gt;');
highlighted = `<pre><code>${escaped}</code></pre>`;
Expand Down
Loading