From 748f249704c844f68f51b94f28f545aa047b6e93 Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Tue, 18 Aug 2026 12:05:04 +0200 Subject: [PATCH 1/2] feat(highlight): theme editor page --- src/components/LibraryLayout.tsx | 115 +++++-- .../ThemeEditorPage.tsx | 313 ++++++++++++++++++ .../highlight-theme-editor/snippets.ts | 311 +++++++++++++++++ .../highlight-theme-editor/tokenGroups.ts | 96 ++++++ src/routeTree.gen.ts | 23 ++ .../highlight.$version.theme-editor.tsx | 17 + src/utils/docsNavTabs.ts | 1 + 7 files changed, 841 insertions(+), 35 deletions(-) create mode 100644 src/components/highlight-theme-editor/ThemeEditorPage.tsx create mode 100644 src/components/highlight-theme-editor/snippets.ts create mode 100644 src/components/highlight-theme-editor/tokenGroups.ts create mode 100644 src/routes/_library/highlight.$version.theme-editor.tsx diff --git a/src/components/LibraryLayout.tsx b/src/components/LibraryLayout.tsx index 09d56c174..58285cb95 100644 --- a/src/components/LibraryLayout.tsx +++ b/src/components/LibraryLayout.tsx @@ -830,6 +830,10 @@ export function LibraryLayout({ d.routeId.startsWith('/_library/charts/catalog'), ) + const isThemeEditor = matches.some((d) => + d.routeId.startsWith('/_library/highlight/$version/theme-editor'), + ) + const isNpmStats = matches.some((d) => d.pathname.includes('/docs/npm-stats')) const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false) @@ -860,30 +864,58 @@ export function LibraryLayout({ const tabbedMenuConfig = React.useMemo(() => { const tabs = getTabbedMenuConfig(menuConfig) - return libraryId === 'charts' - ? tabs.map((tab) => - tab.id === 'examples' - ? { - ...tab, - firstItem: { - label: 'Examples', - to: '/charts/catalog', - tab: 'examples', - }, - } - : tab, - ) - : tabs + if (libraryId === 'charts') { + return tabs.map((tab) => + tab.id === 'examples' + ? { + ...tab, + firstItem: { + label: 'Examples', + to: '/charts/catalog', + tab: 'examples', + }, + } + : tab, + ) + } + + if (libraryId === 'highlight') { + return [ + ...tabs, + { + id: 'theme-editor' as const, + label: 'Theme Editor', + groups: [], + firstItem: { + label: 'Theme Editor', + to: '/highlight/$version/theme-editor', + tab: 'theme-editor' as const, + }, + }, + ] + } + + return tabs }, [libraryId, menuConfig]) const activeTabId = React.useMemo(() => { + if (isThemeEditor) { + return 'theme-editor' as const + } + return getActiveDocsNavTabId({ isExample, menuConfig, pathname: lastMatch.pathname, relativePathname, }) - }, [isExample, lastMatch.pathname, menuConfig, relativePathname]) + }, [ + isExample, + isThemeEditor, + lastMatch.pathname, + menuConfig, + relativePathname, + ]) const visibleMenuConfig = React.useMemo(() => { return ( @@ -1187,23 +1219,29 @@ export function LibraryLayout({ return null } - const linkOptions = getLibraryTabLinkOptions({ - libraryId, - version, - to: target.to, - }) + const isCustomToolRoute = + target.to === '/charts/catalog' || + target.to === '/highlight/$version/theme-editor' + const linkParams = + !target.to.startsWith('/') || + target.to.includes('/$libraryId') || + target.to.includes('/$version') + ? ({ libraryId, version } as never) + : undefined const isActive = tab.id === activeTabId return (
  • ( + 'highlight-theme-editor-draft', + defaultDraft, + ) + const { notify } = useToast() + + const { lang, presetId, theme } = draft + + const updateTheme = (updates: Partial) => { + setDraft((prev) => ({ ...prev, theme: { ...prev.theme, ...updates } })) + } + + const updateToken = (token: HighlightThemeToken, value: string) => { + setDraft((prev) => ({ + ...prev, + theme: { + ...prev.theme, + tokens: { ...prev.theme.tokens, [token]: value }, + }, + })) + } + + const selectPreset = (preset: ThemePreset) => { + setDraft((prev) => ({ ...prev, presetId: preset.id, theme: preset.theme })) + } + + const previewCss = React.useMemo( + () => + `${createThemeBaseCss()}\n\n${createThemeRule(PREVIEW_SELECTOR, theme)}`, + [theme], + ) + + const codeBlock = React.useMemo( + () => + defaultHighlighter.renderCodeBlockData({ + code: themeEditorSnippets[lang], + lang, + lineNumbers: true, + }), + [lang], + ) + + const copyThemeObject = async () => { + await copyTextToClipboard(buildThemeObjectSnippet(theme)) + notify('Copied theme object to clipboard', { id: 'theme-copy' }) + } + + const copyAgentPrompt = async () => { + await copyTextToClipboard(buildAgentPrompt(theme)) + notify('Copied AI prompt to clipboard', { id: 'theme-copy-prompt' }) + } + + return ( +
    +
    +
    +

    Highlight Theme Editor

    + +

    + Pick a language, tune every token, then copy the result out. +

    +
    + + + + setDraft((prev) => ({ ...prev, lang: option.value })) + } + /> + + +
    + + Base + + updateTheme({ name: event.target.value })} + placeholder="Theme name" + /> + updateTheme({ background: value })} + /> + updateTheme({ foreground: value })} + /> + updateToken('token', value)} + /> +
    + + {tokenGroups.map((group) => ( +
    + + {group.label} + + {group.tokens.map((token) => ( + updateToken(token, value)} + /> + ))} +
    + ))} + +
    + + +
    +
    + +
    + +
    + +
    +
    +
    + ) +} + +function Field({ + children, + label, +}: { + children: React.ReactNode + label: string +}) { + return ( + + ) +} + +function ColorField({ + label, + onChange, + value, +}: { + label: string + onChange: (next: string) => void + value: string +}) { + const [draft, setDraft] = React.useState(value) + + React.useEffect(() => { + setDraft(value) + }, [value]) + + const isValidDraft = HEX_PATTERN.test(draft) + const swatchValue = isValidDraft ? normalizeHex(draft) : normalizeHex(value) + + return ( +
    + { + setDraft(event.target.value) + onChange(event.target.value) + }} + className="h-9 w-9 shrink-0 cursor-pointer rounded-md border border-border-default bg-transparent p-0.5" + /> +
    + {label} + setDraft(event.target.value)} + onBlur={() => { + if (isValidDraft) onChange(normalizeHex(draft)) + else setDraft(value) + }} + className="h-7 px-2 py-0 font-mono text-xs" + spellCheck={false} + /> +
    +
    + ) +} + +function normalizeHex(value: string) { + if (!HEX_PATTERN.test(value)) return '#000000' + if (value.length === 4) { + const [, r, g, b] = value + return `#${r}${r}${g}${g}${b}${b}` + } + return value +} + +function buildThemeObjectSnippet(theme: HighlightTheme) { + const identifier = toThemeIdentifier(theme.name) + const tokenLines = themeTokenClasses + .map((token) => ` '${token}': '${theme.tokens[token]}',`) + .join('\n') + + return `import type { HighlightTheme } from '@tanstack/highlight/theme' + +export const ${identifier} = { + name: '${theme.name}', + type: '${theme.type}', + background: '${theme.background}', + foreground: '${theme.foreground}', + tokens: { +${tokenLines} + }, +} satisfies HighlightTheme +` +} + +function buildAgentPrompt(theme: HighlightTheme) { + return `Add this @tanstack/highlight theme to my project. + +Find where this project configures @tanstack/highlight (look for a call to \`createThemeCss\` from '@tanstack/highlight/theme', usually near wherever the site's global styles are set up), and register the theme below there — as a new light/dark pair, or as an additional entry in a \`themes: [...]\` list, matching however the existing config is structured. Don't rewrite the surrounding setup beyond wiring this one in. + +${buildThemeObjectSnippet(theme)}` +} + +function toThemeIdentifier(name: string) { + const words = name + .trim() + .split(/[^a-zA-Z0-9]+/) + .filter(Boolean) + const camel = words + .map((word, index) => + index === 0 + ? word.toLowerCase() + : word[0].toUpperCase() + word.slice(1).toLowerCase(), + ) + .join('') + return `${camel || 'custom'}Theme` +} diff --git a/src/components/highlight-theme-editor/snippets.ts b/src/components/highlight-theme-editor/snippets.ts new file mode 100644 index 000000000..81a04398a --- /dev/null +++ b/src/components/highlight-theme-editor/snippets.ts @@ -0,0 +1,311 @@ +import type { HighlightLanguage } from '@tanstack/highlight' + +export const languageOptions: Array<{ + value: HighlightLanguage + label: string +}> = [ + { value: 'ts', label: 'TypeScript' }, + { value: 'tsx', label: 'TSX' }, + { value: 'js', label: 'JavaScript' }, + { value: 'jsx', label: 'JSX' }, + { value: 'tsrx', label: 'TSRX (Octane)' }, + { value: 'json', label: 'JSON' }, + { value: 'html', label: 'HTML' }, + { value: 'css', label: 'CSS' }, + { value: 'vue', label: 'Vue' }, + { value: 'svelte', label: 'Svelte' }, + { value: 'markdown', label: 'Markdown' }, + { value: 'mermaid', label: 'Mermaid' }, + { value: 'yaml', label: 'YAML' }, + { value: 'toml', label: 'TOML' }, + { value: 'sql', label: 'SQL' }, + { value: 'python', label: 'Python' }, + { value: 'scheme', label: 'Scheme' }, + { value: 'shell', label: 'Shell' }, + { value: 'dockerfile', label: 'Dockerfile' }, + { value: 'nginx', label: 'Nginx' }, + { value: 'apache', label: 'Apache' }, + { value: 'http', label: 'HTTP' }, + { value: 'env', label: 'Env' }, + { value: 'ejs', label: 'EJS' }, + { value: 'diff', label: 'Diff' }, + { value: 'plaintext', label: 'Plain Text' }, +] + +export const themeEditorSnippets: Record = { + ts: `import { createThemeCss } from '@tanstack/highlight/theme' +import type { HighlightTheme } from '@tanstack/highlight/theme' + +/** Merge overrides onto a complete base theme. */ +export function extendTheme( + base: HighlightTheme, + overrides: Partial, +): HighlightTheme { + const tokens = { ...base.tokens, ...overrides } + const isValid = Object.keys(tokens).length > 0 + + return isValid ? { ...base, tokens } : base +} + +export const css = createThemeCss({ light: extendTheme(base, { keyword: '#ff79c6' }) }) +`, + tsx: `import * as React from 'react' + +type SwatchProps = { + label: string + value: string + onChange: (next: string) => void +} + +export function Swatch({ label, value, onChange }: SwatchProps) { + const [isFocused, setIsFocused] = React.useState(false) + + return ( + + ) +} +`, + js: `export function debounce(fn, delayMs = 250) { + let timeoutId = null + + return function debounced(...args) { + if (timeoutId !== null) clearTimeout(timeoutId) + console.log('scheduling', fn.name) + timeoutId = setTimeout(() => fn.apply(this, args), delayMs) + } +} + +// Only fires once the user stops typing for 250ms +const onSearchInput = debounce((query) => fetchResults(query)) +`, + jsx: `function Avatar({ src, name }) { + const initials = name + .split(' ') + .map((part) => part[0]) + .join('') + + return src ? ( + {name} + ) : ( + {initials} + ) +} +`, + tsrx: `import { Markdown } from '@tanstack/markdown/octane' + +@for (post of posts) { +
    +

    @{post.title}

    + @if (post.description) { + {post.description} + } @else { +

    No description yet.

    + } +
    +} +`, + json: `{ + "name": "@tanstack/highlight", + "version": "0.0.10", + "private": false, + "sideEffects": false, + "keywords": ["syntax-highlighting", "documentation"], + "engines": { + "node": ">=18" + } +} +`, + html: ` +
    +

    Live preview

    +

    Pick a language, then tune every token below.

    + +
    +`, + css: `/* Scoped to the preview panel only */ +[data-theme-editor-preview] { + --preview-radius: 0.75rem; +} + +.th-code { + border-radius: var(--preview-radius); + font-size: 0.875rem; +} + +.th-code:hover { + outline: 2px solid var(--th-keyword); +} +`, + vue: ` + + +`, + svelte: ` + + + + +`, + markdown: `# Theme Editor + +Pick a **language** on the left, then adjust any token below. + +> [!TIP] +> Use \`createThemeCss\` to turn your picks into real CSS. + +See the [themes guide](https://github.com/TanStack/highlight) for more. +`, + mermaid: `sequenceDiagram + participant Editor + participant Highlighter + participant Preview + + Editor->>Highlighter: highlightToHtml(snippet) + Highlighter->>Preview: htmlMarkup + Preview-->>Editor: rendered tokens +`, + yaml: `# theme-editor.yaml +name: theme-editor +version: 1 +enabled: true +tokens: + keyword: "#cf222e" + string: "#0a7f64" + count: 20 +`, + toml: `# railway.toml +[deploy] +startCommand = "pnpm workflow:sweep" +cronSchedule = "*/5 * * * *" +restartPolicyType = "NEVER" +retries = 3 +enabled = true +`, + sql: `CREATE TABLE themes ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + is_dark INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +SELECT name, is_dark FROM themes WHERE is_dark = 1 ORDER BY created_at DESC; +`, + python: `from dataclasses import dataclass + +@dataclass +class Theme: + name: str + background: str + is_dark: bool = False + +def contrast_ratio(theme: Theme) -> float: + # Placeholder — real formula lives in colorimetry.py + return 4.5 if theme.is_dark else 7.0 +`, + scheme: `; sandbox policy for theme exports +(version 1) +(allow default) +(deny file-write* (subpath "/")) +(allow file-write* (subpath "/tmp/theme-editor")) +(define (clamp x lo hi) (max lo (min x hi))) +`, + shell: `# Install the highlighter and preview it locally +npm install @tanstack/highlight + +# Regenerate the compare report after editing themes +pnpm run report:compare -- --theme=$THEME_NAME +`, + dockerfile: `# Build stage +FROM node:18-alpine AS build +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +# Production stage +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html +EXPOSE 80 +`, + nginx: `server { + listen 80; + server_name theme-editor.example.com; + root /var/www/theme-editor; + + location / { + try_files $uri $uri/ /index.html; + } +} +`, + apache: ` + RewriteEngine On + RewriteBase / + RewriteRule ^index\\.html$ - [L] + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule . /index.html [L] + +`, + http: `HTTP/1.1 200 OK +Content-Type: application/json +Cache-Control: no-cache +X-Theme-Id: github-light +`, + env: `# .env.local +VITE_HIGHLIGHT_DEFAULT_THEME=github-light +VITE_HIGHLIGHT_DEFAULT_LANG=ts +`, + ejs: ` +<% if (theme.isDark) { %> + +<% } else { %> + +<% } %> +`, + diff: `theme({ +- keyword: '#cf222e', ++ keyword: '#ff79c6', +- background: '#ffffff', ++ background: '#282a36', + comment: '#6e7781', +}) +`, + plaintext: `This language has no syntax rules, so every +character renders with the default "token" color +and background only — a good way to check contrast +before tuning the rest of the palette. +`, +} diff --git a/src/components/highlight-theme-editor/tokenGroups.ts b/src/components/highlight-theme-editor/tokenGroups.ts new file mode 100644 index 000000000..de984c6b7 --- /dev/null +++ b/src/components/highlight-theme-editor/tokenGroups.ts @@ -0,0 +1,96 @@ +import { auroraXTheme } from '@tanstack/highlight/themes/aurora-x' +import { draculaTheme } from '@tanstack/highlight/themes/dracula' +import { githubDarkTheme } from '@tanstack/highlight/themes/github-dark' +import { githubLightTheme } from '@tanstack/highlight/themes/github-light' +import { monokaiTheme } from '@tanstack/highlight/themes/monokai' +import { nordTheme } from '@tanstack/highlight/themes/nord' +import { oneDarkProTheme } from '@tanstack/highlight/themes/one-dark-pro' +import { solarizedDarkTheme } from '@tanstack/highlight/themes/solarized-dark' +import { solarizedLightTheme } from '@tanstack/highlight/themes/solarized-light' +import type { + HighlightTheme, + HighlightThemeToken, +} from '@tanstack/highlight/theme' + +export type TokenGroup = { + id: string + label: string + tokens: ReadonlyArray +} + +export const tokenGroups: ReadonlyArray = [ + { + id: 'syntax', + label: 'Syntax', + tokens: ['keyword', 'string', 'number', 'literal', 'operator', 'comment'], + }, + { + id: 'structure', + label: 'Structure', + tokens: ['function', 'type', 'property', 'variable'], + }, + { + id: 'markup', + label: 'Markup', + tokens: ['tag', 'attr', 'selector'], + }, + { + id: 'docs-and-diff', + label: 'Docs & Diff', + tokens: [ + 'heading', + 'link', + 'code-inline', + 'inserted', + 'deleted', + 'meta', + 'command', + ], + }, +] + +export const tokenLabels: Record = { + token: 'Default text', + attr: 'Attribute', + 'code-inline': 'Inline code', + command: 'Command', + comment: 'Comment', + deleted: 'Deleted line', + function: 'Function', + heading: 'Heading', + inserted: 'Inserted line', + keyword: 'Keyword', + link: 'Link', + literal: 'Literal', + meta: 'Meta', + number: 'Number', + operator: 'Operator', + property: 'Property', + selector: 'Selector', + string: 'String', + tag: 'Tag', + type: 'Type', + variable: 'Variable', +} + +export type ThemePreset = { + id: string + label: string + theme: HighlightTheme +} + +export const themePresets: ReadonlyArray = [ + { id: 'github-light', label: 'GitHub Light', theme: githubLightTheme }, + { id: 'github-dark', label: 'GitHub Dark', theme: githubDarkTheme }, + { id: 'dracula', label: 'Dracula', theme: draculaTheme }, + { id: 'nord', label: 'Nord', theme: nordTheme }, + { id: 'monokai', label: 'Monokai', theme: monokaiTheme }, + { id: 'one-dark-pro', label: 'One Dark Pro', theme: oneDarkProTheme }, + { id: 'aurora-x', label: 'Aurora X', theme: auroraXTheme }, + { + id: 'solarized-light', + label: 'Solarized Light', + theme: solarizedLightTheme, + }, + { id: 'solarized-dark', label: 'Solarized Dark', theme: solarizedDarkTheme }, +] diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 1ad9cc6e9..6fa8754ff 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -189,6 +189,7 @@ import { Route as ApiBuilderDeployGithubRouteImport } from './routes/api/builder import { Route as ApiBuilderDeployCheckNameRouteImport } from './routes/api/builder/deploy/check-name' import { Route as ApiAuthCliCreateTicketRouteImport } from './routes/api/auth/cli/create-ticket' import { Route as ApiAuthCallbackProviderRouteImport } from './routes/api/auth/callback/$provider' +import { Route as LibraryHighlightVersionThemeEditorRouteImport } from './routes/_library/highlight.$version.theme-editor' import { Route as LibraryChartsCatalogAllRouteImport } from './routes/_library/charts.catalog.all' import { Route as LibraryLibraryIdVersionLlmsDottxtRouteImport } from './routes/_library/$libraryId/$version.llms[.]txt' import { Route as LibraryLibraryIdVersionDocsRouteImport } from './routes/_library/$libraryId/$version.docs' @@ -1144,6 +1145,12 @@ const ApiAuthCallbackProviderRoute = ApiAuthCallbackProviderRouteImport.update({ path: '/api/auth/callback/$provider', getParentRoute: () => rootRouteImport, } as any) +const LibraryHighlightVersionThemeEditorRoute = + LibraryHighlightVersionThemeEditorRouteImport.update({ + id: '/highlight/$version/theme-editor', + path: '/highlight/$version/theme-editor', + getParentRoute: () => LibraryRoute, + } as any) const LibraryChartsCatalogAllRoute = LibraryChartsCatalogAllRouteImport.update({ id: '/all', path: '/all', @@ -1424,6 +1431,7 @@ export interface FileRoutesByFullPath { '/$libraryId/$version/docs': typeof LibraryLibraryIdVersionDocsRouteWithChildren '/$libraryId/$version/llms.txt': typeof LibraryLibraryIdVersionLlmsDottxtRoute '/charts/catalog/all': typeof LibraryChartsCatalogAllRoute + '/highlight/$version/theme-editor': typeof LibraryHighlightVersionThemeEditorRoute '/api/auth/callback/$provider': typeof ApiAuthCallbackProviderRoute '/api/auth/cli/create-ticket': typeof ApiAuthCliCreateTicketRoute '/api/builder/deploy/check-name': typeof ApiBuilderDeployCheckNameRoute @@ -1614,6 +1622,7 @@ export interface FileRoutesByTo { '/stats/npm': typeof StatsNpmIndexRoute '/$libraryId/$version/llms.txt': typeof LibraryLibraryIdVersionLlmsDottxtRoute '/charts/catalog/all': typeof LibraryChartsCatalogAllRoute + '/highlight/$version/theme-editor': typeof LibraryHighlightVersionThemeEditorRoute '/api/auth/callback/$provider': typeof ApiAuthCallbackProviderRoute '/api/auth/cli/create-ticket': typeof ApiAuthCliCreateTicketRoute '/api/builder/deploy/check-name': typeof ApiBuilderDeployCheckNameRoute @@ -1818,6 +1827,7 @@ export interface FileRoutesById { '/_library/$libraryId/$version/docs': typeof LibraryLibraryIdVersionDocsRouteWithChildren '/_library/$libraryId/$version/llms.txt': typeof LibraryLibraryIdVersionLlmsDottxtRoute '/_library/charts/catalog/all': typeof LibraryChartsCatalogAllRoute + '/_library/highlight/$version/theme-editor': typeof LibraryHighlightVersionThemeEditorRoute '/api/auth/callback/$provider': typeof ApiAuthCallbackProviderRoute '/api/auth/cli/create-ticket': typeof ApiAuthCliCreateTicketRoute '/api/builder/deploy/check-name': typeof ApiBuilderDeployCheckNameRoute @@ -2022,6 +2032,7 @@ export interface FileRouteTypes { | '/$libraryId/$version/docs' | '/$libraryId/$version/llms.txt' | '/charts/catalog/all' + | '/highlight/$version/theme-editor' | '/api/auth/callback/$provider' | '/api/auth/cli/create-ticket' | '/api/builder/deploy/check-name' @@ -2212,6 +2223,7 @@ export interface FileRouteTypes { | '/stats/npm' | '/$libraryId/$version/llms.txt' | '/charts/catalog/all' + | '/highlight/$version/theme-editor' | '/api/auth/callback/$provider' | '/api/auth/cli/create-ticket' | '/api/builder/deploy/check-name' @@ -2415,6 +2427,7 @@ export interface FileRouteTypes { | '/_library/$libraryId/$version/docs' | '/_library/$libraryId/$version/llms.txt' | '/_library/charts/catalog/all' + | '/_library/highlight/$version/theme-editor' | '/api/auth/callback/$provider' | '/api/auth/cli/create-ticket' | '/api/builder/deploy/check-name' @@ -3823,6 +3836,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiAuthCallbackProviderRouteImport parentRoute: typeof rootRouteImport } + '/_library/highlight/$version/theme-editor': { + id: '/_library/highlight/$version/theme-editor' + path: '/highlight/$version/theme-editor' + fullPath: '/highlight/$version/theme-editor' + preLoaderRoute: typeof LibraryHighlightVersionThemeEditorRouteImport + parentRoute: typeof LibraryRoute + } '/_library/charts/catalog/all': { id: '/_library/charts/catalog/all' path: '/all' @@ -4131,6 +4151,7 @@ const LibraryChartsCatalogRouteWithChildren = interface LibraryRouteChildren { LibraryLibraryIdRouteRoute: typeof LibraryLibraryIdRouteRouteWithChildren LibraryChartsCatalogRoute: typeof LibraryChartsCatalogRouteWithChildren + LibraryHighlightVersionThemeEditorRoute: typeof LibraryHighlightVersionThemeEditorRoute LibraryAiVersionIndexRoute: typeof LibraryAiVersionIndexRoute LibraryChartsVersionIndexRoute: typeof LibraryChartsVersionIndexRoute LibraryCliVersionIndexRoute: typeof LibraryCliVersionIndexRoute @@ -4156,6 +4177,8 @@ interface LibraryRouteChildren { const LibraryRouteChildren: LibraryRouteChildren = { LibraryLibraryIdRouteRoute: LibraryLibraryIdRouteRouteWithChildren, LibraryChartsCatalogRoute: LibraryChartsCatalogRouteWithChildren, + LibraryHighlightVersionThemeEditorRoute: + LibraryHighlightVersionThemeEditorRoute, LibraryAiVersionIndexRoute: LibraryAiVersionIndexRoute, LibraryChartsVersionIndexRoute: LibraryChartsVersionIndexRoute, LibraryCliVersionIndexRoute: LibraryCliVersionIndexRoute, diff --git a/src/routes/_library/highlight.$version.theme-editor.tsx b/src/routes/_library/highlight.$version.theme-editor.tsx new file mode 100644 index 000000000..2ceb3e9a4 --- /dev/null +++ b/src/routes/_library/highlight.$version.theme-editor.tsx @@ -0,0 +1,17 @@ +import { createFileRoute } from '@tanstack/react-router' +import { docsConfigQueryOptions } from '~/queries/docsConfig' +import { ThemeEditorPage } from '~/components/highlight-theme-editor/ThemeEditorPage' + +export const Route = createFileRoute( + '/_library/highlight/$version/theme-editor', +)({ + loader: async ({ params, context: { queryClient } }) => { + return { + config: await queryClient.ensureQueryData( + docsConfigQueryOptions('highlight', params.version), + ), + } + }, + component: ThemeEditorPage, + ssr: false, +}) diff --git a/src/utils/docsNavTabs.ts b/src/utils/docsNavTabs.ts index 600f549db..4c08df264 100644 --- a/src/utils/docsNavTabs.ts +++ b/src/utils/docsNavTabs.ts @@ -8,6 +8,7 @@ export const docsNavTabIds = [ 'guides', 'api', 'examples', + 'theme-editor', ] as const export type DocsNavTabId = (typeof docsNavTabIds)[number] From 192063fdd12ca8e5baa4499d5186d66c15e267ea Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Tue, 18 Aug 2026 12:52:08 +0200 Subject: [PATCH 2/2] chore: pr comments --- .../ThemeEditorPage.tsx | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/components/highlight-theme-editor/ThemeEditorPage.tsx b/src/components/highlight-theme-editor/ThemeEditorPage.tsx index 7229b5590..ae7d8b027 100644 --- a/src/components/highlight-theme-editor/ThemeEditorPage.tsx +++ b/src/components/highlight-theme-editor/ThemeEditorPage.tsx @@ -82,13 +82,21 @@ export function ThemeEditorPage() { ) const copyThemeObject = async () => { - await copyTextToClipboard(buildThemeObjectSnippet(theme)) - notify('Copied theme object to clipboard', { id: 'theme-copy' }) + try { + await copyTextToClipboard(buildThemeObjectSnippet(theme)) + notify('Copied theme object to clipboard', { id: 'theme-copy' }) + } catch { + notify('Failed to copy theme object', { id: 'theme-copy' }) + } } const copyAgentPrompt = async () => { - await copyTextToClipboard(buildAgentPrompt(theme)) - notify('Copied AI prompt to clipboard', { id: 'theme-copy-prompt' }) + try { + await copyTextToClipboard(buildAgentPrompt(theme)) + notify('Copied AI prompt to clipboard', { id: 'theme-copy-prompt' }) + } catch { + notify('Failed to copy AI prompt', { id: 'theme-copy-prompt' }) + } } return ( @@ -173,10 +181,11 @@ export function ThemeEditorPage() { ))}
    - -
    @@ -272,16 +281,16 @@ function normalizeHex(value: string) { function buildThemeObjectSnippet(theme: HighlightTheme) { const identifier = toThemeIdentifier(theme.name) const tokenLines = themeTokenClasses - .map((token) => ` '${token}': '${theme.tokens[token]}',`) + .map((token) => ` '${token}': ${JSON.stringify(theme.tokens[token])},`) .join('\n') return `import type { HighlightTheme } from '@tanstack/highlight/theme' export const ${identifier} = { - name: '${theme.name}', - type: '${theme.type}', - background: '${theme.background}', - foreground: '${theme.foreground}', + name: ${JSON.stringify(theme.name)}, + type: ${JSON.stringify(theme.type)}, + background: ${JSON.stringify(theme.background)}, + foreground: ${JSON.stringify(theme.foreground)}, tokens: { ${tokenLines} },