feat: expose theme colors as CSS custom properties - #490
Merged
Merged
Conversation
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.
Contributor
There was a problem hiding this comment.
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
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.
Co-authored-by: kevinchappell <1457540+kevinchappell@users.noreply.github.com>
Collaborator
Author
|
🎉 This PR is included in version 5.2.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
--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 everyvar()with its default and compares byte-for-byte against a baseline captured before any SCSS change..formeo-darkpreset, 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.--formeo-icon. The plus and minus icons usecurrentColor.tools/check-color-literals.mjsrejects hard-coded colors in component SCSS: hex, the rgb/hsl/hwb/lab/lch/oklab/oklch functions, named colors, andcolorsmodule imports. It runs innpm run lintand, viacolor-literals.test.mjs, innpm test, so CI enforces it.Visual changes with default settings
#444(header, paragraph, triangles) now use--formeo-icon, which defaults to#000.--formeo-column-outline-soft, and--formeo-column-outlineon hover). Before,fill="#444"on the icon overrode those existing rules..editing-field) has a--formeo-bgbackground. 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
formeo-darkon:root/<body>, or on a scope that also covers.formeo-dialogand a re-homed.formeo-controls, because dialogs are appended todocument.body.color-scheme: dark. Without it, native inputs and the dialog keep the browser's light colors..formeo-darksetscolor-scheme: darkon its subtree.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 guardnpm run build:lib:dist/formeo.min.csskeeps the:where(:root)defaults.formeo-darkand a mapped theme: field hover, edit panel, conditions, autocomplete, dialog, row and column hoverRefs Draggable/formeo.io#63