{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "code-themes", "title": "Code Themes", "description": "Shared Formance Shiki/Monaco themes, language config, and highlighter singleton using CSS variables.", "dependencies": [ "shiki" ], "files": [ { "path": "registry/default/ui/code/code-themes.ts", "content": "import {\n createCssVariablesTheme,\n type Highlighter,\n type LanguageRegistration,\n type ThemeRegistration,\n} from 'shiki';\n\nimport numscriptGrammar from '@/registry/default/ui/code/numscript.tmLanguage.json';\n\n// ---------------------------------------------------------------------------\n// Languages\n// ---------------------------------------------------------------------------\n\nexport const CODE_LANGUAGES = [\n 'typescript',\n 'tsx',\n 'javascript',\n 'yaml',\n 'json',\n 'toml',\n 'bash',\n 'css',\n 'html',\n 'go',\n 'python',\n 'sql',\n 'plaintext',\n 'markdown',\n 'numscript',\n] as const;\n\nexport type TCodeLanguage = (typeof CODE_LANGUAGES)[number];\n\n// ---------------------------------------------------------------------------\n// CSS-variables Shiki theme\n// ---------------------------------------------------------------------------\n\n/**\n * A single Shiki theme that defers every color to CSS custom properties.\n * Define `--shiki-token-*` in your global CSS (light + dark) and it Just Works™.\n *\n * Extended with Numscript-specific scopes so the grammar's token types\n * (monetary literals, accounts, etc.) get their own CSS variables.\n */\nconst baseTheme = createCssVariablesTheme({\n name: 'formance-css-vars',\n variablePrefix: '--shiki-',\n variableDefaults: {},\n fontStyle: true,\n}) as ThemeRegistration & { tokenColors?: Record[] };\n\nexport const cssVarsTheme: ThemeRegistration = {\n ...baseTheme,\n tokenColors: [\n ...(baseTheme.tokenColors ?? []),\n {\n scope: 'support.type.property-name',\n settings: { foreground: 'var(--shiki-token-keyword)' },\n },\n {\n scope: 'support.type.property-name.json',\n settings: { foreground: 'var(--shiki-token-keyword)' },\n },\n {\n scope: ['constant.other.option', 'constant.other.option.dash.shell'],\n settings: { foreground: 'var(--shiki-token-constant)' },\n },\n {\n scope: [\n 'variable.other.normal.shell',\n 'variable.other.assignment.shell',\n 'variable.other.for.shell',\n ],\n settings: { foreground: 'var(--shiki-token-parameter)' },\n },\n {\n scope: 'constant.other.monetary.numscript',\n settings: { foreground: 'var(--shiki-token-monetary)' },\n },\n ],\n} as ThemeRegistration;\n\n// ---------------------------------------------------------------------------\n// Fallback theme for Monaco tokenization (shikiToMonaco needs real colors)\n// ---------------------------------------------------------------------------\n\nconst monacoFallbackTheme: ThemeRegistration = {\n name: 'formance-monaco-fallback',\n type: 'dark',\n colors: {\n 'editor.background': '#01353c',\n 'editor.foreground': '#D9E6E7',\n },\n tokenColors: [\n { scope: 'comment', settings: { foreground: '#999988' } },\n { scope: 'string', settings: { foreground: '#cac8f9' } },\n {\n scope: ['number', 'constant.numeric'],\n settings: { foreground: '#36ACAA' },\n },\n { scope: 'keyword', settings: { foreground: '#97c39a' } },\n {\n scope: ['property', 'variable.other.property'],\n settings: { foreground: '#36acaa' },\n },\n {\n scope: 'support.type.property-name',\n settings: { foreground: '#97c39a' },\n },\n {\n scope: 'support.type.property-name.json',\n settings: { foreground: '#97c39a' },\n },\n {\n scope: 'support.type.property-name.json punctuation',\n settings: { foreground: '#97c39a' },\n },\n { scope: 'variable', settings: { foreground: '#36ACAA' } },\n { scope: ['plain', 'entity.name'], settings: { foreground: '#D9E6E7' } },\n {\n scope: ['builtin', 'support.function'],\n settings: { foreground: '#97c39a' },\n },\n { scope: ['symbol', 'storage.type'], settings: { foreground: '#7a78b5' } },\n { scope: 'storage', settings: { foreground: '#97c39a' } },\n { scope: 'punctuation', settings: { foreground: '#D9E6E7' } },\n { scope: 'constant', settings: { foreground: '#36ACAA' } },\n {\n scope: 'constant.other.monetary.numscript',\n settings: { foreground: '#cac8f9' },\n },\n { scope: 'keyword.control.numscript', settings: { foreground: '#97c39a' } },\n { scope: 'string.other.numscript', settings: { foreground: '#D9E6E7' } },\n {\n scope: 'variable.parameter.function.numscript',\n settings: { foreground: '#36ACAA' },\n },\n {\n scope: 'support.function.numscript',\n settings: { foreground: '#97c39a' },\n },\n { scope: 'storage.type.numscript', settings: { foreground: '#7a78b5' } },\n ],\n};\n\n// ---------------------------------------------------------------------------\n// Highlighter singleton\n// ---------------------------------------------------------------------------\n\nlet highlighter: Highlighter | null = null;\nlet pending: Promise | null = null;\n\nexport async function getHighlighter(): Promise {\n if (highlighter) return highlighter;\n if (pending) return pending;\n\n pending = (async () => {\n const { createHighlighter } = await import('shiki');\n\n const instance = await createHighlighter({\n themes: [monacoFallbackTheme, cssVarsTheme],\n langs: [\n ...CODE_LANGUAGES.filter((l) => l !== 'numscript'),\n numscriptGrammar as unknown as LanguageRegistration,\n ],\n });\n\n highlighter = instance;\n\n return instance;\n })().catch((err) => {\n pending = null;\n throw err;\n });\n\n return pending;\n}\n\n// ---------------------------------------------------------------------------\n// Monaco helpers (used by code-editor)\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve any CSS color value (including oklch, hsl, var()) to a hex string\n * by letting the browser's color engine do the conversion.\n * Reuses a single hidden element to avoid DOM thrashing.\n */\nlet _canvas: HTMLCanvasElement | null = null;\nlet _ctx: CanvasRenderingContext2D | null = null;\n\nfunction resolveColorToHex(color: string): string {\n if (!color || typeof document === 'undefined') return '#000000';\n\n if (!_canvas) {\n _canvas = document.createElement('canvas');\n _canvas.width = 1;\n _canvas.height = 1;\n _ctx = _canvas.getContext('2d', { willReadFrequently: true });\n }\n\n if (!_ctx) return '#000000';\n\n // Draw a single pixel with the color, then read it back as RGBA\n _ctx.clearRect(0, 0, 1, 1);\n _ctx.fillStyle = color;\n _ctx.fillRect(0, 0, 1, 1);\n const data = _ctx.getImageData(0, 0, 1, 1).data;\n const hex = (n: number) => n.toString(16).padStart(2, '0');\n\n return `#${hex(data[0]!)}${hex(data[1]!)}${hex(data[2]!)}`;\n}\n\n/**\n * Read a CSS variable's raw value, then resolve it through the browser\n * color engine to get a hex color Monaco can understand.\n */\nfunction resolveCSSVar(el: Element, name: string, fallback: string): string {\n const raw = getComputedStyle(el).getPropertyValue(name).trim();\n if (!raw) return fallback;\n\n return resolveColorToHex(raw);\n}\n\n/**\n * Read resolved `--shiki-*` CSS variable values from the DOM and build a\n * Monaco `IStandaloneThemeData` on the fly. CSS is the single source of truth.\n */\nexport function buildMonacoThemeFromCSSVars(\n el: Element\n): Record {\n const v = (name: string, fallback: string) =>\n resolveCSSVar(el, name, fallback);\n\n const fg = v('--shiki-foreground', '#3e3838');\n const bg = v('--shiki-background', '#ffffff');\n const isDark = document.documentElement.classList.contains('dark');\n\n const comment = v('--shiki-token-comment', '#999988');\n const string = v('--shiki-token-string', '#74739e');\n const constant = v('--shiki-token-constant', '#1C5655');\n const keyword = v('--shiki-token-keyword', '#507051');\n const fn = v('--shiki-token-function', '#507051');\n const punctuation = v('--shiki-token-punctuation', fg);\n\n return {\n base: isDark ? 'vs-dark' : 'vs',\n inherit: false,\n rules: [\n { token: '', foreground: fg },\n { token: 'comment', foreground: comment },\n { token: 'string', foreground: string },\n { token: 'string.quoted', foreground: string },\n { token: 'string.other', foreground: string },\n { token: 'number', foreground: constant },\n { token: 'constant', foreground: constant },\n { token: 'constant.numeric', foreground: constant },\n {\n token: 'constant.other.monetary',\n foreground: v('--shiki-token-monetary', constant),\n },\n { token: 'constant.language', foreground: constant },\n { token: 'keyword', foreground: keyword },\n { token: 'keyword.control', foreground: keyword },\n { token: 'keyword.operator', foreground: keyword },\n { token: 'storage', foreground: keyword },\n { token: 'storage.type', foreground: keyword },\n { token: 'type', foreground: keyword },\n { token: 'support.function', foreground: fn },\n { token: 'support.type.property-name', foreground: keyword },\n { token: 'support.type.property-name.json', foreground: keyword },\n { token: 'variable', foreground: fg },\n { token: 'variable.parameter', foreground: fg },\n { token: 'property', foreground: constant },\n { token: 'punctuation', foreground: punctuation },\n { token: 'source', foreground: fg },\n ],\n colors: {\n 'editor.background': bg,\n 'editor.foreground': fg,\n 'editor.lineHighlightBackground': '#00000000',\n 'editorLineNumber.foreground': comment,\n 'editor.selectionBackground': isDark ? '#024851' : '#ebf0f099',\n },\n };\n}\n\nexport const MONACO_EDITOR_OPTIONS = {\n automaticLayout: true,\n scrollBeyondLastLine: false,\n overviewRulerBorder: false,\n fontFamily: 'Berkeley Mono, ui-monospace, monospace',\n fontSize: 14,\n padding: { top: 12, bottom: 12 },\n folding: true,\n guides: { indentation: false },\n minimap: { enabled: false },\n quickSuggestions: false,\n matchBrackets: 'never' as const,\n lineNumbers: 'off' as const,\n bracketPairColorization: { enabled: false },\n} as const;\n\nconst _global = typeof window !== 'undefined' ? (window as any) : undefined;\n\nlet _workerUrl: string | undefined;\n\nfunction releaseWorkerUrl(): void {\n if (_workerUrl) URL.revokeObjectURL(_workerUrl);\n _workerUrl = undefined;\n}\n\n/**\n * Owns the lifetime of the worker URL: Monaco asks for it once per worker and\n * the answer is always the same empty script, so the URL is created once and\n * freed when the page goes away. A fresh Blob URL per call is never freed.\n */\nfunction keepWorkerUrl(url: string): string {\n _workerUrl = url;\n _global.addEventListener('pagehide', releaseWorkerUrl, { once: true });\n\n return url;\n}\n\nexport function setupMonacoEnvironment(): void {\n if (!_global || _global.MonacoEnvironment) return;\n\n _global.MonacoEnvironment = {\n getWorkerUrl: () =>\n _workerUrl ??\n keepWorkerUrl(\n URL.createObjectURL(new Blob([''], { type: 'application/javascript' }))\n ),\n };\n}\n", "type": "registry:ui", "target": "components/code/code-themes.ts" }, { "path": "registry/default/ui/code/code-themes.css", "content": "/* Formance brand syntax highlighting — CSS variable tokens for Shiki & Monaco */\n\n/* Light theme (default) */\n:root,\n[data-shiki-theme='light'],\n[data-shiki-theme='light'] * {\n --shiki-foreground: var(--emerald-800);\n --shiki-background: var(--emerald-50);\n --shiki-color-text: var(--emerald-800);\n --shiki-color-background: var(--emerald-50);\n --shiki-token-constant: var(--emerald-600);\n --shiki-token-string: var(--lilac-700);\n --shiki-token-comment: var(--emerald-500);\n --shiki-token-keyword: var(--mint-800);\n --shiki-token-parameter: var(--emerald-600);\n --shiki-token-function: var(--mint-800);\n --shiki-token-string-expression: var(--lilac-700);\n --shiki-token-punctuation: var(--emerald-800);\n --shiki-token-link: var(--emerald-600);\n --shiki-token-monetary: var(--lilac-800);\n}\n\n/* Dark theme */\n.dark,\n[data-shiki-theme='dark'],\n[data-shiki-theme='dark'] * {\n --shiki-foreground: var(--emerald-200);\n --shiki-background: var(--emerald-800);\n --shiki-color-text: var(--emerald-200);\n --shiki-color-background: var(--emerald-800);\n --shiki-token-constant: var(--mint-500);\n --shiki-token-string: var(--lilac-400);\n --shiki-token-comment: var(--emerald-400);\n --shiki-token-keyword: var(--mint-600);\n --shiki-token-parameter: var(--mint-500);\n --shiki-token-function: var(--mint-600);\n --shiki-token-string-expression: var(--lilac-400);\n --shiki-token-punctuation: var(--emerald-300);\n --shiki-token-link: var(--mint-500);\n --shiki-token-monetary: var(--lilac-300);\n}\n", "type": "registry:ui", "target": "components/code/code-themes.css" }, { "path": "registry/default/ui/code/numscript.tmLanguage.json", "content": "{\n \"fileTypes\": [\"num\"],\n \"name\": \"numscript\",\n \"scopeName\": \"source.numscript\",\n \"patterns\": [\n {\n \"match\": \"(//[^\\n]*)\",\n \"name\": \"comment.line.double-slash.numscript\"\n },\n {\n \"begin\": \"/\\\\*\",\n \"end\": \"\\\\*/\",\n \"name\": \"comment.block.numscript\",\n \"patterns\": [\n {\n \"match\": \"[\\\\s\\\\S]*\",\n \"name\": \"comment.block.numscript\"\n }\n ]\n },\n {\n \"match\": \"(\\\"[^\\\"]*\\\")\",\n \"name\": \"string.quoted.double.numscript\"\n },\n {\n \"match\": \"\\\\[[A-Z0-9]+(/\\\\d+)?\\\\s+(\\\\d+|\\\\*)\\\\]\",\n \"name\": \"constant.other.monetary.numscript\"\n },\n {\n \"match\": \"\\\\b[0-9]+/[0-9]+\\\\b\",\n \"name\": \"constant.numeric.fraction.numscript\"\n },\n {\n \"match\": \"\\\\b[0-9]+%\\\\b\",\n \"name\": \"constant.numeric.percentage.numscript\"\n },\n {\n \"match\": \"\\\\b[0-9]+\\\\b\",\n \"name\": \"constant.numeric.decimal.numscript\"\n },\n {\n \"match\": \"\\\\$[a-zA-Z_][A-Za-z0-9_]*\",\n \"name\": \"variable.parameter.function.numscript\"\n },\n {\n \"match\": \"@[\\\\w:$]+\",\n \"name\": \"string.other.numscript\"\n },\n {\n \"match\": \"\\\\b(?i:send|save|vars|remaining|to|kept|allowing|unbounded|overdraft|up|max|from|oneof)\\\\b\",\n \"name\": \"keyword.control.numscript\"\n },\n {\n \"match\": \"\\\\b(?i:source|destination)\\\\b\",\n \"name\": \"constant.language.numscript\"\n },\n {\n \"match\": \"\\\\b(?i:set_tx_meta|set_account_meta|meta|balance|overdraft|get_asset|get_amount)\\\\b\",\n \"name\": \"support.function.numscript\"\n },\n {\n \"match\": \"\\\\b(?i:account|monetary|asset|number|string|portion)\\\\b\",\n \"name\": \"storage.type.numscript\"\n },\n {\n \"match\": \"[+\\\\-/]\",\n \"name\": \"keyword.operator.numscript\"\n },\n {\n \"match\": \"[{}()\\\\[\\\\]=,]\",\n \"name\": \"punctuation.definition.numscript\"\n }\n ]\n}\n", "type": "registry:ui", "target": "components/code/numscript.tmLanguage.json" } ], "type": "registry:ui" }