Skip to content

feat: visual Style editor with live preview#72

Merged
devCluna merged 1 commit into
mainfrom
feat/style-editor
Jul 10, 2026
Merged

feat: visual Style editor with live preview#72
devCluna merged 1 commit into
mainfrom
feat/style-editor

Conversation

@devCluna

@devCluna devCluna commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Summary

A dedicated Style mode for theming the public form, with a live Desktop/Mobile preview. Gated behind a "Custom styles" setting.

Architecture (single source of truth)

  • theme.ts — the theme model + resolveThemeVars() → a small set of --sfb-* CSS variables (plus a data-sfb-fields attribute for field-style variants).
  • form-css.ts — the form's stylesheet, imported by both the public embed (server) and the admin live preview. One stylesheet drives both surfaces; no duplicated styling logic.

Controls

Accent · font · card & page background · field corners / style / label weight · button style / width · form width / spacing / card shadow / card border / card corners / placement (top | center).

  • Editable colour palettes — add colours via the native picker, remove any swatch; persisted with the form. "Reset to default theme" restores them.
  • Enabling Custom styles in Settings reveals the Style tab immediately (no save required) with a link into the editor.
  • Style changes follow the existing draft / publish flow (the theme lives in settings, snapshotted on publish). Toasts announce the state on save/publish.

Fix included

The public page now applies theme variables via a <style> block instead of an inline style attribute — a quoted font value ("Inter") no longer breaks the attribute and falls back to the browser's serif default.

Verification

  • 26/26 tests (theme resolver: colours, corners, border, placement, palettes), type-check (server + admin), build, verify, embed syntax.
  • No dependency changes.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a Style editor for customizing form colors, typography, spacing, fields, buttons, borders, shadows, and layout.
    • Added desktop and mobile live previews while editing form styles.
    • Added palette management with color add/remove controls and a reset-to-default option.
    • Added optional style editing mode to the form builder.
    • Custom styles now apply consistently to published forms and embedded forms.
    • Added save and publish status notifications.
  • Bug Fixes

    • Improved contrast, field styling, button appearance, and responsive presentation based on selected themes.
  • Tests

    • Added coverage for theme resolution, layout options, field styles, borders, buttons, and color contrast.

A dedicated Style mode (Fields | Style toggle) for theming the public form,
gated behind a "Custom styles" setting.

- feat: theme model + `resolveThemeVars` (single source) → `--sfb-*` variables
  shared by the admin live preview and the public embed/page via one stylesheet
  (`form-css.ts`), so there is no duplicated styling logic.
- feat: Style mode sidebar — accent / font / card + page background, field
  corners / style / label weight, button style / width, form width / spacing /
  card shadow / card border / card corners / placement (top | center), with a
  Desktop | Mobile live preview.
- feat: editable, persisted colour palettes (add via picker, remove per swatch)
  per colour group; "Reset to default theme" restores them.
- feat: enabling "Custom styles" in Settings reveals the Style tab immediately
  (no save required) with a link into the editor.
- feat: style changes follow the existing draft / publish flow (theme lives in
  settings, snapshotted on publish); toasts announce the state on save/publish.
- fix: public page applies theme vars via a <style> block instead of an inline
  attribute — a quoted font value no longer breaks the style and falls back to
  the browser's serif default.
- test: theme resolver coverage (colors, corners, border, placement, palettes).
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds typed theme resolution, an admin style editor with live preview, style-mode integration in the form builder, and theme application to generated public and embedded forms.

Changes

Form styling

Layer / File(s) Summary
Theme contracts and CSS variable resolution
admin/src/theme.ts, admin/src/types.ts, tests/theme.test.ts
Defines theme tokens, defaults, palettes, CSS variable resolution, structural attributes, and tests for color, spacing, borders, radii, alignment, and button styles.
Style editor and live preview
admin/src/components/StyleMode.tsx, admin/src/form-css.ts
Adds theme controls, palette editing, reset behavior, device previews, reusable form CSS, and theme-driven preview rendering.
Admin style-mode integration
admin/src/pages/FormBuilderPage.tsx
Adds style settings, fields/style tabs, style editor navigation, theme updates, and save or publish notifications.
Public and embedded themed rendering
server/src/controllers/form.ts
Injects sanitized theme variables into public pages and applies theme variables, field attributes, and shared CSS in embedded forms.

Estimated code review effort: 4 (Complex) | ~45 minutes

Poem

I’m a rabbit with colors to spare,
Styling form fields with fluffy care.
Themes hop in, previews glow,
Public pages now match the show.
CSS carrots sprout everywhere!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: a new visual Style editor with live preview.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/style-editor

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.

@devCluna devCluna merged commit 3833e59 into main Jul 10, 2026
3 checks passed
@devCluna devCluna deleted the feat/style-editor branch July 10, 2026 00:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
tests/theme.test.ts (1)

38-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for solid button text contrast on a light accent.

The current outline-button test verifies the transparent background and accent border, but no test covers --sfb-btn-color for solid buttons. A test with a light accent (e.g. #f5f5f9) would catch the bug where solid button text is always #fff regardless of accent luminance.

🧪 Suggested test
it('solid button uses dark text on a light accent', () => {
  const v = resolveThemeVars({ ...DEFAULT_THEME, buttonStyle: 'solid', accent: '`#f5f5f9`' });
  expect(v['--sfb-btn-color']).toBe('`#1f1f33`');
});

it('solid button uses white text on a dark accent', () => {
  const v = resolveThemeVars({ ...DEFAULT_THEME, buttonStyle: 'solid', accent: '`#1e1e2e`' });
  expect(v['--sfb-btn-color']).toBe('`#fff`');
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/theme.test.ts` around lines 38 - 42, Add solid-button contrast coverage
alongside the existing outline-button test in theme.test.ts: add cases using
light and dark accents that assert resolveThemeVars returns `#1f1f33` and `#fff`
respectively for --sfb-btn-color, ensuring buttonStyle is set to solid.
admin/src/theme.ts (1)

115-115: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

--sfb-accent-contrast is computed but never consumed.

This variable is resolved and returned but not referenced in FORM_CSS or any inline style in the codebase. Either wire it into the button color (see adjacent comment) or remove it to avoid confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@admin/src/theme.ts` at line 115, The computed --sfb-accent-contrast variable
is unused. In the theme generation logic, either reference it from FORM_CSS or
the relevant inline button styles as the button foreground color, or remove its
definition and related computation if no consumer is needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@admin/src/pages/FormBuilderPage.tsx`:
- Line 191: Remove the onToggleStyle prop and callback usage from
SettingsDrawer, including its invocation in the “Custom styles” ToggleRow. Keep
the toggle updating only the drawer’s local settings via patch({ enableStyle: v
}); ensure the parent’s Style tab visibility and enableStyle state continue
deriving from settings and changing only through onSave, so Cancel discards the
change.

In `@admin/src/theme.ts`:
- Line 134: Fix the solid-button text color in the theme mapping by removing the
dead `isDark(t.accent)` branches and using the existing `--sfb-accent-contrast`
value (or equivalent contrast-aware dark fallback) for light accents while
retaining white text for dark accents. Update the `--sfb-btn-color` assignment
in the theme generation logic.

In `@server/src/controllers/form.ts`:
- Line 60: Escape form.settings.theme.fieldStyle with the existing escapeHtml
helper before interpolating it into the data-sfb-fields attribute, preserving
the current conditional rendering and preventing malformed markup from persisted
values.

---

Nitpick comments:
In `@admin/src/theme.ts`:
- Line 115: The computed --sfb-accent-contrast variable is unused. In the theme
generation logic, either reference it from FORM_CSS or the relevant inline
button styles as the button foreground color, or remove its definition and
related computation if no consumer is needed.

In `@tests/theme.test.ts`:
- Around line 38-42: Add solid-button contrast coverage alongside the existing
outline-button test in theme.test.ts: add cases using light and dark accents
that assert resolveThemeVars returns `#1f1f33` and `#fff` respectively for
--sfb-btn-color, ensuring buttonStyle is set to solid.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5555ae7e-8eef-44c1-8690-07b7a84f097d

📥 Commits

Reviewing files that changed from the base of the PR and between e182ba6 and 1bf91f9.

📒 Files selected for processing (7)
  • admin/src/components/StyleMode.tsx
  • admin/src/form-css.ts
  • admin/src/pages/FormBuilderPage.tsx
  • admin/src/theme.ts
  • admin/src/types.ts
  • server/src/controllers/form.ts
  • tests/theme.test.ts


<div style={{ padding: '20px 0', borderBottom: `1px solid ${C.n150}` }}>
<div style={dGroupT}>Style</div>
<ToggleRow label="Custom styles" hint="Enable the Style editor to theme the public form." on={!!settings.enableStyle} onClick={() => { const v = !settings.enableStyle; patch({ enableStyle: v }); onToggleStyle(v); }} />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Style toggle bypasses the save/cancel buffer.

The "Custom styles" toggle calls both patch({ enableStyle: v }) (local buffer) and onToggleStyle(v) (parent state immediately). Every other setting in the drawer stays in the local buffer and only reaches the form on "Save settings" (per the comment on line 104). If a user toggles styles on then clicks Cancel, the parent's enableStyle is already true and the Style tab persists — the toggle is not reverted.

onToggleStyle should not mutate the parent state directly. Instead, the Style tab visibility should be derived from the local settings state inside the drawer, and the actual enableStyle change should only flow through onSave.

🐛 Proposed fix

Remove onToggleStyle from the SettingsDrawer props and the immediate parent mutation. The Style tab visibility in the parent already derives from settings.enableStyle, which only changes when the user clicks "Save settings".

 function SettingsDrawer({ initialDescription, initialSettings, slug, publishedAt, onClose, onSave, onOpenStyle }: {
   initialDescription: string;
   initialSettings: FormSettings;
   slug: string | null;
   publishedAt: string | null;
   onClose: () => void;
   onSave: (description: string, settings: FormSettings) => void;
-  onToggleStyle: (v: boolean) => void;
   onOpenStyle: () => void;
 }) {
-            <ToggleRow label="Custom styles" hint="Enable the Style editor to theme the public form." on={!!settings.enableStyle} onClick={() => { const v = !settings.enableStyle; patch({ enableStyle: v }); onToggleStyle(v); }} />
+            <ToggleRow label="Custom styles" hint="Enable the Style editor to theme the public form." on={!!settings.enableStyle} onClick={() => patch({ enableStyle: !settings.enableStyle })} />
         <SettingsDrawer
           initialDescription={description}
           initialSettings={settings}
           slug={slug}
           publishedAt={publishedAt}
           onClose={() => setShowSettings(false)}
           onSave={(desc, s) => { setDescription(desc); setSettings(s); setShowSettings(false); }}
-          onToggleStyle={(v) => setSettings((s) => ({ ...s, enableStyle: v }))}
           onOpenStyle={() => { setShowSettings(false); setMode('style'); }}
         />

Also applies to: 526-527

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@admin/src/pages/FormBuilderPage.tsx` at line 191, Remove the onToggleStyle
prop and callback usage from SettingsDrawer, including its invocation in the
“Custom styles” ToggleRow. Keep the toggle updating only the drawer’s local
settings via patch({ enableStyle: v }); ensure the parent’s Style tab visibility
and enableStyle state continue deriving from settings and changing only through
onSave, so Cancel discards the change.

Comment thread admin/src/theme.ts
'--sfb-btn-width': t.buttonWidth === 'full' ? '100%' : 'auto',
// button: solid = accent bg / contrast text; outline = transparent bg / accent text + border
'--sfb-btn-bg': t.buttonStyle === 'solid' ? t.accent : 'transparent',
'--sfb-btn-color': t.buttonStyle === 'solid' ? (isDark(t.accent) ? '#fff' : '#fff') : t.accent,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Solid button text is always white regardless of accent luminance.

isDark(t.accent) ? '#fff' : '#fff' — both branches return '#fff', making the isDark check dead logic. For light accent colors (e.g. a user-added #f5f5f9), white button text will have poor contrast. The --sfb-accent-contrast variable (line 115) was likely intended to be used here, or the else branch should return a dark color.

🐛 Proposed fix
-    '--sfb-btn-color': t.buttonStyle === 'solid' ? (isDark(t.accent) ? '`#fff`' : '`#fff`') : t.accent,
+    '--sfb-btn-color': t.buttonStyle === 'solid' ? (isDark(t.accent) ? '`#fff`' : '`#1f1f33`') : t.accent,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'--sfb-btn-color': t.buttonStyle === 'solid' ? (isDark(t.accent) ? '#fff' : '#fff') : t.accent,
'--sfb-btn-color': t.buttonStyle === 'solid' ? (isDark(t.accent) ? '`#fff`' : '`#1f1f33`') : t.accent,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@admin/src/theme.ts` at line 134, Fix the solid-button text color in the theme
mapping by removing the dead `isDark(t.accent)` branches and using the existing
`--sfb-accent-contrast` value (or equivalent contrast-aware dark fallback) for
light accents while retaining white text for dark accents. Update the
`--sfb-btn-color` assignment in the theme generation logic.

</head>
<body>
<div class="card">
<div class="card"${form.settings && form.settings.theme && form.settings.theme.fieldStyle ? ` data-sfb-fields="${form.settings.theme.fieldStyle}"` : ''}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

fieldStyle value is not escaped in the data-sfb-fields attribute.

form.settings.theme.fieldStyle is interpolated directly into the attribute value without escapeHtml. While the TypeScript type constrains it to 'outline' | 'filled' | 'underline', this is server-side JavaScript with any-typed data from the database. A tampered persisted value containing " would break the attribute markup. The escapeHtml function is available in the same file.

🛡️ Proposed fix
-  <div class="card"${form.settings && form.settings.theme && form.settings.theme.fieldStyle ? ` data-sfb-fields="${form.settings.theme.fieldStyle}"` : ''}>
+  <div class="card"${form.settings && form.settings.theme && form.settings.theme.fieldStyle ? ` data-sfb-fields="${escapeHtml(form.settings.theme.fieldStyle)}"` : ''}>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="card"${form.settings && form.settings.theme && form.settings.theme.fieldStyle ? ` data-sfb-fields="${form.settings.theme.fieldStyle}"` : ''}>
<div class="card"${form.settings && form.settings.theme && form.settings.theme.fieldStyle ? ` data-sfb-fields="${escapeHtml(form.settings.theme.fieldStyle)}"` : ''}>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/controllers/form.ts` at line 60, Escape
form.settings.theme.fieldStyle with the existing escapeHtml helper before
interpolating it into the data-sfb-fields attribute, preserving the current
conditional rendering and preventing malformed markup from persisted values.

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.

1 participant