-
Notifications
You must be signed in to change notification settings - Fork 2k
[codex] Fix font panel globals CSS setup #3097
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| import { addFontToCssTheme, removeFontFromCssTheme } from '@onlook/fonts'; | ||
| import type { Font, RouterConfig } from '@onlook/models'; | ||
| import type { EditorEngine } from '@/components/store/editor/engine'; | ||
| import type { SandboxManager } from '@/components/store/editor/sandbox'; | ||
| import { normalizePath } from '@/components/store/editor/sandbox/helpers'; | ||
|
|
||
| const GLOBAL_CSS_FILE = 'globals.css'; | ||
|
|
||
| function rankGlobalCssPath(path: string, routerConfig: RouterConfig): number { | ||
| const normalizedPath = normalizePath(path); | ||
|
|
||
| if (normalizedPath === normalizePath(`${routerConfig.basePath}/${GLOBAL_CSS_FILE}`)) { | ||
| return 0; | ||
| } | ||
|
|
||
| if (normalizedPath === normalizePath(`src/${GLOBAL_CSS_FILE}`)) { | ||
| return 1; | ||
| } | ||
|
|
||
| if (normalizedPath === GLOBAL_CSS_FILE) { | ||
| return 2; | ||
| } | ||
|
|
||
| return 3; | ||
| } | ||
|
|
||
| export const getGlobalCssPath = async (sandbox: SandboxManager): Promise<string | null> => { | ||
| const routerConfig = await sandbox.getRouterConfig(); | ||
| if (!routerConfig) { | ||
| return null; | ||
| } | ||
|
|
||
| const files = await sandbox.listAllFiles(); | ||
| const globalCssFiles = files | ||
| .filter( | ||
| (file) => | ||
| file.type === 'file' && | ||
| (file.path === GLOBAL_CSS_FILE || file.path.endsWith(`/${GLOBAL_CSS_FILE}`)), | ||
| ) | ||
| .sort( | ||
| (a, b) => | ||
| rankGlobalCssPath(a.path, routerConfig) - rankGlobalCssPath(b.path, routerConfig), | ||
| ); | ||
|
|
||
| return globalCssFiles[0]?.path ?? null; | ||
| }; | ||
|
|
||
| const updateGlobalCss = async ( | ||
| editorEngine: EditorEngine, | ||
| transform: (css: string) => string, | ||
| errorLabel: string, | ||
| ): Promise<boolean> => { | ||
| try { | ||
| const sandbox = editorEngine.activeSandbox; | ||
| const cssPath = await getGlobalCssPath(sandbox); | ||
| if (!cssPath) { | ||
| return false; | ||
| } | ||
|
|
||
| const file = await sandbox.readFile(cssPath); | ||
| if (typeof file !== 'string') { | ||
| return false; | ||
| } | ||
|
|
||
| const result = transform(file); | ||
| if (result === file) { | ||
| return true; | ||
| } | ||
|
|
||
| await sandbox.writeFile(cssPath, result); | ||
| return true; | ||
| } catch (error) { | ||
| console.error(errorLabel, error); | ||
| return false; | ||
| } | ||
| }; | ||
|
Comment on lines
+48
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On line 61-63, a non-string 🤖 Prompt for AI Agents |
||
|
|
||
| export const addFontToGlobalCss = async ( | ||
| font: Font, | ||
| editorEngine: EditorEngine, | ||
| ): Promise<boolean> => { | ||
| return updateGlobalCss( | ||
| editorEngine, | ||
| (css) => addFontToCssTheme(font, css), | ||
| 'Error adding font to global CSS:', | ||
| ); | ||
| }; | ||
|
|
||
| export const removeFontFromGlobalCss = async ( | ||
| fontId: string, | ||
| editorEngine: EditorEngine, | ||
| ): Promise<boolean> => { | ||
| return updateGlobalCss( | ||
| editorEngine, | ||
| (css) => removeFontFromCssTheme(fontId, css), | ||
| 'Error removing font from global CSS:', | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| import type { Font } from '@onlook/models'; | ||
|
|
||
| const THEME_BLOCK_REGEX = /@theme(?:\s+inline)?\s*\{/g; | ||
|
|
||
| function findThemeBlock(content: string): { bodyStart: number; bodyEnd: number } | null { | ||
| const match = THEME_BLOCK_REGEX.exec(content); | ||
| THEME_BLOCK_REGEX.lastIndex = 0; | ||
|
|
||
| if (!match) { | ||
| return null; | ||
| } | ||
|
|
||
| const bodyStart = match.index + match[0].length; | ||
| let depth = 1; | ||
|
|
||
| for (let index = bodyStart; index < content.length; index++) { | ||
| const char = content[index]; | ||
| if (char === '{') { | ||
| depth++; | ||
| } else if (char === '}') { | ||
| depth--; | ||
| if (depth === 0) { | ||
| return { | ||
| bodyStart, | ||
| bodyEnd: index, | ||
| }; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
Comment on lines
+5
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Yes, Tailwind CSS v4 supports multiple Citations:
🏁 Script executed: cat -n packages/fonts/src/helpers/css-theme.tsRepository: onlook-dev/onlook Length of output: 3788 Only the first Tailwind v4 explicitly supports multiple
Consider iterating over all Additionally, the brace-depth loop doesn't skip CSS comments or strings — 🤖 Prompt for AI Agents |
||
|
|
||
| function escapeRegExp(value: string): string { | ||
| return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | ||
| } | ||
|
|
||
| function getThemeFontVariableName(fontId: string): string { | ||
| return `--font-${fontId}`; | ||
| } | ||
|
|
||
| function getFontThemeDeclaration(font: Font): string { | ||
| const themeVariable = getThemeFontVariableName(font.id); | ||
| const fontVariable = font.variable || themeVariable; | ||
|
|
||
| return `${themeVariable}: var(${fontVariable});`; | ||
| } | ||
|
|
||
| /** | ||
| * Adds or updates a Tailwind CSS `@theme` font token for an Onlook-managed font. | ||
| */ | ||
| export function addFontToCssTheme(font: Font, content: string): string { | ||
| const declaration = getFontThemeDeclaration(font); | ||
| const themeVariable = getThemeFontVariableName(font.id); | ||
|
|
||
| const block = findThemeBlock(content); | ||
| if (!block) { | ||
| const separator = content.endsWith('\n') ? '\n' : '\n\n'; | ||
| return `${content}${separator}@theme inline {\n ${declaration}\n}\n`; | ||
| } | ||
|
|
||
| const beforeBody = content.slice(0, block.bodyStart); | ||
| const body = content.slice(block.bodyStart, block.bodyEnd); | ||
| const afterBody = content.slice(block.bodyEnd); | ||
| const escapedThemeVariable = escapeRegExp(themeVariable); | ||
| const existingDeclarationRegex = new RegExp(`(^\\s*)${escapedThemeVariable}\\s*:[^;]+;`, 'm'); | ||
|
|
||
| if (existingDeclarationRegex.test(body)) { | ||
| return `${beforeBody}${body.replace(existingDeclarationRegex, `$1${declaration}`)}${afterBody}`; | ||
| } | ||
|
|
||
| const trimmedBody = body.replace(/\s*$/, ''); | ||
| return `${beforeBody}${trimmedBody}\n ${declaration}\n${afterBody}`; | ||
| } | ||
|
|
||
| /** | ||
| * Removes a Tailwind CSS `@theme` font token for an Onlook-managed font. | ||
| */ | ||
| export function removeFontFromCssTheme(fontId: string, content: string): string { | ||
| const block = findThemeBlock(content); | ||
| if (!block) { | ||
| return content; | ||
| } | ||
|
|
||
| const themeVariable = getThemeFontVariableName(fontId); | ||
| const escapedThemeVariable = escapeRegExp(themeVariable); | ||
| const declarationRegex = new RegExp(`\\n?\\s*${escapedThemeVariable}\\s*:[^;]+;`, 'g'); | ||
| const beforeBody = content.slice(0, block.bodyStart); | ||
| const body = content.slice(block.bodyStart, block.bodyEnd); | ||
| const afterBody = content.slice(block.bodyEnd); | ||
|
|
||
| return `${beforeBody}${body.replace(declarationRegex, '')}${afterBody}`; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| export * from './class-utils'; | ||
| export * from './ast-generators'; | ||
| export * from './ast-manipulators'; | ||
| export * from './css-theme'; | ||
| export * from './font-extractors'; | ||
| export * from './validators'; | ||
| export * from './import-export-manager'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { describe } from 'bun:test'; | ||
| import type { Font } from '@onlook/models'; | ||
| import { addFontToCssTheme, removeFontFromCssTheme } from '../src/helpers/css-theme'; | ||
| import { runDataDrivenTests } from './test-utils'; | ||
| import path from 'path'; | ||
|
|
||
| const __dirname = import.meta.dir; | ||
|
|
||
| describe('addFontToCssTheme', () => { | ||
| runDataDrivenTests( | ||
| { | ||
| casesDir: path.resolve(__dirname, 'data/css-theme/add-font-to-css-theme'), | ||
| inputFileName: 'config', | ||
| expectedFileName: 'expected', | ||
| }, | ||
| async (input: { font: Font; content: string }) => { | ||
| return addFontToCssTheme(input.font, input.content); | ||
| }, | ||
| async (content: string, filePath?: string) => { | ||
| const config = JSON.parse(content); | ||
| const inputContent = await Bun.file( | ||
| path.resolve(path.dirname(filePath || ''), 'input.css'), | ||
| ).text(); | ||
| return { font: config.font, content: inputContent }; | ||
| }, | ||
| ); | ||
| }); | ||
|
|
||
| describe('removeFontFromCssTheme', () => { | ||
| runDataDrivenTests( | ||
| { | ||
| casesDir: path.resolve(__dirname, 'data/css-theme/remove-font-from-css-theme'), | ||
| inputFileName: 'config', | ||
| expectedFileName: 'expected', | ||
| }, | ||
| async (input: { fontId: string; content: string }) => { | ||
| return removeFontFromCssTheme(input.fontId, input.content); | ||
| }, | ||
| async (content: string, filePath?: string) => { | ||
| const config = JSON.parse(content); | ||
| const inputContent = await Bun.file( | ||
| path.resolve(path.dirname(filePath || ''), 'input.css'), | ||
| ).text(); | ||
| return { fontId: config.fontId, content: inputContent }; | ||
| }, | ||
| ); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "font": { | ||
| "id": "inter", | ||
| "family": "Inter", | ||
| "variable": "--font-inter", | ||
| "subsets": ["latin"], | ||
| "weight": ["400"], | ||
| "styles": ["normal"], | ||
| "type": "google" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| @import 'tailwindcss'; | ||
|
|
||
| @theme { | ||
| --color-background: #ffffff; | ||
| --font-inter: var(--font-inter); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| @import 'tailwindcss'; | ||
|
|
||
| @theme { | ||
| --color-background: #ffffff; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "font": { | ||
| "id": "open-sans", | ||
| "family": "Open Sans", | ||
| "variable": "--font-open-sans", | ||
| "subsets": ["latin"], | ||
| "weight": ["400"], | ||
| "styles": ["normal"], | ||
| "type": "google" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| @import 'tailwindcss'; | ||
|
|
||
| body { | ||
| margin: 0; | ||
| } | ||
|
|
||
| @theme inline { | ||
| --font-open-sans: var(--font-open-sans); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| @import 'tailwindcss'; | ||
|
|
||
| body { | ||
| margin: 0; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 7377
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 43
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 659
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 3716
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 2688
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 1703
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 83
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 1643
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 43
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 2650
getGlobalCssPathrescans the entire sandbox on every call and may select non-sourceglobals.cssfiles.Performance:
sandbox.listAllFiles()walks the entire file tree on every invocation—inaddFont,removeFont, and per-font duringsyncFontsWithConfigs. For larger projects, this is expensive and runs serially in loops. Consider memoizing the result per sync cycle or invalidating on file system events.Correctness: The filter
file.path === GLOBAL_CSS_FILE || file.path.endsWith('/globals.css')matches anyglobals.cssanywhere, includingnode_modules/,dist/, or vendored directories. If none of the preferred paths ({basePath}/globals.css,src/globals.css,globals.css) exist, the ranking fallback will select from whateverlistAllFiles()returns first, which could be a dependency's file. Restrict the search to source directories or excludenode_modulesand build output explicitly.🤖 Prompt for AI Agents