Skip to content
Open
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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,23 @@ The project uses Semantic Versioning. Each release contains the same three chang

- None.

## [0.1.36] - 2026-09-28

### New Features

- Deploy free documentation hosting for public GitHub repositories with `cortex deploy`, publishing your existing Markdown to a `*.cortexdocs.dev` subdomain.
- Serve a live MCP server directly from a public repository with `cortex mcp-serve https://github.com/OWNER/REPO`, without a local checkout.
- Documentation sites are now published through Cloudflare Static Assets for direct, low-latency delivery of hosted docs.

### Bug Fixes

- Fixed in-page links to Markdown headings so they scroll to the correct section in the hosted documentation site, even before content has fully loaded.
- Removed a conflicting cache-control header on static hosted assets that could cause stale or inconsistent caching behavior.

### Improvements

- None.

## [0.1.35] - 2026-09-22

### New Features
Expand Down
23 changes: 23 additions & 0 deletions e2e/docs-ui-static.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,29 @@ test.describe('Cloudflare Static Assets export', () => {
await expect(page.locator('.cortex-cookie-settings-button')).toHaveCount(0);
});

for (const appearance of ['light', 'dark']) {
for (const [color, background, foreground] of [
['#c2410c', 'rgb(194, 65, 12)', 'rgb(255, 255, 255)'],
['#2563eb', 'rgb(37, 99, 235)', 'rgb(255, 255, 255)'],
['#7c3aed', 'rgb(124, 58, 237)', 'rgb(255, 255, 255)'],
['#ffffff', 'rgb(255, 255, 255)', 'rgb(10, 10, 10)'],
]) {
test(`adapts the homepage button text to ${color} in ${appearance} mode`, async ({
page,
}) => {
await page.route('**/api/config*', async (route) => {
const response = await route.fetch();
const config = await response.json();
await route.fulfill({ json: { ...config, primaryColor: color } });
});
await page.goto(`/?appearance=${appearance}`);
const button = page.getByRole('link', { name: 'Getting Started', exact: true });
await expect(button).toHaveCSS('background-color', background);
await expect(button).toHaveCSS('color', foreground);
});
}
}

test('supports client navigation between generated documentation pages', async ({ page }) => {
await page.goto('/docs/quickstart');
await expect(page).toHaveTitle('Petstore Docs');
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cortex-docs/cli",
"version": "0.1.35",
"version": "0.1.36",
"description": "Cortex Docs CLI for SDKs, API documentation, and MCP servers",
"license": "MIT",
"repository": {
Expand Down
4 changes: 2 additions & 2 deletions packages/docs-site/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cortex-docs/docs-site",
"version": "0.1.35",
"version": "0.1.36",
"private": true,
"description": "Product documentation for Cortex Docs",
"scripts": {
Expand All @@ -9,6 +9,6 @@
"clean": "rm -rf .cortex"
},
"dependencies": {
"@cortex-docs/cli": "0.1.35"
"@cortex-docs/cli": "0.1.36"
}
}
28 changes: 28 additions & 0 deletions packages/docs-ui/__tests__/primary-theme.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest';
import { primaryPalette, primaryThemeCss } from '../lib/primary-theme';

describe('primary button contrast', () => {
it.each(['#c2410c', '#2563eb', '#7c3aed', '#000000', '#0000ff'])(
'uses white text on %s',
(color) => {
expect(primaryPalette(color).foreground).toBe('#ffffff');
},
);

it.each(['#ffffff', '#ffff00', '#00ff00', '#f97316', '#ff0000'])(
'uses dark text on a light fill (%s)',
(color) => {
expect(primaryPalette(color).foreground).toBe('#0a0a0a');
},
);

it('keeps the chosen button fill instead of brightening it in dark mode', () => {
expect(primaryPalette('#2563eb').background).toBe('rgb(37,99,235)');
expect(primaryThemeCss('#2563eb').match(/--color-primary:rgb\(37,99,235\)/g)).toHaveLength(3);
});

it('ignores invalid colors before producing CSS', () => {
expect(primaryThemeCss('red')).toBe('');
expect(primaryThemeCss('#fff; color:red')).toBe('');
});
});
36 changes: 2 additions & 34 deletions packages/docs-ui/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { SearchProvider } from '@/components/docs/search-provider';
import { GoogleAnalytics } from '@/components/docs/google-analytics';
import { sanitizeSvg } from '@/lib/sanitize-svg';
import { primaryThemeCss } from '@/lib/primary-theme';

interface LoadedSiteConfig extends SiteConfig {
customHeadHtml?: string;
Expand All @@ -27,33 +28,6 @@ function emptySiteConfig(): LoadedSiteConfig {
};
}

function adj(r: number, g: number, b: number, mul: number) {
return `rgb(${Math.min(255, Math.round(r * mul))},${Math.min(255, Math.round(g * mul))},${Math.min(255, Math.round(b * mul))})`;
}

function parsePrimary(hex: string) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
const lightBgMul = lum < 0.15 ? 1.8 : lum < 0.3 ? 1.3 : 1;
const darkBgMul = lum < 0.15 ? 4 : lum < 0.3 ? 2.2 : lum < 0.4 ? 1.4 : 1;
const textMul =
lum > 0.9 ? 0.45 : lum > 0.7 ? 0.88 : lum > 0.5 ? 0.92 : lum < 0.15 ? 3.5 : lum < 0.3 ? 2 : 1;
const lightColor = adj(r, g, b, lightBgMul);
const darkColor = adj(r, g, b, darkBgMul);
const lightText = adj(r, g, b, textMul);
const darkText = adj(r, g, b, darkBgMul);
const lightLum = lum * lightBgMul;
const darkLum = Math.min(1, lum * darkBgMul);
const lightFg = lightLum > 0.5 ? '#0a0a0a' : '#fafafa';
const darkFg = darkLum > 0.5 ? '#0a0a0a' : '#fafafa';
const targetTintLum = 0.45;
const tintMul = lum > 0 ? Math.max(1, targetTintLum / lum) : 6;
const cardTint = adj(r, g, b, Math.min(tintMul, 10));
return { lightColor, darkColor, lightFg, darkFg, lightText, darkText, cardTint };
}

function createThemeInitializationScript(defaultTheme: 'light' | 'dark' | 'system'): string {
return `(()=>{try{const e=document.documentElement,p=new URLSearchParams(location.search).get("appearance");let t=p==="light"||p==="dark"?p:"";if(!t){try{const s=localStorage.getItem("theme");if(s==="light"||s==="dark"||s==="system")t=s}catch{}}if(!t)t="${defaultTheme}";const r=t==="system"?(matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"):t;e.classList.remove("light","dark");e.classList.add(r);e.style.colorScheme=r}catch{}})();`;
}
Expand Down Expand Up @@ -196,13 +170,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
const { customHeadHtml, ...siteConfig } = readSiteConfig();
const defaultTheme = siteConfig.theme ?? 'system';

const pc = siteConfig.primaryColor;
const primaryCss = pc
? (() => {
const p = parsePrimary(pc);
return `html{--color-primary:${p.lightColor}!important;--color-primary-foreground:${p.lightFg}!important;--primary-text:${p.lightText};--primary-card-tint:${p.cardTint}}html.dark{--color-primary:${p.darkColor}!important;--color-primary-foreground:${p.darkFg}!important;--primary-text:${p.darkText};--primary-card-tint:${p.cardTint}}@media(prefers-color-scheme:dark){html:not(.light){--color-primary:${p.darkColor}!important;--color-primary-foreground:${p.darkFg}!important;--primary-text:${p.darkText};--primary-card-tint:${p.cardTint}}}`;
})()
: '';
const primaryCss = primaryThemeCss(siteConfig.primaryColor ?? '');

return (
<html
Expand Down
34 changes: 2 additions & 32 deletions packages/docs-ui/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type HomeConfig,
} from '@/components/docs/site-config-provider';
import { useProjectWatch } from '@/lib/use-project-watch';
import { primaryThemeCss } from '@/lib/primary-theme';

const COLS = 45;
const ROWS = 16;
Expand Down Expand Up @@ -111,44 +112,13 @@ export default function Home() {
if (!data) return;
if (data.home) setHome(data.home);
if (data.primaryColor && /^#[0-9a-fA-F]{6}$/.test(data.primaryColor)) {
const pc = data.primaryColor;
const r = parseInt(pc.slice(1, 3), 16);
const g = parseInt(pc.slice(3, 5), 16);
const b = parseInt(pc.slice(5, 7), 16);
const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
const a = (m: number) =>
`rgb(${Math.min(255, Math.round(r * m))},${Math.min(255, Math.round(g * m))},${Math.min(255, Math.round(b * m))})`;
const lbm = lum < 0.15 ? 1.8 : lum < 0.3 ? 1.3 : 1;
const dbm = lum < 0.15 ? 4 : lum < 0.3 ? 2.2 : lum < 0.4 ? 1.4 : 1;
const tm =
lum > 0.9
? 0.45
: lum > 0.7
? 0.88
: lum > 0.5
? 0.92
: lum < 0.15
? 3.5
: lum < 0.3
? 2
: 1;
const tintMul = lum > 0 ? Math.max(1, 0.45 / lum) : 6;
const lightColor = a(lbm),
darkColor = a(dbm),
lightText = a(tm),
darkText = a(dbm),
cardTint = a(Math.min(tintMul, 10));
const lightLum = lum * lbm,
darkLum = Math.min(1, lum * dbm);
const lightFg = lightLum > 0.5 ? '#0a0a0a' : '#fafafa';
const darkFg = darkLum > 0.5 ? '#0a0a0a' : '#fafafa';
let s = document.querySelector('style[data-primary]') as HTMLStyleElement;
if (!s) {
s = document.createElement('style');
s.setAttribute('data-primary', '');
document.body.prepend(s);
}
s.textContent = `html{--color-primary:${lightColor}!important;--color-primary-foreground:${lightFg}!important;--primary-text:${lightText};--primary-card-tint:${cardTint}}html.dark{--color-primary:${darkColor}!important;--color-primary-foreground:${darkFg}!important;--primary-text:${darkText};--primary-card-tint:${cardTint}}@media(prefers-color-scheme:dark){html:not(.light){--color-primary:${darkColor}!important;--color-primary-foreground:${darkFg}!important;--primary-text:${darkText};--primary-card-tint:${cardTint}}}`;
s.textContent = primaryThemeCss(data.primaryColor);
}
})
.catch(() => {});
Expand Down
2 changes: 1 addition & 1 deletion packages/docs-ui/components/docs/docs-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ export function DocsHeader() {
)}
{!hasCustomLogo && (
<span className="truncate text-lg font-semibold tracking-tight">
{project || siteTitle || 'Cortex'}
{siteTitle || project || 'Cortex'}
</span>
)}
</Link>
Expand Down
54 changes: 54 additions & 0 deletions packages/docs-ui/lib/primary-theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
type Rgb = [number, number, number];

function luminance(color: Rgb): number {
const linear = color.map((channel) => {
const value = channel / 255;
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
});
return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];
}

export function primaryPalette(hex: string) {
const color: Rgb = [
parseInt(hex.slice(1, 3), 16),
parseInt(hex.slice(3, 5), 16),
parseInt(hex.slice(5, 7), 16),
];
const brightness = (0.299 * color[0] + 0.587 * color[1] + 0.114 * color[2]) / 255;
const adjusted = (multiplier: number) =>
`rgb(${color.map((channel) => Math.min(255, Math.round(channel * multiplier))).join(',')})`;
const backgroundLuminance = luminance(color);
const whiteContrast = 1.05 / (backgroundLuminance + 0.05);
const darkContrast = (backgroundLuminance + 0.05) / (luminance([10, 10, 10]) + 0.05);
const foreground = whiteContrast >= darkContrast ? '#ffffff' : '#0a0a0a';
const textMultiplier =
brightness > 0.9
? 0.45
: brightness > 0.7
? 0.88
: brightness > 0.5
? 0.92
: brightness < 0.15
? 3.5
: brightness < 0.3
? 2
: 1;
const darkTextMultiplier =
brightness < 0.15 ? 4 : brightness < 0.3 ? 2.2 : brightness < 0.4 ? 1.4 : 1;
const tintMultiplier = brightness > 0 ? Math.max(1, 0.45 / brightness) : 6;
return {
// Keep the configured fill in both themes. Brighten standalone accents separately.
background: adjusted(1),
foreground,
lightText: adjusted(textMultiplier),
darkText: adjusted(darkTextMultiplier),
cardTint: adjusted(Math.min(tintMultiplier, 10)),
};
}

export function primaryThemeCss(hex: string): string {
if (!/^#[0-9a-fA-F]{6}$/.test(hex)) return '';
const p = primaryPalette(hex);
const shared = `--color-primary:${p.background}!important;--color-primary-foreground:${p.foreground}!important;--primary-card-tint:${p.cardTint}`;
return `html{${shared};--primary-text:${p.lightText}}html.dark{${shared};--primary-text:${p.darkText}}@media(prefers-color-scheme:dark){html:not(.light){${shared};--primary-text:${p.darkText}}}`;
}
Loading