Skip to content

[codex] Fix font panel globals CSS setup#3097

Open
malsomesh9 wants to merge 2 commits intoonlook-dev:mainfrom
malsomesh9:codex/fix-font-panel-globals-css
Open

[codex] Fix font panel globals CSS setup#3097
malsomesh9 wants to merge 2 commits intoonlook-dev:mainfrom
malsomesh9:codex/fix-font-panel-globals-css

Conversation

@malsomesh9
Copy link
Copy Markdown

@malsomesh9 malsomesh9 commented Apr 17, 2026

Summary

  • wire font panel adds/removes through globals.css Tailwind v4 theme tokens
  • apply Tailwind config, root layout variable imports, and globals CSS immediately after font additions
  • add data-driven coverage for CSS theme token insertion, updates, and removal

Fixes #2912

Validation

  • git diff --check
  • bun test packages/fonts/test/css-theme.test.ts packages/fonts/test/ast-manipulators.test.ts (not run: bun is not installed in this shell)

Summary by CodeRabbit

  • New Features

    • Global CSS theme now updates when fonts are added or removed from the editor; font changes are synchronized with layout and theme settings for consistent styling
  • Tests

    • Added data-driven tests covering adding and removing fonts in CSS theme scenarios (multiple fixtures and edge cases)

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 17, 2026

@malsomesh9 is attempting to deploy a commit to the Onlook Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

📝 Walkthrough

Walkthrough

Adds CSS @theme font management: a helper to insert/remove font variables in CSS, editor-side logic to locate/update globals.css in a sandbox, tests/fixtures for the helper, and integration of global-css updates into the font manager add/remove/sync flows.

Changes

Cohort / File(s) Summary
CSS Theme Helper
packages/fonts/src/helpers/css-theme.ts, packages/fonts/src/helpers/index.ts
New helper functions addFontToCssTheme and removeFontFromCssTheme that find or create @theme/@theme inline blocks and insert/remove font variable declarations; re-exported from helpers barrel.
Editor Global CSS Integration
apps/web/client/src/components/store/editor/font/css-theme.ts
New module to locate the sandbox globals.css (ranked by router basePath and common locations), read/transform content with CSS helper, and write back only when changed; exports getGlobalCssPath, addFontToGlobalCss, removeFontFromGlobalCss.
Font Manager Integration
apps/web/client/src/components/store/editor/font/index.ts
Font add/remove/sync flows now call global CSS update helpers (concurrently with Tailwind/root-layout updates); uploadFonts triggers sync. Error handling updated to fail when sub-steps fail.
Layout manager tweak
apps/web/client/src/components/store/editor/font/layout-manager.ts
Adjusted removeFontVariableFromRootLayout boolean return path to report success in the fallthrough case.
Tests & Fixtures
packages/fonts/test/css-theme.test.ts, packages/fonts/test/data/css-theme/**
Added data-driven Bun tests and many input/expected fixtures covering add/remove scenarios (existing theme, no-theme, updates, regex ids, outside theme tokens).

Sequence Diagram

sequenceDiagram
    participant Manager as Font Manager
    participant Engine as EditorEngine
    participant Sandbox as SandboxManager
    participant FS as FileSystem
    participant CSS as CSS Helper

    Manager->>Engine: getActiveSandbox()
    Engine-->>Manager: sandbox
    Manager->>Sandbox: listFiles()
    Sandbox-->>Manager: file list
    Manager->>Manager: rank/filter globals.css candidates
    Manager->>Sandbox: readFile(globalCssPath)
    Sandbox->>FS: read operation
    FS-->>Manager: cssContent
    Manager->>CSS: addFontToCssTheme(font, cssContent)
    CSS-->>Manager: updatedCss
    alt content changed
        Manager->>Sandbox: writeFile(path, updatedCss)
        Sandbox->>FS: write operation
        FS-->>Manager: success
    end
    Manager-->>Manager: return boolean result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • Kitenite

Poem

🐰 I hopped through CSS with care and cheer,
Poked a @theme and added a font near,
Globals now listen, tokens align,
Typography hums — everything's fine! 🎩

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive One change in layout-manager.ts (returning true instead of false in removeFontVariableFromRootLayout) appears potentially out of scope and may warrant clarification regarding its necessity. Clarify whether the layout-manager.ts return value change is intentional and necessary for the globals CSS setup fix, or if it should be addressed separately.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title '[codex] Fix font panel globals CSS setup' clearly summarizes the main change: fixing how the font panel handles globals.css setup.
Description check ✅ Passed The PR description covers the summary, related issues, and validation steps. However, it lacks explicit sections for 'Type of Change' and 'Testing' as required by the template.
Linked Issues check ✅ Passed The changes successfully address issue #2912 by implementing automatic globals.css updates when fonts are added/removed from the font panel through CSS theme tokens.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@malsomesh9 malsomesh9 marked this pull request as ready for review April 18, 2026 05:21
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/web/client/src/components/store/editor/font/index.ts (2)

357-368: ⚠️ Potential issue | 🟠 Major

Remove stale global CSS tokens during config sync removals.

syncFontsWithConfigs() adds global CSS tokens for newly detected fonts, but removed fonts only clear Tailwind config and root layout. If the font config changes outside removeFont(), the @theme font token remains in globals.css.

Mirror global CSS cleanup for removed fonts
             if (removedFonts.length > 0) {
                 for (const font of removedFonts) {
                     await removeFontFromTailwindConfig(font, sandbox);
                     await removeFontVariableFromRootLayout(font.id, this.editorEngine);
+                    await removeFontFromGlobalCss(font.id, this.editorEngine);
                 }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/client/src/components/store/editor/font/index.ts` around lines 357 -
368, syncFontsWithConfigs currently only removes fonts from Tailwind config and
root layout via removeFontFromTailwindConfig and
removeFontVariableFromRootLayout, leaving stale `@theme` tokens in globals.css;
mirror the add path by removing global CSS tokens for removed fonts. Add a call
to the counterpart removal function (e.g., removeFontFromGlobalCss or similar)
inside the removedFonts loop (the same place that calls
removeFontFromTailwindConfig and removeFontVariableFromRootLayout) and ensure it
accepts the font id and this.editorEngine just like addFontToGlobalCss so
globals.css is cleaned when fonts are removed or configs change externally.

107-123: ⚠️ Potential issue | 🟠 Major

Do not report success when project config updates fail.

addFontToGlobalCss, addFontToTailwindConfig, and the layout helpers return booleans, but these results are ignored. That allows the UI/config state to say the font was added or removed even when globals.css was not updated, which is the partial-application failure this PR is trying to prevent.

Guard the follow-up config writes before mutating visible state
-                await Promise.all([
+                const [tailwindUpdated, layoutUpdated, globalCssUpdated] = await Promise.all([
                     addFontToTailwindConfig(font, this.editorEngine.activeSandbox),
                     addFontVariableToRootLayout(font.id, this.editorEngine),
                     addFontToGlobalCss(font, this.editorEngine),
                 ]);
+
+                if (!tailwindUpdated || !layoutUpdated || !globalCssUpdated) {
+                    console.error('Failed to apply font to one or more project config files');
+                    return false;
+                }

For removeFont, apply the same pattern before updating _fonts, _defaultFont, and returning result.

Also applies to: 147-165

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/client/src/components/store/editor/font/index.ts` around lines 107 -
123, The current flow ignores the boolean results of addFontToTailwindConfig,
addFontVariableToRootLayout, and addFontToGlobalCss so the UI/state (_fonts,
_defaultFont) is mutated and a success returned even if config writes fail;
update the logic in the add flow (around addFontToTailwindConfig,
addFontVariableToRootLayout, addFontToGlobalCss,
fontSearchManager.updateFontsList, and fontSearchManager.loadFontFromBatch) to
capture each boolean result, only push to this._fonts, call
fontSearchManager.updateFontsList, and await loadFontFromBatch if all config
helper calls returned true, and return false (without mutating state) when any
helper fails; apply the same guarded-check pattern in removeFont so
_fonts/_defaultFont are only changed when all config removals succeed.
🧹 Nitpick comments (4)
apps/web/client/src/components/store/editor/font/index.ts (1)

9-9: Use the configured src alias for this new import.

This new import maps inside apps/web/client/src, so it should use the project alias.

Proposed import update
-import { addFontToGlobalCss, removeFontFromGlobalCss } from './css-theme';
+import { addFontToGlobalCss, removeFontFromGlobalCss } from '@/components/store/editor/font/css-theme';

As per coding guidelines, apps/web/client/src/**/*.{ts,tsx}: Use path aliases @/* and ~/* for imports mapping to src/*.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/client/src/components/store/editor/font/index.ts` at line 9, The
import in index.ts uses a relative path "./css-theme" but per project alias
rules for files under apps/web/client/src, replace it with the src alias form
(e.g., import from "@/components/store/editor/font/css-theme" or using "~/" if
preferred) so the symbols addFontToGlobalCss and removeFontFromGlobalCss are
imported via the configured alias rather than a relative path.
apps/web/client/src/components/store/editor/font/css-theme.ts (3)

3-5: Prefer path aliases over relative imports per coding guidelines.

Consider using ~/components/store/editor/engine, ~/components/store/editor/sandbox, and ~/components/store/editor/sandbox/helpers (or the equivalent @/* mapping) instead of ../engine, ../sandbox, ../sandbox/helpers.

As per coding guidelines: "Use path aliases @/* and ~/* for imports that map to apps/web/client/src/*".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/client/src/components/store/editor/font/css-theme.ts` around lines 3
- 5, Update the three imports to use the project's path alias instead of
relative paths: replace references to EditorEngine from "../engine",
SandboxManager from "../sandbox", and normalizePath from "../sandbox/helpers"
with their aliased module paths (e.g. "~/components/store/editor/engine",
"~/components/store/editor/sandbox", and
"~/components/store/editor/sandbox/helpers" or the equivalent "@/components/..."
mapping) so the import specifiers use the configured path alias convention.

51-107: Minor: duplication between add/remove; consider sharing the read/transform/write skeleton.

addFontToGlobalCss and removeFontFromGlobalCss are identical except for the transform call. A small helper like updateGlobalCss(editorEngine, (css) => ...) would remove the duplication and make future error-handling changes (logging, telemetry) single-sourced.

♻️ Sketch
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) await sandbox.writeFile(cssPath, result);
        return true;
    } catch (error) {
        console.error(errorLabel, error);
        return false;
    }
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/client/src/components/store/editor/font/css-theme.ts` around lines
51 - 107, Introduce a shared helper (e.g., updateGlobalCss) that encapsulates
the read/transform/write skeleton used by addFontToGlobalCss and
removeFontFromGlobalCss: it should accept (editorEngine: EditorEngine,
transform: (css: string) => string, errorLabel: string) and internally use
editorEngine.activeSandbox, getGlobalCssPath(sandbox), sandbox.readFile(cssPath)
and sandbox.writeFile(cssPath) and preserve the original return semantics (false
on missing path or non-string file, true if unchanged or after successful
write), logging errors with the provided errorLabel; then refactor
addFontToGlobalCss to call updateGlobalCss(editorEngine, (css) =>
addFontToCssTheme(font, css), 'Error adding font to global CSS:') and
removeFontFromGlobalCss to call updateGlobalCss(editorEngine, (css) =>
removeFontFromCssTheme(fontId, css), 'Error removing font from global CSS:').

16-21: src/-prefixed basePath fallback is redundant / unreachable.

If routerConfig.basePath starts with "src/", the only way the second branch matches is when basePath === "src" exactly (so that src/globals.css equals ${basePath}/globals.css) — but then the first branch already matched at rank 0. For any deeper src/... basePath (e.g. src/app), normalizedPath === "src/globals.css" won't be a reasonable candidate anyway unless you actually want to fall back there. Consider dropping the startsWith('src/') guard so this branch ranks a src/globals.css sibling regardless of whether basePath is app or src/app, or remove it entirely if that's not a supported layout.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/client/src/components/store/editor/font/css-theme.ts` around lines
16 - 21, The second branch that checks routerConfig.basePath.startsWith('src/')
before comparing normalizedPath to normalizePath(`src/${GLOBAL_CSS_FILE}`) is
redundant/unreachable; update the conditional in the ranking logic to either
remove the startsWith('src/') guard so the
normalizePath(`src/${GLOBAL_CSS_FILE}`) sibling is considered regardless of
basePath, or delete the whole branch if a src/ fallback is not supported—modify
the code that references routerConfig.basePath, normalizedPath, normalizePath
and GLOBAL_CSS_FILE accordingly to implement the chosen behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/fonts/src/helpers/css-theme.ts`:
- Around line 46-71: Limit regex edits to the `@theme` block body and escape font
IDs: update addFontToCssTheme and removeFontFromCssTheme to call
findThemeBlock() and operate only on the block body range (use
block.bodyStart/bodyEnd) instead of the whole file, build their search/replace
against that substring, and then reassemble the full content; add an
escapeRegExp(fontId: string) helper and use it when creating
themeVariable/getThemeFontVariableName-based regexes so any regex metacharacters
in font IDs are escaped; ensure you reference getThemeFontVariableName(font.id)
when creating the escaped pattern and update both existingDeclarationRegex and
declarationRegex accordingly; finally add a test fixture where the same --font-*
property exists outside `@theme` to assert only the `@theme` block is modified.

---

Outside diff comments:
In `@apps/web/client/src/components/store/editor/font/index.ts`:
- Around line 357-368: syncFontsWithConfigs currently only removes fonts from
Tailwind config and root layout via removeFontFromTailwindConfig and
removeFontVariableFromRootLayout, leaving stale `@theme` tokens in globals.css;
mirror the add path by removing global CSS tokens for removed fonts. Add a call
to the counterpart removal function (e.g., removeFontFromGlobalCss or similar)
inside the removedFonts loop (the same place that calls
removeFontFromTailwindConfig and removeFontVariableFromRootLayout) and ensure it
accepts the font id and this.editorEngine just like addFontToGlobalCss so
globals.css is cleaned when fonts are removed or configs change externally.
- Around line 107-123: The current flow ignores the boolean results of
addFontToTailwindConfig, addFontVariableToRootLayout, and addFontToGlobalCss so
the UI/state (_fonts, _defaultFont) is mutated and a success returned even if
config writes fail; update the logic in the add flow (around
addFontToTailwindConfig, addFontVariableToRootLayout, addFontToGlobalCss,
fontSearchManager.updateFontsList, and fontSearchManager.loadFontFromBatch) to
capture each boolean result, only push to this._fonts, call
fontSearchManager.updateFontsList, and await loadFontFromBatch if all config
helper calls returned true, and return false (without mutating state) when any
helper fails; apply the same guarded-check pattern in removeFont so
_fonts/_defaultFont are only changed when all config removals succeed.

---

Nitpick comments:
In `@apps/web/client/src/components/store/editor/font/css-theme.ts`:
- Around line 3-5: Update the three imports to use the project's path alias
instead of relative paths: replace references to EditorEngine from "../engine",
SandboxManager from "../sandbox", and normalizePath from "../sandbox/helpers"
with their aliased module paths (e.g. "~/components/store/editor/engine",
"~/components/store/editor/sandbox", and
"~/components/store/editor/sandbox/helpers" or the equivalent "@/components/..."
mapping) so the import specifiers use the configured path alias convention.
- Around line 51-107: Introduce a shared helper (e.g., updateGlobalCss) that
encapsulates the read/transform/write skeleton used by addFontToGlobalCss and
removeFontFromGlobalCss: it should accept (editorEngine: EditorEngine,
transform: (css: string) => string, errorLabel: string) and internally use
editorEngine.activeSandbox, getGlobalCssPath(sandbox), sandbox.readFile(cssPath)
and sandbox.writeFile(cssPath) and preserve the original return semantics (false
on missing path or non-string file, true if unchanged or after successful
write), logging errors with the provided errorLabel; then refactor
addFontToGlobalCss to call updateGlobalCss(editorEngine, (css) =>
addFontToCssTheme(font, css), 'Error adding font to global CSS:') and
removeFontFromGlobalCss to call updateGlobalCss(editorEngine, (css) =>
removeFontFromCssTheme(fontId, css), 'Error removing font from global CSS:').
- Around line 16-21: The second branch that checks
routerConfig.basePath.startsWith('src/') before comparing normalizedPath to
normalizePath(`src/${GLOBAL_CSS_FILE}`) is redundant/unreachable; update the
conditional in the ranking logic to either remove the startsWith('src/') guard
so the normalizePath(`src/${GLOBAL_CSS_FILE}`) sibling is considered regardless
of basePath, or delete the whole branch if a src/ fallback is not
supported—modify the code that references routerConfig.basePath, normalizedPath,
normalizePath and GLOBAL_CSS_FILE accordingly to implement the chosen behavior.

In `@apps/web/client/src/components/store/editor/font/index.ts`:
- Line 9: The import in index.ts uses a relative path "./css-theme" but per
project alias rules for files under apps/web/client/src, replace it with the src
alias form (e.g., import from "@/components/store/editor/font/css-theme" or
using "~/" if preferred) so the symbols addFontToGlobalCss and
removeFontFromGlobalCss are imported via the configured alias rather than a
relative path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5f518a90-a7de-420d-8594-22c1603e249d

📥 Commits

Reviewing files that changed from the base of the PR and between a242be5 and 78bb735.

📒 Files selected for processing (17)
  • apps/web/client/src/components/store/editor/font/css-theme.ts
  • apps/web/client/src/components/store/editor/font/index.ts
  • packages/fonts/src/helpers/css-theme.ts
  • packages/fonts/src/helpers/index.ts
  • packages/fonts/test/css-theme.test.ts
  • packages/fonts/test/data/css-theme/add-font-to-css-theme/existing-theme/config.json
  • packages/fonts/test/data/css-theme/add-font-to-css-theme/existing-theme/expected.css
  • packages/fonts/test/data/css-theme/add-font-to-css-theme/existing-theme/input.css
  • packages/fonts/test/data/css-theme/add-font-to-css-theme/no-theme/config.json
  • packages/fonts/test/data/css-theme/add-font-to-css-theme/no-theme/expected.css
  • packages/fonts/test/data/css-theme/add-font-to-css-theme/no-theme/input.css
  • packages/fonts/test/data/css-theme/add-font-to-css-theme/updates-existing/config.json
  • packages/fonts/test/data/css-theme/add-font-to-css-theme/updates-existing/expected.css
  • packages/fonts/test/data/css-theme/add-font-to-css-theme/updates-existing/input.css
  • packages/fonts/test/data/css-theme/remove-font-from-css-theme/removes-font/config.json
  • packages/fonts/test/data/css-theme/remove-font-from-css-theme/removes-font/expected.css
  • packages/fonts/test/data/css-theme/remove-font-from-css-theme/removes-font/input.css

Comment thread packages/fonts/src/helpers/css-theme.ts Outdated
Comment on lines +46 to +71
export function addFontToCssTheme(font: Font, content: string): string {
const declaration = getFontThemeDeclaration(font);
const themeVariable = getThemeFontVariableName(font.id);
const existingDeclarationRegex = new RegExp(`(^\\s*)${themeVariable}\\s*:[^;]+;`, 'm');

if (existingDeclarationRegex.test(content)) {
return content.replace(existingDeclarationRegex, `$1${declaration}`);
}

const block = findThemeBlock(content);
if (!block) {
const separator = content.endsWith('\n') ? '\n' : '\n\n';
return `${content}${separator}@theme inline {\n ${declaration}\n}\n`;
}

const beforeClose = content.slice(0, block.end).replace(/\s*$/, '');
const afterClose = content.slice(block.end);

return `${beforeClose}\n ${declaration}\n${afterClose}`;
}

export function removeFontFromCssTheme(fontId: string, content: string): string {
const themeVariable = getThemeFontVariableName(fontId);
const declarationRegex = new RegExp(`\\n?\\s*${themeVariable}\\s*:[^;]+;`, 'g');

return content.replace(declarationRegex, '');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find packages/fonts -type f -name "*.ts" -o -name "*.tsx" | head -20

Repository: onlook-dev/onlook

Length of output: 1024


🏁 Script executed:

cat -n packages/fonts/src/helpers/css-theme.ts

Repository: onlook-dev/onlook

Length of output: 2840


🏁 Script executed:

# Find the findThemeBlock function
rg "findThemeBlock" packages/fonts --type ts -B 3 -A 10

Repository: onlook-dev/onlook

Length of output: 2039


🏁 Script executed:

# Find the Font interface definition
rg "interface Font|type Font" packages/fonts --type ts -A 5

Repository: onlook-dev/onlook

Length of output: 397


🏁 Script executed:

# Find getThemeFontVariableName function
rg "getThemeFontVariableName" packages/fonts --type ts -B 2 -A 5

Repository: onlook-dev/onlook

Length of output: 2208


🏁 Script executed:

# Check for test files for css-theme
find packages/fonts -type f -name "*css-theme*" | grep -E "test|spec"

Repository: onlook-dev/onlook

Length of output: 98


🏁 Script executed:

cat -n packages/fonts/test/css-theme.test.ts

Repository: onlook-dev/onlook

Length of output: 2135


🏁 Script executed:

# Check the Font interface/type definition
rg "export.*Font" packages -t ts --type-list | grep -E "interface|type" | head -20

Repository: onlook-dev/onlook

Length of output: 450


🏁 Script executed:

# Search for Font interface in models
find packages -name "*.ts" -type f | xargs grep -l "interface Font\|type Font\|export.*Font" | head -10

Repository: onlook-dev/onlook

Length of output: 560


🏁 Script executed:

find . -path "*/models*" -name "*.ts" -type f | head -20

Repository: onlook-dev/onlook

Length of output: 895


🏁 Script executed:

# Search for Font type/interface definition
rg "interface Font|type Font" --type ts -A 10 | head -50

Repository: onlook-dev/onlook

Length of output: 2632


🏁 Script executed:

# Look at existing test cases for css-theme
find packages/fonts/test/data/css-theme -type f | head -20

Repository: onlook-dev/onlook

Length of output: 1061


🏁 Script executed:

# Check if there are any test cases with special characters in font IDs
find packages/fonts/test/data/css-theme -type f -name "config.json" -exec cat {} \;

Repository: onlook-dev/onlook

Length of output: 772


🏁 Script executed:

# Check test cases more carefully - look at input/expected CSS
cat packages/fonts/test/data/css-theme/add-font-to-css-theme/updates-existing/input.css

Repository: onlook-dev/onlook

Length of output: 113


🏁 Script executed:

cat packages/fonts/test/data/css-theme/add-font-to-css-theme/updates-existing/expected.css

Repository: onlook-dev/onlook

Length of output: 123


🏁 Script executed:

cat packages/fonts/test/data/css-theme/remove-font-from-css-theme/removes-font/input.css

Repository: onlook-dev/onlook

Length of output: 148


🏁 Script executed:

cat packages/fonts/test/data/css-theme/remove-font-from-css-theme/removes-font/expected.css

Repository: onlook-dev/onlook

Length of output: 111


🏁 Script executed:

# Check if font IDs can have special regex characters
rg "fontId|font\.id" packages/fonts --type ts -B 2 -A 2 | head -40

Repository: onlook-dev/onlook

Length of output: 2698


Scope font declaration edits to @theme block body and escape regex special characters in font IDs.

The regex patterns in both functions scan the entire CSS file. This means :root { --font-inter: ... } or another selector outside @theme can be matched and modified/removed instead of the Tailwind theme token. Additionally, themeVariable is interpolated directly into the regex without escaping, so font IDs with regex metacharacters (e.g., [, ], (, ), *, +, etc.) could cause incorrect matches or throw errors.

The findThemeBlock() function already returns bodyStart, so constrain both operations to the block body range. Add an escapeRegExp() helper to safely handle arbitrary font IDs.

Scope edits to the theme block body and escape special characters
+function escapeRegExp(value: string): string {
+    return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
 export function addFontToCssTheme(font: Font, content: string): string {
     const declaration = getFontThemeDeclaration(font);
     const themeVariable = getThemeFontVariableName(font.id);
-    const existingDeclarationRegex = new RegExp(`(^\\s*)${themeVariable}\\s*:[^;]+;`, 'm');
-
-    if (existingDeclarationRegex.test(content)) {
-        return content.replace(existingDeclarationRegex, `$1${declaration}`);
-    }
+    const existingDeclarationRegex = new RegExp(
+        `(^\\s*)${escapeRegExp(themeVariable)}\\s*:[^;]+;`,
+        'm',
+    );
 
     const block = findThemeBlock(content);
     if (!block) {
         const separator = content.endsWith('\n') ? '\n' : '\n\n';
         return `${content}${separator}@theme inline {\n    ${declaration}\n}\n`;
     }
 
-    const beforeClose = content.slice(0, block.end).replace(/\s*$/, '');
-    const afterClose = content.slice(block.end);
+    const beforeBlockBody = content.slice(0, block.bodyStart);
+    const blockBody = content.slice(block.bodyStart, block.end);
+    const afterBlockBody = content.slice(block.end);
+
+    if (existingDeclarationRegex.test(blockBody)) {
+        return `${beforeBlockBody}${blockBody.replace(existingDeclarationRegex, `$1${declaration}`)}${afterBlockBody}`;
+    }
 
-    return `${beforeClose}\n    ${declaration}\n${afterClose}`;
+    return `${beforeBlockBody}${blockBody.replace(/\s*$/, '')}\n    ${declaration}\n${afterBlockBody}`;
 }
 
 export function removeFontFromCssTheme(fontId: string, content: string): string {
     const themeVariable = getThemeFontVariableName(fontId);
-    const declarationRegex = new RegExp(`\\n?\\s*${themeVariable}\\s*:[^;]+;`, 'g');
+    const block = findThemeBlock(content);
+    if (!block) {
+        return content;
+    }
+
+    const declarationRegex = new RegExp(
+        `\\n?\\s*${escapeRegExp(themeVariable)}\\s*:[^;]+;`,
+        'g',
+    );
 
-    return content.replace(declarationRegex, '');
+    const beforeBlockBody = content.slice(0, block.bodyStart);
+    const blockBody = content.slice(block.bodyStart, block.end);
+    const afterBlockBody = content.slice(block.end);
+
+    return `${beforeBlockBody}${blockBody.replace(declarationRegex, '')}${afterBlockBody}`;
 }

Also add a test fixture where the same --font-* custom property exists outside @theme to verify the helpers only modify the Tailwind theme block.

🧰 Tools
🪛 ast-grep (0.42.1)

[warning] 48-48: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp((^\\s*)${themeVariable}\\s*:[^;]+;, 'm')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)


[warning] 68-68: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\n?\\s*${themeVariable}\\s*:[^;]+;, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/fonts/src/helpers/css-theme.ts` around lines 46 - 71, Limit regex
edits to the `@theme` block body and escape font IDs: update addFontToCssTheme and
removeFontFromCssTheme to call findThemeBlock() and operate only on the block
body range (use block.bodyStart/bodyEnd) instead of the whole file, build their
search/replace against that substring, and then reassemble the full content; add
an escapeRegExp(fontId: string) helper and use it when creating
themeVariable/getThemeFontVariableName-based regexes so any regex metacharacters
in font IDs are escaped; ensure you reference getThemeFontVariableName(font.id)
when creating the escaped pattern and update both existingDeclarationRegex and
declarationRegex accordingly; finally add a test fixture where the same --font-*
property exists outside `@theme` to assert only the `@theme` block is modified.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/web/client/src/components/store/editor/font/layout-manager.ts (1)

103-112: ⚠️ Potential issue | 🟠 Major

Remove the layout import even when no className changed.

Line 112 now treats the no-op path as success, but the import cleanup is still gated by hasUpdated. If the layout has a stale ./fonts import and no matching className token, removal reports success while leaving the import behind.

Suggested direction
-        if (hasUpdated && ast) {
+        if (ast) {
             // Remove the font import if it exists
             const newContent = removeFontImportFromFile(fontImportPath, fontName, ast);
-            if (!newContent) {
-                return false;
+            if (newContent) {
+                await editorEngine.activeSandbox.writeFile(layoutPath, newContent);
+                return true;
+            }
+
+            if (hasUpdated) {
+                const { code } = generate(ast);
+                await editorEngine.activeSandbox.writeFile(layoutPath, code);
             }
-            await editorEngine.activeSandbox.writeFile(layoutPath, newContent);
-            return true;
         }
         return true;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/client/src/components/store/editor/font/layout-manager.ts` around
lines 103 - 112, The cleanup currently only runs when hasUpdated is true, so
stale ./fonts imports can remain; change the logic so that whenever ast is
available you call removeFontImportFromFile(fontImportPath, fontName, ast)
regardless of hasUpdated, check its return value and if it returns a newContent
write it via editorEngine.activeSandbox.writeFile(layoutPath, newContent), and
if removeFontImportFromFile signals failure return false—keep returning true for
the overall no-op path only when removal succeeded or no AST is present; adjust
uses of hasUpdated, ast, removeFontImportFromFile, fontImportPath, fontName,
editorEngine.activeSandbox.writeFile, and layoutPath accordingly.
apps/web/client/src/components/store/editor/font/index.ts (2)

366-380: ⚠️ Potential issue | 🟡 Minor

syncFontsWithConfigs silently ignores failures from each step.

The return values of removeFontFromTailwindConfig, removeFontVariableFromRootLayout, removeFontFromGlobalCss (and their add counterparts) are discarded, so a transient failure on any one step leaves the font partially synced and the loop moves on. After this PR, the same anti-pattern now also covers global CSS. Consider logging per-step failures and at minimum surfacing them through console.error with the font id, so a later sync run can notice and retry.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/client/src/components/store/editor/font/index.ts` around lines 366 -
380, The syncFontsWithConfigs loop ignores failures from each helper so fonts
can be partially synced; update syncFontsWithConfigs to handle errors from
removeFontFromTailwindConfig, removeFontVariableFromRootLayout,
removeFontFromGlobalCss, addFontToTailwindConfig, addFontVariableToRootLayout
and addFontToGlobalCss by wrapping each awaited call in a try/catch, log
failures with console.error including the font.id and the caught error, and
optionally collect failed font ids to return or rethrow after the loop so a
later sync can retry; reference these exact helper names and the
syncFontsWithConfigs loop when making the change.

105-162: ⚠️ Potential issue | 🟠 Major

Partial-failure leaves project in an inconsistent half-configured state.

Both addFont (lines 106-117) and removeFont (lines 150-162) perform the font-config mutation first, then run the three side-effect updates (tailwind / root-layout / global CSS) concurrently via Promise.all. If any subset of the three fails:

  • addFont returns false after addFontToConfig has already written the config and, potentially, 1–2 of the three downstream files have been updated. The in-memory _fonts array is not updated, so subsequent operations diverge from disk.
  • removeFont returns false after removeFontFromConfig succeeded (so the code diff and font-file deletions have already been committed). The _fonts array is not pruned, and a re-run will not find the font to clean up the remaining side-effects because the config entry is already gone.

Consider (a) logging which specific step failed instead of the generic "one or more", (b) still updating the in-memory state on partial success so re-sync can recover, and (c) at minimum, relying on a subsequent syncFontsWithConfigs() to reconcile. The error message also loses actionable information — pass the individual booleans to a logger.

🛠️ Illustrative fix (addFont)
-                const [tailwindUpdated, layoutUpdated, globalCssUpdated] = await Promise.all([
-                    addFontToTailwindConfig(font, this.editorEngine.activeSandbox),
-                    addFontVariableToRootLayout(font.id, this.editorEngine),
-                    addFontToGlobalCss(font, this.editorEngine),
-                ]);
-
-                if (!tailwindUpdated || !layoutUpdated || !globalCssUpdated) {
-                    console.error('Failed to apply font to one or more project config files');
-                    return false;
-                }
+                const [tailwindUpdated, layoutUpdated, globalCssUpdated] = await Promise.all([
+                    addFontToTailwindConfig(font, this.editorEngine.activeSandbox),
+                    addFontVariableToRootLayout(font.id, this.editorEngine),
+                    addFontToGlobalCss(font, this.editorEngine),
+                ]);
+
+                if (!tailwindUpdated || !layoutUpdated || !globalCssUpdated) {
+                    console.error('Failed to fully apply font; partial state on disk', {
+                        fontId: font.id,
+                        tailwindUpdated,
+                        layoutUpdated,
+                        globalCssUpdated,
+                    });
+                    return false;
+                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/client/src/components/store/editor/font/index.ts` around lines 105 -
162, The addFont/removeFont flows currently mutate the config first via
addFontToConfig/removeFontFromConfig then run three concurrent side-effect
updates, but on partial failure they return false leaving disk and in-memory
(_fonts and fontSearchManager) inconsistent and logs unhelpful; update both
functions so that after the config mutation you (1) run the three side-effect
updates and capture each boolean result, (2) log which specific updates failed
(include the three booleans) instead of a generic message, and (3) always
reconcile in-memory state: if addFontToConfig succeeded then push to
this._fonts, update fontSearchManager.updateFontsList and await
loadFontFromBatch even if some side-effects failed (for removeFont, if
removeFontFromConfig succeeded then remove from this._fonts and update
fontSearchManager), and finally on any partial failure call or schedule
syncFontsWithConfigs() (or return a value that triggers a later resync) so the
repo can fully reconcile; locate symbols addFont, removeFont, addFontToConfig,
removeFontFromConfig, addFontToTailwindConfig, addFontVariableToRootLayout,
addFontToGlobalCss, removeFontFromTailwindConfig,
removeFontVariableFromRootLayout, removeFontFromGlobalCss, this._fonts,
fontSearchManager.updateFontsList, and syncFontsWithConfigs when making the
changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/client/src/components/store/editor/font/css-theme.ts`:
- Around line 48-76: The updateGlobalCss function currently returns false
silently when sandbox.readFile(cssPath) yields a non-string (the "file is not
text" case); modify updateGlobalCss to log this failure before returning by
calling console.error(errorLabel, 'globals.css was not readable as text') (or
similar) right before the existing "return false" that follows the typeof file
!== 'string' check so callers can diagnose encoding/binary read issues;
reference the updateGlobalCss function, the file variable from
sandbox.readFile(cssPath), and the errorLabel parameter when adding the log.
- Around line 27-46: The getGlobalCssPath function currently calls
sandbox.listAllFiles() on every invocation (used by addFont, removeFont, and
per-font in syncFontsWithConfigs), which is expensive and also matches
globals.css in vendor/build dirs; to fix, cache/memoize the file list for the
duration of a single sync operation (or accept an optional files list param) so
listAllFiles() is called once per sync cycle and invalidate that cache on
filesystem events, and tighten the filter in getGlobalCssPath/rankGlobalCssPath
to prefer exact source locations (e.g., check paths like "globals.css",
"src/globals.css", "<basePath>/globals.css") and explicitly exclude paths
containing "node_modules", "dist", "build" or other vendored output before
falling back to other matches.

In `@packages/fonts/src/helpers/css-theme.ts`:
- Around line 5-32: findThemeBlock currently returns only the first `@theme` match
which causes addFontToCssTheme and removeFontFromCssTheme to target the wrong
block; update the logic to enumerate all `@theme` blocks (use
THEME_BLOCK_REGEX.exec in a loop resetting lastIndex) and return their body
ranges instead of a single range, then change addFontToCssTheme to prefer a
block that already contains a --font- declaration or an `@theme` inline block and
otherwise fall back to the last `@theme` block, and change removeFontFromCssTheme
to remove the --font- declaration from every `@theme` body; also make the
brace-depth scan in findThemeBlock robust to comments/strings by stripping or
skipping CSS comments (/* */) and string literals before counting braces.

In
`@packages/fonts/test/data/css-theme/add-font-to-css-theme/regex-font-id/expected.css`:
- Line 3: getThemeFontVariableName currently interpolates raw font IDs into CSS
custom property names causing invalid identifiers (e.g., "."); update
packages/fonts/src/helpers/css-theme.ts to sanitize/escape the font ID before
constructing the custom property (for example replace or encode characters not
allowed in CSS custom property names such as "." with a safe character like "-"
or use a strict whitelist) in the getThemeFontVariableName function, and then
update the regex-font-id fixtures/expected files to reflect the new
escaped/sanitized property names (e.g., --font-font-test).

---

Outside diff comments:
In `@apps/web/client/src/components/store/editor/font/index.ts`:
- Around line 366-380: The syncFontsWithConfigs loop ignores failures from each
helper so fonts can be partially synced; update syncFontsWithConfigs to handle
errors from removeFontFromTailwindConfig, removeFontVariableFromRootLayout,
removeFontFromGlobalCss, addFontToTailwindConfig, addFontVariableToRootLayout
and addFontToGlobalCss by wrapping each awaited call in a try/catch, log
failures with console.error including the font.id and the caught error, and
optionally collect failed font ids to return or rethrow after the loop so a
later sync can retry; reference these exact helper names and the
syncFontsWithConfigs loop when making the change.
- Around line 105-162: The addFont/removeFont flows currently mutate the config
first via addFontToConfig/removeFontFromConfig then run three concurrent
side-effect updates, but on partial failure they return false leaving disk and
in-memory (_fonts and fontSearchManager) inconsistent and logs unhelpful; update
both functions so that after the config mutation you (1) run the three
side-effect updates and capture each boolean result, (2) log which specific
updates failed (include the three booleans) instead of a generic message, and
(3) always reconcile in-memory state: if addFontToConfig succeeded then push to
this._fonts, update fontSearchManager.updateFontsList and await
loadFontFromBatch even if some side-effects failed (for removeFont, if
removeFontFromConfig succeeded then remove from this._fonts and update
fontSearchManager), and finally on any partial failure call or schedule
syncFontsWithConfigs() (or return a value that triggers a later resync) so the
repo can fully reconcile; locate symbols addFont, removeFont, addFontToConfig,
removeFontFromConfig, addFontToTailwindConfig, addFontVariableToRootLayout,
addFontToGlobalCss, removeFontFromTailwindConfig,
removeFontVariableFromRootLayout, removeFontFromGlobalCss, this._fonts,
fontSearchManager.updateFontsList, and syncFontsWithConfigs when making the
changes.

In `@apps/web/client/src/components/store/editor/font/layout-manager.ts`:
- Around line 103-112: The cleanup currently only runs when hasUpdated is true,
so stale ./fonts imports can remain; change the logic so that whenever ast is
available you call removeFontImportFromFile(fontImportPath, fontName, ast)
regardless of hasUpdated, check its return value and if it returns a newContent
write it via editorEngine.activeSandbox.writeFile(layoutPath, newContent), and
if removeFontImportFromFile signals failure return false—keep returning true for
the overall no-op path only when removal succeeded or no AST is present; adjust
uses of hasUpdated, ast, removeFontImportFromFile, fontImportPath, fontName,
editorEngine.activeSandbox.writeFile, and layoutPath accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 77ea37b6-c398-4df8-b51b-be163605446e

📥 Commits

Reviewing files that changed from the base of the PR and between 78bb735 and d40bf65.

📒 Files selected for processing (16)
  • apps/web/client/src/components/store/editor/font/css-theme.ts
  • apps/web/client/src/components/store/editor/font/index.ts
  • apps/web/client/src/components/store/editor/font/layout-manager.ts
  • packages/fonts/src/helpers/css-theme.ts
  • packages/fonts/test/data/css-theme/add-font-to-css-theme/outside-theme-token/config.json
  • packages/fonts/test/data/css-theme/add-font-to-css-theme/outside-theme-token/expected.css
  • packages/fonts/test/data/css-theme/add-font-to-css-theme/outside-theme-token/input.css
  • packages/fonts/test/data/css-theme/add-font-to-css-theme/regex-font-id/config.json
  • packages/fonts/test/data/css-theme/add-font-to-css-theme/regex-font-id/expected.css
  • packages/fonts/test/data/css-theme/add-font-to-css-theme/regex-font-id/input.css
  • packages/fonts/test/data/css-theme/remove-font-from-css-theme/outside-theme-token/config.json
  • packages/fonts/test/data/css-theme/remove-font-from-css-theme/outside-theme-token/expected.css
  • packages/fonts/test/data/css-theme/remove-font-from-css-theme/outside-theme-token/input.css
  • packages/fonts/test/data/css-theme/remove-font-from-css-theme/regex-font-id/config.json
  • packages/fonts/test/data/css-theme/remove-font-from-css-theme/regex-font-id/expected.css
  • packages/fonts/test/data/css-theme/remove-font-from-css-theme/regex-font-id/input.css
✅ Files skipped from review due to trivial changes (4)
  • packages/fonts/test/data/css-theme/remove-font-from-css-theme/outside-theme-token/config.json
  • packages/fonts/test/data/css-theme/remove-font-from-css-theme/regex-font-id/config.json
  • packages/fonts/test/data/css-theme/add-font-to-css-theme/regex-font-id/config.json
  • packages/fonts/test/data/css-theme/add-font-to-css-theme/outside-theme-token/config.json

Comment on lines +27 to +46
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;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP --type=ts -C5 '\blistAllFiles\s*\('

Repository: onlook-dev/onlook

Length of output: 7377


🏁 Script executed:

#!/bin/bash
# Find the fs implementation used in SandboxManager
rg -nP --type=ts 'this\.fs\s*=' apps/web/client/src/components/store/editor/sandbox/index.ts -A 3

Repository: onlook-dev/onlook

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Search for listAll implementation
rg -nP --type=ts 'listAll\s*\(' apps/web/client/src/components/store/editor/ -A 5 | head -100

Repository: onlook-dev/onlook

Length of output: 659


🏁 Script executed:

#!/bin/bash
# Check the full SandboxManager class to understand fs initialization
sed -n '1,100p' apps/web/client/src/components/store/editor/sandbox/index.ts

Repository: onlook-dev/onlook

Length of output: 3716


🏁 Script executed:

#!/bin/bash
# Find EXCLUDED_SYNC_PATHS definition
rg -nP --type=ts 'EXCLUDED_SYNC_PATHS' apps/web/client/src/ -B 2 -A 5

Repository: onlook-dev/onlook

Length of output: 2688


🏁 Script executed:

#!/bin/bash
# Search for CodeFileSystem definition and listAll method
rg -nP --type=ts 'class CodeFileSystem|interface CodeFileSystem|listAll' --glob='*.ts' --glob='*.tsx'

Repository: onlook-dev/onlook

Length of output: 1703


🏁 Script executed:

#!/bin/bash
# Look in packages for file-system module
find . -name 'file-system' -type d 2>/dev/null | grep -E '(packages|node_modules)' | head -5

Repository: onlook-dev/onlook

Length of output: 83


🏁 Script executed:

#!/bin/bash
# Check the listAll() implementation in FileSystem
sed -n '536,580p' packages/file-system/src/fs.ts

Repository: onlook-dev/onlook

Length of output: 1643


🏁 Script executed:

#!/bin/bash
# Find EXCLUDED_SYNC_PATHS in `@onlook/constants`
find . -path '*/@onlook/constants*' -type f -name '*.ts' 2>/dev/null | head -5

Repository: onlook-dev/onlook

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Also check CodeFileSystem implementation
sed -n '31,100p' packages/file-system/src/code-fs.ts

Repository: onlook-dev/onlook

Length of output: 2650


getGlobalCssPath rescans the entire sandbox on every call and may select non-source globals.css files.

  1. Performance: sandbox.listAllFiles() walks the entire file tree on every invocation—in addFont, removeFont, and per-font during syncFontsWithConfigs. For larger projects, this is expensive and runs serially in loops. Consider memoizing the result per sync cycle or invalidating on file system events.

  2. Correctness: The filter file.path === GLOBAL_CSS_FILE || file.path.endsWith('/globals.css') matches any globals.css anywhere, including node_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 whatever listAllFiles() returns first, which could be a dependency's file. Restrict the search to source directories or exclude node_modules and build output explicitly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/client/src/components/store/editor/font/css-theme.ts` around lines
27 - 46, The getGlobalCssPath function currently calls sandbox.listAllFiles() on
every invocation (used by addFont, removeFont, and per-font in
syncFontsWithConfigs), which is expensive and also matches globals.css in
vendor/build dirs; to fix, cache/memoize the file list for the duration of a
single sync operation (or accept an optional files list param) so listAllFiles()
is called once per sync cycle and invalidate that cache on filesystem events,
and tighten the filter in getGlobalCssPath/rankGlobalCssPath to prefer exact
source locations (e.g., check paths like "globals.css", "src/globals.css",
"<basePath>/globals.css") and explicitly exclude paths containing
"node_modules", "dist", "build" or other vendored output before falling back to
other matches.

Comment on lines +48 to +76
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;
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

updateGlobalCss swallows the "file is not text" case silently.

On line 61-63, a non-string readFile result returns false without any log, while all other non-happy paths either log or are benign. If globals.css ever comes back as Uint8Array (e.g., encoding detection glitch), callers will see the font add/remove fail generically with no diagnostic. Add a console.error(errorLabel, 'globals.css was not readable as text') before returning so the failure is attributable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/client/src/components/store/editor/font/css-theme.ts` around lines
48 - 76, The updateGlobalCss function currently returns false silently when
sandbox.readFile(cssPath) yields a non-string (the "file is not text" case);
modify updateGlobalCss to log this failure before returning by calling
console.error(errorLabel, 'globals.css was not readable as text') (or similar)
right before the existing "return false" that follows the typeof file !==
'string' check so callers can diagnose encoding/binary read issues; reference
the updateGlobalCss function, the file variable from sandbox.readFile(cssPath),
and the errorLabel parameter when adding the log.

Comment on lines +5 to +32
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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Tailwind CSS v4 multiple @theme blocks in same stylesheet

💡 Result:

Yes, Tailwind CSS v4 supports multiple @theme blocks in the same stylesheet. They are merged together, with later blocks overriding values from earlier ones if there are conflicts. This allows organizing theme definitions across multiple blocks, such as separating base theme values from inline or theme-specific ones. For example, from official Tailwind CSS sources and GitHub discussions: @theme { --color-primary: #aab9ff; } @theme inline { --color-secondary: var(--some-var); } This generates utilities for both, combining the definitions. The @theme directive processes all blocks top-level, extending or overriding the theme cumulatively. Official docs emphasize @theme must be top-level (not nested), but multiple sequential blocks are valid and commonly used in practice for modularity, as seen in PR #14095 and discussions like #18560. When importing external CSS files with theme(reference) or theme(inline), they can contain @theme blocks that merge similarly.

Citations:


🏁 Script executed:

cat -n packages/fonts/src/helpers/css-theme.ts

Repository: onlook-dev/onlook

Length of output: 3788


Only the first @theme block is ever considered — can cause missed removals and wrong-block additions.

Tailwind v4 explicitly supports multiple @theme blocks in one stylesheet (commonly one for colors, another @theme inline { … } dedicated to fonts). findThemeBlock returns only the first match, so:

  • addFontToCssTheme will insert the font variable into the first @theme block even if a downstream @theme inline { } is the intended font block — mixing font tokens into the colors block.
  • removeFontFromCssTheme will leave orphaned --font-* declarations in any @theme block other than the first.

Consider iterating over all @theme blocks: for add, prefer an existing block that already contains a --font-* declaration (or an @theme inline) and fall back to the last block; for remove, strip the declaration from every @theme body.

Additionally, the brace-depth loop doesn't skip CSS comments or strings — /* } */ inside a @theme body would terminate the scan early. Unlikely in practice for generated globals.css, but worth a defensive comment-stripping pass if user-authored input is expected.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/fonts/src/helpers/css-theme.ts` around lines 5 - 32, findThemeBlock
currently returns only the first `@theme` match which causes addFontToCssTheme and
removeFontFromCssTheme to target the wrong block; update the logic to enumerate
all `@theme` blocks (use THEME_BLOCK_REGEX.exec in a loop resetting lastIndex) and
return their body ranges instead of a single range, then change
addFontToCssTheme to prefer a block that already contains a --font- declaration
or an `@theme` inline block and otherwise fall back to the last `@theme` block, and
change removeFontFromCssTheme to remove the --font- declaration from every
`@theme` body; also make the brace-depth scan in findThemeBlock robust to
comments/strings by stripping or skipping CSS comments (/* */) and string
literals before counting braces.

@@ -0,0 +1,4 @@
@theme inline {
--font-font-test: var(--font-old);
--font-font.test: var(--font-font-test);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find unescaped dots in generated CSS custom property names and inspect token construction.

# Expectation after the fix: no unescaped-dot custom property declarations in fixtures/helper outputs.
rg -nP -- '^\s*--[A-Za-z0-9_-]*\.[A-Za-z0-9_.-]*\s*:' packages/fonts/test/data/css-theme packages/fonts/src || true

# Inspect the helper implementation for raw font id / fontId interpolation into --font-* tokens.
rg -nP --type=ts -C4 -- '(--font-\$\{|`--font-|font\.id|fontId)' packages/fonts/src/helpers/css-theme.ts

Repository: onlook-dev/onlook

Length of output: 1993


Escape font IDs before creating CSS custom property names.

Line 3 expects --font-font.test, but an unescaped . is not valid in a CSS custom property identifier. The helper function getThemeFontVariableName in packages/fonts/src/helpers/css-theme.ts (line 38-39) directly interpolates font IDs without escaping. Sanitize the font ID before constructing the custom property name, then update the regex-font-id fixtures accordingly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/fonts/test/data/css-theme/add-font-to-css-theme/regex-font-id/expected.css`
at line 3, getThemeFontVariableName currently interpolates raw font IDs into CSS
custom property names causing invalid identifiers (e.g., "."); update
packages/fonts/src/helpers/css-theme.ts to sanitize/escape the font ID before
constructing the custom property (for example replace or encode characters not
allowed in CSS custom property names such as "." with a safe character like "-"
or use a strict whitelist) in the getThemeFontVariableName function, and then
update the regex-font-id fixtures/expected files to reflect the new
escaped/sanitized property names (e.g., --font-font-test).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] Fonts loaded from the font panel are not implemented correctly

1 participant