Skip to content

feat: expose theme colors as CSS custom properties - #490

Merged
kevinchappell merged 19 commits into
mainfrom
feat/css-custom-properties
Sep 24, 2026
Merged

kevinchappell merged 19 commits into
mainfrom
feat/css-custom-properties

Conversation

@kevinchappell

Copy link
Copy Markdown
Collaborator

Summary

  • Every editor and renderer color is now a --formeo-* custom property (48 in total), declared on :where(:root) with zero specificity. The defaults are exactly 5.1.3's compiled values. A resolved-CSS characterization test enforces this: it substitutes every var() with its default and compares byte-for-byte against a baseline captured before any SCSS change.
  • Opt-in .formeo-dark preset, applied on any ancestor or on the container itself. It covers surfaces, text, lines, structural highlights and dialogs, and adds a higher-contrast THEN label. It never overrides --formeo-on-accent, so icons on colored hover buttons stay white.
  • Icons inherit --formeo-icon. The plus and minus icons use currentColor.
  • tools/check-color-literals.mjs rejects hard-coded colors in component SCSS: hex, the rgb/hsl/hwb/lab/lch/oklab/oklch functions, named colors, and colors module imports. It runs in npm run lint and, via color-literals.test.mjs, in npm test, so CI enforces it.
  • The demo has a Toggle Dark button. The README has a new Theming section.

Visual changes with default settings

  • Icons that hard-coded #444 (header, paragraph, triangles) now use --formeo-icon, which defaults to #000.
  • The column resize-handle triangles now use the column outline color (--formeo-column-outline-soft, and --formeo-column-outline on hover). Before, fill="#444" on the icon overrode those existing rules.
  • The field being edited (.editing-field) has a --formeo-bg background. The rule has zero specificity, so highlights still win.

Nothing else changes. Resolved output, both unminified and minified, matches 5.1.3.

Notes for hosts

  • Put overrides or formeo-dark on :root/<body>, or on a scope that also covers .formeo-dialog and a re-homed .formeo-controls, because dialogs are appended to document.body.
  • Hosts that map their own dark theme should also set color-scheme: dark. Without it, native inputs and the dialog keep the browser's light colors.
  • .formeo-dark sets color-scheme: dark on its subtree.
  • Derived properties (bg-hover, danger-subtle, column-outline-soft, overlay, *-highlight*) don't follow their base color and need their own overrides.

Test plan

  • npm test: 202 pass (baseline equality, property list, dark preset and each allowed addition's exact content, resolver purity, color guard, icon fills and strokes)
  • npm run lint: Biome plus the color guard
  • npm run build:lib: dist/formeo.min.css keeps the :where(:root) defaults
  • Demo screenshots in default, .formeo-dark and a mapped theme: field hover, edit panel, conditions, autocomplete, dialog, row and column hover
  • Link into the formeo.io builder, light and dark (Plan B)

Refs Draggable/formeo.io#63

Adds --formeo-* properties for every editor and renderer color (defaults
unchanged) and an opt-in .formeo-dark preset.
Formeo containers otherwise inherit the host page's text color, which can
read dark-on-dark under .formeo-dark. Explicitly sets --formeo-text as the
color for .formeo, .formeo-controls, and .formeo-dialog under the preset.
The plus/minus condition icons hard-coded a black stroke, so they stayed black
under a dark theme. They now stroke with currentColor, and :where(.svg-icon) sets
color as well as fill from --formeo-icon, which defaults to black, so nothing
changes by default.

The icon test now also rejects hex stroke attributes in any icon and hex
fill/stroke values inside style attributes in the generated sprite.
- :where(.formeo-dialog, .component-edit[popover]) takes its background and text
  from --formeo-bg/--formeo-text instead of the UA Canvas colors. Zero
  specificity, and the defaults render the same as the UA colors.
- The dark container text rule also matches when .formeo-dark is on the
  container itself.
- Under .formeo-dark the THEN condition label uses --formeo-border-strong behind
  its white text; --formeo-text-muted was about 2.5:1.

Each new rule has an ALLOWED_ADDITIONS entry and a test that pins its compiled
text, and every entry must match exactly one compiled rule.
resolveFormeoProperties now throws when the compiled CSS has more than one
:where(:root) or :where(.formeo-dark) block, when the root block declares
anything but --formeo-* properties, or when the dark block declares anything
but --formeo-* properties and color-scheme. Synthetic stylesheets cover each case.
tools/check-color-literals.mjs now exports findColorLiterals(text, file) and the
guarded file list; run directly it still prints the problems and exits 1. It now
also flags hsl/hsla/hwb/lab/lch/oklab/oklch, any @use of the colors module, its
namespace (including colors.$), and CSS named colors in declaration values. It
ignores block comments.

The colors import in base/_animation.scss carries the OK marker, since its one
derived literal still needs it. color-literals.test.mjs checks the guarded tree
and unit-tests each rule, which puts the guard into npm test (CI and pre-push).
"Toggle Dark" puts .formeo-dark on <body>, but the demo page kept its light
gradient, header and footer, so the stage and row handles and the header text
were hard to see. body.formeo-dark now darkens the page, header, footer and
formData popover using the formeo properties, and inverts the logo and GitHub
images. Library styles are unchanged.
Say where overrides and the formeo-dark class belong (dialogs are appended to
document.body), what color-scheme does and why a custom dark theme needs it,
that container text inherits the page color, and which derived properties
don't follow their base color. List the property groups, link to
_properties.scss on GitHub (it isn't in the npm package), and list the visual
changes from 5.1.3, including the column resize-handle triangles.
The resolver's rule matcher now also treats an opening brace as a rule start, so
a second :where(:root) block inside @media is counted (and rejected) instead of
being silently left in the output.
The zero-specificity dialog rule changed the dialog from the UA Canvas colors
to --formeo-bg for hosts that set color-scheme: dark without mapping formeo's
properties. It now applies only under .formeo-dark, either on an ancestor or
on the dialog itself. The unused .component-edit[popover] selector is gone.

The README says dialogs follow the preset, and that hosts with their own
mapping should set color-scheme: dark and include .formeo-dialog in its scope.
Moves the .editing-field background into a top-level :where() rule so field and
column highlights still win, and lists it as an allowed addition with a content test.
Copilot AI lite review requested due to automatic review settings September 24, 2026 11:35

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Six unresolved moderate findings affect themeable animation and icon behavior and the completeness of the color guard.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
What changed in this PR

This PR exposes Formeo colors as 48 CSS custom properties, adds a .formeo-dark preset, and updates icons, tooling, tests, demo, and documentation.

Changes:

  • Tokenizes Sass colors while preserving default compiled output.
  • Adds dark-mode theming and themeable icon assets.
  • Adds baseline, color-guard, icon tests, lint integration, and documentation.
File Summary
tools/​resolve-formeo-properties.mjs Resolves themed CSS for baseline comparisons.
tools/​compile-scss.mjs Compiles SCSS for tests.
tools/​check-color-literals.mjs Guards against hard-coded colors; moderate follow-ups remain for case-insensitive functions, data URIs, and multiline values.
src/​lib/​sass/​formeo.scss Loads theme property definitions.
src/​lib/​sass/​css-properties.test.mjs Tests properties, themes, and baseline output.
src/​lib/​sass/​components/​component.scss Migrates component colors to theme tokens.
src/​lib/​sass/​components/​_stage.scss Migrates stage colors to theme tokens.
src/​lib/​sass/​components/​_row.scss Migrates row colors to theme tokens.
src/​lib/​sass/​components/​_panels.scss Migrates panel colors to theme tokens.
src/​lib/​sass/​components/​_group-actions.scss Migrates group action colors to theme tokens.
src/​lib/​sass/​components/​_field.scss Migrates field colors to theme tokens.
src/​lib/​sass/​components/​_field-edit.scss Migrates field editor colors to theme tokens.
src/​lib/​sass/​components/​_dialog.scss Migrates dialog colors to theme tokens.
src/​lib/​sass/​components/​_controls.scss Migrates control colors to theme tokens.
src/​lib/​sass/​components/​_column.scss Migrates column colors to theme tokens.
src/​lib/​sass/​components/​_autocomplete.scss Migrates autocomplete colors to theme tokens.
src/​lib/​sass/​color-literals.test.mjs Tests color-literal detection.
src/​lib/​sass/​base/​_variables.scss Uses tokenized borders.
src/​lib/​sass/​base/​_tokens.scss Defines Sass aliases for CSS properties.
src/​lib/​sass/​base/​_properties.scss Defines default and dark theme properties.
src/​lib/​sass/​base/​_mixins.scss Migrates shared styles to theme tokens.
src/​lib/​sass/​base/​_icons.scss Applies tokenized icon colors; colored hover rules need color updates for stroked icons.
src/​lib/​sass/​base/​_bs.scss Migrates base styles; the embedded select arrow remains hard-coded and is not themeable.
src/​lib/​sass/​base/​_animation.scss Migrates animation colors; the pulse midpoint still uses a Sass-time color.
src/​lib/​sass/​_render.scss Migrates renderer colors to theme tokens.
src/​lib/​sass/​__fixtures__/​formeo-baseline.css Stores the pre-change CSS baseline.
src/​lib/​icons/​icons.test.mjs Validates icon color attributes.
src/​lib/​icons/​icon-triangle-up.svg Removes hard-coded fill.
src/​lib/​icons/​icon-triangle-right.svg Removes hard-coded fill.
src/​lib/​icons/​icon-triangle-left.svg Removes hard-coded fill.
src/​lib/​icons/​icon-triangle-down.svg Removes hard-coded fill.
src/​lib/​icons/​icon-plus.svg Uses currentColor for strokes.
src/​lib/​icons/​icon-paragraph.svg Removes hard-coded fill.
src/​lib/​icons/​icon-minus.svg Uses currentColor for strokes.
src/​lib/​icons/​icon-header.svg Removes hard-coded fill.
src/​lib/​icons/​icon-hash.svg Removes hard-coded fill.
src/​lib/​icons/​icon-email.svg Removes hard-coded fill.
src/​lib/​icons/​formeo-sprite.svg Regenerates the icon sprite.
src/​demo/​sass/​demo.scss Adds demo dark-mode styling.
src/​demo/​js/​actionButtons.js Adds the dark-mode toggle.
README.md Documents theming and custom properties.
package.json Runs the color guard during linting.
biome.json Excludes the CSS fixture from formatting checks.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/lib/sass/base/_animation.scss Outdated
Co-authored-by: kevinchappell <1457540+kevinchappell@users.noreply.github.com>
@kevinchappell
kevinchappell merged commit 9cb6eea into main Sep 24, 2026
@kevinchappell
kevinchappell deleted the feat/css-custom-properties branch September 24, 2026 11:53
@kevinchappell

Copy link
Copy Markdown
Collaborator Author

🎉 This PR is included in version 5.2.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants